Scrapy爬蟲框架

Scrapy爬蟲框架

前言

我們前面已經說明各類模組可以執行網路爬蟲設計,針對一般的網頁抓取已經足夠,這裡我們將介紹的 Scrapy ,其實是一個爬蟲框架,是一個快速的高階網頁抓取和網頁淬取框架,用於抓取網站並從其頁面中提取結構化數據。它可用於廣泛的用途,從數據挖掘到監控和自動化測試。適合大型爬蟲設計,我們將簡單以範例說明此框架 。

大綱

安裝 Scrapy

早期在安裝 Scrapy 需要先安裝 Twisted,Twisted 是異步網路請求框架,現在這個 Twisted 安裝已經被整合在 Scrapy 安裝內,讀者可以使用下列方式安裝 Scrapy。

pip install Scrapy

安裝成功後可以看到下列畫面

從上述可以看到 Twisted 也安裝了,若是不知道是否安裝成功,可以使用輸入下列指令了解相關 Scrapy 的模組版本 。

scrapy version

從簡單的範例開始 - 建立 Scrapy 專案

Scrapy 專案框架

這裡我們先用一個簡單的網頁測試 Scrapy 的功能,對於 Scrapy 而言,想要執行設計爬蟲程式必須建立 Scrapy 專案,專案建立方式是使用下列指令:

scrapy startproject 專案名稱

此例我們在 Windows PowerShell 環境進入~/scrapyProject 資料夾,然後使用下列指令建立專案:

scrapy startproject yahoo

可以看到下列結果

這時 Scrapy 模組就自動在我們的 ~/scrapyProject/yahoo 資料夾下方建立了一系列專案檔案。

上述就是我們建立 Scrapy 專案 yahoo時,模組 Scrapy 自動產生的框架完整結構與內容,有關各個 Python 檔案內容意義,將在下面說明。

Scrapy 專案框架的檔案說明

在 ~/scrapyProject/ 資料夾底下的檔案

  • scrapy.cfg : 這個檔案是位於~/scrapyProject/ 資料夾底下,也就是在專案的根目錄,這是 Scrapy 項目預設配置的檔案。

在 ~/scrapyProject/yahoo 資料夾底下的檔案

  • items.py : 定義未來要爬取得欄位資料。
  • middlewares.py : 是介於 request/response之中間件。
  • pipelines.py : 是自行在此設計數據擷取的流程,數據來自items.py。
  • setting.py : 可以設定資料輸入方式。
  • spider 資料夾 : 我們設計與儲存爬蟲程式的資料夾。

爬蟲程式設計

在 Scrapy 專案框架 第一張圖,我們建立專案成功後,在提示訊息上方可以看到下列指令:

$cd yahoo
$scrapy genspider example example.com

上述這個指令,其實是引導我們實質建立專案爬蟲程式內容,假設筆者想要設計爬取 https://tw.stock.yahoo.com/

首先請執行 ”cd yahoo”,進入 ~/yahoo 資料夾,然後使用下列指令,讓系統自動建立一個爬蟲程式框架,不是自己動手寫此爬蟲框架。

$scrapy genspider yahooStock tw.stock.yahoo.com

結果如下

從上圖可以看到已經建立成功了,實質上是 Scrapy 為我們在資料夾~/scrapyProject\yahoo\yahoo\spiders\的 spider 資料夾建立 yahooStock.py 爬蟲程式了。此時開啟 yahooStock.py,可以看到最初的爬蟲程式框架畫面 :

上述程式 Scrapy 內有幾個重要內容如下 :

  • name : 爬蟲程式的名稱,此例是 "yahooStock"。
  • allowed_domains : 我們可以爬取網頁清單,如果沒有定義,則不受限制。
  • start_urls : 我們想要爬取得目標網址,可以接受一個到多個。
  • parse() : 這是程式核心,也是 Scrapy 的 callback() 方法,不可以更改名稱,也是我們撰寫爬蟲程式內容的地方。

範例 pythonScrapy-01.py:  使用 Scrapy 框架設計爬取出 yahoo 股市連結。

# pythonScrapy-01.py
import scrapy
import bs4

class YahoostockSpider(scrapy.Spider):
    name = 'yahooStock'
    allowed_domains = ['tw.stock.yahoo.com']
    start_urls = ['http://tw.stock.yahoo.com/']

    def parse(self, response):
        objSoup = bs4.BeautifulSoup(response.text, 'lxml')
        navBar = objSoup.find_all('li', class_='_yb_sb4pm')
        for h in navBar:
            links = {
                'navBar_info' : h.text,
                'links_info ': h.get('href')
                }
            yield links

