(數據科學學習手札31)基於Python的網絡數據采集(初級篇)


一、簡介

  在實際的業務中,我們手頭的數據往往難以滿足需求,這時我們就需要利用互聯網上的資源來獲取更多的補充數據,但是很多情況下,有價值的數據往往是沒有提供源文件的直接下載渠道的(即所謂的API),這時我們該如何批量獲取這些嵌入網頁中的信息呢?

  這時網絡數據采集就派上用處了,你通過瀏覽器可以觀看到的絕大多數數據,都可以利用爬蟲來獲取,而所謂的爬蟲,就是我們利用編程語言編寫的腳本,根據其規模大小又分為很多種,本篇便要介紹基本的Python編寫的爬蟲腳本來進行單機形式的網絡數據采集,這也是一個進階的數據挖掘工程師或數據分析師的基本技能之一,大量的應用場景都會需要這種幾乎可以毫無阻礙地獲取數據的手段,譬如市場預測、機器語言翻譯亦或是醫療診斷領域,通過對新聞網站、文章中的文本數據進行采集以進行進一步的數據挖掘工作,也是爬蟲很常見的應用場景之一;

  本篇博客將通過介紹基礎的爬蟲知識,並附上兩個實戰項目的例子(爬取網易財經海南板塊歷史股票數據、爬取網易新聞多個分類板塊的新聞文本數據),對基礎的爬蟲做一個小小的總結。

*本篇以jupyter notebook作為開發工具

 

二、建立連接

  為了抓取互聯網上的數據資源,第一步顯然是要建立起網絡連接(即登入你的目標網址),在Python中,我們使用urllib.request中的urlopen()來建立起與目標網址的連接,這個函數可以用來打開並讀取一個從網絡獲取的遠程對象,可以輕松讀取HTML文件、圖像文件或其他寄存在網絡端的文件,下面是一個簡單的例子:

from urllib.request import urlopen

'''賦值我們需要登入的網址'''
html = urlopen('http://news.163.com/')

'''打印采集回的目標網頁的源代碼'''
print(html.read())

運行結果:

可以看出,通過上面非常簡單的幾行語句,我們就采集回http://news.163.com/的網頁源代碼,與瀏覽器中查看源代碼的方式進行比較:

 

  可以看出,只通過這幾行語句采集回的網頁內容,和瀏覽器中展示的網頁源碼信息有很大的出入,這是因為我們通過urlopen只是傳回來朴素的源代碼,沒有經過任何解析操作,下面介紹如何對返回的朴素的網頁源碼進行解析;

 

三、BeautifulSoup庫

  通過上一節我們舉的例子可以看出,我們需要對返回的網頁源碼進行結構化的解析,而BeautifulSoup就是這樣一個神奇的第三方庫,它通過對HTML標簽進行定位,以達到格式化和組織復雜網絡信息的目的,我們基於BeautifulSoup對上一節中的簡單代碼進行擴充:

from urllib.request import urlopen
from bs4 import BeautifulSoup

'''賦值我們需要登入的網址'''
html = urlopen('http://news.163.com/')

'''利用BeautifulSoup對朴素的網頁源代碼進行結構化解析(包括對utf編碼的內容進行轉碼)'''
obj1 = BeautifulSoup(html.read())

'''打印采集回的目標網頁的源代碼'''
print(obj1)

運行結果:

可以看出這時我們得到的內容與我們之前在瀏覽器中查看的網頁源代碼一致(中文內容也被展示出來),更重要的是,我們已經對目標網頁的結構進行了解析,意味着我們可以通過調用不同結構標簽來查看相應內容:

print(obj1.html.h1)

print(obj1.html.title)

運行結果:

這對之后我們對所需內容的定位提取很有幫助,事實上,任何HTML、XML文件的任意節點信息都可以被提取出來,只要目標信息的附近有標記即可;

 

四、錯誤的處理策略

  相比大家都有經驗,當我們登入某些網址時,因為網絡不穩定或其它原因,會導致網頁連接失敗,而在我們的網絡爬蟲持續采集數據的過程中,因為網頁數據格式不友好、網絡服務器宕機、目標數據的標簽尋找失敗等原因,會導致你的爬蟲中途因發生錯誤而中斷,這在需要長時間工作的爬蟲項目來說尤為關鍵;

  爬蟲工作過程中常見的錯誤如下:

  對於urlopen的過程,服務器上不存在目標網頁(或是獲取頁面的時候出現錯誤),這種異常發生時,程序會返回HTTP錯誤,這包含很多種詳細的錯誤類型,但urlopen統一反饋“HTTPError”,於是乎利用Python中處理錯誤的try...except機制,就可以在爬蟲遇到這種錯誤時,進行相應的處理方法(通常是選擇跳過),下面是一個簡單的例子:

