天天看點

Python編寫知乎爬蟲實踐

Python編寫知乎爬蟲實踐

爬蟲的基本流程

Python編寫知乎爬蟲實踐

網絡爬蟲的基本工作流程如下:

首先選取一部分精心挑選的種子url

将種子url加入任務隊列

從待抓取url隊列中取出待抓取的url,解析dns,并且得到主機的ip,并将url對應的網頁下載下傳下來,存儲進已下載下傳網頁庫中。此外,将這些url放進已抓取url隊列。

分析已抓取url隊列中的url,分析其中的其他url,并且将url放入待抓取url隊列,進而進入下一個循環。

解析下載下傳下來的網頁,将需要的資料解析出來。

資料持久話,儲存至資料庫中。

爬蟲的抓取政策

在爬蟲系統中,待抓取url隊列是很重要的一部分。待抓取url隊列中的url以什麼樣的順序排列也是一個很重要的問題,因為這涉及到先抓取那個頁面,後抓取哪個頁面。而決定這些url排列順序的方法,叫做抓取政策。下面重點介紹幾種常見的抓取政策:

Python編寫知乎爬蟲實踐

深度優先政策(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 

        result = true 

            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", 

    "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: 

        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('\r\n') 

                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哈,同時也友善看到後續功能的更新)

運作截圖:

Python編寫知乎爬蟲實踐

作者:cpselvis

來源:51cto