大規模非同步新聞爬蟲:實現一個同步定向新聞爬蟲

王平發表於2018-12-03

前面,我們先寫了一個簡單的百度新聞爬蟲,可是它槽點滿滿。接著,我們實現了一些模組,來為我們的爬蟲提供基礎功能,包括:網路請求、網址池、MySQL封裝。

有了這些基礎模組,我們的就可以實現一個更通用化的新聞爬蟲了。為什麼要加“定向”這個修飾詞呢?因為我們的爬蟲不是漫無目的的廣撒網(廣撒網給我們帶來的伺服器、頻寬壓力是指數級增長的),而是在我們規定的一個範圍內進行爬取。

用python實現一個同步定向新聞爬蟲

這個範圍如何規定呢?我們稱之為:hub列表。在實現網址池的到時候,我們簡單介紹了hub頁面是什麼,這裡我們再簡單定義一下它:hub頁面就是含有大量新聞連結、不斷更新的網頁。我們收集大量不同新聞網站的hub頁面組成一個列表,並配置給新聞爬蟲,也就是我們給爬蟲規定了抓取範圍:host跟hub列表裡面提到的host一樣的新聞我們才抓。這樣可以有些控制爬蟲只抓我們感興趣的新聞而不跑偏亂抓一氣。

這裡要實現的新聞爬蟲還有一個定語“同步”,沒錯,這次實現的是同步機制下的爬蟲。後面會有非同步爬蟲的實現。

同步和非同步的思維方式不太一樣,同步的邏輯更清晰,所以我們先把同步爬蟲搞清楚,後面再實現非同步爬蟲就相對簡單些,同時也可以對比同步和非同步兩種不同機制下爬蟲的抓取效率。

這個爬蟲需要用到MySQL資料庫,在開始寫爬蟲之前,我們要簡單設計一下資料庫的表結構:

1. 資料庫設計

建立一個名為crawler的資料庫,並建立爬蟲需要的兩個表:

crawler_hub :此表用於儲存hub頁面的url

+------------+------------------+------+-----+-------------------+----------------+
| Field      | Type             | Null | Key | Default           | Extra          |
+------------+------------------+------+-----+-------------------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL              | auto_increment |
| url        | varchar(64)      | NO   | UNI | NULL              |                |
| created_at | timestamp        | NO   |     | CURRENT_TIMESTAMP |                |
+------------+------------------+------+-----+-------------------+----------------+

建立該表的語句就是:

CREATE TABLE `crawler_hub` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `url` varchar(64) NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `url` (`url`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

對url欄位建立唯一索引,可以防止重複插入相同的url。

crawler_html :此表儲存html內容
html是大量的文字內容,壓縮儲存會大大減少磁碟使用量。這裡,我們選用lzma壓縮演算法。表的結構如下:

+------------+---------------------+------+-----+-------------------+----------------+
| Field      | Type                | Null | Key | Default           | Extra          |
+------------+---------------------+------+-----+-------------------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL              | auto_increment |
| urlhash    | bigint(20) unsigned | NO   | UNI | NULL              |                |
| url        | varchar(512)        | NO   |     | NULL              |                |
| html_lzma  | longblob            | NO   |     | NULL              |                |
| created_at | timestamp           | YES  |     | CURRENT_TIMESTAMP |                |
+------------+---------------------+------+-----+-------------------+----------------+

建立該表的語句為:

CREATE TABLE `crawler_html` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `urlhash` bigint(20) unsigned NOT NULL COMMENT 'farmhash',
  `url` varchar(512) NOT NULL,
  `html_lzma` longblob NOT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `urlhash` (`urlhash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

該表中,我們儲存了url的64位的farmhash,並對這個urlhash建立了唯一索引。id型別為無符號的bigint,也就是2的64次方,足夠放下你能抓取的網頁。

farmhash是Google開源的一個hash演算法。64位的hash空間有2的64次方那麼大,大到隨意把url對映為一個64位無符號整數,也不會出現hash碰撞。老猿使用它多年也未發現hash碰撞的問題。

由於上傳到pypi時,farmhash這個包名不能用,就以pyfarmhash上傳到pypi上了,所以要安裝farmhash的python包,應該是:

pip install pyfarmhash

(多謝評論的朋友反饋這個問題。)

資料庫建立好後,我們就可以開始寫爬蟲的程式碼了。

2. 新聞爬蟲的程式碼實現

#!/usr/bin/env python3
# Author: veelion

import urllib.parse as urlparse
import lzma
import farmhash
import traceback


from ezpymysql import Connection
from urlpool import UrlPool
import functions as fn
import config

class NewsCrawlerSync:
    def __init__(self, name):
        self.db = Connection(
            config.db_host,
            config.db_db,
            config.db_user,
            config.db_password
        )
        self.logger = fn.init_file_logger(name + '.log')
        self.urlpool = UrlPool(name)
        self.hub_hosts = None
        self.load_hubs()

    def load_hubs(self,):
        sql = 'select url from crawler_hub'
        data = self.db.query(sql)
        self.hub_hosts = set()
        hubs = []
        for d in data:
            host = urlparse.urlparse(d['url']).netloc
            self.hub_hosts.add(host)
            hubs.append(d['url'])
        self.urlpool.set_hubs(hubs, 300)

    def save_to_db(self, url, html):
        urlhash = farmhash.hash64(url)
        sql = 'select url from crawler_html where urlhash=%s'
        d = self.db.get(sql, urlhash)
        if d:
            if d['url'] != url:
                msg = 'farmhash collision: %s <=> %s' % (url, d['url'])
                self.logger.error(msg)
            return True
        if isinstance(html, str):
            html = html.encode('utf8')
        html_lzma = lzma.compress(html)
        sql = ('insert into crawler_html(urlhash, url, html_lzma) '
               'values(%s, %s, %s)')
        good = False
        try:
            self.db.execute(sql, urlhash, url, html_lzma)
            good = True
        except Exception as e:
            if e.args[0] == 1062:
                # Duplicate entry
                good = True
                pass
            else:
                traceback.print_exc()
                raise e
        return good

    def filter_good(self, urls):
        goodlinks = []
        for url in urls:
            host = urlparse.urlparse(url).netloc
            if host in self.hub_hosts:
                goodlinks.append(url)
        return goodlinks

    def process(self, url, ishub):
        status, html, redirected_url = fn.downloader(url)
        self.urlpool.set_status(url, status)
        if redirected_url != url:
            self.urlpool.set_status(redirected_url, status)
        # 提取hub網頁中的連結, 新聞網頁中也有“相關新聞”的連結,按需提取
        if status != 200:
            return
        if ishub:
            newlinks = fn.extract_links_re(redirected_url, html)
            goodlinks = self.filter_good(newlinks)
            print("%s/%s, goodlinks/newlinks" % (len(goodlinks), len(newlinks)))
            self.urlpool.addmany(goodlinks)
        else:
            self.save_to_db(redirected_url, html)

    def run(self,):
        while 1:
            urls = self.urlpool.pop(5)
            for url, ishub in urls.items():
                self.process(url, ishub)


if __name__ == '__main__':
    crawler = NewsCrawlerSync('yuanrenxyue')
    crawler.run()

3. 新聞爬蟲的實現原理

上面程式碼就是在基礎模組的基礎上,實現的完整的新聞爬蟲的程式碼。

它的流程大致如下圖所示:

新聞爬蟲流程圖

我們把爬蟲設計為一個類,類在初始化時,連線資料庫,初始化logger,建立網址池,載入hubs並設定到網址池。

爬蟲開始執行的入口就是run(),它是一個while迴圈,設計為永不停息的爬。先從網址池獲取一定數量的url,然後對每個url進行處理,

處理url也就是實施抓取任務的是process(),它先透過downloader下載網頁,然後在網址池中設定該url的狀態。接著,從下載得到的html提取網址,並對得到的網址進行過濾(filter_good()),過濾的原則是,它們的host必須是hubs的host。最後把下載得到的html儲存到資料。

執行這個新聞爬蟲很簡單,生成一個NewsCrawlerSync的物件,然後呼叫run()即可。當然,在執行之前,要先在config.py裡面配置MySQL的使用者名稱和密碼,也要在crawler_hub表裡面新增幾個hub網址才行。

##思考題: 如何收集大量hub列表

比如,我想要抓新浪新聞 news.sina.com.cn , 其首頁是一個hub頁面,但是,如何透過它獲得新浪新聞更多的hub頁面呢?小猿們不妨思考一下這個問題,並用程式碼來實現一下。

這個時候已經抓取到很多網頁了,但是怎麼抽取網頁裡的文字呢?
下一篇我們講:
網頁正文抽取

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章