Python爬蟲開發與專案實踐(3)

_王泥煤發表於2020-10-26

第三章 初識網路爬蟲

博主說明

  • 原書中使用的Python2執行,而urllib2/urllib在Python3中有較大改動,本次總結(抄書)將原書的程式碼都改成了Python3,程式碼相較原書有一定變化,但儘量不改動原書中的程式碼邏輯
  • 在有些情況下,原書中的程式碼直接執行會產生錯誤(如不加headers訪問www.zhihu.com可能產生HTTPError),這種情況下會選擇修改程式碼(如增加headers)或放棄截圖(仍然會修改成Python3執行不會報語法錯誤的形式)
  • 由於我本人也處於學習階段,修改後的程式碼執行結果可能並不能達到原書的期望,特別是對於放棄截圖的程式碼,我也不清楚如果正常執行會產生什麼效果。參考本部落格時請務必注意。
  • 關於urllib和urllib2在Python3中的改變,可以參考https://blog.csdn.net/fengxinlinux/article/details/77281253(抄錄在下面)
Python2程式碼對應的Python3程式碼
import urllib2import urllib.request, urllib.error
import urllibimport urllib.request, urllib.error, urllib.parse
import urlparseimport urllib.parse
import urlopenimport urllib.request.urlopen
import urlencodeimport urllib.parse.urlencode
import urllib.quoteimport urllib.request.quote
cookielib.CookieJarhttp.CookieJar
urllib2.Requesturllib.request.Request

3.1 網路爬蟲概述

3.1.1 網路爬蟲及其應用

  • 網路爬蟲是一種按照一定規則,自動地抓取全球資訊網資訊的程式或指令碼

  • 網路爬蟲按照系統構建和實現技術,可以分為:

    • 通用網路爬蟲
    • 聚焦網路爬蟲
    • 增量式網路爬蟲
    • 深層網路爬蟲

    實際的網路爬蟲系統通常是幾種爬蟲技術相結合實現的

  • 通用網路爬蟲:搜尋引擎是一種大型複雜的網路爬蟲,屬於通用性網路爬蟲的範疇。但存在一些侷限性:

    • 通用搜尋引擎所返回的結果包含大量使用者不關心的網頁
    • 有限的搜尋引擎伺服器資源與無限的網路資料資源之間存在巨大的矛盾
    • 通用搜尋引擎對圖片、資料庫、音訊、視訊等資訊含量密集且具有一定結構的資料不能很好的發現和獲取
    • 通用搜尋引擎大多提供基於關鍵詞的檢索,難以支援根據語義資訊提出的查詢
  • 聚焦網路爬蟲:通用網路爬蟲解決了通用網路爬蟲的一些侷限性。聚焦網路爬蟲是一個自動下載網頁的程式,根據既定的抓取目標,有選擇地訪問全球資訊網上的網頁與相關的連結,獲取所需要的資訊。為面向主題的使用者查詢準備資料資源

  • 增量式網路爬蟲:增量式網路爬蟲是指對已下載網頁採取增量式更新和只爬行新產生的或者已經發生變化網頁的爬蟲。增量式爬蟲只會在需要的時候爬行新產生或發生更新的頁面,並不重新下載沒有變化的頁面,可有效減少資料下載量,減少時間和空間上的花費,但增加了爬行演算法的複雜度和實現難度

  • 深層網路爬蟲:Web頁面按存在方式可以分為表層網頁和深層網頁。

    • 表層網頁是指傳統搜尋引擎可以索引的頁面,以超連結可以到達的靜態網頁為主構成的Web頁面
    • 深層網頁是那些大部分內容不能通過靜態連結獲取的、隱藏在搜尋表單後的,只有使用者提交一些關鍵詞才能獲得的Web頁面

