Pyspider — 實作篇

Bing Kuo
9 min readJun 25, 2019

--

適合本篇閱讀的人:

  • 剛接觸Pyspider
  • 剛入門爬蟲

範例流程:

爬取ptt的套房租屋版,並且在指定頁數下,往前爬取10頁,首頁清單命名為index_page,單篇內文命名為detail_page,深度兩層的爬蟲。

全部程式(GitHub):

全部程式不會太多,大致可以分為五個部分,以下盡量簡化流程,分別介紹,若是你跟我類似架構的爬蟲,可以稍微修改一下。另外,在介紹與安裝篇有提到有兩種啟動方式,一種是將taskdb, projectdb, resultdb存成文件,一種是存在資料庫,而我的作法是後者,存放在Mysql,所以需要用config.json來啟動,這裡附上我的config.json

from pyspider.libs.base_handler import *
import re
class Handler(BaseHandler):
crawl_config = {
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'
}
@every(minutes=24 * 60)
def on_start(self):
pageIndexStart = 4008
for pageIndex in range(pageIndexStart, pageIndexStart-10 ,-1):
url = 'https://www.ptt.cc/bbs/Rent_tao/index{}.html'.format(pageIndex)
self.crawl(url, callback=self.index_page)
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('.title > a').items():
_title = each.text()
_title = _title.replace('[','[').replace(']',']')

title = re.search(r'^\[(.+)\](.*)', _title)

info = {
"keyword": title.group(1).strip(),
"title": title.group(2).strip()
}

if '新竹' not in info['keyword']:
continue

self.crawl(each.attr.href, callback=self.detail_page, save=info)
@config(priority=2)
def detail_page(self, response):
headers = response.doc('.article-metaline > .article-meta-value').items()
headerInfo = [header.text() for header in headers]

result = {
"keyword": response.save['keyword'],
"title": response.save['title'],
"author": headerInfo[0],
"datetime": headerInfo[2],
"content": response.doc('#main-container > .bbs-content').text()
}

return {
"url": response.url,
"title": response.doc('title').text(),
}

第一部分:

載入需要的套件,這裡看到Pyspider的librarys都是放置在libs路徑下,若有需要二次開發,我習慣也會把程式放在這資料夾下,供之後專案讀取。

from pyspider.libs.base_handler import *
import re

第二部分:

crawl_config負責設定此爬蟲的種種,我個人比較常用的像user-agent、retries次數,其他可以從官方網站查看 (http://docs.pyspider.org/en/latest/apis/self.crawl/#handlercrawl_config)

crawl_config = {
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0'
}

第三部分:

正如前面所說,爬蟲需要給定一個起始網址(種子網址),這個範例抓取index4008.html,並且往前抓10頁,在Pyspider中,每一個大大小小要爬的網址都需要經過crawl函式 (http://docs.pyspider.org/en/latest/apis/self.crawl/#selfcrawl),常用的參數有幾個:

  • url:要抓的網站網址,
  • callback:fetcher抓取下網站html後,會去呼叫的Function,下一步驟的概念
  • priority:當很多的crawl在佇列等待處理時,priority數值大的會優先處理,預設為0
  • retries:失敗要重抓的次數,若失敗會進入到佇列當中,等待下次執行,直到所有retries次數都用完
  • fetch_type:若帶js代表使用phantomjs進行抓取
  • save:可以帶入一個Object,在callback function時可以使用response.save[‘key’]讀取,傳遞參數的概念

@every的用途類似排程,當執行第一次之後,會定時一段時間啟動爬蟲,除了minutes還可以使用second操作,可以參考文件(http://docs.pyspider.org/en/latest/apis/@every/)

@every(minutes=24 * 60)
def on_start(self):
pageIndexStart = 4008
for pageIndex in range(pageIndexStart, pageIndexStart-10 ,-1):
url = 'https://www.ptt.cc/bbs/Rent_tao/index{}.html'.format(pageIndex)
self.crawl(url, callback=self.index_page)

第四部份:

index_page可以想成是爬蟲的第一層,爬取清單列表,經過pyquery解析html後,將要往下一層爬取的資料,使用crawl函式加入任務佇列中,並且指定呼叫detail_page,這樣當scheduler處理到該任務,就會傳遞到第五部分了。

這邊補充一下,很多人在第三部分設定了@every,但卻沒有按時執行,這是因為關於age這參數,在官網文件中有寫到(http://docs.pyspider.org/en/latest/apis/@every/),當age > every時,儘管crawl執行還是不會更新,直到every超過age時間才會更新,好比這個範例來說,every是1天,那10天後才會更新。

@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('.title > a').items():
_title = each.text()
_title = _title.replace('[','[').replace(']',']')

title = re.search(r'^\[(.+)\](.*)', _title)

info = {
"keyword": title.group(1).strip(),
"title": title.group(2).strip()
}

if '新竹' not in info['keyword']:
continue

self.crawl(each.attr.href, callback=self.detail_page, save=info)

第五部分:

detail_page是這個爬蟲的第二層,這階段要做(1)儲存跟(2)紀錄result table。把解析完的資料整理妥當後存到資料庫或是文字檔,下方程式中,result物件就是我爬完一篇文章的結果,搭配mysql-connector-python或是pyodbc等連到所需的資料庫中。最後return的物件將會回傳到Result Table當中,依據你Result Worker所訂定的格式去回傳。

@config(priority=2)
def detail_page(self, response):
headers = response.doc('.article-metaline > .article-meta-value').items()
headerInfo = [header.text() for header in headers]

result = {
"keyword": response.save['keyword'],
"title": response.save['title'],
"author": headerInfo[0],
"datetime": headerInfo[2],
"content": response.doc('#main-container > .bbs-content').text()
}

return {
"url": response.url,
"title": response.doc('title').text(),
}

若是要寫入Mysql,我有寫一小段連接資料庫的程式在GitHub,下載下來放在pyspider\database\mysql下,修改內部連線資訊,並在「第一部分」寫

from pyspider.database.mysql.mysqldb import MYSQLDB

來載入,在「第五部分」要寫入資料庫部分,寫下面語法來插入資料

MYSQLDB().insert('Table name', **result)

後語

此篇介紹Pyspider基本寫法,撰寫過程踩了不少坑,另外寫了一篇「踩坑篇」,紀錄我遇到的問題,以及怎麼去解決,希望能讓各位網友少走點冤忘路。

最後,喜歡我的文章幫我拍拍手,給予我鼓勵我會很開心的,以上。

延伸閱讀

--

--