Python3學習筆記2:簡易Web爬蟲

開發環境

基礎語法那章的內容我是在Docker容器中玩的,可是真正作項目的時候,沒有IDE的強大輔助功能來協助的話是很累人的一件事。所以本文中,我選擇使用JetbrainPycharm這個IDE來開發、調試代碼。IDE的好處多多,好比:html

  • 強大的智能提示
  • 強大的斷點調試
  • 性能追蹤
  • 方便好用的各類插件
  • 各類自定義配置


需求

爲了實踐Python,最早想到的就是要完成一個爬蟲程序,大概需求以下:python

requirements.jpg


實施

可配置化我自己是計劃經過DI(Dependency Injection)這個技術來完成,不過查了下資料,因爲Python和其餘語言不太同樣,Python是能夠多父類繼承,而且遵循Duck Typing原則,所以DI在Python中並不實用(Python也是沒有Interface概念的)。但能夠經過以下方式實現相似的邏輯:git

# 假設a-class-name這個類包含在xxx.py文件中,首先引入這個文件中的內容
from xxx import *
# 而後執行如下這行代碼,這將初始化一個a-class-name類的實例
(lambda x: globals()[x])('a-class-name')


入口程序文件main.py

main.py主要有幾個功能:github

  • 經過交互讓用戶輸入:項目名稱、網站首頁、線程數三個初始化變量
  • 初始化數據庫訪問對象
  • 初始化爬蟲對象
  • 初始化線程池
  • 執行程序


核心代碼以下:數據庫

from db_queue import *
...

def execute():
    ...

    (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)(home_page, project_name + '_pages')
    Spider(project_name, home_page, DomainHelpers.get_domain_name(home_page), project_settings.HTML_RESOLVER_NAME)

    worker = Worker(thread_count, project_name)
    worker.create_threads()
    worker.crawl()

execute()

邏輯解釋:dom

  1. (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)(home_page, project_name + '_pages'),本例中DB_CLASS_NAME = 'MongoDbQueue',所以Python將在當前頁面的應用中查找名爲MongoDbQueue的類來執行初始化並傳入構造函數的參數:home_pageproject_name + '_pages'
  2. 初始化Spider類,以便在線程中執行爬取頁面
  3. 初始化指定數量的現成做爲線程池以備後續使用,main.py執行完畢,線程將被自動回收
  4. 開始執行爬蟲程序


線程建立類worker.py文件

from db_queue import *


class Worker:
    ...
    def __init__(self, thread_count, project_name):
        Worker.DB = (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)
        ...

    def create_threads(self):
        for _ in range(self.thread_count):
            t = threading.Thread(target=self.__run_thread)
            t.daemon = True
            t.start()

    def __run_thread(self):
        while True:
            url = self.queue.get()
            Spider.crawl_page(threading.current_thread().name, url)
            self.queue.task_done()

    def __create_jobs(self):
        for link in Worker.DB.get_pending_queue():
            self.queue.put(link)
        self.queue.join()
        self.crawl()

    def crawl(self):
        urls = Worker.DB.get_pending_queue()
        if len(urls) > 0:
            self.__create_jobs()

邏輯解釋:ide

  1. __init__中將數據庫鏈接類保存到全局變量DB中
  2. create_threads將初始化指定數量的線程數,設置爲datmon=true以便線程被建立以後一直存在,隨時能夠被調用
  3. crawl將獲取待爬列表以後,將其放入Spider所需的待爬隊列中
  4. self.queue.join()是用來阻塞隊列,這樣隊列中的每一項都將只被調用一次
  5. __run_thread__create_jobs這兩個方法是Worker內部調用的方法,不須要公開給其餘人,所以加上前綴__(兩個下劃線)


數據庫操做基礎類

因爲須要將數據庫操做作成可替換,所以必須實現數據庫操做的接口,而Python沒有Interface,可是可使用abc(Abstract Based Class)來實現相似於Interface所需的功能。函數

代碼以下:性能

from abc import ABCMeta, abstractmethod

class DbBase(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, file_name):
        pass

    @staticmethod
    @abstractmethod
    def get_pending_queue():
        pass

    @staticmethod
    @abstractmethod
    def is_page_in_queue():
        pass

    @staticmethod
    @abstractmethod
    def save_pending_queue():
        pass

    @staticmethod
    @abstractmethod
    def set_page_crawled():
        pass