3.1.2 網路爬蟲結構

  • 通用的網路爬蟲結構

    讀取URL&網頁下載
    種子URL
    待抓取的URL
    已下載網頁資料
    抽取URL
    已抓取的URL
  • 網路爬蟲的基本工作流程:

    • 首先選取一部分精心挑選的種子URL
    • 將這些URL放入待抓取URL佇列
    • 從待抓取URL佇列中讀取待抓取網頁的URL,解析DNS,並且得到主機的IP,並將URL對應的網頁下載下來,儲存進已下載網頁庫中。此外,將這些URL放進已抓取URL佇列。
    • 分析已抓取URL佇列中的URL,從已下載的網頁資料中分析出其他URL,並和已抓取的URL進行比較去重,最後將去重過的URL放入待抓取URL佇列

3.2 HTTP請求的Python實現

  • Python中有三種方式實現HTTP請求

    • urllib2/urllib
    • httplib/urllib
    • Requests

3.2.1 urllib2/urllib實現

Python3中urllib2被整合到urllib中,請參考文首說明,本博並不修改原書標題

1. 首先實現一個完整的請求與響應模型

  • 最簡單的形式:

    import urllib.request
    response=urllib.request.urlopen('http://www.zhihu.com')
    html=response.read()
    print(html)
    

    image-20201023232112218

  • 上面的步驟可以分為兩步,一步是請求,一步是響應,形式如下:

    import urllib.request
    #請求
    request=urllib.request.Request('http://www.zhihu.com')
    #響應
    response = urllib.request.urlopen(request)
    html=response.read()
    print(html)
    

    image-20201023232319306

  • 以上兩種形式都是GET請求,接下來演示POST請求,通過urllib.parse.urlencode()增加了請求資料:

    import urllib
    url = 'http://www.xxxxxx.com/login'
    postdata = {'username' : 'qiye',
               'password' : 'qiye_pass'}
    #info 需要被編碼為urllib2能理解的格式,這裡用到的是urllib
    data = urllib.parse.urlencode(postdata).encode("utf-8")  
    req = urllib.request.Request(url, data)
    response = urllib.request.urlopen(req)
    html = response.read().decode("utf-8")
    print(html)
    
    # 這裡沒有具體網址,所以沒有截圖
    
  • 有時也會出現這種情況:即使POST請求的資料是對的,但是伺服器拒絕你的訪問。問題可能出在請求中的頭資訊,伺服器會檢驗請求頭,來判斷是否是來自瀏覽器的訪問,這也是反爬蟲的常用手段。

2. 請求頭headers處理

  • 將上面的例子加上請求頭資訊,設定請求頭中的User-Agent域和Referer域資訊

    import urllib
    url = 'http://www.xxxxxx.com/login'
    user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
    referer='http://www.xxxxxx.com/'
    postdata = {'username' : 'qiye',
               'password' : 'qiye_pass'}
    # 將user_agent,referer寫入頭資訊
    headers={'User-Agent':user_agent,'Referer':referer}
    data = urllib.parse.urlencode(postdata).encode("utf-8")
    req = urllib.request.Request(url, data, headers)
    response = urllib.request.urlopen(req)
    html = response.read().decode("utf-8")
    
  • 也可以用add_header來新增請求頭資訊

    import urllib
    url = 'http://www.xxxxxx.com/login'
    user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
    referer='http://www.xxxxxx.com/'
    postdata = {'username' : 'qiye',
               'password' : 'qiye_pass'}
    data = urllib.parse.urlencode(postdata).encode("utf-8")
    req = urllib.request.Request(url)
    # 將user_agent,referer寫入頭資訊
    req.add_header('User-Agent',user_agent)
    req.add_header('Referer',referer)
    # req.add_data(data) # 這個方法在Python3中不存在
    # urllib.request.data = data # 這個在Python3中可以執行,但是沒測試過是否能真的傳送data
    # response = urllib.request.urlopen(req)
    response = urllib.request.urlopen(req, data) # 沒測試過上一條命令,所以最好還是直接放這裡
    html = response.read().decode("utf-8")
    
  • 對有些header要特別留意,伺服器會針對這些header做檢查:

    • User-Agent:有些伺服器或Proxy會通過該值來判斷是否是瀏覽器發出的請求
    • Content-Type:在使用REST介面時,伺服器會檢查該值,用來確定HTTP Body中的內容該怎樣解析。在使用伺服器提供的RESTful或SOAP服務時,Content-Type設定錯誤會導致伺服器拒絕服務。常見的取值有:
      • application/xml:在XML RPC,如RESTful/SOAP呼叫時使用
      • application/json:在JSON RPC呼叫時使用
      • application/x-www-form-urlencoded:瀏覽器提交Web表單時使用
    • Referer:伺服器有時候會檢查防盜鏈

