Python網絡爬蟲二三事
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 協議鏈接,簡化了網絡請求的流程,使得用戶不需要關注三次握手,丟包超時等底層交互。
3.2 前端技術
作為新手,個人覺得入門的話懂一點HTML與JavaScript就可以實現基本的爬蟲項目,HTML主要協助我們處理靜態頁面,而實際上很多數據并不是我們簡單的右擊查看網頁源碼便可以看到的,而是存在JSON(JavaScript Object Notation)文件中,這時我們便需要采取抓包分析
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,關系如下圖:
實戰——爬取當當網
爬取網頁
In [5]:importurllib.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>
查看編碼格式
In [5]:import chardet
...:chardet.detect(data)
Out[5]: {'confidence': 0.99, 'encoding': 'GB2312'}
可知爬取到的網頁是GB2312編碼,這是漢字的國標碼,專門用來表示漢字。
解碼
In [5]:decodeData = data.decode("gbk")
#此時bytes已經解碼成str,<title>標簽內容解碼結果如下:
<title>當當—網上購物中心:圖書、母嬰、美妝、家居、數碼、家電、服裝、鞋包等,正品低價,貨到付款</title>
重編碼
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 超時設置
- 允許超時
data = urllib.request.urlopen(“http://www.dangdang.com/”,timeout=3).read()
- 線程推遲(單位為秒)
import time time.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語句
try: block except Exception as e: block else: block
- try 語句:捕獲異常
- except語句:處理不同的異常,Exception是異常的種類,在爬蟲中常見如上文所述。
- e:異常的信息,可供后面打印輸出
- else: 表示若沒有發生異常,當try執行完畢之后,就會執行else
- try-except-finally語句
try: block except Exception as e: block finally: block
4.4.3 實戰——爬取CSDN博客
#(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.URLErroras 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. HTTPErroras er3:
print("HTTPError異常概要:")
print(er3)
Exception異常捕獲輸出結果如下:
...:
異常概要:
HTTPError 403: Forbidden
異常類型:<class 'urllib.error.HTTPError'>
異常信息或參數:HTTPError 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/百度搜索簡書
import urllib.request import urllib.parse def 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 data def getData(): '''2_GET方式搜索簡書''' keyword = "簡書" #搜索關鍵詞 keyword = urllib.request.quote(keyword)#編碼 url = "http://www.baidu.com/s?wd="+keyword try: req = urllib.request.Request(url) data = urllib.request.urlopen(req).read() except Exception as er: print("異常概要:") print(er) return data if __name__=="__main__": print(postData()) print(getData())
4.6 cookies處理
cookies是某些網站為了辨別用戶身份、進行session跟蹤而儲存在用戶本地終端上的數據(通常經過加密)。
4.7 瀏覽器偽裝
- 原理
瀏覽器偽裝是防屏蔽的方法之一,簡言之,其原理就是在客戶端在向服務端發送的請求中添加報頭信息,告訴服務器“我是瀏覽器” - 如何查看客戶端信息?
通過Chrome瀏覽器按F12==》選擇Network==》刷新后點擊Name下任一個地址,便可以看到請求報文和相應報文信息。以下是在百度上搜索簡書的請求報文信息,在爬蟲中我們只需添加報頭中的User-Agent便可實現瀏覽器偽裝。 - 實戰——爬取CSDN博客
在上面的實例中我們已知道對CSDN博客直接進行爬取的時候會返回403錯誤,接下來將我們偽裝成瀏覽器爬取CSDN博客'''瀏覽器偽裝''' import urllib.request url = "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() #自定義opener opener.addheaders = [headers] #添加客戶端信息 #urllib.request.install_opener(opener) #如解除注釋,則可以使用方法2 try: data = opener.open(url,timeout=10).read() #打開方法1 #data=urllib.request.urlopen(url).read() #打開方法2 except 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的質量殘次不齊,出錯是在所難免的,所以在使用之前我們要對其進行有效性測試。
- 實戰——代理服務器爬取百度首頁
import urllib.request def 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) #偽裝為瀏覽器并封裝request data = urllib.request.urlopen(req).read().decode("utf-8","ignore") except Exception as er: print("爬取時發生錯誤,具體如下:") print(er) return data url = "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 = 10 data = use_proxy(url,proxy_addr,iHeaders,timeoutSec) print(len(data))
4.9 抓包分析
- Ajax(異步加載)的技術
網站中用戶需求的數據如聯系人列表,可以從獨立于實際網頁的服務端取得并且可以被動態地寫入網頁中。簡單講就是打開網頁,先展現部分內容,再慢慢加載剩下的內容。顯然,這樣的網頁因為不用一次加載全部內容其加載速度特別快,但對于我們爬蟲的話就比較麻煩了,我們總爬不到我們想要的內容,這時候就需要進行抓包分析。 - 抓包工具
推薦Fiddler與Chrome瀏覽器 - 實戰
請轉《5.2 爬取基于Ajax技術網頁數據》。
4.10多線程爬蟲
一般我們程序是單線程運行,但多線程可以充分利用資源,優化爬蟲效率。實際上Python 中的多線程并行化并不是真正的并行化,但是多線程在一定程度上還是能提高爬蟲的執行效率,下面我們就針對單線程和多線程進行時間上的比較。
- 實戰——爬取豆瓣科幻電影網頁
'''多線程''' import urllib from multiprocessing.dummy import Pool import time def getResponse(url): '''獲取響應信息''' try: req = urllib.request.Request(url) res = urllib.request.urlopen(req) except Exception as er: print("爬取時發生錯誤,具體如下:") print(er) return res def getURLs(): '''獲取所需爬取的所有URL''' urls = [] for i in range(0, 101,20):#每翻一頁其start值增加20 keyword = "科幻" keyword = urllib.request.quote(keyword) newpage = "https://movie.douban.com/tag/"+keyword+"?start="+str(i)+"&type=T" urls.append(newpage) return urls def 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')
- 結果:
單線程耗時 : 3.850554943084717 s 多線程耗時 : 1.3288819789886475 s
- 更多詳情請參考:
Python 并行任務技巧
Python中的多進程處理
4.11 數據存儲
- 本地文件(excel、txt)
- 數據庫(如MySQL)
備注:具體實戰請看5.1
4.12 驗證碼處理
在登錄過程中我們常遇到驗證碼問題,此時我們有必要對其進行處理。
- 簡單驗證碼識別
利用pytesser識別簡單圖形驗證碼,有興趣的童鞋, - 復雜驗證碼識別
這相對有難度,可以調用第三方接口(如打碼兔)、利用數據挖掘算法如SVM,
5 綜合實戰案例
5.1 爬取靜態網頁數據
(1)需求
爬取豆瓣網出版社名字并分別存儲到excel、txt與MySQL數據庫中。
(2)分析
- 查看源碼
- Ctrl+F搜索任意出版社名字,如博集天卷
- 確定正則模式
"<div class="name">(.*?)</div>"
(3)思路
- 下載目標頁面
- 正則匹配目標內容
- Python列表存儲
- 寫入Excel/txt/MySQL
(4)源碼
'''信息存儲'''
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)源碼
import urllib.request import urllib.parse import socket from multiprocessing.dummy import Pool import json import time import 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_addr proxy_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_addr def 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_ip def 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_dict page = 1 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") 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轉成dict except 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_dict def 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_list def 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 = 0 key_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 = 0 page=1 while page<10: #每個關鍵詞搜索拉鉤顯示30頁,在此只爬取10頁 pos_words_one = pos_words[j] #k = 1 proxy_addr_one = proxy_addr[page] #page += 1 time.sleep(3) pos_info = getInfoDict(url,page,pos_words_one,proxy_addr_one) #獲取單頁信息列表 pos_infoList = getInfoList(pos_info) posInfo_result += pos_infoList #累加所有頁面信息 page += 1 return 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() #獲取原始代理IP proxy_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異步網絡庫來處理網絡通訊。整體架構大致如下(注:圖片來自互聯網):
5.3.2 Scrapy自動爬蟲
前面的實戰中我們都是通過循環構建URL進行數據爬取,其實還有另外一種實現方式,首先設定初始URL,獲取當前URL中的新鏈接,基于這些鏈接繼續爬取,直到所爬取的頁面不存在新的鏈接為止。
(1)需求
采用自動爬蟲的方式爬取糗事百科文章鏈接與內容,并將文章頭部內容與鏈接存儲到MySQL數據庫中。
(2)分析
- 怎么提取首頁文章鏈接?
怎么提取首頁文章鏈接?
- 怎么提取詳情頁文章內容與鏈接
- 內容
打開詳情頁后,查看文章內容如下:分析可知利用包含屬性class且其值為content的div標簽可唯一確定文章內容,表達式如下:
"http://div[@class='content']/text()" - 鏈接 打開任一詳情頁,復制詳情頁鏈接,查看詳情頁源碼,搜索鏈接如下:
- 采用以下XPath表達式可提取文章鏈接。
["http://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
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
# -*- coding: utf-8 -*- import scrapy from scrapy.linkextractorsimport LinkExtractor from scrapy.spidersimport CrawlSpider, Rule from qsbkauto.itemsimport QsbkautoItem from scrapy.httpimport Request class 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("http://div[@class='content']/text()").extract() i["link"]=response.xpath("http://link[@rel='canonical']/@href").extract() return i
- pipelines.py
import MySQLdb import time class QsbkautoPipeline(object): def exeSQL(self,sql): ''' 功能:連接MySQL數據庫并執行sql語句 @sql:定義SQL語句 ''' con = MySQLdb.connect( host='localhost', # port user='root', # usr_name passwd='xxxx', # passname db='spdRet', # db_name charset='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_header if (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: pass return item
- setting.py 關閉ROBOTSTXT_OBEY
設置USER_AGENT
開啟ITEM_PIPELINES
執行爬蟲
scrapycrawlqsbkauto --nolog
結果
來自:http://python.jobbole.com/87234/