Web爬蟲:多線程、異步與動態代理初步

2021-02-14 白帽子


        今天轉發一篇文章關於爬蟲的,用python寫的,思路挺好的,如果感興趣的話,請私下告訴下我,我看情況,是否發下關於爬蟲的基礎入門的。


0×00 前言

在採集數據的時候,經常會碰到有反採集策略規則的WAF,使得本來很簡單事情變得複雜起來。黑名單、限制訪問頻率、檢測HTTP頭等這些都是常見的策略,不按常理出牌的也有檢測到爬蟲行為,就往裡注入假數據返回,以假亂真,但為了良好的用戶體驗,一般都不會這麼做。在遇有反採集、IP位址不夠的時候,通常我們想到的是使用大量代理解決這個問題,因代理具有時效、不穩定、訪問受限等不確定因素,使得有時候使用起來總會碰到一些問題。

進入正題,使用Python3簡單實現一個單機版多線程/異步+多代理的爬蟲,沒有分布式、不談高效率,先跑起來再說,腦補開始。。。

0×01 基礎知識1.1 代理類型

使用代理轉發數據的同時,代理伺服器也會改變REMOTE_ADDR、HTTP_VIA、HTTP_X_FORWARDED_FOR這三個變量發送給目標伺服器,一般做爬蟲的選擇優先級為高匿 > 混淆 > 匿名 > 透明 > 高透

REMOTE_ADDR = Your IPHTTP_VIA = Your IPHTTP_X_FORWARDED_FOR = Your IP

REMOTE_ADDR = Proxy IPHTTP_VIA = Proxy IPHTTP_X_FORWARDED_FOR = Your IP

REMOTE_ADDR = Proxy IPHTTP_VIA = Proxy IPHTTP_X_FORWARDED_FOR = Your Proxy

REMOTE_ADDR = Proxy IPHTTP_VIA = N/AHTTP_X_FORWARDED_FOR = N/A

REMOTE_ADDR = Proxy IPHTTP_VIA = Proxy IPHTTP_X_FORWARDED_FOR = Random IP

1.2 代理協議

一般有HTTP/HTTPS/Socks類型,Web爬蟲一般只用到前面兩者。

1.3 動態代理

實現動態代理一般是建立代理池,使用的時候通常有以下幾種方式

1.4 多線程

多線程是實現任務並發的方式之一。在Python中實現多線程的方案比較多,最常見的是隊列和線程類

que = queue.Queue()def worker():    while not que.empty():        do(que.get())threads = []nloops = 256for i in range(nloops):    t = threading.Thread(target=worker)    t.start()    threads.append(t)for i in range(nloops):    threads[i].join()

另外也可以使用map實現,map可以通過序列來實現兩個函數之間的映射,並結合multiprocessing.dummy實現並發任務

from multiprocessing.dummy import Pool as ThreadPoolurls = ['http://www.freebuf.com/1',        'http://www.freebuf.com/2']pool = ThreadPool(256) res  = map(urllib.request.urlopen, urls)pool.close()pool.join()

似乎更簡潔,多線程實現還有其他方式,具體哪一種更好,不能一概而論,但多線程操作資料庫可能會產生大量的資料庫TCP/socket連接,這個需要調整資料庫的最大連接數或採用線程池之類的解決。

1.5 異步IO

asyncio是在Python3.4中新增的模塊,它提供可以使用協程、IO復用在單線程中實現並發模型的機制。async/await這對關鍵字是在Python3.5中引入的新語法,用於協成方面的支持,這無疑給寫爬蟲多了一種選擇,asyncio包括一下主要組件:

事件循環(Event loop)

I/O機制

Futures

Tasks

一個簡單例子:

que = asyncio.Queue()urls = ['http://www.freebuf.com/1',        'http://www.freebuf.com/2']async def woker():    while True:        q = await que.get()        try:            await do(q)        finally:            que.task_done()async def main():    await asyncio.wait([que.put(i) for i in urls])    tasks = [asyncio.ensure_future(self.woker())]    await que.join()    for task in tasks:        task.cancel()loop = asyncio.get_event_loop()loop.run_until_complete(main())loop.close()

使用隊列是因為後面還要往裡面回填數據,註:asyncio中的隊列Queue不是線程安全的