3. Cookie處理

  • 以下演示得到某個Cookie的值:

    import urllib
    from http import cookiejar
    cookie = cookiejar.CookieJar()
    opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie))
    response = opener.open('http://www.zhihu.com')
    for item in cookie:
        print(item.name + ':' + item.value)
        
    # 輸出:
    # _xsrf:2VJj0A5wbVnf3er3A7jKoYGs9yptD58Q
    # _zap:c1528c61-5946-45d6-ab3a-c9580195572c
    # KLBRSID:81978cf28cf03c58e07f705c156aa833|1603469046|1603469046
    

    image-20201024000413190

  • 如果想自己新增Cookie內容,可以通過設定請求頭中的Cookie域來做:

    import  urllib
    opener = urllib.request.build_opener()
    opener.addheaders.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) )
    req = urllib.request.Request( "http://www.zhihu.com/" )
    response = opener.open(req)
    print(response.headers)
    retdata = response.read()
    
    # 注:知乎不加user_agent和refer可能會返回403(只是可能,上一張截圖就不是403),所以截圖程式碼加上了這兩項
    # 以後的程式碼如因沒有加headers無法正常訪問,則直接不附截圖
    # 輸出:
    # Server: nginx/1.13.5
    # Date: Fri, 23 Oct 2020 16:10:54 GMT
    # Content-Type: text/html; charset=utf-8
    # Transfer-Encoding: chunked
    # Connection: close
    

    image-20201024001104873

4. Timeout設定超時

  • 設定全域性Timeout值:

    import urllib
    import socket
    socket.setdefaulttimeout(10) # 10 秒鐘後超時
    urllib.request.socket.setdefaulttimeout(10) # 另一種方式
    
  • 設定urlopen函式的Timeout值:

    import urllib
    request=urllib.request.Request('http://www.zhihu.com')
    response = urllib.request.urlopen(request,timeout=2)
    html=response.read()
    print(html)
    

5. 獲取HTTP響應碼

  • 對於200 OK狀態來說,只要使用urlopen返回的response物件的getcode()方法就可以得到HTTP的返回碼

  • 對其他返回碼來說,urlopen會丟擲異常,可以使用以下程式碼來檢查異常物件的code屬性:

    import urllib
    try:
        response = urllib.request.urlopen('http://www.google.com')
        print(response)
    except urllib.error.HTTPError as e:
        if hasattr(e, 'code'):
            print('Error code:',e.code)
    
    # 輸出:
    # Error code: 403
    

    image-20201026102728264

6. 重定向

  • urllib(原書中為urllib2,但Python3將urllib2和urllib合併了)預設情況下會針對HTTP 3XX返回碼自動進行重定向操作

  • 要檢測是否發生了重定向,只需要檢查Response的URL和Request的URL是否一致即可

    import urllib
    response = urllib.request.urlopen('http://www.zhihu.cn')
    isRedirected = response.geturl() == 'http://www.zhihu.cn'
    
  • 如果不想自動重定向,可以自定義HTTPRedirectHandler類:

    import urllib
    class RedirectHandler(urllib.request.HTTPRedirectHandler):
        # 301:永久性轉移
        def http_error_301(self, req, fp, code, msg, headers):
            pass
        # 302:暫時性轉移
        def http_error_302(self, req, fp, code, msg, headers):
            result = urllib.request.HTTPRedirectHandler.http_error_301(self, req, fp, code, msg, headers)
            result.status = code
            result.newurl = result.geturl()
            return result
    opener = urllib.request.build_opener(RedirectHandler)
    opener.open('http://www.zhihu.cn')
    

