之前爬蟲是用 BeautifulSoup + Selenium,而在 2017 PyCon 得知了 PyQuery,就在想要來嘗試看看的時候,與強者朋友一聊,言下之意是說爬蟲還是要用 Scrapy,BeautifulSoup 跟 PyQuery 都是一種輕量型的 library,但 Scrapy 是一個 framework,能處理的東西比較多。其實在初學程式語言的時候曾經嘗試過,當時失敗得一蹋糊塗,但相信經過幾個月的歷練後,應該有辦法掌握它了吧(握拳)!在網路上找來的教學是這篇,非常淺顯易懂。
一、基本安裝測試
1. 安裝
pip install scrapy
2. 新增資料夾跟並在裡面新增檔案
mkdir brickset-scraper
cd brickset-scraper
touch scraper.py
3. 編輯 scraper.py:
import scrapyclass BrickSetSpider(scrapy.Spider):
name = "brickset_spider"
start_urls = ['http://brickset.com/sets/year-2016']
在裡面先 import scrapy 進來,建立一個 Class 繼承 scrapy.Spider(Scrapy 裡一個基本的 spider class),裡面包含兩個屬性:
- name — spider 的名字
- start_urls — URLs 的 list
4. 測試:
scrapy runspider scraper.py
輸出結果:
2017-06-20 16:04:40 [scrapy.core.engine] INFO: Spider closed (finished)
二、取得資料
剛才的步驟只是確認 Scrapy 能正常運作,什麼資料都還沒取得,因此我們還要定義一個 parse 的 function 來解析網頁。Scrapy 支援 CSS selectors 以及 XPath selectors,這邊我們會使用 CSS selectors,目標是取得每個商品的資訊。編輯 scraper.py:
- 取得商品
看一下目標網站,每個商品都是由一個 class 為 set 的標籤包住,所以先把 set 傳進 response 裡:
class BrickSetSpider(scrapy.Spider):
name = "brickset_spider"
start_urls = ['http://brickset.com/sets/year-2016'] def parse(self, response):
SET_SELECTOR = '.set'
for brickset in response.css(SET_SELECTOR):
pass
2. 取得商品名稱
每個商品名稱在 set 裡面的 h1 a 裡面的 html 文字,:: text 為 CSS pseudo-selector,並用 extract_first() 取得第一個符合的元素(回傳字串非清單):
class BrickSetSpider(scrapy.Spider):
name = "brickset_spider"
start_urls = ['http://brickset.com/sets/year-2016']
def parse(self, response):
SET_SELECTOR = '.set'
for brickset in response.css(SET_SELECTOR):
NAME_SELECTOR = 'h1 a ::text'
yield {
'name': brickset.css(NAME_SELECTOR).extract_first(),
}
3. 測試
發現網站改版過了,按照原作者的 code 只能抓到商品編號,因此我稍微改了一下 code,由於商品名稱沒有被標籤包住,所以我直接用正規表示法來抓:
import scrapyclass BrickSetSpider(scrapy.Spider):
name = "brickset_spider"
start_urls = ['http://brickset.com/sets/year-2016']def parse(self, response):
SET_SELECTOR = '.set'
for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 a:nth-last-of-type(1)'
yield {
'name': brickset.css(NAME_SELECTOR).re('</span>(.*)</a>')[0],
}
成功抓到商品名稱!
4. 繼續擴充:
接著來抓價格跟圖片連結,由於這邊每個商品下面的細項都沒有 class 或 id 好抓,所以作者建議用 XPath 來進行:
import scrapyclass BrickSetSpider(scrapy.Spider):
name = 'brick_spider'
start_urls = ['http://brickset.com/sets/year-2016']def parse(self, response):
SET_SELECTOR = '.set'
for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 a:nth-last-of-type(1)'
RRP_SELECTOR = './/dl[dt/text() = "RRP"]/dd/text()'
IMAGE_SELECTOR = 'img ::attr(src)'
yield {
'name': brickset.css(NAME_SELECTOR).re('</span>(.*)</a>')[0],
'price': brickset.xpath(RRP_SELECTOR).extract_first(),
'image': brickset.css(IMAGE_SELECTOR).extract_first(),
}
5. 測試成功
三、處理多頁
首先觀察一下可以發現,下一頁的連結包在一個 next 的 class 裡面,所以我們可以先做判斷 next 是否存在,存在的話取得下一頁連結,然後送進 request 裡面:
import scrapyclass BrickSetSpider(scrapy.Spider):
name = 'brick_spider'
start_urls = ['http://brickset.com/sets/year-2016']def parse(self, response):
SET_SELECTOR = '.set'
for brickset in response.css(SET_SELECTOR): NAME_SELECTOR = 'h1 a:nth-last-of-type(1)'
RRP_SELECTOR = './/dl[dt/text() = "RRP"]/dd/text()'
IMAGE_SELECTOR = 'img ::attr(src)'
yield {
'name': brickset.css(NAME_SELECTOR).re('</span>(.*)</a>')[0],
'price': brickset.xpath(RRP_SELECTOR).extract_first(),
'image': brickset.css(IMAGE_SELECTOR).extract_first(),
} NEXT_PAGE_SELECTOR = '.next a ::attr(href)'
next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
if next_page:
yield scrapy.Request(
response.urljoin(next_page),
callback=self.parse
)
scrapy.Request 裡的 response.urljoin 表示告訴爬蟲目標網址,而 callback 則是告訴爬蟲找到 HTML 之後,將它傳給這個 function 做處理。
接著來測試,成功的話,做到這邊就完成教學裡面的所有內容囉。