爬蟲(1)--- Python網絡爬蟲二三事

1 前言

<p>作為一名合格的數據分析師,其完整的技術知識體系必須貫穿數據獲取、數據存儲、數據提取、數據分析、數據挖掘、數據可視化等各大部分。在此作為初出茅廬的數據小白,我將會把自己學習數據科學過程中遇到的一些問題記錄下來,以便后續的查閱,同時也希望與各路同學一起交流、一起進步。剛好前段時間學習了Python網絡爬蟲,在此將網絡爬蟲做一個總結。

2 何為網絡爬蟲?

2.1 爬蟲場景

<p>我們先自己想象一下平時到天貓商城購物(PC端)的步驟,可能就是打開瀏覽器==》搜索天貓商城==》點擊鏈接進入天貓商城==》選擇所需商品類目(站內搜索)==》瀏覽商品(價格、詳情參數、評論等)==》點擊鏈接==》進入下一個商品頁面,......這樣子周而復始。當然這其中的搜索也是爬蟲的應用之一。<u>簡單講,網絡爬蟲是類似又區別于上述場景的一種程序。</u></p>

2.2 爬蟲分類

  • 分類與關系
    <p>一般最常用的爬蟲類型主要有通用爬蟲和聚焦爬蟲,其中聚焦爬蟲又分為淺聚焦與深聚焦,三者關系如下圖:
  • 區別
    <p>通用爬蟲與聚焦爬蟲的區別就在有沒有對信息進行過濾以盡量保證只抓取與主題相關的網頁信息。
  • 聚焦爬蟲過濾方法
  • 淺聚焦爬蟲
    選取符合目標主題的種子URL,例如我們定義抓取的信息為招聘信息,我們便可將招聘網站的URL(拉勾網、大街網等)作為種子URL,這樣便保證了抓取內容與我們定義的主題的一致性。
  • 深聚焦爬蟲
    一般有兩種,一是針對內容二是針對URL。其中針對內容的如頁面中絕大部分超鏈接都是帶有錨文本的,我們可以根據錨文本進行篩選;針對URL的如現有鏈接http://geek.csdn.net/news/detail/126572 ,該鏈接便向我們透漏主題是新聞(news)。

2.3 爬蟲原理

<p>總的來說,爬蟲就是從種子URL開始,通過 HTTP 請求獲取頁面內容,并從頁面內容中通過各種技術手段解析出更多的 URL,遞歸地請求獲取頁面的程序網絡爬蟲,總結其主要原理如下圖(其中紅色為聚焦爬蟲相對通用爬蟲所需額外進行步驟):


<p>當然,如果對于網絡爬蟲原理細節有興趣的同學可參考一下兩篇博文:
網絡爬蟲基本原理(一)
網絡爬蟲基本原理(二)

2.4 爬蟲應用

<p>網絡爬蟲可以做的事情很多,如以下列出:

  • 搜索引擎
  • 采集數據(金融、商品、競品等)
  • 廣告過濾
  • ……

<p>其實就我們個人興趣,學完爬蟲我們可以看看當當網上哪種技術圖書賣得比較火(銷量、評論等信息)、看某個在線教育網站哪門網絡課程做得比較成功、看雙十一天貓的活動情況等等,只要我們感興趣的數據,一般的話都可以爬取得到,不過有些網站比較狡猾,設置各種各樣的反扒機制。總而言之,網絡爬蟲可以幫助我們做很多有趣的事情。

3 網絡爬蟲基礎

<p>個人建議本章除3.3以外,其他內容可以大致先看一下,有些許印象即可,等到后面已經完成一些簡單爬蟲后或者在寫爬蟲過程中遇到一些問題再回頭來鞏固一下,這樣子或許更有助于我們進一步網絡理解爬蟲。

3.1 HTTP協議

HTTP 協議是爬蟲的基礎,通過封裝 TCP/IP 協議鏈接,簡化了網絡請求的流程,使得用戶不需要關注三次握手,丟包超時等底層交互。

<p>關于HTTP協議可以參考一下博文(推薦第一篇必讀):

3.2 前端技術

<p>作為新手,個人覺得入門的話懂一點HTML與JavaScript就可以實現基本的爬蟲項目,HTML主要協助我們處理靜態頁面,而實際上很多數據并不是我們簡單的右擊查看網頁源碼便可以看到的,而是存在JSON(JavaScript Object Notation)文件中,這時我們便需要采取抓包分析,詳見《5.2 爬取基于Ajax技術網頁數據》。

