本文經作者授權發布。
文 | 程柳鋒@Tencent
爬蟲的基本流程
網絡爬蟲的基本工作流程如下:
首先選取一部分精心挑選的種子 URL
將種子 URL 加入任務隊列
從待抓取 URL 隊列中取出待抓取的 URL,解析 DNS,并且得到主機的 ip,并將 URL 對應的網頁下載下來,存儲進已下載網頁庫中。此外,將這些 URL 放進已抓取 URL 隊列。
分析已抓取 URL 隊列中的 URL,分析其中的其他 URL,并且將 URL 放入待抓取 URL 隊列,從而進入下一個循環。
解析下載下來的網頁,將需要的數據解析出來。
數據持久話,保存至數據庫中。
爬蟲的抓取策略
在爬蟲系統中,待抓取 URL 隊列是很重要的一部分。待抓取 URL 隊列中的 URL 以什么樣的順序排列也是一個很重要的問題,因為這涉及到先抓取那個頁面,后抓取哪個頁面。而決定這些 URL 排列順序的方法,叫做抓取策略。下面重點介紹幾種常見的抓取策略:
深度優先策略(DFS)?
深度優先策略是指爬蟲從某個 URL 開始,一個鏈接一個鏈接的爬取下去,直到處理完了某個鏈接所在的所有線路,才切換到其它的線路。?
此時抓取順序為:A -> B -> C -> D -> E -> F -> G -> H -> I -> J廣度優先策略(BFS)?
寬度優先遍歷策略的基本思路是,將新下載網頁中發現的鏈接直接插入待抓取 URL 隊列的末尾。也就是指網絡爬蟲會先抓取起始網頁中鏈接的所有網頁,然后再選擇其中的一個鏈接網頁,繼續抓取在此網頁中鏈接的所有網頁。?
此時抓取順序為:A -> B -> E -> G -> H -> I -> C -> F -> J -> D
了解了爬蟲的工作流程和爬取策略后,就可以動手實現一個爬蟲了!那么在 python 里怎么實現呢?
技術棧
requests?人性化的請求發送
Bloom Filter?布隆過濾器,用于判重
XPath?解析 HTML 內容
murmurhash
Anti crawler strategy 反爬蟲策略
MySQL 用戶數據存儲
基本實現
下面是一個偽代碼
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): #一直進行
? ?if url_queue.size()>0:
? ? ? ?current_url = url_queue.get() ? ? ? ? ? ? ?#拿出隊例中第一個的 url
? ? ? ?store(current_url) ? ? ? ? ? ? ? ? ? ? ? ? #把這個 url 代表的網頁存儲好
? ? ? ?for next_url in extract_urls(current_url): #提取把這個 url 里鏈向的 url
? ? ? ? ? ?if next_url not in seen: ? ? ?
? ? ? ? ? ? ? ?seen.put(next_url)
? ? ? ? ? ? ? ?url_queue.put(next_url)
? ?else:
? ? ? ?break
如果你直接加工一下上面的代碼直接運行的話,你需要很長的時間才能爬下整個知乎用戶的信息,畢竟知乎有 6000 萬月活躍用戶。更別說 Google 這樣的搜索引擎需要爬下全網的內容了。那么問題出現在哪里?
布隆過濾器
需要爬的網頁實在太多太多了,而上面的代碼太慢太慢了。設想全網有 N 個網站,那么分析一下判重的復雜度就是 N*log(N),因為所有網頁要遍歷一次,而每次判重用 set 的話需要 log(N) 的復雜度。OK,我知道 python 的 set 實現是 hash——不過這樣還是太慢了,至少內存使用效率不高。
通常的判重做法是怎樣呢?Bloom Filter. 簡單講它仍然是一種 hash 的方法,但是它的特點是,它可以使用固定的內存(不隨 url 的數量而增長)以 O(1) 的效率判定 url 是否已經在 set 中。可惜天下沒有白吃的午餐,它的唯一問題在于,如果這個 url 不在 set 中,BF 可以 100%確定這個 url 沒有看過。但是如果這個 url 在 set 中,它會告訴你:這個 url 應該已經出現過,不過我有 2%的不確定性。注意這里的不確定性在你分配的內存足夠大的時候,可以變得很小很少。
# 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 詳細的原理參考我之前寫的文章:?布隆過濾器(Bloom Filter) 的原理和實現
建表
用戶有價值的信息包括用戶名、簡介、行業、院校、專業及在平臺上活動的數據比如回答數、文章數、提問數、粉絲數等等。
用戶信息存儲的表結構如下:
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 '個人簡介',
?`industry` varchar(50) COMMENT '所處行業', ? ? ? ? ? ?
?`education` varchar(50) COMMENT '畢業院校', ? ? ? ? ? ?
?`major` varchar(50) COMMENT '主修專業',
?`answer_count` int(10) unsigned DEFAULT 0 COMMENT '回答數',
?`article_count` int(10) unsigned DEFAULT 0 COMMENT '文章數',
?`ask_question_count` int(10) unsigned DEFAULT 0 COMMENT '提問數',
?`collection_count` int(10) unsigned DEFAULT 0 COMMENT '收藏數',
?`follower_count` int(10) unsigned DEFAULT 0 COMMENT '被關注數',
?`followed_count` int(10) unsigned DEFAULT 0 COMMENT '關注數',
?`follow_live_count` int(10) unsigned DEFAULT 0 COMMENT '關注直播數',
?`follow_topic_count` int(10) unsigned DEFAULT 0 COMMENT '關注話題數',
?`follow_column_count` int(10) unsigned DEFAULT 0 COMMENT '關注專欄數',
?`follow_question_count` int(10) unsigned DEFAULT 0 COMMENT '關注問題數',
?`follow_collection_count` int(10) unsigned DEFAULT 0 COMMENT '關注收藏夾數',
?`gmt_create` datetime NOT NULL COMMENT '創建時間', ?
?`gmt_modify` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次編輯', ? ? ? ? ? ?
?PRIMARY KEY (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用戶基本信息表';
網頁下載后通過 XPath 進行解析,提取用戶各個維度的數據,最后保存到數據庫中。
反爬蟲策略應對-Headers
一般網站會從幾個維度來反爬蟲:用戶請求的 Headers,用戶行為,網站和數據加載的方式。從用戶請求的 Headers 反爬蟲是最常見的策略,很多網站都會對 Headers 的 User-Agent 進行檢測,還有一部分網站會對 Referer 進行檢測(一些資源網站的防盜鏈就是檢測 Referer)。
如果遇到了這類反爬蟲機制,可以直接在爬蟲中添加 Headers,將瀏覽器的 User-Agent 復制到爬蟲的 Headers 中;或者將 Referer 值修改為目標網站域名。對于檢測 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)
反爬蟲策略應對-代理 IP 池
還有一部分網站是通過檢測用戶行為,例如同一 IP 短時間內多次訪問同一頁面,或者同一賬戶短時間內多次進行相同操作。
大多數網站都是前一種情況,對于這種情況,使用 IP 代理就可以解決。這樣的代理 ip 爬蟲經常會用到,最好自己準備一個。有了大量代理 ip 后可以每請求幾次更換一個 ip,這在 requests 或者 urllib2 中很容易做到,這樣就能很容易的繞過第一種反爬蟲。目前知乎已經對爬蟲做了限制,如果是單個 IP 的話,一段時間系統便會提示異常流量,無法繼續爬取了。因此代理 IP 池非常關鍵。網上有個免費的代理 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
后續
使用日志模塊記錄爬取日志和錯誤日志
分布式任務隊列和分布式爬蟲
爬蟲源代碼:zhihu-crawler?下載之后通過 pip 安裝相關三方包后,運行$ python crawler.py 即可(喜歡的幫忙點個 star 哈,同時也方便看到后續功能的更新)
運行截圖:?
題圖:pexels,CC0 授權。
點擊閱讀原文,查看更多 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