7. Proxy的設定

  • urllib(原書中為urllib2)預設會使用環境變數http_proxy來設定HTTP Proxy

  • 但我們一般不選擇這種方式,而是使用ProxyHandler在程式中動態設定代理:

    import urllib
    proxy = urllib.request.ProxyHandler({'http': '127.0.0.1:8087'})
    opener = urllib.request.build_opener(proxy)
    urllib.request.install_opener(opener)
    response = urllib.request.urlopen('http://www.zhihu.com/')
    print(response.read().decode("utf8"))
    
  • 使用urllib.request.install_opener()會設定urllib的全域性opener,之後所有的HTTP訪問都會使用這個代理

  • 比較好的做法是不使用install_opener()去更改全域性的設定,而只是呼叫opener的open方法代替全域性的urlopen方法

    import urllib
    proxy = urllib.request.ProxyHandler({'http': '127.0.0.1:8087'})
    opener = urllib.request.build_opener(proxy)
    response = opener.open("http://www.zhihu.com/")
    print(response.read().decode("utf8"))
    

3.2.2 httplib/urllib實現

  • httplib是一個底層基礎模組,可以看到建立HTTP請求的每一步,但實現的功能比較少,正常情況下比較少用到

  • 在爬蟲開發中基本用不到httplib模組,所以在此只是進行普及,簡單介紹常用物件和函式:

    功能命令
    建立HTTPConnection物件class httplib.HTTPConnection(host[, port[, strict[, timeout[, source_address]]]])
    傳送請求HTTPConnection.request(method, url[, body[, headers]])
    獲得響應HTTPConnection.getresponse()
    讀取響應資訊HTTPResponse.read([amt])
    獲得指定頭資訊HTTPResponse.getheader(name[, default])
    獲得響應頭(header, value)的元組列表HTTPResponse.getheaders()
    獲得底層socket檔案描述符HTTPResponse.fileno()
    獲得頭內容HTTPResponse.msg
    獲得頭http版本HTTPResponse.version
    獲得返回狀態碼HTTPResponse.status
    獲得返回說明HTTPResponse.reason
  • Python 2.x中的httplib模組在Python 3.x中變為http.client,以上程式碼和描述仍然是從原書中摘錄,以下程式碼將修改為Python3程式碼

傳送GET請求示例

import http.client
conn =None
try:
    conn = http.client.HTTPConnection("www.zhihu.com")
    conn.request("GET", "/")
    response = conn.getresponse()
    print(response.status, response.reason)
    print('-' * 40)
    headers = response.getheaders()
    for h in headers:
        print(h)
    print('-' * 40)
    print(response.msg)
except Exception as e:
    print(e)
finally:
    if conn:
        conn.close()
        
# 輸出:
# 302 Found
# ----------------------------------------
# ('Location', 'https://www.zhihu.com/')
# ('Content-Length', '0')
# ('X-NWS-LOG-UUID', '4206638636747267114')
# ('Connection', 'keep-alive')
# ('Server', 'Lego Server')
# ('Date', 'Mon, 26 Oct 2020 03:31:19 GMT')
# ('X-Cache-Lookup', 'Return Directly')
# ----------------------------------------
# Location: https://www.zhihu.com/
# Content-Length: 0
# X-NWS-LOG-UUID: 4206638636747267114
# Connection: keep-alive
# Server: Lego Server
# Date: Mon, 26 Oct 2020 03:31:19 GMT
# X-Cache-Lookup: Return Directly

image-20201026113154055

傳送POST請求示例:

import http.client, urllib
conn = None
try:
    params = urllib.parse.urlencode({'name': 'qiye', 'age': 22})
    headers = {"Content-type": "application/x-www-form-urlencoded"
    , "Accept": "text/plain"}
    conn = http.client.HTTPConnection("www.zhihu.com", 80, timeout=3)
    conn.request("POST", "/login", params, headers)
    response = conn.getresponse()
    print(response.getheaders()) #獲取頭資訊
    print(response.status)
    print(response.read())
except Exception as e:
    print(e)
finally:
    if conn:
        conn.close()
      