執行結果

執行語法如下 :

$scrapy crawl yahooStock

上述程式第3行,我們依舊使用 BeautifulSoup 模組解析網頁,程式第18行最後傳回值是使用 Python 另一個關鍵字 yield(參閱:yield 的用法詳解yield 可以產生 return 回傳資料的效果,所傳回的是個產生器(generator),但是又不會讓程式中斷。

Scrapy 定位元素

在前面我們使用 BeautifulSoup 模組定位元素,這裡我們將講解 Scrapy 模組的方法,Scrapy 使用 CSS 和 Xpath 定位元素,基本方法如下:

  • css() : 使用 css 處理選擇節點元素。
  • xpath() : 使用 xpath 選擇節點元素。
  • extract() : 傳回選擇的 unicode 字串。
  • extract_first() : 傳回第一個符合的字串。

此外我們在使用 Scrapy 框架設計爬蟲程式時,也可以使用 print() 列內容,本程式範例將會說明。下列是使用 Chrome 的分析環境分析 PTT 的NBA看板。

範例 pythonScrapy-02.py:  使用 Scrapy 框架設計爬取 PTT 的 NBA看板,此例除了使用 yield 傳回內容,全程使用 css 處理選擇節點元素。

# pythonScrapy-02.py
import scrapy

class NbaSpider(scrapy.Spider):
    name = 'nba'
    allowed_domains = ['ptt.cc']
    start_urls = ['https://www.ptt.cc/bbs/NBA/index.html']

    def parse(self, response):
        titles = response.css("div.r-ent div.title a::text").extract()
        authors = response.css("div.r-ent div.author::text").extract()
        for info in zip(titles, authors):
            print('標題 : ', info[0]),        
            print('作者 : ', info[1]), 
            nba_item = {                      
                "title" : info[0],
                "author" : info[1]
            }
            yield nba_item

執行結果

讀者可以看到有標題和作者的是 print() 列印的結果,另外則是 yield 回傳的結果。

範例 pythonScrapy-03.py:  使用 Scrapy 框架設計爬取 PTT 的 Baseball 看板,此列除了使用 yield 傳回內容,全程使用 xpath() 處理選擇節點元素 。

# pythonScrapy-03.py
import scrapy

class BaseballSpider(scrapy.Spider):
    name = 'baseball'
    allowed_domains = ['ptt.cc']
    start_urls = ['https://www.ptt.cc/bbs/Baseball/index.html']

    def parse(self, response):
        titles = response.xpath("//div[@class='title']/a/text()").extract()
        authors = response.xpath("//div[@class='meta']/div[@class='author']/text()").extract()
        for info in zip(titles, authors):
            print('標題 : ', info[0]),        
            print('作者 : ', info[1]), 
            baseball_item = {                      
                "title" : info[0],
                "author" : info[1]
            }
            yield baseball_item

執行結果

使用cookie 登入

前面進入 PTT 爬取 NBA 或 Baseball 看板資料很順利,可是如果爬取部分看板是需要年滿18歲,例如 Gossiping 看板因為沒有 cookie 設定,就會出問題,解決方法是我們必須使用 Scrapy 的 Request() 執行請求,然後將 cookie 放在此請求方法內,下列是此 Request() 方法的設定方式 :

scrapy.Request(url, cookie={‘over18’:’1’}, callback=self.parser_info)

上述 parser_info() 是我們另外建立的方法,名稱可以自己設定 。

範例 pythonScrapy-04.py:  使用 Scrapy 框架設計爬取 PTT 的 Gossiping 看板,此例增加 cookie 設定。

# pythonScrapy-04.py
import scrapy

class GossipingSpider(scrapy.Spider):
    name = 'gossiping'
    allowed_domains = ['ptt.cc']
    start_urls = ['https://www.ptt.cc/bbs/Gossiping/index.html']
    def parse(self, response):
        url = 'https://www.ptt.cc/bbs/Gossiping/index.html'
        yield scrapy.Request(url, cookies={'over18':'1'}, callback=self.parse_info)   

    def parse_info(self, response):
        titles = response.xpath("//div[@class='title']/a/text()").extract()
        authors = response.xpath("//div[@class='meta']/div[@class='author']/text()").extract()
        for info in zip(titles, authors):
            print('標題 : ', info[0]),        
            print('作者 : ', info[1]), 
            gossiping_item = {                      
                "title" : info[0],
                "author" : info[1]
            }
            yield gossiping_item

執行結果

保存文件為 JSON 和 CSV 檔案

如果想要將爬取的結果使用 JSON 格式儲存,同時中文部分以中文顯示,可以使用下列指令格式,下列是 範例 pythonScrapy-04.py 為例:

scrapy crawl gossiping -o myjson.json -s FEED_EXPORT_ENCODING=UTF-8

執行後,可以在 ~\scrapyProject\pttgossiping\ 資料夾中發現 myjson.json 檔案

可使用類似方法儲存結果為 CSV 檔案

scrapy crawl gossiping -o myCsv.csv -s FEED_EXPORT_ENCODING=UTF-8

Scrapy 架構圖

下圖是 Scrapy 的架構圖,顯示 Scrapy 架構內各個控件(components) 和 數據流(dataflow)的相關流程。詳細內容,請參考 : Scrapy Architecture overview

數據流 DataFlow

Scrapy 中的數據流由執行引擎控制,如下所示:

  1. 引擎 Engin 從 Spider 獲取要抓取的初始請求。
  2. 引擎 Engin 在 調度器 Scheduler 中調度請求,並要求抓取下一個請求。
  3. 調度器 Scheduler 將下一個請求返回給引擎。
  4. 引擎 Engine 將請求發送到下載器 Downloader,通過下載器 Downloader Middlewares中間件(參見 process_request())。
  5. 一旦頁面完成下載,下載器 Downloader 生成一個響應 Response(帶有該頁面)並將其發送到引擎 Engin,通過下載器 Downloader中間件 Middlewares(參見 process_response())。
  6. 引擎 Engin 從下載器 Downloader 接收響應 Response 並將其發送給 Spider 進行處理,通過Spider Middlewares 中間件(參見 process_spider_input())。
  7. Spider 處理響應 Response 並將抓取的項目和新的請求 Request(要跟踪)返回給引擎,通過 Spider Middlewares 中間件(參見 process_spider_output())。
  8. 引擎 Engin 將處理後的項目發送到項目管道 ItemPipelines,然後將處理後的請求發送到調度器 Scheduler 並詢問可能的下一個請求進行爬網。
  9. 此過程重複(從第 1 步開始),直到不再有來自調度器 Scheduler 的請求。

Scrapy Components 控件

  • Scrapy Engine 引擎 : 引擎負責控制系統所有組件之間的數據流,並在發生某些動作時觸發事件。有關更多詳細信息,請參閱 : 數據流 Data Flow
  • Scheduler 調度器 : 調度程序接收來自引擎的請求,並在引擎請求時將它們排入隊列以供稍後(也給引擎)提供。
  • Downloader 下載器 : 下載器負責獲取網頁並將它們提供給引擎,然後引擎再將它們提供給Spider 蜘蛛。
  • Spiders 蜘蛛 : Spiders 是由 Scrapy 用戶編寫的自定義類別,用於解析響應 Response 並從中提取 項目 item 或要遵循的其他請求 Request。有關更多信息,請參閱 : 蜘蛛 Spiders
  • Item Pipeline 項目管道 : 一旦項目被蜘蛛提取(或抓取),項目管道負責處理項目。典型的任務包括清理、驗證和持久化(如將項目存儲在數據庫中)。有關更多信息,請參閱 : Itme Pipeline 項目管道
  • Downloader Middlewares 下載器中間件 : 下載器中間件是位於引擎和下載器之間的特定掛鉤,並在請求從引擎傳遞到下載器時處理請求 Request,以及從下載器傳遞到引擎的響應 Response。有關更多信息,請參閱 : Downloader Middlewares 下載程序中間件。如果您需要執行以下操作之一,請使用 Downloader Middlewares中間件:
    • 在請求發送到下載器之前處理請求(即在 Scrapy 將請求發送到網站之前);
    • 在將其傳遞給蜘蛛之前更改收到的響應;
    • 發送一個新的請求而不是將接收到的響應傳遞給蜘蛛;
    • 將響應傳遞給蜘蛛而不獲取網頁;
    • 默默地放棄一些請求。
  • Spider Middleware蜘蛛中間件 : Spider 中間件是位於 Engine 和 Spider 之間的特定掛鉤,能夠處理 Spider 輸入(響應)和輸出(項目和請求)。有關更多信息,請參閱 : Spider Middleware 蜘蛛中間件。如果需要以下操作,請使用 Spider 中間件 :
    • 蜘蛛回調的後處理輸出 - 更改/添加/刪除請求或項目;
    • 後處理 start_requests;
    • 處理蜘蛛異常;
    • 根據響應內容為某些請求調用 errback 而不是回調。
  • Even-driven networking 事件驅動的網絡 : Scrapy 是用 Twisted 編寫的,這是一個流行的 Python 事件驅動網絡框架。因此,它使用非阻塞(又名異步)代碼實現並行性。有關異步編程和 Twisted 的更多信息,請參閱以下鏈接:

專題爬取多頁 PTT 資料

請進入 items.py 定義想要抓取的項目,我們這次要抓取 PTT 標題、作者、發文日期,由於計畫抓取 10 頁資料,首先我們進入 NBA 的 PTT 網頁了解網頁結構。

按 "上頁" 紐,畫面如下:

我們發現網址改變 :

https://www.ptt.cc/bbs/NBA/index.html => 變為 => https://www.ptt.cc/bbs/NBA/index6505.html

再按一次 "上頁" 紐,網址變為 :

https://www.ptt.cc/bbs/NBA/index6504.html

從以上我們可以瞭解,6505、6504 是頁次編號,如果要爬取前一頁,只要將頁次減 1即可,有了以上的知識我們就可以爬取多個 PTT 網頁了。

專案 nbaptt :  使用 Scrapy 框架設計爬取 PTT 的 NBA 看板,要抓取 PTT 標題、作者、發文日期,然後將結果存入 SQLite 資料庫 nbaptt.db, 表單是 nba_ptt

首先,定義專案名稱 nbaptt,指令如下 :

scrapy startproject nbaptt

下列是專案中的各程式 :

程式 nba.py:  這是爬蟲主程式。

# -*- coding: utf-8 -*-
import scrapy
import time
import random
from nbaptt.items import NbapttItem

class NbaSpider(scrapy.Spider):
    name = 'nba'
    allowed_domains = ['ptt.cc']
    start_urls = ['https://www.ptt.cc/bbs/NBA/index.html']

    def parse(self, response):
        for i in range(10):
            time.sleep(random.randint(1,3))
            url = 'https://www.ptt.cc/bbs/NBA/index'+str(6498-i)+'.html'
            yield scrapy.Request (url,callback=self.parse_info)
            
    def parse_info(self, response):
        titles = response.xpath("//div[@class='title']/a/text()").extract()
        authors = response.xpath("//div[@class='meta']/div[@class='author']/text()").extract()
        dates = response.xpath("//div[@class='meta']/div[@class='date']/text()").extract()
        for info in zip(titles, authors, dates):
            nba_item = {                      
                "title" : info[0],
                "author" : info[1],
                "date" : info[2]
            }
            yield nba_item 

程式 settings.py:  下列是將預設程式碼的註解開啟,300是優先順序的預設值,數字愈大優先權越高。

程式 items.py:  下這是定義類別 NbapttItem,此類別有 title、author、date 等3個屬性,NbapttItem 類別繼承 scrapy.Item 類別,並透過 scrapy.Field() 方法設定屬性值。

# -*- coding: utf-8 -*-
import scrapy

class NbapttItem(scrapy.Item):
    title = scrapy.Field()
    author = scrapy.Field()
    date = scrapy.Field()
程式 pipelines.py:  這個程式有一個預設值的類別 NbapptPipeline,其中 open_spider() 方法是連結 nbappt.db 資料庫,同時建立 nba_ptt 表單。 close_spider() 方法是確認資料寫入資料庫,同時關閉資料庫連線。process_item() 將資料些入資料庫。
# -*- coding: utf-8 -*-
import sqlite3

class NbapttPipeline(object):
    def open_spider(self, spider):
        self.conn = sqlite3.connect ("nbaptt.db") 
        self.cur = self.conn.cursor()
        sql = '''Create table nba_ptt(  
                title TEXT,
                author TEXT,
                date TEXT)'''
        self.cur.execute(sql)
    def close_spider(self, spider):
        self.conn.commit()
        self.conn.close()
    def process_item(self, item, spider):
        title = item['title']
        author = item['author']
        price = item['date']
        x = (title, author, price)
        sql = '''insert into nba_ptt values(?,?,?)'''  
        self.conn.execute(sql,x)
        return item

執行結果

執行以下指令

$scrapy crawl nba




參考資料

特色、摘要,Feature、Summary:

關鍵字、標籤,Keyword、Tag:

  • Web-Crawler,Data-Mining,Data-Science,Python,

留言

這個網誌中的熱門文章

Ubuntu 常用指令、分類與簡介

iptables的觀念與使用

網路設定必要參數IP、netmask(遮罩)、Gateway(閘道)、DNS

了解、分析登錄檔 - log

Python 與SQLite 資料庫

Blogger文章排版範本

Pandas 模組

如何撰寫Shell Script

查詢指令或設定 -Linux 線上手冊 - man

下載網頁使用 requests 模組