前面,我們先寫了一個簡單的百度新聞爬蟲,可是它槽點滿滿。接著,我們實現了一些模組,來為我們的爬蟲提供基礎功能,包括:網路請求、網址池、MySQL封裝。
有了這些基礎模組,我們的就可以實現一個更通用化的新聞爬蟲了。為什麼要加“定向”這個修飾詞呢?因為我們的爬蟲不是漫無目的的廣撒網(廣撒網給我們帶來的伺服器、頻寬壓力是指數級增長的),而是在我們規定的一個範圍內進行爬取。
這個範圍如何規定呢?我們稱之為: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頁面呢?小猿們不妨思考一下這個問題,並用程式碼來實現一下。
這個時候已經抓取到很多網頁了,但是怎麼抽取網頁裡的文字呢?
下一篇我們講:
網頁正文抽取
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***