# 輸出:
# [('Location', 'https://www.zhihu.com/login'), ('Content-Length', '0'), ('X-NWS-LOG-UUID', '11739497059846929414'), ('Server', 'Lego Server'), ('Date', 'Mon, 26 Oct 2020 03:32:38 GMT'), ('X-Cache-Lookup', 'Return Directly'), ('Connection', 'close')]
# 302
# b''

image-20201026113310674

3.2.3 更人性化的Requests

0. 安裝

  • 安裝:
    • pip install requests
    • 下載連結:https://github.com/kennethreitz/requests/releases,下載解壓後執行setup.py
    • 安裝後再Python的shell中輸入import requests,不報錯即為安裝成功

1. 首先還是實現一個完整的請求與響應模型

  • 以GET請求為例,最簡單的形式如下:

    import requests
    r = requests.get('http://www.baidu.com')
    print(r.content)
    

    image-20201026115410093

  • POST請求示例:

    import requests
    postdata={'key':'value'}
    r = requests.post('http://www.xxxxxx.com/login',data=postdata)
    print(r.content)
    
  • HTTP中的其他請求方式也可以用Requests來實現:

    • r = requests.put('http://www.xxxxxx.com/put', data = {'key': 'value'})
    • r = requests.delete('http://www.xxxxxx.com/delete')
    • r = requests.head('http://www.xxxxxx.com/get')
    • r = requests.options('http://www.xxxxxx.com/get')
  • 在網頁URL中使用?後面跟明碼引數的形式,在Requests中也有支援:

    import requests
    payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
    r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
    print(r.url)
    # 輸出:
    # https://zzk.cnblogs.com/s/blogpost?Keywords=blog%3Aqiyeboy&pageindex=1
    

    image-20201026115958723

2. 響應與編碼

  • 示例程式碼:

    import requests
    r = requests.get('http://www.baidu.com')
    print('content-->'+r.content)
    print('text-->'+r.text)
    print('encoding-->'+r.encoding)
    r.encoding='utf-8'
    print('new text-->'+r.text)
    
    # 輸出過長,省略
    
    • r.content返回的是位元組格式
    • t.text返回的是文字格式
    • r.encoding返回的是根據HTTP頭猜測的網頁編碼格式
    • 輸出結果:text-->之後的內容是亂碼,encoding-->之後的內容是ISO-8859-1(實際編碼為UTF-8)的亂碼
  • 可以通過r.encoding來設定字元編碼

  • 可以通過更簡單的chardet庫來實現自動更新編碼:

    • 安裝:pip install chardet
    • 使用chardet.detect()返回字典,其中confidence是檢測精確度,encoding是編碼形式
    • 直接將chardet探測到的編碼賦給r.encoding實現解碼
    import requests
    import chardet
    r = requests.get('http://www.baidu.com')
    print(chardet.detect(r.content))
    r.encoding = chardet.detect(r.content)['encoding']
    print(r.text)
    

    image-20201026120756379

  • 除了上面那種直接獲取全部響應的方式,還有一種流模式,使響應以位元組流方式進行讀取,r.raw.read函式指定讀取的位元組數

    import requests
    r = requests.get('http://www.baidu.com', stream=True)
    print(r.raw.read(10))
    
    # 輸出:
    # b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03'
    

    image-20201026121123346

3. 請求頭headers處理

  • 在Requests的get函式中新增headers引數

    import requests
    user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
    headers={'User-Agent':user_agent}
    r = requests.get('http://www.baidu.com',headers=headers)
    print(r.content)
    

    image-20201026113530226