邏輯解釋:網站

  1. class DbBase(metaclass=ABCMeta)表示DbBase類的元類爲ABCMeta
  2. @abstractmethod則代表該方法在繼承了DbBase的類中必須被實現,若是沒有被實現,執行時將會報錯:TypeError: Can't instantiate abstract class XXXX with abstract methods xxxx


數據庫存儲操做db_queue.py文件

from pymongo import *
from abc_base.db_base import DbBase
...

class MongoDbQueue(DbBase):

    def __init__(self, home_page, tbl_name='pages'):
        ...

        MongoDbQueue.db = MongoClient(project_settings.DB_CONNECTION_STRING)[project_settings.DB_REPOSITORY_NAME]

        ...
        
        # create unique index
        MongoDbQueue.db[MongoDbQueue.tbl_name].create_index('url', unique=True)

    @staticmethod
    def get_pending_queue():
        ...

    @staticmethod
    def is_page_in_queue(url):
        ...

    @staticmethod
    def save_pending_queue(urls):
        ...

    @staticmethod
    def set_page_crawled(url):
        ...

邏輯解釋:

  1. class MongoDbQueue(DbBase):表示該類繼承了DbBase,所以必須實現DbBase中定義的幾個方法__init__get_pending_queueis_page_in_queuesave_pending_queueset_page_crawled
  2. 爲了確保相同的url絕對不會重複,在數據庫層也增長一個Unique Index以便從數據庫層面也作好驗證
  3. get_pending_queue將全部未被爬過的頁面列表返回
  4. is_page_in_queue判斷是否頁面在待爬列表中
  5. save_pending_queue,這個方法是在爬取某個頁面,抓取了該頁面上全部新的代碼連接以後,將數據庫中不存在的鏈接保存爲待爬頁面
  6. set_page_crawled,這個方法將數據庫中已存在,且狀態爲未爬過的頁面,設置爲已爬,該方法將在爬蟲爬好某個頁面以後被調用


爬蟲文件spider.py文件

...
class Spider:
    ...

    def __init__(self, base_url, domain_name, html_resolver):
        ...
        Spider.crawl_page('First spider', Spider.BASE_URL)

    @staticmethod
    def crawl_page(thread_name, page_url):
        if Spider.DB.is_page_in_queue(page_url):
            ...
            urls = Spider.add_links_to_queue(Spider.gather_links(page_url))
            Spider.DB.save_pending_queue(urls)
            Spider.DB.set_page_crawled(page_url)

    @staticmethod
    def gather_links(page_url):
        html_string = ''
        ...
        # to make self-signed ssl works, pass variable 'context' to function 'urlopen'
        context = ssl._create_unverified_context()
        response = urlopen(page_url, context=context)
        ...
        finder = (lambda x: globals()[x])(Spider.HTML_RESOLVER)(Spider.BASE_URL, page_url)
        return finder.page_links()

    @staticmethod
    def add_links_to_queue(urls):
        ...
        for url in urls:
            if Spider.DOMAIN_NAME != DomainHelpers.get_domain_name(url):
                continue
        ...

邏輯解釋:

  1. Spider.DB = (lambda x: globals()[x])(project_settings.DB_CLASS_NAME)這一行依然是動態初始化數據庫操做類
  2. context = ssl._create_unverified_context(),有時候有些自簽名ssl證書,執行urlopen方法時會報錯,須要建立這個context變量來避免這個錯誤產生
  3. finder = (lambda x: globals()[x])(Spider.HTML_RESOLVER)(Spider.BASE_URL, page_url)這行也是經過動態初始化的方式,按照配置文件中指定的解析類來解析html內容,若是想自定義解析內容,只要從新實現一個解析類便可
  4. add_links_to_queue 這個方法是確保只會將當前域名相關的頁面保存起來以便後續繼續爬,若是不加這個判斷,一旦頁面上有一個www.weibo.com這樣的連接的話,那爬蟲估計會把整個互聯網上的內容都爬一遍。。。

html解析html_resolver.py文件

class HtmlResolver(HTMLParser):
    ...
    def handle_starttag(self, tag, attrs):
        if tag == 'a':
            for (attribute, value) in attrs:
                if attribute == 'href':
                    url = parse.urljoin(self.base_url, value)
                    self.links.add(url)

    ...

這個類決定了咱們爬取頁面的邏輯,這裏咱們只抓去連接(也就是a標籤)中的href屬性中的內容。


執行過程動圖

python-crawler.gif

附錄

本Demo完整代碼已經放到Github上: https://github.com/fisherdan/crawler



本文在博客園和個人我的博客www.fujiabin.com上同步發佈。轉載請註明來源。

相關文章
相關標籤/搜索