0×02 獲取與存儲數據2.1 加代理的GET請求

代理類型支持http、https,其他類型沒有去測試

pxy = {'http': '8.8.8.8:80'}proxy_handler = urllib.request.ProxyHandler(pxy)opener = urllib.request.build_opener(proxy_handler)opener.addheaders = [('User-agent', 'Mozilla/5.0'),('Host','www.freebuf.com')]html = opener.open(url).read().decode('utf-8','ignore')

aiohttp中的代理類型目前好像只支持http,測試https會拋處異常

conn = aiohttp.ProxyConnector(proxy="http://some.proxy.com")session = aiohttp.ClientSession(connector=conn)async with session.get('http://python.org') as resp:    print(resp.read().decode('utf-8','ignore'))

POST請求實現也類似

2.2 解碼

有時候網頁中夾有一些特殊的字符導致無法正常解碼而掉丟整條記錄,可以加ignore參數忽略掉

>>> b'freebuf.com\xff'.decode('utf8')Traceback (most recent call last):  File "<stdin>", line 1, in <module>UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 11: invalid start byte>>> b'freebuf.com\xff'.decode('utf8', 'ignore')'freebuf.com'>>>

2.3 HTML解析

from bs4 import BeautifulSoupsoup = BeautifulSoup(html, 'lxml')

使用正則匹配時應使用懶惰模式使匹配結果更準確

import rere.compile(r'<div>(.*?)</div>').findall(html)

2.4 保存副本

在採集的同時建議保存一份副本在本地,如有某個元素匹配錯了,可以從本地快速獲取,而無須再去採集。

with open('bak_xxx.html', 'wt') as f:    f.write(html)

2.5 數據存儲

以MySQL為例,一般分3張表

data 存放要採集的數據

proxy 存放代理

temp 臨時表,一般用來存放無效的任務id

2.6 連接資料庫

import pymysqlconn = pymysql.connect(host='127.0.0.1', user='test', passwd='test', db='test'                        ,unix_socket='/var/run/mysqld/mysqld.sock', charset='utf8')

import aiomysqlpool = await aiomysql.create_pool(host='127.0.0.1', db='test', user='test', password='test'                                    ,unix_socket='/var/run/mysqld/mysqld.sock', charset='utf8'                                    ,loop=loop, minsize=1, maxsize=20)

0×03 維持爬蟲

所謂維持,就是得保證爬蟲能正常地進行任務,而不會被異常中斷而重啟無法續抓、低代理情況下無法繼續進行等情況。

3.1 整體流程

循環任務隊列至空,代理循環載入,一個任務配一個代理,丟棄無法使用的代理並將任務填回隊列,一個簡單的圖

3.2 更新代理

將代理記錄存到MySQL資料庫,每隔一定時間腳本就去資料庫抽取載入腳本。更新代理一般可以通過以下兩種方式

為了防止在在proxy隊列為空時其他線程也進入造成多次加載,使用加鎖堵塞線程加載完畢再釋放

lock = threading.Lock()if lock.acquire():    if pxy_queue.empty():        await load_proxy()    lock.release()

有點類似於執行完sleep(100)後再次初始化執行從而將代理更新進去,直到結束exit掉進程

while True:    main()    time.sleep(100)

3.3 驗證代理

使用代理一般會有以下幾種情況

無法建立連接

可以連接,請求超時

正常返回(包括200,30x,40x,50x)

你看到的未必是真的,先來看個例子

明明有資源,代理卻給你來個404,為了解決這個問題,在採集前先對代理進行首次測試,通過了再次使用

if pxy['test']:    data = get_html(test_url, pxy)    if data['status']==200 and test_txt in data['html']:        pass    else:        return

3.4 保持代理

代理較少時,通過時間延時使代理隔30~60秒去訪問一次目標,這樣就不會觸發攔截。

另外一種情況代理的穩定性較差,代理數量較少的情況,可以通過計數的方式維持代理,比如:一個代理連續三次出現不可連接或超時再做排除,成功返回從新計算,不至於一下子把代理全部幹掉了。

3.5 快速啟動

中途中斷重啟腳本,為了將已經獲取的和已經排除的目標id快速去除,可以一次性查詢出來用集合差獲取未完成的任務id,舉個慄子:

pid_set  = set([r['pid'] for r in res]) if res else set()task_set = set([r for r in range(1, max_pid)])task_set = task_set - pid_set

差集s-t的時間複雜度是O(len(s)),這比使用for和x in s再append()要快許多。

3.6 異常記錄

腳本運行在可能出錯的地方加try…except,在logging記錄時加exc_info參數記錄異常信息到日誌,以便分析

try:    passexcept:    logger.error(sql, exc_info=True)

0×04 案例演示

腦補完畢,我以採集Freebuf.COM所有文章為例

4.1 分析

在FB首頁展現文章的連結都是經過分類的,像這樣:

這就不太符合爬蟲可遍歷規則,畢竟不知道文章id屬於哪個分類,總不能把分頁爬一次再抓一次,可再尋找尋找。投過搞的童鞋都知道有個文章預覽功能,它的連結是這樣的:

它並沒有分類,經過測試,已經發表的文章依然可以使用預覽功能,也可以通過文章ID遍歷去提前查看哪些未發表的文章

4.2 提取數據

把HTML源碼GET回來後,那就是提取所要的數據了

使用BeautifulSoup對數據進行查找提取,從一篇文章頁面可以獲得

文章ID

文章作者

文章標題

發表時間

文章分類

是否金幣/現金獎勵

文章主體內容

其他數據按需要而定,數據表結構按以上設計,數據解析部分

soup = BeautifulSoup(html, 'lxml')arct = soup.find(class_='articlecontent')head     = arct.find(class_='title')title    = head.find('h2').get_text().strip()               content  = str(arct.find(id='contenttxt')).strip()          .

4.3 排除錯誤

200、301、302、404、500這幾個一般是常用的,錯誤也有可能是通過200返回的

if status in (301, 302, 404):    return await self.insert_temp(pid, status)if status != 200:    await asyncio.wait([self.pid_queue.put(pid)])    return await self.update_proxy('status', 'status+1', pxy)

4.4 結果分析

採集數據大多情況下是做統計分析,從而得到些什麼有價值的信息,比如哪些文章比較受歡迎、每月發布數量走勢等等。有時還需要過濾掉不需要的數據,比如從抓取的數據中看有大量測試的、未審核的文章

判斷文章是否已經審核/發表的一個簡單技巧是:該文章是否有分類

4.5 可視化展示

有時候數據太多的時候,進行可視化展示更直觀,可以使用Excel,前端D3.js、C3.js等工具生成圖表,以下是抓取到的數據FB歷年每個月發布文章數量展示圖(測試量:105,000,獲取有效數量:16,807,有分類數量::6,750,下圖為有分類文章統計,數據不一定準確,以官方的為準)

0×05 總結

本文只是記錄我的一些想法和實踐,以及碰到的問題採取的一些解決辦法,很多地方可能寫的不完善或不妥,或有更好的解決方案,只有把話題引出來了,才有機會去發現、改進。腳本是前段時間準備離職的時候採用多線程去抓某勾網數據時候寫的,不常寫爬蟲,總會遇到或忽略某些問題。異步方式是寫文章時臨時碼的,寫得也比較簡陋,只是想簡單說明下多代理的一種實現方式,畢竟沒有涉及Cookie、驗證碼、Token、動態解析等內容。

Python,人家用來做科學計算,而我卻用來採集數據…

0×06 參考

本文中涉及的腳本:GitHub,存放在freebuf_spider目錄。

*本文作者:zrools,本文屬FreeBuf黑客與極客(FreeBuf.COM)原創獎勵計劃,未經許可禁止轉載