from urllib.request import urlopen

'''創造一系列網址,其中第四個為偽造的不存在網站'''
html = ['http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=1',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=2',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=3',
        'http://www.pythonscraping.com/pages/page10000.html',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=4']

'''循環反饋對應網址的源代碼信息'''
for i in range(5):
        token = urlopen(html[i])
        print(token.read()[:10])

這時我們沒有進行錯誤處理,因此在程序運行到第四個網址時,會出現打不開網頁的錯誤,如下:

HTTPError出現了,這時由於這個網址的打開失敗,導致后續的任務都被迫中斷,下面我們使用錯誤處理機制對這種遍歷任務中的潛在錯誤風險進行處理:

from urllib.request import urlopen
from urllib.error import HTTPError#注意,這里需要import urllib中具體的錯誤類型

'''創造一系列網址,其中第四個為偽造的不存在網站'''
html = ['http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=1',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=2',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=3',
        'http://www.pythonscraping.com/pages/page10000.html',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=4']

'''循環反饋對應網址的源代碼信息'''
for i in range(5):
    try:
        token = urlopen(html[i])
        print(token.read()[:10])
    except HTTPError as e:
        print('錯誤出現!跳過')

運行結果:

這樣就可以對各種潛在的錯誤進行處理,而不打斷整個程序的進行,但運行大的爬蟲項目時,潛在的錯誤類型是多種多樣的,一旦沒有在程序開頭import全對應的錯誤類型,依舊會因為未預料到的錯誤類型打斷程序,這時我們可以利用try...except中的泛型錯誤Exception來識別所有錯誤類型,並打印具體的錯誤類型以作后期分析:

from urllib.request import urlopen

'''創造一系列網址,其中第四個為偽造的不存在網站'''
html = ['http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=1',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=2',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=3',
        'http://www.pythonscraping.com/pages/page10000.html',
        'http://quotes.money.163.com/trade/lsjysj_600221.html?year=2012&season=4']

'''循環反饋對應網址的源代碼信息'''
for i in range(5):
    try:
        token = urlopen(html[i])
        print(token.read()[:10])
    except Exception as e:#泛型錯誤處理機制
        print('錯誤','< ',e,' >','出現!跳過')

運行結果:

可以看到,在利用Exception時,會處理所有可能的錯誤,非常方便;

 

五、目標內容的粗略提取(基於CSS)

  前面說了這么多,實際上還是在對我們的目的做鋪墊,即介紹了 獲取信息--抽取目標信息 這個過程中的獲取信息部分,在獲得了結構化的全量信息之后,我們就要開始着手如何提取其中想要的信息了;

  先普及一個知識:幾乎每一個網站都會存在層疊樣式報(cascading style sheet,CSS),這種機制使得瀏覽器和人類得以理解網頁的層次內容,CSS可以讓HTML元素呈現出差異化,使得不同的數據歸屬於其對應的標簽下,我們再通過BeautifulSoup解析后的網頁內容(帶有各層次標簽),利用對應內容的標簽屬性,即可有選擇的獲取我們想要的數據內容;

  我們用findAll()方法來對BeautifulSoup對象進行指定標簽內容的提取,下面是一個簡單的例子:

 

我們對http://sports.163.com/18/0504/10/DGV2STDA00058782.html這個新聞網頁,先是提取它的新聞標題內容,通過觀察網頁源代碼,發現其文章標題內容隱藏在<title>標簽下,於是利用findAll()對title標簽內內容進行提取:

 

from urllib.request import urlopen
from bs4 import BeautifulSoup

'''連接目標網址'''
html = urlopen('http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

'''將反饋回的網頁源代碼解析為BeautifulSoup對象'''
obj = BeautifulSoup(html)

'''提取obj對象下title標簽內的內容'''
text = obj.findAll('title')

'''打印結果'''
print(text)

 

運行結果:

  

  從上面的小例子中可以看出findAll()的強大功能,下面對其進行詳細的介紹:

  BeautifulSoup中的find()與findAll()是網頁內容提取中最常用的兩個函數,我們可以利用它們通過標簽的不同屬性輕松地過濾HTML頁面,查找需要的單個或多個標簽下的內容。

  find()與findAll()用法幾乎一樣,先介紹findAll()的主要參數:

tag:這個參數傳遞字符串形式的單個標題標簽或由多個標題標簽組成的列表,如'title',['h1','h2','h3']

attributes:屬性參數,接受用字典封裝的一個標簽的若干屬性和對應的屬性值,例如{'property':'og:description'}

recursive:bool型變量,默認為True,代表findAll會根據你的要求去查找標簽參數的所有子標簽,以及子標簽的子標簽;如果設置為False,則findAll只查找文檔的一級標簽;

text:字符型輸入,設置該參數以后,提取信息就不是用標簽的屬性,而是用標簽的文本內容,即content中的內容來匹配

limit:范圍限制參數,只用於findAll,換句話說,find等價於findAll的limit參數為1時的特殊情況,因為根據其他參數設定的條件返回的,是滿足條件的所有標簽下內容按順序排列的一個序列,limit設置的值即控制了最終留下前多少個結果

keyword:這個參數的用法不是對keyword賦值,而是將你感興趣的標簽內屬性聲明項,如name="keywords"這樣的,在findAll中附加上

下面還是基於之前舉例子的那篇新聞網頁,對findAll進行演示:

單個標題標簽內容的粗略提取:

from urllib.request import urlopen
from bs4 import BeautifulSoup

html =urlopen( 'http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

obj = BeautifulSoup(html,'lxml')

'''獲取標簽為<p>的內容'''
text = obj.findAll('p')

print(text)

 

 

運行結果:

多個標簽內容的捆綁提取:

from urllib.request import urlopen
from bs4 import BeautifulSoup

html =urlopen( 'http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

obj = BeautifulSoup(html,'lxml')

'''保存多個標題標簽的列表'''
tag = ['title','meta']

'''獲取tag中標簽的內容'''
text = obj.findAll(tag)

print(text)

運行結果:

對指定標簽下指定屬性值對應內容的提取:

from urllib.request import urlopen
from bs4 import BeautifulSoup

html =urlopen( 'http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

obj = BeautifulSoup(html,'lxml')

'''獲取meta標簽下屬性name為author的對應內容'''
text = obj.findAll('meta',{'name':'author'})

print(text)

運行結果:

 

六、正則表達式

  即使你之前完全沒有接觸過網絡爬蟲,也可能接觸過正則表達式(regular expression,簡稱regex),之所以叫正則表達式,是因為它們可以識別正則字符串(regular string),通俗的理解就是,我只識別我編寫的正則表達式所匹配的內容,而忽視不符合我的表達式所構造的規則的字符串,這在很多方面都十分的方便;

  正則字符串是任意可以用一系列線性規則構成的字符串,例如:

  1、字母“a”至少出現一次;

  2、后面接上重復5次的“b”;

  3、后面再接上重復任意偶數次的字母“c”;

  4、最后一位字母是“d”或沒有。

滿足上述組合條件的字符串有無數個,如“aaabbbbbccccd”,“abbbbbcc”等,相信你應該理解了,正則表達式就是用一個對於目標語句的格式普適的規則,來識別目標內容。

  你可以將正則表達式理解為SQL中的LIKE運算符后跟着的通配符,還是以上面介紹過的組合條件為例,用正則表達式來表示:

aa*bbbbb(cc)*(d|)

  首先,開頭的a表示a出現一次,a*表示a出現任意次,因此aa*的組合代表a至少出現一次;bbbbb表示連續出現5次b;(cc)*表示cc出現任意次,對應重復任意次(包括0次)的c;(d|)表示出現d或無任何字符,對應“最后一位是字母d或沒有”,這樣一個由若干規則按順序組合起來的字符串,就是正則字符串;

*有很多網站可以在線測試你的正則表達式,我喜歡用的是http://regexpal.com.s3-website-us-east-1.amazonaws.com/?_ga=2.164205119.1679442026.1514793856-2027450969.1514793856

  再舉一個更常見的正則表達式使用場景——識別郵箱,以我個人的郵箱為例:pengzyill@foxmail.com,這是個常見的郵箱格式,若要編寫正則表達式來識別它,就會按順序用到以下識別規則:

  1、郵箱的第一部分至少包括一種內容:大寫字母、小寫字母、數字0-9、點號.、加號+或下划線_,因此為了識別這一部分,我們構造的正則字符串如下:

[A-Za-z0-9\.+_]+

 []中放入的內容是所有可能出現的內容的最簡形式,A-Z表示所有大寫字母,a-z表示所有小寫字母,0-9表示所有數字,\.表示點號.(這里用\轉義),+表示加號,_表示下划線,[]后緊跟的+表示前面[]內的所有部件可以出現多次,且至少有一種部件至少出現1次,可以看出,非常簡潔;

  2、緊跟着,會出現一個@符號,很簡單,對應的正則字符串為:

@

  3、在@之后,是指明郵箱所屬域名的部分,由大小寫字母組成,如我的郵箱中的foxmail,於是對應的正則字符串為:

[A-Za-z]+

  4、緊跟着是一個點號,即:

\.

  5、最后一部分,是郵箱地址的頂級域名,如com,org,edu或net等,這是四種最常見的,因此以這四種作為全部(雖然有些以偏概全),對應的正則字符串如下:

(com|org|edu|net)

將上述的子正則字符串按照順序連接起來,便得到了我們的用於識別郵箱地址的正則字符串:

[A-Za-z0-9\.+_]+@[A-Za-z]+\.(com|org|edu|net)

 我們在前面提到的在線測試網站中測試一下~

可以看出,我的郵箱地址被准確的識別出來(完全被黃色底紋包裹),你也可以試試你自己的郵箱地址;所以,在使用正則表達式之前,最好分塊的理清楚各個部分需要對應的正則字符串,這對提高效率很有幫助。

  下面用一些簡單的說明和例子來總結一下正則表達式中的常用符號:

符號 含義 例子 匹配結果
* 匹配前面的單個字符、子表達式或括號里的所有字符0次或多次 a*(bb)* aaaa  aabbbb
+ 匹配前面的字符、子表達式或括號里的所有字符至少1次 a+b+ ab  aabbb
[] 匹配括號中任意一個字符(配合*實現多次出現的匹配) [A-Z]* LOVE  PEACE
() 表達式編組(類似數學運算,()里的規則會優先運行) (a*b)* aabab  abababab
{m,n} 匹配前面的字符、子表達式或括號里的字符m到n次(包含m或n) a{2,3}b{2,3} aabbb  aaabb
[^] 匹配任意一個不在中括號里的字符 [^A-Z]* apple  love%++
| 匹配任意一個由豎線|分割的字符、子表達式 b(a|i|e)d bad  bid  bed
. 匹配任意單個字符(包括符號、數字和空格等) b.d bed  b?d  bod
^ 表示以某個字符或子表達式開頭的字符串 ^a adshdjsh  a?di
\ 轉義字符(把有特殊含義的字符轉換成字面形式,譬如本表中的一些常用符號) \.\|\\ .|\
$ 常用於正則表達式的末尾,表示“從字符串的末端匹配”,如果不使用它,每個正則表達式實際上都相當於外套一個.*,默認從字符串開頭進行匹配。可以將這個符號視為^的反義詞 [A-Z]*[a-z]*$ ABCabc
?! 表示“不包含”,這個符號通常放在字符或正則表達式前面,表示指定字符不可以出現在目標字符串中,若字符在字符串的不規則部位出現,則需要在整個字符串中排除某個字符,就需要加上^與$符號 ^((?![A-Z]).)*$ nojoasdn-\

 

 七、正則表達式與BeautifulSoup

  基於前面介紹的正則表達式,下面我們來介紹如何將正則表達式與BeautifulSoup結合起來:

  這里要使用到一個新的模塊——re,這時Python中專門進行正則表達式相關操作的模塊,為了與BeautifulSoup結合起來,我們需要進行的操作是將re.compile('正則表達式內容')作為findAll內適配參數的輸入值,即可將以前確切賦參的方法,轉換為利用正則表達式進行模式賦參,這大大提高了findAll對網頁內容提取的自由度和效率,下面是幾個簡單的例子:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html =urlopen( 'http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

obj = BeautifulSoup(html,'lxml')

'''匹配meta標簽下,name屬性值為k開頭,緊跟着任意數目小寫字母'''
text = obj.findAll('meta',{'name':re.compile('k[a-z]*')})

print(text)

運行結果:

接下來我們來實現更復雜一些的數據爬取,我在本篇博客中反復舉例的網頁是一篇關於台球的新聞報道,那么我們最關注的信息就應該是新聞的正文內容,下面我們就將針對此目的進行數據的爬取:

  通過對網頁源代碼的觀察后,確定了新聞內容屬於標簽p下,因此利用正則表達式配合findAll爬取這部分內容,這里.*?表示匹配所有類型任意出現次數的字符:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

html =urlopen( 'http://sports.163.com/18/0504/10/DGV2STDA00058782.html')

obj = BeautifulSoup(html,'lxml')

'''匹配p標簽下的內容'''
text = obj.findAll('p',text=re.compile('.*?'))

'''打印未經處理的內容'''
print(text)

運行結果:

雖然將全部新聞內容爬取了下來,但其中參雜着許多<>包裹的標簽內容,下面我們利用re.sub來對這些無關內容進行處理:

'''將爬下來的粗略內容轉為字符串形式'''
text = str(text)

'''利用re.sub將所有的<>及內部信息替換為空字符,等價於將這些干擾部分刪去'''
print(re.sub('<.*?>','',text))

運行結果:

相信你此時一定在驚嘆re這個模塊的功能之強大,接下來的一篇博客,我就將詳細介紹re模塊的常見功能和特性;

 

  以上就是關於Python網絡爬蟲的初級知識,今后會繼續更進階的介紹,敬請期待。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM