1. 程式人生 > >關於近期爬蟲學習的總結

關於近期爬蟲學習的總結

在之前的三篇文章中,我嘗試了使用python爬蟲實現的對於特定站點的《劍來》小說的爬取,對於豆瓣的短評的爬取,也有對於爬取的短評資料進行的詞雲展示,期間運用了不少的知識,現在是時間回顧一下。在此之後,我會再關注一些爬蟲框架的使用,以及更多的爬蟲的優化方法,爭取做到儘量多的吸收新知識,鞏固舊知識。

在參考文章爬蟲(1)— Python網路爬蟲二三事的基礎上,我寫了這篇文章。

這篇文章主要的目的有兩個,收集新知識,鞏固舊知識。

關於爬蟲背後的(這一節是主要是http的概要,下一節是我的一些總結)

要想不限於程式碼表面,深入理解爬蟲,就得需要了解一些關於網路的知識。

HTTP 協議

HTTP(Hypertext Transfer Protocol)是應用級協議,它適應了分散式超媒體協作系統對靈活性及速度的要求。它是一個一般的、無狀態的、基於物件的協議。

可以參考的文章:

想要細緻瞭解協議的可以檢視

一些術語

  • 請求(request):HTTP的請求訊息
  • 響應(response):HTTP的迴應訊息
  • 資源(resource):網路上可以用URI來標識的資料物件或服務。URI有許多名字,如WWW地址、通用檔案標識(Universal Document Identifiers)、通用資源標識(Universal Resource Identifiers),以及最終的統一資源定位符(Uniform Resource Locators (URL))和統一資源名(URN)。
  • 實體(entity):可被附在請求或迴應訊息中的特殊的表示法、資料資源的表示、服務資源的迴應等,由實體標題(entity header)或實體主體(entity body)內容形式存在的元資訊組成。
  • 客戶端(client):指以發出請求為目的而建立連線的應用程式。
  • 使用者代理(user agent):指初始化請求的客戶端,如瀏覽器、編輯器、蜘蛛(web爬行機器人)或其它終端使用者工具。使用者代理請求標題域包含使用者原始請求的資訊,這可用於統計方面的用途。通過跟蹤協議衝突、自動識別使用者代理以避免特殊使用者代理的侷限性,從而做到更好的迴應。雖然沒有規定,使用者代理應當在請求中包括此域。
  • 伺服器(server):指接受連線,並通過傳送迴應來響應服務請求的應用程式。
  • 代理(proxy):同時扮演伺服器及客戶端角色的中間程式,用來為其它客戶產生請求。請求經過變換,被傳遞到最終的目的伺服器,在代理程式內部,請求或被處理,或被傳遞。代理必須在訊息轉發前對訊息進行解釋,而且如有必要還得重寫訊息。代理通常被用作經過防火牆的客戶端出口,用以輔助處理使用者代理所沒實現的請求。

任何指定的程式都有能力同時做為客戶端和伺服器。我們在使用這個概念時,不是看程式功能上是否能實現客戶及伺服器,而是看程式在特定連線時段上扮演何種角色(客戶或伺服器)。同樣,任何伺服器可以扮演原始伺服器、代理、閘道器、隧道等角色,行為的切換取決於每次請求的內容。

簡單流程

HTTP協議是基於請求/迴應機制的。客戶端與伺服器端建立連線後,以請求方法、URI、協議版本等方式向伺服器端發出請求,該請求可跟隨包含請求修飾符、客戶資訊、及可能的請求體(body)內容的MIME型別訊息。伺服器端通過狀態佇列(status line)來回應,內容包括訊息的協議版本、成功或錯誤程式碼,也跟隨著包含伺服器資訊、實體元資訊及實體內容的MIME型別訊息。

絕大多數HTTP通訊由使用者代理進行初始化,並通過它來組裝請求以獲取儲存在一些原始伺服器上的資源。

