python爬虫实时更新数据_爬虫的增量式抓取和数据更新
一些想法
頁面爬的多了,量上去了之后,就會遇到其他的問題,其實不管做什么技術量大了都會有問題。一般情況下,我認為解決"大量"問題的思路有兩個:一種是著力于優化系統的能力,讓原本只能一分鐘處理100條的系統提升到一分鐘1000條之類的,在我看來并行、分布式、集群都屬于這個范疇,這種思路下,系統處理的內容沒有變化只是單純的處理速度變快了;另一種是著力于提高系統的工作效率, 比如說降低某算法的復雜度。
爬蟲領域的增量式爬取屬于后者,每種網站都有每種網站的特點。比如說小說連載網站、新聞或者知乎首頁,這里拿知乎時間線舉例,我基本每天醒來和睡覺前都會刷一波知乎,從頭開始看直到看到上次載入的地方,假設我要抓取知乎的數據并保存到本地,不難發現最好的選擇其實是每次只抓取上次沒讀過的新內容,抓評論也是一樣,最優的選擇是每次只抓取在上次抓取之后出現的新評論,然后再進行保存。有的時候,還有另外一種情況,就是原本存在的網頁內容更新了,比如說有人在知乎上修改了他的回答。這時候,我們的爬蟲就需要有分辨這些區別變化的能力。但這幾個都是很簡單的例子,實際情況會復雜很多。
不管是產生新頁面,還是原本的頁面更新,這種變化都被稱為增量, 而爬取過程則被稱為增量爬取。那如何進行增量式的爬取工作呢?回想一下爬蟲的工作流程:
發送URL請求 ----- 獲得響應 ----- 解析內容 ----- 存儲內容
我們可以從幾種思路入手:
在發送請求之前判斷這個URL是不是之前爬取過
在解析內容后判斷這部分內容是不是之前爬取過
寫入存儲介質時判斷內容是不是已經在介質中存在
實現增量式爬取
不難發現,其實增量爬取的核心是去重, 至于去重的操作在哪個步驟起作用,只能說各有利弊,就像我說的,everything is tradeoff。
在我看來,前兩種思路需要根據實際情況取一個(也可能都用)。第一種思路適合不斷有新頁面出現的網站,比如說小說的新章節,每天的最新新聞等等;第二種思路則適合頁面內容會更新的網站。第三個思路是相當于是最后的一道防線。這樣做可以最大程度上達到去重的目的。
去重的方法
最簡單的去重方式自然是將所有訪問過的URL和其對應的內容保存下來,然后過一段時間重新爬取一次并進行比較,然后決定是否需要覆蓋。這顯然是不實際的,因為會消耗很多資源。目前比較實際的做法就是給URL或者其內容(取決于這個網站采用哪種更新方式)上一個標識,這個標識有個比較好聽的名字,叫數據指紋。
這里很容易想到的一種數據指紋就是哈希值,根據哈希函數的特性,我們可以為任意內容生成一個獨一無二的定長字符串,之后只要比較這個哈希值就行了。哈希值是一個很偉大的發明,幾乎在任何地方都有它的影子,它利用數學特性,計算機只要經過簡單的計算就可以得到唯一的特征值,這個計算過程的開銷基本可以忽略不計,當然這是題外話了。
不過即使用了哈希值,你仍需要一個地方存儲所有的哈希值,并且要能做到方便的取用。如果你的存儲介質是數據庫,一般的數據庫系統都能提供索引,如果把哈希值作為唯一索引呢,這應該是可行的。有些數據庫也提供查詢后再插入的操作,不過本質上應該也是索引。和哈希值類似的還有MD5校驗碼,殊途同歸。
除了自建指紋,其實在發送請求時還有一些技巧,比如說304狀態碼,Last-modified字段,文件大小和MD5簽名。具體參考[8],很好理解,就不細說了。
綜上所述,在數據量不大的時候,幾百個或者就幾千個的時候,簡單自己寫個小函數或者利用集合的特性去重就行了。如果數據量夠大,數據指紋的價值就體現出來了,它可以節省可觀的空間,同時可以引入BloomFilter作為去重的手段。另外,如果要對數據做持久化(簡單說就是去重操作不會被事故影響,比如說斷電),就需要用到Redis數據庫。
BloomFilter
布朗過濾器雖然不是因為爬蟲才出現的,但是卻在這種情況下顯得異常有用。布朗過濾器可以通過計算來判斷某項數據是否存在于集合中,它原理和概念可以參考1和英文版的維基百科Bloom filter, 里面有詳細的數學推理,它解釋了為什么布朗會有誤判情況出現,感興趣可以學習一下,并不難。這里只提幾點:
布朗過濾器是有誤判率的,它會把原本不屬于這個集合的數據誤判為屬于,但不會把原本屬于集合的數據誤判為不屬于。
它是一個典型且高效的空間換時間的例子。
它的誤判率是:
\left(1-\left[1-\frac{1}{m}\right]^{kn}\right)^k \approx \left( 1-e^{-kn/m} \right)^k
這里元素的數量n、 過濾容器的大小m(bits)和哈希函數的數量k存在的一定關系,它們三個共同確定了誤判率;同樣如果已知其中兩項,通過調整另外一項也可以達到降低誤判率的目的,具體參見Bloom Filters - the math。
Redis的集合使用
簡單來說,Redis的集合就是Redis數據庫中的集合類型,它具有無序不重復的特點。Python有redis客戶端庫,這里主要涉及到的就是SADD和SISMEMBER命令。下面會具體解釋。
具體實現
BloomFilter
這里我們使用pybloom庫,需要pip或者源碼安裝。pybloom庫用起來非常簡單,這里給兩段最基本的代碼:
from pybloom import BloomFilter
# 新建一個過濾器,長度為m,錯誤率為0.1%
bf = BloomFilter(capacity=1000, error_rate=0.001)
'''
不難理解,這句就相當于
for x in range(bf.capacity):
bd.add(x)
但說實話這種寫法我第一次見到
'''
[bf.add(x) for x in range(bf.capacity)]
print (0 in bf)
print (5 in bf)
print (10 in bf)
# 這里是計算它的錯誤率
count = 0
amount = bf.capacity
for i in range(bf.capacity, bf.capacity + amount + 1):
if i in bf:
count += 1
print ("FP: {:2.4f}".format(count / float(amount)))
我從網上搜到文章大多只是介紹了如何新建一個Filter、怎么add以及查看元素是否屬于這個Filter。實際上,如果閱讀過源碼,其實filter還提供了很多其他方法,同時這個庫還提供了一個可自動擴展的Filter,作者比較推薦后者。
from pybloom import BloomFilter
# 新建
bf1 = BloomFilter(capacity=1000, error_rate=0.001)
bf2 = BloomFilter(capacity=1000, error_rate=0.001)
# 添加
[bf1.add(x) for x in range(3)]
[bf2.add(x) for x in range(3,6)]
# 復制
bf3 = bf1.copy()
# | 操作,三種都行
bf3.union(bf1)
bf3 = bf3 | bf2
bf3 = bf3 or bf2
# & 操作, 三種都行
bf3.intersection(bf1)
bf3 = bf3 & bf1
bf3 = bf3 and bf1
# 成員變量和支持的操作符
len(bf3)
3 in bf3
bf3.capacity
bf3.error_rate
# 也支持tofile和fromfile操作
# 具體的代碼可參照源碼中tests.py中的test_serialization()方法
可擴展的過濾器:
from pybloom import ScalableBloomFilter
# 新建, mode目前只有2種
# SMALL_SET_GROWTH = 2, LARGE_SET_GROWTH = 4
# 前者占內存少但速度慢,后者消耗內存快但速度快
bf1 = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.SMALL_SET_GROWTH)
bf2 = ScalableBloomFilter(initial_capacity=1000, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
# 添加
[bf1.add(x) for x in range(3)]
[bf2.add(x) for x in range(3,6)]
# 兩個屬性(裝飾器)
bf1.capacity
bf1.count
# 成員變量和支持的操作符
len(bf1)
3 in bf1
bf1.initial_capacity
bf1.error_rate
# 也支持tofile和fromfile操作
# 具體的代碼可參照源碼中tests.py中的test_serialization()方法
這里我建議看下這個庫源碼,核心部分差不多500行,里面很多寫法很值得學習,而且都很容易理解。里面也涉及到了如何選取哈希函數。
Redis
Python的Redis客戶端庫也是開源的,地址是:redis-py。不過在開始之前,你首先需要一個有Redis數據庫運行的主機(搭建一個很簡單)。
這里需要解釋不少東西,首先,上文中有一節本來不叫“Redis的集合”而是叫“Redis集合”,我一開始以為這是一種名叫Redis的特殊集合,然后這個集合帶有不可插入重復內容的特性,事實上這里大錯特錯了。還記得我們的初衷是“去重”,實際上,包括Python在內的很多語言已經實現了具有無序不重復特性的內置數據結構:集合(Set)。也就是說從去重這點看的話,有集合這種數據結構就夠了,跟Redis并沒有什么關系。
那么Redis是什么?它是一種數據庫,它的官網是這樣描述的:
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.
關于Redis數據庫還有幾個關鍵詞:key-value,高性能,數據持久化,數據備份,原子操作以及跟這里相關的一個特性:支持集合數據類型。這才是為什么做增量爬取時我們要用到Redis數據庫:我們可以通過將URL或者頁面內容的指紋作為key存入Redis數據庫中的集合里,利用集合的不重復性達到去重的目的,每次爬蟲要處理URL或者頁面時會先去Redis數據庫里檢查一下是否已經存在,因為Redis數據庫著力于key-value形式的存儲,所以這一步的速度將會很可觀;其次Redis可以將內存中的內容持久化到磁盤,并且其每一次操作都是原子操作,這就保證了爬蟲的可靠性,即爬蟲不會應為意外停止而損失數據。
說了這么多,現在就能知道為什么這里要用到Redis的集合。如果只考慮本文相關的內容,那么和本文有關的Redis數據庫操作命令只有兩個:SADD和SISMEMBER,前者可以向集合中插入一條數據,成功返回1,失敗返回0;后者可以查詢某元素是否在集合中存在,存在返回1,不存在返回0。
我在一臺虛擬機Ubuntu-14.04上安裝了Redis數據庫并配置了遠程連接,客戶端測試如下:
>>> import redis
>>> r = redis.StrictRedis(host='192.168.153.131', port=6379, db=0)
>>> r.sadd('1','aa')
1
>>> r.sadd('1','aa')
0
>>> r.sadd('2','aa')
1
>>> r.sadd('3','aa')
1
>>> r.sismember('1','aa')
True
>>> r.sismember('1','b')
False
>>>
但應該如何將這一特性融入到爬蟲中呢?如果是自己寫的爬蟲代碼,添加上述代碼即可;如果使用的是scrapy框架,我們可以在middleware上下功夫,在spider模塊收到要處理的URL時,寫一個Spider中間件用來判斷這條URL的指紋是否在Redis數據庫中存在,如果存在的話,就直接舍棄這條URL;如果是要判斷頁面的內容是否更新,可以在Download中間件中添加代碼來校驗,原理一樣。當然,數據庫的操作可以用類似write()和query()的方法進行封裝,此處不表。
參考
總結
以上是生活随笔為你收集整理的python爬虫实时更新数据_爬虫的增量式抓取和数据更新的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java狐狸游戏_Java继承
- 下一篇: Python爬虫抓取动态数据