今天轉發一篇文章關於爬蟲的,用python寫的,思路挺好的,如果感興趣的話,請私下告訴下我,我看情況,是否發下關於爬蟲的基礎入門的。
在採集數據的時候,經常會碰到有反採集策略規則的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 異步IOasyncio是在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)原創獎勵計劃,未經許可禁止轉載