一次HTTP操作稱為一個事務,綜上,其工作過程可分為四步:

  1. 首先客戶機與伺服器需要建立連線。只要單擊某個超級連結,HTTP的工作開始。
  2. 建立連線後,客戶機發送一個請求給伺服器,請求方式的格式為:統一資源識別符號(URL)、協議版本號,後邊是MIME資訊包括請求修飾符、客戶機資訊和可能的內容。
  3. 伺服器接到請求後,給予相應的響應資訊,其格式為一個狀態行,包括資訊的協議版本號、一個成功或錯誤的程式碼,後邊是MIME資訊包括伺服器資訊、實體資訊和可能的內容。
  4. 客戶端接收伺服器所返回的資訊通過瀏覽器顯示在使用者的顯示屏上,然後客戶機與伺服器斷開連線。

如果在以上過程中的某一步出現錯誤,那麼產生錯誤的資訊將返回到客戶端,有顯示屏輸出。對於使用者來說,這些過程是由HTTP自己完成的,使用者只要用滑鼠點選,等待資訊顯示就可以了。

HTTP 訊息(HTTP Message)

HTTP訊息由客戶端到伺服器的請求和由伺服器到客戶端的迴應組成。

HTTP-message   = Simple-Request             ; HTTP/0.9 messages
                    | Simple-Response
                    | Full-Request          ; HTTP/1.0 messages
                    | Full-Response

完整的請求(Full-Request)和完整的迴應(Full-Response)都使用RFC822中實體傳輸部分規定的訊息格式。兩者的訊息都可能包括標題域(headers,可選)、實體主體(entity body)。實體主體與標題間通過空行來分隔(即CRLF前沒有內容的行)。

Full-Request        = Request-Line
                        *( General-Header
                        | Request-Header
                        | Entity-Header )
                        CRLF
                        [ Entity-Body ]

Full-Response       = Status-Line           
                        *( General-Header
                        | Response-Header   
                        | Entity-Header )   
                        CRLF
                        [ Entity-Body ]         