3.3 正則表達式與XPath

<p>做爬蟲必不可少的步驟便是做解析。正則表達式是文本匹配提取的利器,并且被各種語言支持。XPath即為XML路徑語言,類似Windows的文件路徑,區別就在XPath是應用在網頁頁面中來定位我們所需內容的精確位置。具體用法參考以下資料:

4 網絡爬蟲常見問題

4.1爬蟲利器——python

<p>Python 是一種十分便利的腳本語言,廣泛被應用在各種爬蟲框架。Python提供了如urllib、re、json、pyquery等模塊,同時前人又利用Python造了許許多多的輪,如Scrapy框架、PySpider爬蟲系統等,所以做爬蟲Python是一大利器。

  • 說明:本章開發環境細節如下
  • 系統環境:windows 8.1
  • 開發語言:Python3.5
  • 開發工具:Spyder、Pycharm
  • 輔助工具:Chrome瀏覽器

4.2 編碼格式

<p>Python3中,只有Unicode編碼的為str,其他編碼格式如gbk,utf-8,gb2312等都為bytes,在編解碼過程中字節bytes通過解碼方法decode()解碼為字符串str,然后字符串str通過編碼方法encode()編碼為字節bytes,關系如下圖:


實戰——爬取當當網

爬取網頁

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>

查看編碼格式

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 異常處理

<p>每個程序都不可避免地要進行異常處理,爬蟲也不例外,假如不進行異常處理,可能導致爬蟲程序直接崩掉。

4.4.1 網絡爬蟲中處理異常的種類與關系

  • URLError
    <p>通常,URLError在沒有網絡連接(沒有路由到特定服務器),或者服務器不存在的情況下產生。
  • ** HTTPError**
    <p>首先我們要明白服務器上每一個HTTP 應答對象response都包含一個數字“狀態碼”,該狀態碼表示HTTP協議所返回的響應的狀態,這就是HTTPError。比如當產生“404 Not Found”的時候,便表示“沒有找到對應頁面”,可能是輸錯了URL地址,也可能IP被該網站屏蔽了,這時便要使用代理IP進行爬取數據,關于代理IP的設定我們下面會講到。</p>
  • ** 兩者關系**
    <p>兩者是父類與子類的關系,即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

<p>假如try沒有捕獲到錯誤信息,則直接跳過except語句轉而執行finally語句,其實無論是否捕獲到異常都會執行finally語句,因此一般我們都會將一些釋放資源的工作放到該步中,如關閉文件句柄或者關閉數據庫連接等。

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.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異常捕獲輸出結果如下:

    ...:
異常概要:
HTTP Error 403: Forbidden
異常類型:<class 'urllib.error.HTTPError'>
異常信息或參數:HTTP Error 403: Forbidden
調用棧信息的對象:<traceback object at 0x00000089E1507E08>
已從堆棧中“輾轉開解”的函數有關的信息:None

4.5 自動模擬HTTP請求

<p>一般客戶端需要通過HTTP請求才能與服務端進行通信,常見的HTTP請求有POST與GET兩種。例如我們打開淘寶網頁后一旦HTML加載完成,瀏覽器將會發送GET請求去獲取圖片等,這樣子我們才能看到一個完整的動態頁面,假如我們瀏覽后需要下單那么還需要向服務器傳遞登錄信息。

  • GET方式
    <p>向服務器發索取數據的一種請求,將請求數據融入到URL之中,數據在URL中可以看到。

  • POST方式
    <p>向服務器提交數據的一種請求,將數據放置在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處理

<p>cookies是某些網站為了辨別用戶身份、進行session跟蹤而儲存在用戶本地終端上的數據(通常經過加密)。

參考:零基礎自學用Python 3開發網絡爬蟲(四): 登錄

4.7 瀏覽器偽裝

  • 原理
    <p>瀏覽器偽裝是防屏蔽的方法之一,簡言之,其原理就是在客戶端在向服務端發送的請求中添加報頭信息,告訴服務器“我是瀏覽器”
  • ** 如何查看客戶端信息?**
    <p>通過Chrome瀏覽器按F12==》選擇Network==》刷新后點擊Name下任一個地址,便可以看到請求報文和相應報文信息。以下是在百度上搜索簡書的請求報文信息,在爬蟲中我們只需添加報頭中的User-Agent便可實現瀏覽器偽裝。


  • 實戰——爬取CSDN博客
    <p>在上面的實例中我們已知道對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的質量殘次不齊,出錯是在所難免的,所以在使用之前我們要對其進行有效性測試。另外,對開源IP池有興趣的同學可以學習Github上的開源項目:IPProxyPool
  • 實戰——代理服務器爬取百度首頁
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多線程爬蟲

