天天看點

scrapy架構_scrapy架構流程

架構流程詳解

整體架構及流程

架構目錄結構如圖,其中spider檔案下放完成爬取功能的spider,items下寫封裝資料的item,pipeline下寫過濾資料的item pipeline,settings為配置檔案:

scrapy架構_scrapy架構流程

架構流程如圖:

scrapy架構_scrapy架構流程

頁面分析及腳手架

使用scrapy擷取網頁資訊

scrapy shell <url>
view(response)
           

然後使用chrome的開發者工具分析

建立scrapy腳手架

scrapy startproject <project_name>
cd <project_name>
scrapy genspider <spider_name> <domain>
           

spiders

自定義spider類必須繼承于scrapy.Spider類, 且必須覆寫parse方法處理response,spider會從start_requests(self)方法開始調用,預設使用start_urls清單構造Request對象, 回調函數為parse方法

回調函數的常用形式:

yield scrapy.Request(next_url, callback=self.parse)
           

spider中的allowed_domains=[]用于指定域名過濾url,對start_urls中的url不過濾,如果url中的域名不在allowed_domains中将不通路

對于頁面元素中連結的提取一般使用 LinkExtractor 類

LinkExtractor 類 構造函數接受的參數用于指定比對模式 正則比對用allow 和 deny; 域名比對用allow_domains 和 deny_domains xpath表達式 restrict_xpaths; css選擇器 restrict_css 指定标簽 tags; 指定屬性 attrs; 調用回調函數對連結進行處理 process_value

然後調用extract_links(self, response)用于傳回符合條件連結的絕對路徑

le = LinkExtractor(restrict_css='div.toctree-wrapper.compound', deny='/index.html$')
for link in le.extract_links(response):
    yield scrapy.Request(link.url, callback=self.parse_example)
           

如果是用css等選擇器傳回的相對路徑, 可以調用 response.urljoin(rel_url)傳回絕對路徑

有時需要登入才能擷取需要的資訊,這時可以用FormRequest類模拟登入

# 通路登入頁面 得到response
def start_requests(self):
    yield Request(self.login_url, callback=self.login)
# 調用FormRequest.from_response自動解析form表單并送出
def login(self, response):
    fd = {'email': '[email protected]', 'password': 'mMj5xqRH2kamRXT'}
    yield FormRequest.from_response(response, formdata=fd, callback=self.parse_login)
           

有驗證碼的網頁加載過程是先加載html内容,然後攜帶獲得的cookie再次發起HTTP請求加載驗證碼圖檔

# 加載登入頁面 獲得response
def start_requests(self):
    yield Request(self.login_url, callback=self.login, dont_filter=True)

# 該方法既是登入頁面的解析函數,又是下載下傳驗證碼圖檔的響應處理函數
def login(self, response):
    # 如果 response.mete['login_response'] 存在,目前 response 為驗證碼圖檔的響應
    # 否則目前 response 為登入頁面的響應
    login_response = response.meta.get('login_response')

    if not login_response:
        # Step 1:
        # 此時 response 為登入頁面的響應,從中提取驗證碼圖檔的 url,下載下傳驗證碼圖檔,根據要爬取的網頁設定css選擇器
        captchaUrl = response.css('div.login_right img::attr(src)').extract_first()

        captchaUrl = response.urljoin(captchaUrl)
        # 構造 Request 時,将目前 response 儲存到 meta 字典中 再次通路時跳轉到step 2
        yield Request(captchaUrl, callback=self.login, meta={'login_response': response}, dont_filter=True)

    else:
        # Step 2
        # 此時,response 為驗證碼圖檔的響應,response.body 是圖檔二進制資料
        # login_response 為登入頁面的響應,用其構造表單請求并發送
        formdata = {
            'email': self.user,
            'pass': self.password,
            # 使用 OCR 識别 此處調用的是人工識别方法 
            'code': self.get_captcha_by_user(response.body),
        }
        yield FormRequest.from_response(login_response,callback=self.parse_login,formdata=formdata,
                                        dont_filter=True)
           

人工識别驗證碼并輸入

def get_captcha_by_user(self, data):
    # 人工識别
    img = Image.open(BytesIO(data))
    img.show()
    captcha = input('輸入驗證碼:')
    img.close()
    return captcha
           

items.py

在items.py下定義封裝爬取到的資料的自定義item

class BookItem(scrapy.Item):
    name = scrapy.Field(serializer=str)
           

自定義的item需要繼承自scrapy.Item類 其屬性用Field初始化 Field實則就是dict類 Field用于為字段添加中繼資料; 可通過item.fields獲得包含所有Field的字典, 進一步可通過字典方法獲得某個Field的中繼資料

seeting.py

1 導出檔案包含的字段 同時也決定了各字段的順序

FEED_EXPORT_FIELDS = ['price', 'name']
           

注: 該屬性隻會出現标出的字段

2 對資料進行過濾的Item pipeline

ITEM_PIPELINES = {
    'scrapy_flow.pipelines.BookPipeline': 300,
}
           

3 導出檔案路徑, 其中%(name)s會替換成spider的name