(想要了解更多,可以前往中文 HTTP/1.0 RFC文件目錄

程式碼實現的流程

從我之前的程式碼中可以從一些方法中看出,我們利用urllib庫,實際上完成了請求,並接受了響應。通過我們自己構造(例如新增headers)或者使用預設的請求資訊,利用了urllib庫的request模組的Request()方法構造請求,利用urlopen()方法接受響應返回的頁面程式碼。

至於我們的爬蟲工作的過程,在這裡盜用(http://www.jianshu.com/p/0bfd0c48457f)一張圖片:

爬蟲是一個綜合性的工具

在實現爬蟲的過程中,為了實現我們的目的,我們藉助了各種各樣的工具。瀏覽器的開發者工具,正則表示式,以及python的各種功能庫。可謂是無所不用其極。但是,對於這些工具,我們實際上用到的功能,目前來看,其實並不多,核心程式碼就是那點而已,所以說,抽離出彙總起來,日後重新使用,一旦對於這個工具不再熟悉,回過頭來看這些小小的片段,總是會省下很多的時間。

所以,這篇文章的核心內容就要來了。

獲取URL

這裡可以藉助瀏覽器的產看網頁原始碼檢視元素等開發者工具,且快捷鍵一般是F12

獲取頁面及異常處理

常用的是利用urllib庫。在python3中,沒有了原來2中的urllib2,而是用urllib包含了。

主要是用urllib.request.Request()&urllib.request.urlopen()

urllib.request

urllib is a package that collects several modules for working with URLs:

urllib.request  
    for opening and reading URLs
urllib.error 
    containing the exceptions raised by urllib.request
urllib.parse 
    for parsing URLs
urllib.robotparser 
    for parsing robots.txt files
import urllib.request
import urllib.error
import traceback
import sys

url = "..."
try: 
    request = urllib.request.Request(url)
    html = urllib.request.urlopen(request).read().decode("")# 按情況解碼
    print(html) 
except urllib.error.URLError as error_1: 
    if hasattr(error_1,"code"): 
        print("URLError異常程式碼:") 
        print(error_1.code) 
    if hasattr(error_1,"reason"): 
        print("URLError異常原因:") 
        print(error_1.reason)
except urllib.error.HTTPError as error_2: 
    print("HTTPError異常概要:")
    print(error_2)
except Exception as error_3: 
    print("異常概要:") 
    print(error_3) 
    print("---------------------------") 
    errorInfo = sys.exc_info() 
    print("異常型別:"+str(errorInfo[0])) 
    print("異常資訊或引數:"+str(errorInfo[1])) 
    print("呼叫棧資訊的物件:"+str(errorInfo[2])) 
    print("已從堆疊中“輾轉開解”的函式有關的資訊:"+str(traceback.print_exc()))

作者:whenif
連結:http://www.jianshu.com/p/0bfd0c48457f
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

URLError
通常,URLError在沒有網路連線(沒有路由到特定伺服器),或者伺服器不存在的情況下產生。

HTTPError
首先我們要明白伺服器上每一個HTTP 應答物件response都包含一個數字“狀態碼”,該狀態碼錶示HTTP協議所返回的響應的狀態,這就是HTTPError。比如當產生“404 Not Found”的時候,便表示“沒有找到對應頁面”,可能是輸錯了URL地址,也可能IP被該網站遮蔽了,這時便要使用代理IP進行爬取資料。

兩者關係

兩者是父類與子類的關係,即 HTTPError是URLError的子類,HTTPError有異常狀態碼與異常原因,URLError沒有異常狀態碼。所以,我們在處理的時候,不能使用URLError直接代替HTTPError。同時,Python中所有異常都是基類Exception的成員,所有異常都從此基類繼承,而且都在exceptions模組中定義。如果要代替,必須要判斷是否有狀態碼屬性。

異常處理有還有else&finally可以選擇。

偽裝瀏覽器

新增頭資訊。可以在瀏覽器的開發者工具中的網路選項卡中點選對應的html頁面檢視請求報文和相應報文資訊。一般的,只需新增使用者代理資訊即可。

import urllib.request
url = 'http://www.baidu.com'
# 構造方法1
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0'}
request = urllib.request.Request(url, headers = headers)
data = urllib.request.urlopen(request).read().decode('utf-8')

# 構造方法2
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.addheaders = [headers]
# # 開啟方法1
data = opener.open(url, timeout=3).read().decode('utf-8')
# 開啟方法2
urllib.request.install_opener(opener)
data = urllib.request.urlopen(url).read().decode('utf-8')

print(data)
class urllib.request.Request(url[, data][, headers][, origin_req_host][, unverifiable])

data 資料可以是指定要傳送到伺服器的附加資料的字串,如果不需要這樣的資料,則為None。目前HTTP請求是唯一使用資料的請求; 當提供資料引數時,HTTP請求將是POST而不是GET。 資料應該是標準的 application/x-www-form-urlencoded 格式。urllib.parse.urlencode()函式採用對映或2元組的序列,並以此格式返回一個字串。

headers 應該是一個字典,並且將被視為使用每個鍵和值作為引數呼叫 add_header()。 這通常用於“欺騙”使用者代理頭,瀏覽器使用它來識別自己 - 一些HTTP伺服器只允許來自普通瀏覽器的請求而不是指令碼。

時間問題

超時重連

有時候網頁請求太過頻繁,會出現長時間沒有響應的狀態,這時候一般需要設定超時重連。下面是一個例子片段

import urllib.request
import socket

url = "..."
NET_STATUS = False
while not NET_STATUS:
    try:
        response = urllib.request.urlopen(url, data=None, timeout=3)
        html = response.read().decode('GBK')
        print('NET_STATUS is good')
        return html
    except socket.timeout:
        print('NET_STATUS is not good')
        NET_STATUS = False


# 也可以直接設定全域性超時進行捕獲,用的還是`socket.timeout`異常
import socket

timeout = 3
socket.setdefaulttimeout(timeout)

執行緒延遲

執行緒推遲(單位為秒),避免請求太快。

import time
time.sleep(3)

頁面解析

目前掌握的方法是使用正則表示式和bs庫。當然,之後會了解下XPath,這個也提供了一種搜尋的思路。

正則匹配

這個用的函式並不多。有兩種書寫方式,面向物件和

import re
pattern = re.compile('[a-zA-Z]')
result_list = pattern.findall('as3SiOPdj#@23awe')
print(result_list)

# re.search 掃描整個字串並返回第一個成功的匹配。 
searchObj = re.search( r'(.*) are (.*?) .*', "Cats are smarter than dogs", re.M|re.I)
print(searchObj.group())# Cats are smarter than dogs
print(searchObj.group(1))# Cats
print(searchObj.groups())# ('Cats', 'smarter')

python3正則表示式

beautifulsoup4

Python利用Beautiful Soup模組搜尋內容詳解

我個人感覺,這個庫的使用,主要難點在於搜尋時的麻煩。我覺得比較好用的是方法find()&find_all()&select()

使用 find() 方法會從搜尋結果中返回第一個匹配的內容,而 find_all() 方法則會返回所有匹配的項,返回列表。

select()中可以使用CSS選擇器。很方便可以參考瀏覽器開發者工具。

from bs4 import BeautifulSoup
# 可以獲取驗證碼圖片地址並下載圖片
soup = BeautifulSoup(response_login, "html.parser")
captchaAddr = soup.find('img', id='captcha_image')['src']
request.urlretrieve(captchaAddr, "captcha.jpg")
...
totalnum = soup.select("div.mod-hd h2 span a")[0].get_text()[3:-2]

檔案讀寫

# 要讀取非UTF-8編碼的文字檔案,需要給open()函式傳入encoding引數
with open(filename, 'w+') as open_file:
    ...

# 遇到有些編碼不規範的檔案,你可能會遇到UnicodeDecodeError,因為在文字檔案中可能夾雜了一些非法編碼的字元。遇到這種情況,open()函式還接收一個errors引數,表示如果遇到編碼錯誤後如何處理。最簡單的方式是直接忽略
with open(filename, 'r', encoding='gbk', errors='ignore') as open_file:
    all_the_text = open_file.read([size])
    list_of_all_the_lines = open_file.readlines()
    open_file.write(all_the_text)
    open_file.writelines(list_of_text_strings)

登入資訊

我們構造好 POST 請求,這一旦傳送過去, 就登陸上了伺服器, 伺服器就會發給我們 Cookies。繼續保持登入狀態時,就需要藉助相關的庫的方式,可以簡化處理。

Cookies 是某些網站為了辨別使用者身份、進行 session 跟蹤而儲存在使用者本地終端上的資料(通常經過加密)。

from urllib import request
from urllib import parse
from http import cookiejar

main_url = '...'
formdata = {
    # 看網站post頁面需求

    "form_email":"你的郵箱",
    "form_password":"你的密碼",
    "source":"movie",
    "redir":"https://movie.douban.com/subject/26934346/",
    "login":"登入"
}
user_agent = 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.94 Safari/537.36'
headers = {'User-Agnet': user_agent, 'Connection': 'keep-alive'}

cookie = cookiejar.CookieJar()
cookie_support = request.HTTPCookieProcessor(cookie)
opener = request.build_opener(cookie_support)
data = parse.urlencode(formdata).encode('utf-8')

req_ligin = request.Request(url=main_url, data=data, headers=headers)
html = opener.open(req_ligin).read().decode('utf-8')
print(html)

代理

有一種反爬蟲策略就是對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))

作者:whenif
連結:http://www.jianshu.com/p/0bfd0c48457f
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

後續

後面我會在考慮學習多執行緒異或多程序改寫之前的爬蟲,研究研究前面引用的文章裡的提到的一些我之前還未了解的技術,感覺要學的東西還是很多,快要開學了,自己的效率有點低,也不知道還能有多少時間給自己這樣浪。