本文經(jīng)作者授權(quán)發(fā)布。
文 | 程柳鋒@Tencent
爬蟲的基本流程
網(wǎng)絡(luò)爬蟲的基本工作流程如下:
首先選取一部分精心挑選的種子 URL
將種子 URL 加入任務(wù)隊(duì)列
從待抓取 URL 隊(duì)列中取出待抓取的 URL,解析 DNS,并且得到主機(jī)的 ip,并將 URL 對應(yīng)的網(wǎng)頁下載下來,存儲進(jìn)已下載網(wǎng)頁庫中。此外,將這些 URL 放進(jìn)已抓取 URL 隊(duì)列。
分析已抓取 URL 隊(duì)列中的 URL,分析其中的其他 URL,并且將 URL 放入待抓取 URL 隊(duì)列,從而進(jìn)入下一個(gè)循環(huán)。
解析下載下來的網(wǎng)頁,將需要的數(shù)據(jù)解析出來。
數(shù)據(jù)持久話,保存至數(shù)據(jù)庫中。
爬蟲的抓取策略
在爬蟲系統(tǒng)中,待抓取 URL 隊(duì)列是很重要的一部分。待抓取 URL 隊(duì)列中的 URL 以什么樣的順序排列也是一個(gè)很重要的問題,因?yàn)檫@涉及到先抓取那個(gè)頁面,后抓取哪個(gè)頁面。而決定這些 URL 排列順序的方法,叫做抓取策略。下面重點(diǎn)介紹幾種常見的抓取策略:
深度優(yōu)先策略(DFS)?
深度優(yōu)先策略是指爬蟲從某個(gè) URL 開始,一個(gè)鏈接一個(gè)鏈接的爬取下去,直到處理完了某個(gè)鏈接所在的所有線路,才切換到其它的線路。?
此時(shí)抓取順序?yàn)椋篈 -> B -> C -> D -> E -> F -> G -> H -> I -> J廣度優(yōu)先策略(BFS)?
寬度優(yōu)先遍歷策略的基本思路是,將新下載網(wǎng)頁中發(fā)現(xiàn)的鏈接直接插入待抓取 URL 隊(duì)列的末尾。也就是指網(wǎng)絡(luò)爬蟲會先抓取起始網(wǎng)頁中鏈接的所有網(wǎng)頁,然后再選擇其中的一個(gè)鏈接網(wǎng)頁,繼續(xù)抓取在此網(wǎng)頁中鏈接的所有網(wǎng)頁。?
此時(shí)抓取順序?yàn)椋篈 -> B -> E -> G -> H -> I -> C -> F -> J -> D
了解了爬蟲的工作流程和爬取策略后,就可以動手實(shí)現(xiàn)一個(gè)爬蟲了!那么在 python 里怎么實(shí)現(xiàn)呢?
技術(shù)棧
requests?人性化的請求發(fā)送
Bloom Filter?布隆過濾器,用于判重
XPath?解析 HTML 內(nèi)容
murmurhash
Anti crawler strategy 反爬蟲策略
MySQL 用戶數(shù)據(jù)存儲
基本實(shí)現(xiàn)
下面是一個(gè)偽代碼
import Queue
initial_page = "https://www.zhihu.com/people/gaoming623"
url_queue = Queue.Queue()
seen = set()
seen.insert(initial_page)
url_queue.put(initial_page)
while(True): #一直進(jìn)行
? ?if url_queue.size()>0:
? ? ? ?current_url = url_queue.get() ? ? ? ? ? ? ?#拿出隊(duì)例中第一個(gè)的 url
? ? ? ?store(current_url) ? ? ? ? ? ? ? ? ? ? ? ? #把這個(gè) url 代表的網(wǎng)頁存儲好
? ? ? ?for next_url in extract_urls(current_url): #提取把這個(gè) url 里鏈向的 url
? ? ? ? ? ?if next_url not in seen: ? ? ?
? ? ? ? ? ? ? ?seen.put(next_url)
? ? ? ? ? ? ? ?url_queue.put(next_url)
? ?else:
? ? ? ?break
如果你直接加工一下上面的代碼直接運(yùn)行的話,你需要很長的時(shí)間才能爬下整個(gè)知乎用戶的信息,畢竟知乎有 6000 萬月活躍用戶。更別說 Google 這樣的搜索引擎需要爬下全網(wǎng)的內(nèi)容了。那么問題出現(xiàn)在哪里?
布隆過濾器
需要爬的網(wǎng)頁實(shí)在太多太多了,而上面的代碼太慢太慢了。設(shè)想全網(wǎng)有 N 個(gè)網(wǎng)站,那么分析一下判重的復(fù)雜度就是 N*log(N),因?yàn)樗芯W(wǎng)頁要遍歷一次,而每次判重用 set 的話需要 log(N) 的復(fù)雜度。OK,我知道 python 的 set 實(shí)現(xiàn)是 hash——不過這樣還是太慢了,至少內(nèi)存使用效率不高。
通常的判重做法是怎樣呢?Bloom Filter. 簡單講它仍然是一種 hash 的方法,但是它的特點(diǎn)是,它可以使用固定的內(nèi)存(不隨 url 的數(shù)量而增長)以 O(1) 的效率判定 url 是否已經(jīng)在 set 中。可惜天下沒有白吃的午餐,它的唯一問題在于,如果這個(gè) url 不在 set 中,BF 可以 100%確定這個(gè) url 沒有看過。但是如果這個(gè) url 在 set 中,它會告訴你:這個(gè) url 應(yīng)該已經(jīng)出現(xiàn)過,不過我有 2%的不確定性。注意這里的不確定性在你分配的內(nèi)存足夠大的時(shí)候,可以變得很小很少。
# bloom_filter.py
BIT_SIZE = 5000000
class BloomFilter:
? ?def __init__(self):
? ? ? ?# Initialize bloom filter, set size and all bits to 0
? ? ? ?bit_array = bitarray(BIT_SIZE)
? ? ? ?bit_array.setall(0)
? ? ? ?self.bit_array = bit_array
? ?def add(self, url):
? ? ? ?# Add a url, and set points in bitarray to 1 (Points count is equal to hash funcs count.)
? ? ? ?# Here use 7 hash functions.
? ? ? ?point_list = self.get_postions(url)
? ? ? ?for b in point_list:
? ? ? ? ? ?self.bit_array[b] = 1
? ?def contains(self, url):
? ? ? ?# Check if a url is in a collection
? ? ? ?point_list = self.get_postions(url)
? ? ? ?result = True
? ? ? ?for b in point_list:
? ? ? ? ? ?result = result and self.bit_array[b]
? ? ? ?return result
? ?def get_postions(self, url):
? ? ? ?# Get points positions in bit vector.
? ? ? ?point1 = mmh3.hash(url, 41) % BIT_SIZE
? ? ? ?point2 = mmh3.hash(url, 42) % BIT_SIZE
? ? ? ?point3 = mmh3.hash(url, 43) % BIT_SIZE
? ? ? ?point4 = mmh3.hash(url, 44) % BIT_SIZE
? ? ? ?point5 = mmh3.hash(url, 45) % BIT_SIZE
? ? ? ?point6 = mmh3.hash(url, 46) % BIT_SIZE
? ? ? ?point7 = mmh3.hash(url, 47) % BIT_SIZE
? ? ? ?return [point1, point2, point3, point4, point5, point6, point7]
BF 詳細(xì)的原理參考我之前寫的文章:?布隆過濾器(Bloom Filter) 的原理和實(shí)現(xiàn)
建表
用戶有價(jià)值的信息包括用戶名、簡介、行業(yè)、院校、專業(yè)及在平臺上活動的數(shù)據(jù)比如回答數(shù)、文章數(shù)、提問數(shù)、粉絲數(shù)等等。
用戶信息存儲的表結(jié)構(gòu)如下:
CREATE DATABASE `zhihu_user` /*!40100 DEFAULT CHARACTER SET utf8 */;
-- User base information table
CREATE TABLE `t_user` (
?`uid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
?`username` varchar(50) NOT NULL COMMENT '用戶名', ? ? ? ? ? ? ? ? ? ? ?
?`brief_info` varchar(400) ?COMMENT '個(gè)人簡介',
?`industry` varchar(50) COMMENT '所處行業(yè)', ? ? ? ? ? ?
?`education` varchar(50) COMMENT '畢業(yè)院校', ? ? ? ? ? ?
?`major` varchar(50) COMMENT '主修專業(yè)',
?`answer_count` int(10) unsigned DEFAULT 0 COMMENT '回答數(shù)',
?`article_count` int(10) unsigned DEFAULT 0 COMMENT '文章數(shù)',
?`ask_question_count` int(10) unsigned DEFAULT 0 COMMENT '提問數(shù)',
?`collection_count` int(10) unsigned DEFAULT 0 COMMENT '收藏?cái)?shù)',
?`follower_count` int(10) unsigned DEFAULT 0 COMMENT '被關(guān)注數(shù)',
?`followed_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注數(shù)',
?`follow_live_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注直播數(shù)',
?`follow_topic_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注話題數(shù)',
?`follow_column_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注專欄數(shù)',
?`follow_question_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注問題數(shù)',
?`follow_collection_count` int(10) unsigned DEFAULT 0 COMMENT '關(guān)注收藏夾數(shù)',
?`gmt_create` datetime NOT NULL COMMENT '創(chuàng)建時(shí)間', ?
?`gmt_modify` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次編輯', ? ? ? ? ? ?
?PRIMARY KEY (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用戶基本信息表';
網(wǎng)頁下載后通過 XPath 進(jìn)行解析,提取用戶各個(gè)維度的數(shù)據(jù),最后保存到數(shù)據(jù)庫中。
反爬蟲策略應(yīng)對-Headers
一般網(wǎng)站會從幾個(gè)維度來反爬蟲:用戶請求的 Headers,用戶行為,網(wǎng)站和數(shù)據(jù)加載的方式。從用戶請求的 Headers 反爬蟲是最常見的策略,很多網(wǎng)站都會對 Headers 的 User-Agent 進(jìn)行檢測,還有一部分網(wǎng)站會對 Referer 進(jìn)行檢測(一些資源網(wǎng)站的防盜鏈就是檢測 Referer)。
如果遇到了這類反爬蟲機(jī)制,可以直接在爬蟲中添加 Headers,將瀏覽器的 User-Agent 復(fù)制到爬蟲的 Headers 中;或者將 Referer 值修改為目標(biāo)網(wǎng)站域名。對于檢測 Headers 的反爬蟲,在爬蟲中修改或者添加 Headers 就能很好的繞過。
cookies = {
? ?"d_c0": "AECA7v-aPwqPTiIbemmIQ8abhJy7bdD2VgE=|1468847182",
? ?"login": "NzM5ZDc2M2JkYzYwNDZlOGJlYWQ1YmI4OTg5NDhmMTY=|1480901173|9c296f424b32f241d1471203244eaf30729420f0",
? ?"n_c": "1",
? ?"q_c1": "395b12e529e541cbb400e9718395e346|1479808003000|1468847182000",
? ?"l_cap_id": "NzI0MTQwZGY2NjQyNDQ1NThmYTY0MjJhYmU2NmExMGY=|1480901160|2e7a7faee3b3e8d0afb550e8e7b38d86c15a31bc",
? ?"d_c0": "AECA7v-aPwqPTiIbemmIQ8abhJy7bdD2VgE=|1468847182",
? ?"cap_id": "N2U1NmQwODQ1NjFiNGI2Yzg2YTE2NzJkOTU5N2E0NjI=|1480901160|fd59e2ed79faacc2be1010687d27dd559ec1552a"
}
headers = {
? ?"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.3",
? ?"Referer": "https://www.zhihu.com/"
}
r = requests.get(url, cookies = cookies, headers = headers)
反爬蟲策略應(yīng)對-代理 IP 池
還有一部分網(wǎng)站是通過檢測用戶行為,例如同一 IP 短時(shí)間內(nèi)多次訪問同一頁面,或者同一賬戶短時(shí)間內(nèi)多次進(jìn)行相同操作。
大多數(shù)網(wǎng)站都是前一種情況,對于這種情況,使用 IP 代理就可以解決。這樣的代理 ip 爬蟲經(jīng)常會用到,最好自己準(zhǔn)備一個(gè)。有了大量代理 ip 后可以每請求幾次更換一個(gè) ip,這在 requests 或者 urllib2 中很容易做到,這樣就能很容易的繞過第一種反爬蟲。目前知乎已經(jīng)對爬蟲做了限制,如果是單個(gè) IP 的話,一段時(shí)間系統(tǒng)便會提示異常流量,無法繼續(xù)爬取了。因此代理 IP 池非常關(guān)鍵。網(wǎng)上有個(gè)免費(fèi)的代理 IP API:?http://api.xicidaili.com/free2016.txt
import requests
import random
class Proxy:
? ?def __init__(self):
? ? ? ?self.cache_ip_list = []
? ?# Get random ip from free proxy api url.
? ?def get_random_ip(self):
? ? ? ?if not len(self.cache_ip_list):
? ? ? ? ? ?api_url = 'http://api.xicidaili.com/free2016.txt'
? ? ? ? ? ?try:
? ? ? ? ? ? ? ?r = requests.get(api_url)
? ? ? ? ? ? ? ?ip_list = r.text.split('rn')
? ? ? ? ? ? ? ?self.cache_ip_list = ip_list
? ? ? ? ? ?except Exception as e:
? ? ? ? ? ? ? ?# Return null list when caught exception.
? ? ? ? ? ? ? ?# In this case, crawler will not use proxy ip.
? ? ? ? ? ? ? ?print e
? ? ? ? ? ? ? ?return {}
? ? ? ?proxy_ip = random.choice(self.cache_ip_list)
? ? ? ?proxies = {'http': 'http://' ? proxy_ip}
? ? ? ?return proxies
后續(xù)
使用日志模塊記錄爬取日志和錯(cuò)誤日志
分布式任務(wù)隊(duì)列和分布式爬蟲
爬蟲源代碼:zhihu-crawler?下載之后通過 pip 安裝相關(guān)三方包后,運(yùn)行$ python crawler.py 即可(喜歡的幫忙點(diǎn)個(gè) star 哈,同時(shí)也方便看到后續(xù)功能的更新)
運(yùn)行截圖:?
題圖:pexels,CC0 授權(quán)。
點(diǎn)擊閱讀原文,查看更多 Python 教程和資源。
閱讀原文:http://mp.weixin.qq.com/s?timestamp=1497843478&src=3&ver=1&signature=XXQSyCeSJ0xNs*If4kBdACDlNyWpt*WIxVK56I9oVlZaIJ9yTiOCIRmrU1LluyeYYfRk4v0PhP6laAN3TsI4Cs8AkiuPNHUMMSbL41YL2xvUph5-ejFPvpvJ7LtiC1UbPunEfUcMN2znh914fSeYk9kPKwWcTCJHi9Gl9MH31zw=&devicetype=Windows-QQBrowser&version=61030004&pass_ticket=qMx7ntinAtmqhVn+C23mCuwc9ZRyUp20kIusGgbFLi0=&uin=MTc1MDA1NjU1&ascene=1