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 中的數據流由執行引擎控制,如下所示:
- 引擎 Engin 從 Spider 獲取要抓取的初始請求。
- 引擎 Engin 在 調度器 Scheduler 中調度請求,並要求抓取下一個請求。
- 調度器 Scheduler 將下一個請求返回給引擎。
- 引擎 Engine 將請求發送到下載器 Downloader,通過下載器 Downloader Middlewares中間件(參見 process_request())。
- 一旦頁面完成下載,下載器 Downloader 生成一個響應 Response(帶有該頁面)並將其發送到引擎 Engin,通過下載器 Downloader中間件 Middlewares(參見 process_response())。
- 引擎 Engin 從下載器 Downloader 接收響應 Response 並將其發送給 Spider 進行處理,通過Spider Middlewares 中間件(參見 process_spider_input())。
- Spider 處理響應 Response 並將抓取的項目和新的請求 Request(要跟踪)返回給引擎,通過 Spider Middlewares 中間件(參見 process_spider_output())。
- 引擎 Engin 將處理後的項目發送到項目管道 ItemPipelines,然後將處理後的請求發送到調度器 Scheduler 並詢問可能的下一個請求進行爬網。
- 此過程重複(從第 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()
# -*- 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,
留言
張貼留言
Aron阿龍,謝謝您的留言互動!