<p>一般我們程序是單線程運行,但多線程可以充分利用資源,優化爬蟲效率。實際上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

4.11 數據存儲

  • 本地文件(excel、txt)
  • 數據庫(如MySQL)

備注:具體實戰請看5.1

4.12 驗證碼處理

<p>在登錄過程中我們常遇到驗證碼問題,此時我們有必要對其進行處理。

5 綜合實戰案例

5.1 爬取靜態網頁數據

(1)需求

<p>爬取豆瓣網出版社名字并分別存儲到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)需求

<p>爬取拉勾網廣州的數據挖掘崗位信息并存儲到本地Excel文件中

(2)分析

  • 崗位數據在哪里?
    打開拉勾網==》輸入關鍵詞“數據挖掘”==》查看源碼==》沒發現崗位信息
    打開拉勾網==》輸入關鍵詞“數據挖掘”==》按F12==》Network刷新==》按下圖操作

    我們可以發現存在position和company開頭的json文件,這很可能就是我們所需要的崗位信息,右擊選擇open link in new tab,可以發現其就是我們所需的內容。
  • 如何實現翻頁?
    <p>我們在寫爬蟲的時候需要多頁爬取,自動模擬換頁操作。首先我們點擊下一頁,可以看到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

<p>Scrapy使用了Twisted異步網絡庫來處理網絡通訊。整體架構大致如下(注:圖片來自互聯網):


詳情轉Scrapy:Python的爬蟲框架
關于Scrapy的使用方法請參考官方文檔

5.3.2 Scrapy自動爬蟲

<p>前面的實戰中我們都是通過循環構建URL進行數據爬取,其實還有另外一種實現方式,首先設定初始URL,獲取當前URL中的新鏈接,基于這些鏈接繼續爬取,直到所爬取的頁面不存在新的鏈接為止。

(1)需求

<p>采用自動爬蟲的方式爬取糗事百科文章鏈接與內容,并將文章頭部內容與鏈接存儲到MySQL數據庫中。

(2)分析

  • 怎么提取首頁文章鏈接?
    <p>打開首頁后查看源碼,搜索首頁任一篇文章內容,可以看到"/article/118123230"鏈接,點擊進去后發現這就是我們所要的文章內容,所以我們在自動爬蟲中需設置鏈接包含"article"
  • 怎么提取詳情頁文章內容與鏈接

  • 內容
    打開詳情頁后,查看文章內容如下:


    分析可知利用包含屬性class且其值為content的div標簽可唯一確定文章內容,表達式如下:
    "http://div[@class='content']/text()"

  • 鏈接
    <p>打開任一詳情頁,復制詳情頁鏈接,查看詳情頁源碼,搜索鏈接如下:


    采用以下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.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from qsbkauto.items import QsbkautoItem
from scrapy.http import 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
執行爬蟲

scrapy crawl qsbkauto --nolog

結果

參考:
[1] 天善社區韋瑋老師課程
[2] 文中所跳轉的URL
本文所有代碼只用于技術交流,拒絕任何商用活動
文章相關項目代碼已上傳至個人Github
后續的學習細節將會記錄在個人博客DebugNLP中,歡迎各路同學互相交流

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 本內容為《用Python寫網絡爬蟲》書籍內容,有興趣的讀者可以購買本書,本章的代碼皆可在Python3中運行。為了...
    海人為記閱讀 2,256評論 0 5
  • Python學習網絡爬蟲主要分3個大的版塊:抓取,分析,存儲 另外,比較常用的爬蟲框架Scrapy,這里最后也詳細...
    楚江數據閱讀 1,475評論 0 6
  • 你總想撿回少年的夢想 你總想說 當年的我比你聰明比你棒 你總想說 我不應該安于現狀 兒女私情不能把我的腳步阻擋 你...
    獨特的個性閱讀 357評論 0 0
  • 能找一個聊的來的人,是多么的不容易。你在說,他在用心的聽,你說完之后,他會發表自己的見解。你無聊的時候,他會...
    陌上花開花落閱讀 197評論 0 0
  • 【2017年3月9日周1四——咖啡冥想】 ? 一,我近期最想實現的愿望或目標是: 1.公司個人銷售額每月50萬人民...
    mandy其其閱讀 280評論 0 3