FEED_URI = 'export_data/%(name)s.data'
           

4 導出資料格式, 内建支援的格式有JSON, JSON lines, CSV, XML, Pickle, Marshal, 還可以定義自己的Exporter

FEED_FORMAT = 'csv'
           

5 導出檔案編碼

FEED_EXPORT_ENCODING = 'utf-8'
           

6 自定義Exporter

FEED_EXPORTERS = {'excel': 'example.my_exporters.ExcelItemExporter'}
           

自定義Exporter定義方式

class ExcelItemExporter(BaseItemExporter):
    # 每個子類必須實作 處理每項item
    def export_item(self, item)
    # 導出開始時被調用
    def start_exporting(self)
    # 導出完成時調用
    def finish_exporting(self)
           

7 網站伺服器下有一個robots.txt, 決定爬蟲可以通路哪些網頁,scrapy預設開啟

ROBOTSTXT_OBEY = False
           

pipelines.py

定義在該子產品中的類主要用于對item進行處理

處理資料的item_pipeline類不需要繼承于特定基類, 但要實作process_item方法

def process_item(self, item, spider)
           

還有選用的方法如下:

#spider打開時回調, 通常完成資料庫連接配接等工作
def open_spider(self, spider)
#spider關閉時回調, 通常完成關閉資料庫連接配接等工作
def close_spider(self, spider)
#item pipeline建立時回調
def from_crawler(cls, crawler)
           

兩個内置的Item Pipeline, 用于下載下傳檔案和圖檔, 一般将其優先配置, 對于FilesPipeline, 将item中需要下載下傳的url放到item的file_urls字段, 在setting.py中配置FilesPipeline後會自動下載下傳

scrapy架構_scrapy架構流程

ImagesPipeline是FilesPipeline的子類,添加了一些針對圖檔的功能

ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
}
IMAGES_STORE = 'download_images'  # 指定圖檔下載下傳目錄
# 為圖檔生成縮略圖
IMAGES_THUMBS = {
'small': (50, 50),
'big': (270, 270),
}
# 通過尺寸過濾圖檔
IMAGES_MIN_WIDTH = 110
IMAGES_MIN_HEIGHT = 110
           

要修改FilesPipeline的下載下傳路徑或檔案名 可以覆寫FilesPipeline實作 也可以根據儲存的輸出(files字段包含了檔案名)來編寫腳本對檔案進行重命名,推薦使用前者來修改檔案名

# 儲存的輸出檔案 FilesPipeline會自動根據file_urls字段下載下傳檔案
{'file_urls': ['https://matplotlib.org/examples/pyplots/dollar_ticks.py'],
 'files': [{'checksum': '3c3aaa6b64b130d59dc46fc225065087',
            'path': 'full/f028022d9225a160bf7b25523994c5ada9f5896b.py',
            'url': 'https://matplotlib.org/examples/pyplots/dollar_ticks.py'}]}
# 編寫item_pipeline覆寫該方法指定下載下傳檔案路徑
class MyFilesPipeline(FilesPipeline):
    def file_path(self, request, response=None, info=None)
           

将爬取到的資料儲存到資料庫也可以使用item pipeline完成

# 定義一個儲存資料的pipeline
class MySQLPipeline:
    # 該函數在爬取資料前被調用
    def open_spider(self, spider):
        db = spider.settings.get('MYSQL_DB_NAME', 'scrapy_db')
        host = spider.settings.get('MYSQL_HOST', 'localhost')
        port = spider.settings.get('MYSQL_PORT', 3306)
        user = spider.settings.get('MYSQL_USER', 'root')
        passwd = spider.settings.get('MYSQL_PASSWORD', 'password')

        # self.dbpool = adbapi.ConnectionPool('MySQLdb', host=host, port=port,
        #                                     db=db, user=user, passwd=passwd, charset='utf8')
        # 連接配接資料庫的對象
        self.db_conn = MySQLdb.connect(host=host, port=port, db=db, user=user, passwd=passwd, charset='utf8')
        # 執行資料庫語句的對象
        self.db_cur = self.db_conn.cursor()
    # 該函數在爬取完全部資料後被調用
    def close_spider(self, spider):
        # self.dbpool.close()
        # 資料庫語句在着一次送出 縮短時間
        self.db_conn.commit()
        self.db_conn.close()
    # 該函數對每一項資料都會調用
    def process_item(self, item, spider):
        # self.dbpool.runInteraction(self.insert_db, item)
        self.insert_db(item)
        return item

    # def insert_db(self, tx, item):
    def insert_db(self, item):
        values = (
            item['upc'],
            item['name'],
            item['price'],
            item['review_rating'],
            item['review_num'],
            item['stock'],
        )

        sql = 'INSERT INTO books VALUES (%s, %s, %s, %s, %s, %s)'
        self.db_cur.execute(sql, values)
        # tx.execute(sql, values)
           

運作

運作爬蟲

scrapy crawl <scrapy_name> -t <format> -o <filename>
           

參考文獻: 《精通scrapy網絡爬蟲》劉碩著

繼續閱讀