4. 響應碼code和響應頭headers處理

  • 使用Requests中的status_code欄位獲取響應碼

  • 使用Requests中的headers欄位獲取全部響應頭

    • 可以通過get函式獲取其中的某一個欄位,如果沒有這個欄位會返回None
    • 可以通過字典引用的方式獲取字典值,但如果沒有這個欄位則會丟擲異常
  • r.raise_for_status可以主動產生一個異常,當響應碼是4XX或5XX時,raise_for_status會丟擲異常;當響應碼為200時,raise_for_status返回None

    import requests
    r = requests.get('http://www.baidu.com')
    if r.status_code == requests.codes.ok:
        print(r.status_code)#響應碼
        print(r.headers)#響應頭
        print(r.headers.get('content-type'))#推薦使用這種獲取方式,獲取其中的某個欄位
        print(r.headers['content-type'])#不推薦使用這種獲取方式
    else:
        r.raise_for_status()
        
    # 輸出:
    # 200
    # {'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform', 'Connection': 'keep-alive', 'Content-Encoding': 'gzip', 'Content-Type': 'text/html', 'Date': 'Mon, 26 Oct 2020 03:37:28 GMT', 'Last-Modified': 'Mon, 23 Jan 2017 13:28:16 GMT', 'Pragma': 'no-cache', 'Server': 'bfe/1.0.8.18', 'Set-Cookie': 'BDORZ=27315; max-age=86400; domain=.baidu.com; path=/', 'Transfer-Encoding': 'chunked'}
    # text/html
    # text/html
    

    image-20201026113753677

5. Cookie處理

  • 可以通過以下方式獲取Cookie欄位的值:

    import requests
    user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
    headers={'User-Agent':user_agent}
    r = requests.get('http://www.baidu.com',headers=headers)
    #遍歷出所有的cookie欄位的值
    for cookie in r.cookies.keys():
        print(cookie+':'+r.cookies.get(cookie))
        
    # 輸出:
    # BAIDUID:D7C2F6796085FDA28A31BE315DDA891A:FG=1
    # BIDUPSID:D7C2F6796085FDA270112C0795ABEEEB
    # H_PS_PSSID:32755_1429_32840_32230_7516_7605
    # PSTM:1603683926
    # BDSVRTM:15
    # BD_HOME:1
    

    image-20201026114601964

  • 可以通過以下方式自定義Cookie值:

    import requests
    user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
    headers={'User-Agent':user_agent}
    cookies = dict(name='qiye',age='10')
    r = requests.get('http://www.baidu.com',headers=headers,cookies=cookies)
    print(r.text)
    

    image-20201026114721319

  • 可以通過以下方式自動處理Cookie,在每次訪問時,程式自動把Cookie的值帶上,像瀏覽器一樣

    import requests
    loginUrl = 'http://www.xxxxxxx.com/login'
    s = requests.Session()
    #首先訪問登入介面,作為遊客,伺服器會先分配一個cookie
    r = s.get(loginUrl,allow_redirects=True)
    datas={'name':'qiye','passwd':'qiye'}
    #向登入連結傳送post請求,驗證成功,遊客許可權轉為會員許可權
    r = s.post(loginUrl, data=datas, allow_redirects= True)
    print(r.text)
    
  • 在上一步的程式碼中,如果沒有第一步訪問登入的頁面(get請求),系統會把你當做非法使用者,因為訪問登入介面時會分配一個Cookie,需要將這個Cookie在傳送Post請求時帶上

6. 重定向與歷史資訊

  • 處理重定向只需要設定allow_recirect子段即可

    • True:允許重定向
    • Fase:禁止重定向
  • 如果允許重定向,可以通過r.history欄位差好看歷史資訊,即訪問成功之前的所有跳轉資訊

    import requests
    r = requests.get('http://github.com')
    print(r.url)
    print(r.status_code)
    print(r.history)
    
    # 輸出:
    # http://github.com/
    # 200
    # [<Response [301]>]
    

    image-20201026112418708

7. 超時設定

  • 超時選項是通過引數timeout來進行設定的:

    requests.get('http://github.com', timeout=2)
    

8. 代理設定

  • 使用代理Proxy,可以為任意請求方法通過設定proxies引數來配置單個請求:

    import requests
    proxies = {
      "http": "http://10.10.1.10:3128",
      "https": "http://10.10.1.10:1080",
    }
    requests.get("http://example.org", proxies=proxies)
    
  • 也可以通過環境變數HTTP_PROXY和HTTPS_PROXY來配置代理,但是在爬蟲開發中不常用

  • 代理如果需要HTTP Basic Auth,可以使用http://user:password@host語法:

    proxies = {
        "http": "http://user:pass@10.10.1.10:3128/",
    }
    

相關文章