網絡爬蟲的基本工做流程以下:python
首先選取一部分精心挑選的種子 URLgit
將種子 URL 加入任務隊列github
從待抓取 URL 隊列中取出待抓取的 URL,解析 DNS,而且獲得主機的 ip,並將 URL 對應的網頁下載下來,存儲進已下載網頁庫中。此外,將這些 URL 放進已抓取 URL 隊列。sql
分析已抓取 URL 隊列中的 URL,分析其中的其餘 URL,而且將 URL 放入待抓取 URL 隊列,從而進入下一個循環。數據庫
解析下載下來的網頁,將須要的數據解析出來。api
數據持久話,保存至數據庫中。瀏覽器
在爬蟲系統中,待抓取 URL 隊列是很重要的一部分。待抓取 URL 隊列中的 URL 以什麼樣的順序排列也是一個很重要的問題,由於這涉及到先抓取那個頁面,後抓取哪一個頁面。而決定這些 URL 排列順序的方法,叫作抓取策略。下面重點介紹幾種常見的抓取策略:微信
深度優先策略(DFS)
深度優先策略是指爬蟲從某個 URL 開始,一個連接一個連接的爬取下去,直處處理完了某個連接所在的全部線路,才切換到其它的線路。
此時抓取順序爲:A -> B -> C -> D -> E -> F -> G -> H -> I -> Jcookie
廣度優先策略(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 的 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,這在 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 哈,同時也方便看到後續功能的更新)
運行截圖:
[1] Python 編寫知乎爬蟲實踐
[2] zhihu-crawler
https://github.com/cpselvis/zhihu-crawler
[3] 如何構建一個分佈式爬蟲:實戰篇
[4] Python爬蟲開源項目代碼,爬取微信、淘寶、豆瓣、知乎、新浪微博、QQ、去哪網等
https://mp.weixin.qq.com/s/IS9kNt_jqfbjtz48V1zMZA
[5] 什麼車最適合跑滴滴——數據化思惟小記
[6] 爬蟲平臺的架構實現和框架的選型
https://mp.weixin.qq.com/s/hRKHpZFZWwPW8bN5VMWGkw
[7] puppeteer爬蟲的奇妙之旅