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