【爬蟲】總結-豆瓣電影

【爬蟲】總結-豆瓣電影

得到豆瓣返回的狀態碼403('forbidden')了,Cheers

經過連續幾天的奮戰,信息終於爬取下來了。不過感覺在提取內容的過程中,遇到的問題更多,本來只要在提取過程中簡單地處理一下就好。

思考1: 數據匹配

對於豆瓣這種運行了十幾年的網站,網站的頁面結構也隨之變化,在解析時不容易統一位置。這讓我學到了許多關於lxml解析的方法(注:bs4支持lxml)。

lxml解析

在內容定位過程中,主要有兩大問題,一個是頁面結構的調整,另一個是匹配多個值。問題可以通過lxml中的通配符*的語法解決,找不到元素時通過css查找匹配解決。

  1. 通過*匹配任何元素節點

    • 表達式//*選取文件中的所有元素(注://爲當前節點後的任意節點選取);/bookstore/*選取book元素的所有子元素節點
    • 例如:劇情簡介用的xpath爲'//*[@id="link-report"]//*[@property="v:summary"]/text()'
    • 解釋: //*[@id="link-report"]表示從根節點到任意節點屬性id值爲"link-report"的節點,//*[@property="v:summary"]表示從當前節點到之後屬性property值爲"v:summary"的任意節點
  2. 通過@*匹配任何屬性節點

    • 表達式//title[@*]選取所有title元素(至少有一種屬性)
    • 例如:編劇用的xpath爲'//*[@id="info"]/span[2]/span[@class="attrs"]/a[@*]/text()'
    • 解釋: /a[@*]表示從a節點任意屬性的節點

異常處理

雖然通過了通配符*的處理,但有時也有匹配不到的情況,主要原因是匹配的語法並不通用頁面沒有該內容以及頁面獲取超時

  1. 該問題可以直接通過python的異常處理try-except語句解決,出現異常時返回空值。
  2. 使用selenium時,可以通過selenium.webdriver.support模塊中expected_conditions進行條件設定以減少頁面獲取超時問題而得不到頁面內容

注意

  1. python內置的eval()函數獲取相應的變量值,例如eval('egg')返回egg變量的值
  2. 利用字符串中的strip(), split(), join(), replace方法進行數據的提取
  3. 利用字符串中的format()進行print輸出 (ref: PEP 3101 – Advanced String Formatting)
  4. 利用列表(字典)推導式處理(List Comprehensions)迭代數據和數據列標題[expr for val in collection if condition] (ref: PEP 202 – List Comprehensions)
  5. 利用三元表達式(if-then-else (“ternary”) expression)處理簡單的條件: value = true-expr if condition else false-expr (fef: PEP 308 – Conditional Expressions)
  6. 利用re正則表達式查找,替換,獲取內容(匹配中文[\u4e00-\u9fff]{注意該範圍未包含擴展範圍})

思考2: 反爬蟲機制

爬取過程中,最順利的是用selenium進行爬取信息,該軟件和真實的瀏覽器操作類似,但頁面獲取的時間與網速有很大關係。筆者在中午爬取時,喫完中飯爬取了不到200個頁面。

用過一段時間的爬蟲後就會知道客戶端向服務端發送請求後,得到正確響應的狀態碼是200,然而在爬取過程中會出現各種問題,筆者在爬取過程中出現了418和最經典的403

狀態碼(Response [418]: 418 I’m a teapot)

首先,我們先了解一下418的含義