相關焦點

  • 看幾段爬蟲代碼,詳解Python多線程、多進程、協程
    很多時候我們寫了一個爬蟲,實現了需求後會發現了很多值得改進的地方,其中很重要的一點就是爬取速度。本文就通過代碼講解如何使用多進程、多線程、協程來提升爬取速度。注意:我們不深入介紹理論和原理,一切都在代碼中。首先我們寫一個簡化的爬蟲,對各個功能細分,有意識進行函數式編程。
  • Python爬蟲:單線程、多線程和協程的爬蟲性能對比
    今天我要給大家分享的是如何爬取豆瓣上深圳近期即將上映的電影影訊,並分別用普通的單線程、多線程和協程來爬取,從而對比單線程、多線程和協程在網絡爬蟲中的性能。多線程爬蟲單線程的爬取耗時還是挺長的,下面看看使用多線程的爬取效率:import requestsfrom lxml import etreeimport pandas as pdimport refrom concurrent.futures import
  • 玩大數據一定用得到的19款 Java 開源 Web 爬蟲
    爬蟲主要通過Web用戶界面啟動、監控和調整,允許彈性的定義要獲取的url。Heritrix是按多線程方式抓取的爬蟲,主線程把任務分配給Teo線程(處理線程),每個Teo線程每次處理一個URL。Teo線程對每個URL執行一遍URL處理器鏈。URL處理器鏈包括如下5個處理步驟。(1)預取鏈:主要是做一些準備工作,例如,對處理進行延遲和重新處理,否決隨後的操作。
  • 深度好文 | 了解爬蟲技術方方面面
    涉及到大規模的抓取,一定要有良好的爬蟲設計,一般很多開源的爬蟲框架也都是有限制的,因為中間涉及到很多其他的問題,例如數據結構,重複抓取過濾的問題,當然最重要的是要把帶寬利用滿,所以分布式抓取很重要,這時流程控制就會很重要,分布式最重要的就是多臺機器不同線程的調度和配合,通常會共享一個 url 隊列,然後各個線程通過消息通信,如果想要抓的越多越快,那麼對中間的消息系統的吞吐量要求也越高。
  • Python爬蟲:一些常用的爬蟲技巧總結
    ,python應用最多的場景還是web快速開發、爬蟲、自動化運維:寫過簡單網站、寫過自動發帖腳本、寫過收發郵件腳本、寫過簡單驗證碼識別腳本。IP在開發爬蟲過程中經常會遇到IP被封掉的情況,這時就需要用到代理IP;在urllib2包中有ProxyHandler類,通過此類可以設置代理訪問網頁,如下代碼片段:import urllib2 proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
  • 基於Apify+node+react/vue搭建一個有點意思的爬蟲平臺
    在開始文章之前,我們有必要了解爬蟲的一些應用. 我們一般了解的爬蟲, 多用來爬取網頁數據, 捕獲請求信息, 網頁截圖等,如下圖:當然爬蟲的應用遠遠不止如此,我們還可以利用爬蟲庫做自動化測試, 服務端渲染, 自動化表單提交, 測試谷歌擴展程序, 性能診斷等.
  • 手把手教你寫網絡爬蟲:Web應用的漏洞檢測實戰篇!
    請求將爬蟲分為靜態爬蟲和動態爬蟲。具備Ajax解析能力的動態爬蟲就可以解決這類問題。通常情況下,動態爬蟲通過模擬瀏覽器的方式對待爬取的URL進行渲染,這樣爬取的原始碼就是真正的網頁代碼,數據提取自然更加全面。目前動態爬蟲技術主要有Selenium和pyppeteer。
  • Python 爬蟲:8 個常用的爬蟲技巧總結!
    用python也差不多一年多了,python應用最多的場景還是web快速開發、爬蟲、自動化運維:寫過簡單網站、寫過自動發帖腳本、寫過收發郵件腳本
  • Python——爬蟲
    2、聚焦爬蟲聚焦爬蟲是面向特定主題需求的一種網絡爬蟲程序。它與通用搜尋引擎爬蟲的區別在於:聚焦爬蟲在實施網頁抓取時會對內容進行處理篩選,儘量保證只抓取與需求相關的網頁信息。四、關於爬蟲的工作分析1、初級爬蟲工程師從事初級爬蟲工程師需要我們掌握以下幾點:1)、web前端的知識:HTML,css,js,Ajax,jQuery等;
  • 【必收藏】2020 爬蟲面試題目合集
    對於爬蟲來說, 我們用代理就是為了隱藏自身IP , 防止自身的被封鎖。JSON的本質:是一個字符串,JSON是對JS對象的字符串表達式,它使用文本形式表示一個JS對象的信息。在實際開發中,選擇多線程和多進程應該從具體實際開發來進行選擇。最好是多進程和多線程結合。定義:當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制。線程同步能夠保證多個線程安全訪問「競爭資源」,最簡單的同步機制就是引用互斥鎖。
  • Python爬蟲從入門到精通(3): BeautifulSoup用法總結及多線程爬蟲爬取糗事百科
    我們還會利用requests庫和BeauitfulSoup來爬取糗事百科上的段子, 並對比下單線程爬蟲和多線程爬蟲的爬取效率。什麼是BeautifulSoup及如何安裝BeautifulSoup是一個解析HTML或XML文件的第三方庫。
  • Python爬蟲學習的完整路線推薦
    隨著數據的規模化,爬蟲獲取數據的高效性能越來越突出,能夠做的事情越來越多:·商機發現:招投標情報發現、客戶資料發掘、企業客戶發現等進行爬蟲學習,首先要懂得是網頁,那些我們肉眼可見的光鮮亮麗的網頁是由HTML、css、javascript等網頁源碼所支撐起來的。
  • 網絡爬蟲如何採集Surface Web, Deep Web, Dark Web?
    深網(Deep Web)是指那些存儲在Web站點的資料庫系統、文件裡面的數據,這些信息通常需要通過動態網頁才能訪問到。Invisible Web, Hidden Web 是Deep Web的別稱。暗網(Dark Web)包含那些故意隱藏的信息和網站,並且無法通過我們每天使用的瀏覽器訪問,通常只能通過特殊的軟體和特定的URL進入。
  • Python協程與異步編程超全總結
    協程:又稱為微線程,在一個線程中執行,執行函數時可以隨時中斷,由程序(用戶)自身控制,執行效率極高,與多線程比較,沒有切換線程的開銷和多線程鎖機制。Python中異步IO操作是通過asyncio來實現的。異步IO異步IO的asyncio庫使用事件循環驅動的協程實現並發。
  • Python視頻教程網課編程零基礎入門數據分析網絡爬蟲全套Python...
    php 7動態網站基礎,數據持久化和mysql 8web實戰方糖簡歷原生php版本 9web實戰將網站發布到網際網路 10 web實戰用bootstrap搞定樣式 11web進階通過框架貫徹dry原則 12web進階重構和自動化 13前端進階前後端分離 14前端進階 15全平臺開發
  • R語言爬蟲系列6|動態數據抓取範例
    詳情可見推文:R語言爬蟲利器:rvest包+SelectorGadget抓取鏈家杭州二手房數據 但網絡爬蟲這個江湖太險惡,單靠一招rvest行走江湖必然兇多吉少,一不小心碰到什麼AJAX和動態網頁憑僅掌握rvest的各位必定束手無策。本文小編就簡單介紹下在用R語言進行實際的網絡數據抓取時如何將動態數據給弄到手。
  • Python——網絡爬蟲、登錄、代理設置
    在web中,session主要用來在伺服器端存儲特定用戶對象會話所需要的信息。2、cookie和session產生的原因http協議是一個無狀態協議,在特定操作的時候,需要保存信息,進而產生了cookie和session。
  • 異步請求和異步調用有區別?直到看到了7年前的一個問答
    今天就帶大家一起探究一下「異步請求和異步調用」這兩個概念。異步請求和異步調用的區別上面提到的文章中有這樣兩段話來講異步請求和異步調用的區別:區別一:異步請求用來解決並發請求對伺服器造成的壓力,從而提高對請求的吞吐量;而異步調用是用來做一些非主線流程且不需要實時計算和響應的任務,比如同步日誌到kafka中做日誌分析等。
  • 一名合格的數據分析師分享Python網絡爬蟲二三事 豈安低調分享
    ,利用代理伺服器可以很好處理IP限制問題。)的技術網站中用戶需求的數據如聯繫人列表,可以從獨立於實際網頁的服務端取得並且可以被動態地寫入網頁中。 一般我們程序是單線程運行,但多線程可以充分利用資源,優化爬蟲效率。實際上Python 中的多線程並行化並不是真正的並行化,但是多線程在一定程度上還是能提高爬蟲的執行效率,下面我們就針對單線程和多線程進行時間上的比較。
  • 2020 爬蟲面試題目合集
    阻塞狀態:當運行狀態的線程被阻塞變為阻塞狀態,阻塞狀態的線程就會重新變為就緒狀態才能繼續運行。死亡狀態:線程執行完畢。多線程和多進程優缺點多線程的優點:程序邏輯和控制方式複雜;所有線程可以直接共享內存和變量;線程方式消耗的總資源比進程方式好。