1 前言
作為一名合格的資料分析師,其完整的技術知識體系必須貫穿資料獲取、資料儲存、資料提取、資料分析、資料探勘、資料視覺化等各大部分。在此作為初出茅廬的資料小白,我將會把自己學習資料科學過程中遇到的一些問題記錄下來,以便後續的查閱,同時也希望與各路同學一起交流、一起進步。剛好前段時間學習了Python網路爬蟲,在此將網路爬蟲做一個總結。
2 何為網路爬蟲?
2.1 爬蟲場景
我們先自己想象一下平時到天貓商城購物(PC端)的步驟,可能就是開啟瀏覽器==》搜尋天貓商城==》點選連結進入天貓商城==》選擇所需商品類目(站內搜尋)==》瀏覽商品(價格、詳情引數、評論等)==》點選連結==》進入下一個商品頁面,……這樣子周而復始。當然這其中的搜尋也是爬蟲的應用之一。簡單講,網路爬蟲是類似又區別於上述場景的一種程式。
2.2 爬蟲分類
- 分類與關係
一般最常用的爬蟲型別主要有通用爬蟲和聚焦爬蟲,其中聚焦爬蟲又分為淺聚焦與深聚焦,三者關係如下圖: - 區別
通用爬蟲與聚焦爬蟲的區別就在有沒有對資訊進行過濾以儘量保證只抓取與主題相關的網頁資訊。 - 聚焦爬蟲過濾方法
- 淺聚焦爬蟲
選取符合目標主題的種子URL,例如我們定義抓取的資訊為招聘資訊,我們便可將招聘網站的URL(拉勾網、大街網等)作為種子URL,這樣便保證了抓取內容與我們定義的主題的一致性。 - 深聚焦爬蟲
一般有兩種,一是針對內容二是針對URL。其中針對內容的如頁面中絕大部分超連結都是帶有錨文字的,我們可以根據錨文字進行篩選;針對URL的如現有連結http://geek.csdn.net/news/detail/126572 ,該連結便向我們透漏主題是新聞(news)。
- 淺聚焦爬蟲
2.3 爬蟲原理
總的來說,爬蟲就是從種子URL開始,通過 HTTP 請求獲取頁面內容,並從頁面內容中通過各種技術手段解析出更多的 URL,遞迴地請求獲取頁面的程式網路爬蟲,總結其主要原理如下圖(其中紅色為聚焦爬蟲相對通用爬蟲所需額外進行步驟):
當然,如果對於網路爬蟲原理細節有興趣的同學可參考一下兩篇博文:
網路爬蟲基本原理(一)
網路爬蟲基本原理(二)
2.4 爬蟲應用
網路爬蟲可以做的事情很多,如以下列出:
- 搜尋引擎
- 採集資料(金融、商品、競品等)
- 廣告過濾
- ……
其實就我們個人興趣,學完爬蟲我們可以看看噹噹網上哪種技術圖書賣得比較火(銷量、評論等資訊)、看某個線上教育網站哪門網路課程做得比較成功、看雙十一天貓的活動情況等等,只要我們感興趣的資料,一般的話都可以爬取得到,不過有些網站比較狡猾,設定各種各樣的反扒機制。總而言之,網路爬蟲可以幫助我們做很多有趣的事情。
3 網路爬蟲基礎
個人建議本章除3.3以外,其他內容可以大致先看一下,有些許印象即可,等到後面已經完成一些簡單爬蟲後或者在寫爬蟲過程中遇到一些問題再回頭來鞏固一下,這樣子或許更有助於我們進一步網路理解爬蟲。
3.1 HTTP協議
HTTP 協議是爬蟲的基礎,通過封裝 TCP/IP 協議連結,簡化了網路請求的流程,使得使用者不需要關注三次握手,丟包超時等底層互動。
關於HTTP協議可以參考一下博文:
3.2 前端技術
作為新手,個人覺得入門的話懂一點HTML與JavaScript就可以實現基本的爬蟲專案,HTML主要協助我們處理靜態頁面,而實際上很多資料並不是我們簡單的右擊檢視網頁原始碼便可以看到的,而是存在JSON(JavaScript Object Notation)檔案中,這時我們便需要採取抓包分析,詳見《5.2 爬取基於Ajax技術網頁資料》。
3.3 正規表示式與XPath
做爬蟲必不可少的步驟便是做解析。正規表示式是文字匹配提取的利器,並且被各種語言支援。XPath即為XML路徑語言,類似Windows的檔案路徑,區別就在XPath是應用在網頁頁面中來定位我們所需內容的精確位置。具體用法參考以下資料:
4 網路爬蟲常見問題
4.1爬蟲利器——python
Python 是一種十分便利的指令碼語言,廣泛被應用在各種爬蟲框架。Python提供瞭如urllib、re、json、pyquery等模組,同時前人又利用Python造了許許多多的輪,如Scrapy框架、PySpider爬蟲系統等,所以做爬蟲Python是一大利器。
- 說明:本章開發環境細節如下
- 系統環境:windows 8.1
- 開發語言:Python3.5
- 開發工具:Spyder、Pycharm
- 輔助工具:Chrome瀏覽器
4.2 編碼格式
Python3中,只有Unicode編碼的為str,其他編碼格式如gbk,utf-8,gb2312等都為bytes,在編解碼過程中位元組bytes通過解碼方法decode()解碼為字串str,然後字串str通過編碼方法encode()編碼為位元組bytes,關係如下圖:
實戰——爬取噹噹網
爬取網頁
1 2 3 4 5 |
In [5]:import urllib.request ...:data = urllib.request.urlopen("http://www.dangdang.com/").read() #爬取的data中的<title>標籤中的內容如下: <title>\xb5\xb1\xb5\xb1\xa1\xaa\xcd\xf8\xc9\xcf\xb9\xba\xce\xef\xd6\xd0\xd0\xc4\xa3\xba\xcd\xbc\xca\xe9\xa1\xa2\xc4\xb8\xd3\xa4\xa1\xa2\xc3\xc0\xd7\xb1\xa1\xa2\xbc\xd2\xbe\xd3\xa1\xa2\xca\xfd\xc2\xeb\xa1\xa2\xbc\xd2\xb5\xe7\xa1\xa2\xb7\xfe\xd7\xb0\xa1\xa2\xd0\xac\xb0\xfc\xb5\xc8\xa3\xac\xd5\xfd\xc6\xb7\xb5\xcd\xbc\xdb\xa3\xac\xbb\xf5\xb5\xbd\xb8\xb6\xbf\xee</title> |
檢視編碼格式
1 2 3 |
In [5]:import chardet ...:chardet.detect(data) Out[5]: {'confidence': 0.99, 'encoding': 'GB2312'} |
可知爬取到的網頁是GB2312編碼,這是漢字的國標碼,專門用來表示漢字。
解碼
1 2 3 4 |
In [5]:decodeData = data.decode("gbk") #此時bytes已經解碼成str,<title>標籤內容解碼結果如下: <title>噹噹—網上購物中心:圖書、母嬰、美妝、家居、數碼、家電、服裝、鞋包等,正品低價,貨到付款</title> |
重編碼
1 2 3 4 |
dataEncode = decodeData.encode("utf-8","ignore") #重編碼結果 <title>\xe5\xbd\x93\xe5\xbd\x93\xe2\x80\x94\xe7\xbd\x91\xe4\xb8\x8a\xe8\xb4\xad\xe7\x89\xa9\xe4\xb8\xad\xe5\xbf\x83\xef\xbc\x9a\xe5\x9b\xbe\xe4\xb9\xa6\xe3\x80\x81\xe6\xaf\x8d\xe5\xa9\xb4\xe3\x80\x81\xe7\xbe\x8e\xe5\xa6\x86\xe3\x80\x81\xe5\xae\xb6\xe5\xb1\x85\xe3\x80\x81\xe6\x95\xb0\xe7\xa0\x81\xe3\x80\x81\xe5\xae\xb6\xe7\x94\xb5\xe3\x80\x81\xe6\x9c\x8d\xe8\xa3\x85\xe3\x80\x81\xe9\x9e\x8b\xe5\x8c\x85\xe7\xad\x89\xef\xbc\x8c\xe6\xad\xa3\xe5\x93\x81\xe4\xbd\x8e\xe4\xbb\xb7\xef\xbc\x8c\xe8\xb4\xa7\xe5\x88\xb0\xe4\xbb\x98\xe6\xac\xbe</title> |
4.3 超時設定
- 允許超時
1data = urllib.request.urlopen(“http://www.dangdang.com/”,timeout=3).read() - 執行緒推遲(單位為秒)
12import timetime.sleep(3)
4.4 異常處理
每個程式都不可避免地要進行異常處理,爬蟲也不例外,假如不進行異常處理,可能導致爬蟲程式直接崩掉。
4.4.1 網路爬蟲中處理異常的種類與關係
- URLError
通常,URLError在沒有網路連線(沒有路由到特定伺服器),或者伺服器不存在的情況下產生。 - HTTPError首先我們要明白伺服器上每一個HTTP 應答物件response都包含一個數字“狀態碼”,該狀態碼錶示HTTP協議所返回的響應的狀態,這就是HTTPError。比如當產生“404 Not Found”的時候,便表示“沒有找到對應頁面”,可能是輸錯了URL地址,也可能IP被該網站遮蔽了,這時便要使用代理IP進行爬取資料,關於代理IP的設定我們下面會講到。
- 兩者關係
兩者是父類與子類的關係,即HTTPError是URLError的子類,HTTPError有異常狀態碼與異常原因,URLError沒有異常狀態碼。所以,我們在處理的時候,不能使用URLError直接代替HTTPError。同時,Python中所有異常都是基類Exception的成員,所有異常都從此基類繼承,而且都在exceptions模組中定義。如果要代替,必須要判斷是否有狀態碼屬性。
4.4.2 Python中有一套異常處理機制語法
- try-except語句
123456try:blockexcept Exception as e:blockelse:block- try 語句:捕獲異常
- except語句:處理不同的異常,Exception是異常的種類,在爬蟲中常見如上文所述。
- e:異常的資訊,可供後面列印輸出
- else: 表示若沒有發生異常,當try執行完畢之後,就會執行else
- try-except-finally語句
123456try:blockexcept Exception as e:blockfinally:block
假如try沒有捕獲到錯誤資訊,則直接跳過except語句轉而執行finally語句,其實無論是否捕獲到異常都會執行finally語句,因此一般我們都會將一些釋放資源的工作放到該步中,如關閉檔案控制程式碼或者關閉資料庫連線等。
4.4.3 實戰——爬取CSDN部落格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#(1)可捕獲所有異常型別 import urllib.request import urllib.error import traceback import sys try: urllib.request.urlopen("http://blog.csdn.net") except Exception as er1: print("異常概要:") print(er1) print("---------------------------") errorInfo = sys.exc_info() print("異常型別:"+str(errorInfo[0])) print("異常資訊或引數:"+str(errorInfo[1])) print("呼叫棧資訊的物件:"+str(errorInfo[2])) print("已從堆疊中“輾轉開解”的函式有關的資訊:"+str(traceback.print_exc())) #-------------------------------------------------- #(2)捕獲URLError import urllib.request import urllib.error try: urllib.request.urlopen("http://blog.csdn.net") except urllib.error.URLError as er2: if hasattr(er2,"code"): print("URLError異常程式碼:") print(er2.code) if hasattr(er2,"reason"): print("URLError異常原因:") print(er2.reason) #-------------------------------------------------- #(3)捕獲HTTPError import urllib.request import urllib.error try: urllib.request.urlopen("http://blog.csdn.net") except urllib.error. HTTPError as er3: print("HTTPError異常概要:") print(er3) |
Exception異常捕獲輸出結果如下:
1 2 3 4 5 6 7 |
...: 異常概要: HTTP Error 403: Forbidden 異常型別:<class 'urllib.error.HTTPError'> 異常資訊或引數:HTTP Error 403: Forbidden 呼叫棧資訊的物件:<traceback object at 0x00000089E1507E08> 已從堆疊中“輾轉開解”的函式有關的資訊:None |
4.5 自動模擬HTTP請求
一般客戶端需要通過HTTP請求才能與服務端進行通訊,常見的HTTP請求有POST與GET兩種。例如我們開啟淘寶網頁後一旦HTML載入完成,瀏覽器將會傳送GET請求去獲取圖片等,這樣子我們才能看到一個完整的動態頁面,假如我們瀏覽後需要下單那麼還需要向伺服器傳遞登入資訊。
- GET方式
向伺服器發索取資料的一種請求,將請求資料融入到URL之中,資料在URL中可以看到。 - POST方式向伺服器提交資料的一種請求,將資料放置在HTML HEADER內提交。從安全性講,POST方式相對於GET方式較為安全,畢竟GET方式是直接將請求資料以明文的形式展現在URL中。
- 實戰——登入CSDN/百度搜尋簡書
12345678910111213141516171819202122232425262728293031import urllib.requestimport urllib.parsedef postData():'''1_POST方式登入CSDN'''values={}values['username'] = "xxx@qq.com" #賬號values['password']="xxx" #密碼info = urllib.parse.urlencode(values).encode("utf-8")url = "http://passport.csdn.net/account/login"try:req = urllib.request.Request(url,info)data = urllib.request.urlopen(req).read()except Exception as er:print("異常概要:")print(er)return datadef getData():'''2_GET方式搜尋簡書'''keyword = "簡書" #搜尋關鍵詞keyword = urllib.request.quote(keyword)#編碼url = "http://www.baidu.com/s?wd="+keywordtry:req = urllib.request.Request(url)data = urllib.request.urlopen(req).read()except Exception as er:print("異常概要:")print(er)return dataif __name__=="__main__":print(postData())print(getData())
4.6 cookies處理
cookies是某些網站為了辨別使用者身份、進行session跟蹤而儲存在使用者本地終端上的資料(通常經過加密)。
參考:零基礎自學用Python 3開發網路爬蟲(四): 登入
4.7 瀏覽器偽裝
- 原理
瀏覽器偽裝是防遮蔽的方法之一,簡言之,其原理就是在客戶端在向服務端傳送的請求中新增報頭資訊,告訴伺服器“我是瀏覽器” - 如何檢視客戶端資訊?
通過Chrome瀏覽器按F12==》選擇Network==》重新整理後點選Name下任一個地址,便可以看到請求報文和相應報文資訊。以下是在百度上搜尋簡書的請求報文資訊,在爬蟲中我們只需新增報頭中的User-Agent便可實現瀏覽器偽裝。 - 實戰——爬取CSDN部落格
在上面的例項中我們已知道對CSDN部落格直接進行爬取的時候會返回403錯誤,接下來將我們偽裝成瀏覽器爬取CSDN部落格
12345678910111213141516'''瀏覽器偽裝'''import urllib.requesturl = "http://blog.csdn.net/"headers=("User-Agent","Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36")opener = urllib.request.build_opener() #自定義openeropener.addheaders = [headers] #新增客戶端資訊#urllib.request.install_opener(opener) #如解除註釋,則可以使用方法2try:data = opener.open(url,timeout=10).read() #開啟方法1#data=urllib.request.urlopen(url).read() #開啟方法2except Exception as er:print("爬取的時候發生錯誤,具體如下:")print(er)f = open("F:/spider_ret/csdnTest.html","wb") #建立本地HTML檔案f.write(data) #將首頁內容寫入檔案中f.close()
4.8 代理伺服器
- 原理
代理伺服器原理如下圖,利用代理伺服器可以很好處理IP限制問題。個人認為IP限制這一點對爬蟲的影響是很大的,畢竟我們一般不會花錢去購買正規的代理IP,我們一般都是利用網際網路上提供的一些免費代理IP進行爬取,而這些免費IP的質量殘次不齊,出錯是在所難免的,所以在使用之前我們要對其進行有效性測試。另外,對開源IP池有興趣的同學可以學習Github上的開源專案:IPProxyPool。 - 實戰——代理伺服器爬取百度首頁
12345678910111213141516171819202122232425import urllib.requestdef use_proxy(url,proxy_addr,iHeaders,timeoutSec):'''功能:偽裝成瀏覽器並使用代理IP防遮蔽@url:目標URL@proxy_addr:代理IP地址@iHeaders:瀏覽器頭資訊@timeoutSec:超時設定(單位:秒)'''proxy = urllib.request.ProxyHandler({"http":proxy_addr})opener = urllib.request.build_opener(proxy,urllib.request.HTTPHandler)urllib.request.install_opener(opener)try:req = urllib.request.Request(url,headers = iHeaders) #偽裝為瀏覽器並封裝requestdata = urllib.request.urlopen(req).read().decode("utf-8","ignore")except Exception as er:print("爬取時發生錯誤,具體如下:")print(er)return dataurl = "http://www.baidu.com"proxy_addr = "125.94.0.253:8080"iHeaders = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0"}timeoutSec = 10data = use_proxy(url,proxy_addr,iHeaders,timeoutSec)print(len(data))
4.9 抓包分析
- Ajax(非同步載入)的技術
網站中使用者需求的資料如聯絡人列表,可以從獨立於實際網頁的服務端取得並且可以被動態地寫入網頁中。簡單講就是開啟網頁,先展現部分內容,再慢慢載入剩下的內容。顯然,這樣的網頁因為不用一次載入全部內容其載入速度特別快,但對於我們爬蟲的話就比較麻煩了,我們總爬不到我們想要的內容,這時候就需要進行抓包分析。 - 抓包工具
推薦Fiddler與Chrome瀏覽器 - 實戰
請轉《5.2 爬取基於Ajax技術網頁資料》。
4.10多執行緒爬蟲
一般我們程式是單執行緒執行,但多執行緒可以充分利用資源,優化爬蟲效率。實際上Python 中的多執行緒並行化並不是真正的並行化,但是多執行緒在一定程度上還是能提高爬蟲的執行效率,下面我們就針對單執行緒和多執行緒進行時間上的比較。
- 實戰——爬取豆瓣科幻電影網頁
123456789101112131415161718192021222324252627282930313233343536373839404142434445'''多執行緒'''import urllibfrom multiprocessing.dummy import Poolimport timedef getResponse(url):'''獲取響應資訊'''try:req = urllib.request.Request(url)res = urllib.request.urlopen(req)except Exception as er:print("爬取時發生錯誤,具體如下:")print(er)return resdef getURLs():'''獲取所需爬取的所有URL'''urls = []for i in range(0, 101,20):#每翻一頁其start值增加20keyword = "科幻"keyword = urllib.request.quote(keyword)newpage = "https://movie.douban.com/tag/"+keyword+"?start="+str(i)+"&type=T"urls.append(newpage)return urlsdef singleTime(urls):'''單程式計時'''time1 = time.time()for i in urls:print(i)getResponse(i)time2 = time.time()return str(time2 - time1)def multiTime(urls):'''多程式計時'''pool = Pool(processes=4) #開啟四個程式time3 = time.time()pool.map(getResponse,urls)pool.close()pool.join() #等待程式池中的worker程式執行完畢time4 = time.time()return str(time4 - time3)if __name__ == '__main__':urls = getURLs()singleTimes = singleTime(urls) #單執行緒計時multiTimes = multiTime(urls) #多執行緒計時print('單執行緒耗時 : ' + singleTimes + ' s')print('多執行緒耗時 : ' + multiTimes + ' s') - 結果:
12單執行緒耗時 : 3.850554943084717 s多執行緒耗時 : 1.3288819789886475 s - 更多詳情請參考:
Python 並行任務技巧
Python中的多程式處理
4.11 資料儲存
- 本地檔案(excel、txt)
- 資料庫(如MySQL)
備註:具體實戰請看5.1
4.12 驗證碼處理
在登入過程中我們常遇到驗證碼問題,此時我們有必要對其進行處理。
- 簡單驗證碼識別
利用pytesser識別簡單圖形驗證碼,有興趣的童鞋,請參考Python驗證碼識別。 - 複雜驗證碼識別
這相對有難度,可以呼叫第三方介面(如打碼兔)、利用資料探勘演算法如SVM,有興趣參考字元型圖片驗證碼識別完整過程及Python實現
5 綜合實戰案例
5.1 爬取靜態網頁資料
(1)需求
爬取豆瓣網出版社名字並分別儲存到excel、txt與MySQL資料庫中。
(2)分析
- 檢視原始碼
- Ctrl+F搜尋任意出版社名字,如博集天卷
- 確定正則模式
1"<div class="name">(.*?)</div>"
(3)思路
- 下載目標頁面
- 正則匹配目標內容
- Python列表儲存
- 寫入Excel/txt/MySQL
(4)原始碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
'''資訊儲存''' import urllib import re import xlsxwriter import MySQLdb #-----------------(1)儲存到excel與txt-------------------------# def gxls_concent(target_url,pat): ''' 功能:爬取資料 @target_url:爬取目標網址 @pat:資料過濾模式 ''' data = urllib.request.urlopen(target_url).read() ret_concent = re.compile(pat).findall(str(data,'utf-8')) return ret_concent def wxls_concent(ret_xls,ret_concent): ''' 功能:將最終結果寫入douban.xls中 @ret_xls:最終結果儲存excel表的路徑 @ret_concent:爬取資料結果列表 ''' # 開啟最終寫入的檔案 wb1 = xlsxwriter.Workbook(ret_xls) # 建立一個sheet工作物件 ws = wb1.add_worksheet() try: for i in range(len(ret_concent)): data = ret_concent[i] ws.write(i,0,data) wb1.close() except Exception as er: print('寫入“'+ret_xls+'”檔案時出現錯誤') print(er) def wtxt_concent(ret_txt,ret_concent): ''' 功能:將最終結果寫入douban.txt中 @ret_xls:最終結果儲存excel表的路徑 @ret_concent:爬取資料結果列表 ''' fh = open(ret_txt,"wb") try: for i in range(len(ret_concent)): data = ret_concent[i] data = data+"\r\n" data = data.encode() fh.write(data) except Exception as er: print('寫入“'+ret_txt+'”檔案時出現錯誤') print(er) fh.close() def mainXlsTxt(): ''' 功能:將資料儲存到excel表中 ''' target_url = 'https://read.douban.com/provider/all' # 爬取目標網址 pat = '<div class="name">(.*?)</div>' # 爬取模式 ret_xls = "F:/spider_ret/douban.xls" # excel檔案路徑 ret_txt = "F:/spider_ret/douban.txt" # txt檔案路徑 ret_concent = gxls_concent(target_url,pat) # 獲取資料 wxls_concent(ret_xls,ret_concent) # 寫入excel表 wtxt_concent(ret_txt,ret_concent) # 寫入txt檔案 #---------------------END(1)--------------------------------# #-------------------(2)儲存到MySQL---------------------------# def db_con(): ''' 功能:連線MySQL資料庫 ''' con = MySQLdb.connect( host='localhost', # port user='root', # usr_name passwd='xxxx', # passname db='urllib_data', # db_name charset='utf8', local_infile = 1 ) return con def exeSQL(sql): ''' 功能:資料庫查詢函式 @sql:定義SQL語句 ''' print("exeSQL: " + sql) #連線資料庫 con = db_con() con.query(sql) def gdb_concent(target_url,pat): ''' 功能:轉換爬取資料為插入資料庫格式:[[value_1],[value_2],...,[value_n]] @target_url:爬取目標網址 @pat:資料過濾模式 ''' tmp_concent = gxls_concent(target_url,pat) ret_concent = [] for i in range(len(tmp_concent)): ret_concent.append([tmp_concent[i]]) return ret_concent def wdb_concent(tbl_name,ret_concent): ''' 功能:將爬取結果寫入MySQL資料庫中 @tbl_name:資料表名 @ret_concent:爬取資料結果列表 ''' exeSQL("drop table if exists " + tbl_name) exeSQL("create table " + tbl_name + "(pro_name VARCHAR(100));") insert_sql = "insert into " + tbl_name + " values(%s);" con = db_con() cursor = con.cursor() try: cursor.executemany(insert_sql,ret_concent) except Exception as er: print('執行MySQL:"' + str(insert_sql) + '"時出錯') print(er) finally: cursor.close() con.commit() con.close() def mainDb(): ''' 功能:將資料儲存到MySQL資料庫中 ''' target_url = 'https://read.douban.com/provider/all' # 爬取目標網址 pat = '<div class="name">(.*?)</div>' # 爬取模式 tbl_name = "provider" # 資料表名 # 獲取資料 ret_concent = gdb_concent(target_url,pat) # 寫入MySQL資料庫 wdb_concent(tbl_name,ret_concent) #---------------------END(2)--------------------------------# if __name__ == '__main__': mainXlsTxt() mainDb() |
(5)結果
5.2 爬取基於Ajax技術網頁資料
(1)需求
爬取拉勾網廣州的資料探勘崗位資訊並儲存到本地Excel檔案中
(2)分析
- 崗位資料在哪裡?
開啟拉勾網==》輸入關鍵詞“資料探勘”==》檢視原始碼==》沒發現崗位資訊
開啟拉勾網==》輸入關鍵詞“資料探勘”==》按F12==》Network重新整理==》按下圖操作我們可以發現存在position和company開頭的json檔案,這很可能就是我們所需要的崗位資訊,右擊選擇open link in new tab,可以發現其就是我們所需的內容。
- 如何實現翻頁?
我們在寫爬蟲的時候需要多頁爬取,自動模擬換頁操作。首先我們點選下一頁,可以看到url沒有改變,這也就是Ajax(非同步載入)的技術。點選position的json檔案,在右側點選Headers欄,可以發現最底部有如下內容:當我們換頁的時候pn則變為2且first變為false,故我們可以通過構造post表單進行爬取。
- Json資料結構怎麼樣?
(3)原始碼
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187import urllib.requestimport urllib.parseimport socketfrom multiprocessing.dummy import Poolimport jsonimport timeimport xlsxwriter#----------------------------------------------------------#######(1)獲取代理IP###def getProxies():'''功能:呼叫API獲取原始代理IP池'''url = "http://api.xicidaili.com/free2016.txt"i_headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0"}global proxy_addrproxy_addr = []try:req = urllib.request.Request(url,headers = i_headers)proxy = urllib.request.urlopen(req).read()proxy = proxy.decode('utf-8')proxy_addr = proxy.split('\r\n') #設定分隔符為換行符except Exception as er:print(er)return proxy_addrdef testProxy(curr_ip):'''功能:利用百度首頁,逐個驗證代理IP的有效性@curr_ip:當前被驗證的IP'''socket.setdefaulttimeout(5) #設定全域性超時時間tarURL = "https://www.baidu.com/" #測試網址proxy_ip = []try:proxy_support = urllib.request.ProxyHandler({"http":curr_ip})opener = urllib.request.build_opener(proxy_support)opener.addheaders=[("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0")]urllib.request.install_opener(opener)res = urllib.request.urlopen(tarURL).read()proxy_ip.append(curr_ip)print(len(res))except Exception as er:print("驗證代理IP("+curr_ip+")時發生錯誤:"+er)return proxy_ipdef mulTestProxies(proxies_ip):'''功能:構建多程式驗證所有代理IP@proxies_ip:代理IP池'''pool = Pool(processes=4) #開啟四個程式proxies_addr = pool.map(testProxy,proxies_ip)pool.close()pool.join() #等待程式池中的worker程式執行完畢return proxies_addr#----------------------------------------------------------#######(2)爬取資料###def getInfoDict(url,page,pos_words_one,proxy_addr_one):'''功能:獲取單頁職位資料,返回資料字典@url:目標URL@page:爬取第幾頁@pos_words_one:搜尋關鍵詞(單個)@proxy_addr_one:使用的代理IP(單個)'''global pos_dictpage = 1i_headers=("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0")proxy = urllib.request.ProxyHandler({"http":proxy_addr_one})opener = urllib.request.build_opener(proxy,urllib.request.HTTPHandler)opener.addheaders=[i_headers]urllib.request.install_opener(opener)if page==1:tORf = "true"else:tORf = "false"mydata = urllib.parse.urlencode({"first": tORf,"pn": page, #pn變化實現翻頁"kd": pos_words_one } ).encode("utf-8")try:req = urllib.request.Request(url,mydata)data=urllib.request.urlopen(req).read().decode("utf-8","ignore") #利用代理ip開啟pos_dict = json.loads(data) #將str轉成dictexcept urllib.error.URLError as er:if hasattr(er,"code"):print("獲取職位資訊json物件時發生URLError錯誤,錯誤程式碼:")print(er.code)if hasattr(er,"reason"):print("獲取職位資訊json物件時發生URLError錯誤,錯誤原因:")print(er.reason)return pos_dictdef getInfoList(pos_dict):'''功能:將getInfoDict()返回的資料字典轉換為資料列表@pos_dict:職位資訊資料字典'''pos_list = [] #職位資訊列表jcontent = pos_dict["content"]["positionResult"]["result"]for i in jcontent:one_info = [] #一個職位的相關資訊one_info.append(i["companyFullName"])one_info.append(i['companySize'])one_info.append(i['positionName'])one_info.append(i['education'])one_info.append(i['financeStage'])one_info.append(i['salary'])one_info.append(i['city'])one_info.append(i['district'])one_info.append(i['positionAdvantage'])one_info.append(i['workYear'])pos_list.append(one_info)return pos_listdef getPosInfo(pos_words,city_words,proxy_addr):'''功能:基於函式getInfoDict()與getInfoList(),迴圈遍歷每一頁獲取最終所有職位資訊列表@pos_words:職位關鍵詞(多個)@city_words:限制城市關鍵詞(多個)@proxy_addr:使用的代理IP池(多個)'''posInfo_result = []title = ['公司全名', '公司規模', '職位名稱', '教育程度', '融資情況', "薪資水平", "城市", "區域", "優勢", "工作經驗"]posInfo_result.append(title)for i in range(0,len(city_words)):#i = 0key_city = urllib.request.quote(city_words[i])#篩選關鍵詞設定:gj=應屆畢業生&xl=大專&jd=成長型&hy=移動網際網路&px=new&city=廣州url = "https://www.lagou.com/jobs/positionAjax.json?city="+key_city+"&needAddtionalResult=false"for j in range(0,len(pos_words)):#j = 0page=1while page<10: #每個關鍵詞搜尋拉鉤顯示30頁,在此只爬取10頁pos_words_one = pos_words[j]#k = 1proxy_addr_one = proxy_addr[page]#page += 1time.sleep(3)pos_info = getInfoDict(url,page,pos_words_one,proxy_addr_one) #獲取單頁資訊列表pos_infoList = getInfoList(pos_info)posInfo_result += pos_infoList #累加所有頁面資訊page += 1return posInfo_result#----------------------------------------------------------#######(3)儲存資料###def wXlsConcent(export_path,posInfo_result):'''功能:將最終結果寫入本地excel檔案中@export_path:匯出路徑@posInfo_result:爬取的資料列表'''# 開啟最終寫入的檔案wb1 = xlsxwriter.Workbook(export_path)# 建立一個sheet工作物件ws = wb1.add_worksheet()try:for i in range(0,len(posInfo_result)):for j in range(0,len(posInfo_result[i])):data = posInfo_result[i][j]ws.write(i,j,data)wb1.close()except Exception as er:print('寫入“'+export_path+'”檔案時出現錯誤:')print(er)#----------------------------------------------------------#######(4)定義main()函式###def main():'''功能:主函式,呼叫相關函式,最終輸出路徑(F:/spider_ret)下的positionInfo.xls檔案'''#---(1)獲取代理IP池proxies = getProxies() #獲取原始代理IPproxy_addr = mulTestProxies(proxies) #多執行緒測試原始代理IP#---(2)爬取資料search_key = ["資料探勘"] #設定職位關鍵詞(可以設定多個)city_word = ["廣州"] #設定搜尋地區(可以設定多個)posInfo_result = getPosInfo(search_key,city_word,proxy_addr) #爬取職位資訊#---(3)儲存資料export_path = "F:/spider_ret/positionInfo.xls" #設定匯出路徑wXlsConcent(export_path,posInfo_result) #寫入到excel中if __name__ == "__main__":main()
5.3 利用Scrapy框架爬取
5.3.1 瞭解Scrapy
Scrapy使用了Twisted非同步網路庫來處理網路通訊。整體架構大致如下(注:圖片來自網際網路):
詳情轉Scrapy:Python的爬蟲框架
關於Scrapy的使用方法請參考官方文件
5.3.2 Scrapy自動爬蟲
前面的實戰中我們都是通過迴圈構建URL進行資料爬取,其實還有另外一種實現方式,首先設定初始URL,獲取當前URL中的新連結,基於這些連結繼續爬取,直到所爬取的頁面不存在新的連結為止。
(1)需求
採用自動爬蟲的方式爬取糗事百科文章連結與內容,並將文章頭部內容與連結儲存到MySQL資料庫中。
(2)分析
- 怎麼提取首頁文章連結?
開啟首頁後檢視原始碼,搜尋首頁任一篇文章內容,可以看到”/article/118123230″連結,點選進去後發現這就是我們所要的文章內容,所以我們在自動爬蟲中需設定連結包含”article”
- 怎麼提取詳情頁文章內容與連結
- 內容
開啟詳情頁後,檢視文章內容如下:分析可知利用包含屬性class且其值為content的div標籤可唯一確定文章內容,表示式如下:
"//div[@class='content']/text()"
- 連結開啟任一詳情頁,複製詳情頁連結,檢視詳情頁原始碼,搜尋連結如下:
採用以下XPath表示式可提取文章連結。
["//link[@rel='canonical']/@href"]
- 內容
(3)專案原始碼
建立爬蟲專案
開啟CMD,切換到儲存爬蟲專案的目錄下,輸入:
scrapy startproject qsbkauto
- 專案結構說明
- spiders.qsbkspd.py:爬蟲檔案
- items.py:專案實體,要提取的內容的容器,如噹噹網商品的標題、評論數等
- pipelines.py:專案管道,主要用於資料的後續處理,如將資料寫入Excel和db等
- settings.py:專案設定,如預設是不開啟pipeline、遵守robots協議等
- scrapy.cfg:專案配置
建立爬蟲
進入建立的爬蟲專案,輸入:
scrapy genspider -t crawl qsbkspd qiushibaie=ke.com(域名)
定義items
1 2 3 4 5 6 7 |
import scrapy class QsbkautoItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() Link = scrapy.Field() #文章連結 Connent = scrapy.Field() #文章內容 pass |
編寫爬蟲
- qsbkauto.py
12345678910111213141516171819202122232425# -*- coding: utf-8 -*-import scrapyfrom scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Rulefrom qsbkauto.items import QsbkautoItemfrom scrapy.http import Requestclass QsbkspdSpider(CrawlSpider):name = 'qsbkspd'allowed_domains = ['qiushibaike.com']#start_urls = ['http://qiushibaike.com/']def start_requests(self):i_headers={"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0"}yield Request('http://www.qiushibaike.com/',headers=i_headers)rules = (Rule(LinkExtractor(allow=r'article/'), callback='parse_item', follow=True),)def parse_item(self, response):#i = {}#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()#i['name'] = response.xpath('//div[@id="name"]').extract()#i['description'] = response.xpath('//div[@id="description"]').extract()i = QsbkautoItem()i["content"]=response.xpath("//div[@class='content']/text()").extract()i["link"]=response.xpath("//link[@rel='canonical']/@href").extract()return i - pipelines.py
12345678910111213141516171819202122232425262728293031323334import MySQLdbimport timeclass QsbkautoPipeline(object):def exeSQL(self,sql):'''功能:連線MySQL資料庫並執行sql語句@sql:定義SQL語句'''con = MySQLdb.connect(host='localhost', # portuser='root', # usr_namepasswd='xxxx', # passnamedb='spdRet', # db_namecharset='utf8',local_infile = 1)con.query(sql)con.commit()con.close()def process_item(self, item, spider):link_url = item['link'][0]content_header = item['content'][0][0:10]curr_date = time.strftime('%Y-%m-%d',time.localtime(time.time()))content_header = curr_date+'__'+content_headerif (len(link_url) and len(content_header)):#判斷是否為空值try:sql="insert into qiushi(content,link) values('"+content_header+"','"+link_url+"')"self.exeSQL(sql)except Exception as er:print("插入錯誤,錯誤如下:")print(er)else:passreturn item - setting.py關閉ROBOTSTXT_OBEY
設定USER_AGENT
開啟ITEM_PIPELINES
執行爬蟲
1 |
scrapy crawl qsbkauto --nolog |
結果
參考:
[1] 天善社群韋瑋老師課程
[2] 文中所跳轉的URL
本文所有程式碼只用於技術交流,拒絕任何商用活動
文章相關專案程式碼已上傳至個人Github
後續的學習細節將會記錄在個人部落格whenif中,歡迎各路同學互相交流