The HTTP 418 I’m a teapot client error response code indicates that the server refuses to brew coffee because it is, permanently, a teapot. A combined coffee/tea pot that is temporarily out of coffee should instead return 503. This error is a reference to Hyper Text Coffee Pot Control Protocol defined in April Fools’ jokes in 1998 and 2014. (ref: 418 I’m a teapot
418 I’m a teapot (RFC 2324, RFC 7168)
This code was defined in 1998 as one of the traditional IETF April Fools’ jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by teapots requested to brew coffee.This HTTP status is used as an Easter egg in some websites, including Google.com. (ref: List_of_HTTP_status_codes

從上面的內容可以看出,網站協議RFC2324表明客戶端請求(茶壺)時並不是使用http服務器訪問,就像被要求用茶壺倒一杯咖啡(返回),這樣的請求當然被服務器端拒絕。換句話說,豆瓣服務器知道了你這次請求並不是來自瀏覽器的請求,所以拒絕訪問。

那麼如何解決這問題呢?
我們可以從模擬http服務器訪問入手,網上還有專門的python模塊fake_useragent

from fake_useragent import UserAgent
import requests

ua = UserAgent()
user_agent = ua.random
headers = {'user-agent': user_agent}
url = "http://what.you.want.com"
r = requests.get(url, headers=headers)
r.text

狀態碼(Response [403]: 403 Forbidden)

首先,我們先了解一下403的含義

The HTTP 403 Forbidden client error status response code indicates that the server understood the request but refuses to authorize it. This status is similar to 401, but in this case, re-authenticating will make no difference. The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource. (ref: 403 Forbidden)
The request contained valid data and was understood by the server, but the server is refusing action. This may be due to the user not having the necessary permissions for a resource or needing an account of some sort, or attempting a prohibited action (e.g. creating a duplicate record where only one is allowed). This code is also typically used if the request provided authentication via the WWW-Authenticate header field, but the server did not accept that authentication. The request should not be repeated. (ref: List_of_HTTP_status_codes

從上面的內容可以知道,服務器端理解此次請求,但是拒絕訪問,重要的是永久拒絕訪問。換句話說,豆瓣服務器端知道了請求的內容超過了他們給予的普通權限,從此上了他們的黑名單,一般通過ip綁定黑名單。

那麼如何解決這問題呢?我們可以從改變ip入手

  1. ADSL

用動態IP(ADSL)撥號服務器試驗的效果不是很好,因爲需要在服務器端重新部署所有需要的軟件,而且用程序控制adsl撥號時容易連接不上,不過每次都能更換IP地址。

  1. Tor

最後是Tor瀏覽器,和ShadowSocks使用很相似,使用9150端口爲默認的socks端口。Tor瀏覽器在爬取過程中需要一直運行,抓取的效率也會隨着抓取的數量而降低。但是Tor是免費的,更換ip快且穩定。

2.1 使用Tor

import socket
import socks
import requests

socks.set_default_proxy(socks.Socks5, "127.0.0.1", 9150)
socket.socket = socks.socksocket

url = 'http://checkip.amazonaws.com'
r = requests.get(url)
print(r.text)

2.2 更換Tor的ip

import socket
import socks
import requests

from stem import Signal
from stem.control import Controller

controller = Controller.from_port(port=9150)
controller.authenticate()
socks.set_default_proxy(socks.Socks5, "127.0.0.1", 9150)
socket.socket = socks.socksocket

url = 'http://checkip.amazonaws.com'
r = requests.get(url)
print(r.text)

controller.signal(Signal.NEWNY)

url = 'http://checkip.amazonaws.com'
r = requests.get(url)
print(r.text)
  1. 驗證碼

暫時只能用手動解決(在selenium這樣交互式的工具下才可以)

注意

  1. 利用sleep(np.random.randint(3, 5)+np.random.random())來隨機調控間隔時間,模擬人操控瀏覽器
  2. 利用requests-html庫能夠實現動態網站的js加載,requests作者建立的,但2019年7月後就不再更新

思考3: 爬蟲效率

前面提到筆者在午飯期間爬取網站時,直到喫完飯300個沒有爬取完成,可見速度有多慢。見到這種情況,筆者憑着自己並行的經驗,查看了requests相應的異步併發(並行)庫。

這裏插一句,對於我的理解,併發(concurrency)就是客戶端在等待服務器端響應的過程中又發出另一個請求訪問。

先說一下進程和線程的關係,單個CPU只能執行單個進程,一個進程裏面可以包含多個線程。由於python有GIL(Global Interpreter Lock,全局解釋鎖)存在,意思是說進程中的某個線程需要執行時必須要拿到GIL,而且一個線程中只有一個GIL,所以python中的一個進程只能同時執行一個線程。

requests_toolbelt

requests自家(核心成員)的async多線程併發,但是2019年4月後就不再更新,並行庫的名稱爲requests_toolbelt

利用thread和queue控制線程,使用起來非常簡單方便,但是API接口不提供proxies代理接口和休息間隔時間參數。速度很快,大概幾分鐘就爬取400多個網站,這就是我被403的罪魁禍首。

代碼如下

from fake_useragent import UserAgent

from requests_toolbelt import threaded
from requests_toolbelt import user_agent

urls = ["what.you.want.to.do.com", "what.you.have.done.com"]
urls_to_get = []

ua = UserAgent()
headers = {'user-agent': ua.random}
timeout = 5

for url in urls:
    urls_to_get.append({'url': url,
                      'method': 'GET',
                      'headers': headers,
                      'timeout': timeout})

responses, errors = threaded.map(urls_to_get,
                              num_processes=3)

for response in responses:
    print(f'response is {response}')
    print('GET {0}. Returned {1}.'.format(response.request_kwargs['url'],
                                       response.status_code))
    print(f'response.text[:20] is {response.text[:20]}')

aiohttp

aiohttp是現在比較火的異步併發庫,只是功能尚不完善,而且使用的體驗並不友好,API接口支持的參數很多,這點做得不錯,下面的例子我試驗了一下代理ip的功能,內網速度有點慢,但是外網不錯。

import aiohttp
import asyncio

async def fetch(session, url):
    proxy = {
             "http": "http://127.0.0.1:1081",
             "https": "http://127.0.0.1:1081"
    }
    async with session.get(url, proxy=proxy['http']) as response:
        return await response.text()

async def get(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://en.wikipedia.org/wiki/Cat')
        print(f"html[:100] is {html[:100]}")
        pything = await get(session, 'http://python.org')
        print(f"pything[:100] is {pything[:100]}")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

注意

  1. 筆者這裏只是探索了異步併發的加速,並未使用異步並行,本質上還是單線運行,具體參數可以參考相應的文檔庫
  2. 可以通過調用multiprocessing庫來獲取cpu核心數量from multiprocessing import cpu_count
  3. python網絡爬蟲還有一種叫協程(Coroutine)的輕量級線程,單個CPU可以支持上萬個協程,通過gevent庫實現
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章