Scrapyを使ってはてなブログ、といいますか、この日記のクローリングを行います。今回はエントリタイトルを取得するSpiderを作ります。
こちらの書籍を参考にしてます。
Scrapyとは
Scrapyはクローリング&スクレイピングに特化したPythonのフレームワークです。
Scrapy | A Fast and Powerful Scraping and Web Crawling Framework
http://scrapy-ja.readthedocs.io/ja/latest/index.html
以下のように、大きなエコシステムとなってますが( https://doc.scrapy.org/en/1.5/topics/architecture.html より抜粋)、今回はScrapyの肝となるSpiderに絞って実装していきます。
Spiderは大きく2つのことを行います。
- レスポンスをパースして必要な値を得る
- パースした値やリンクから新たなリクエストを実行する
インストールとプロジェクトの作成
まずはScrapyをインストールします。
$ pip install Scrapy
scrapyコマンドでScrapy用のプロジェクトを新規作成します。名前は"kenikki"とします。
$ scrapy startproject kenikki
作成後のプロジェクトディレクトリは以下の構造になります。
マナー
生成されたsettings.pyにScrapyのパラメータを設定します。
その中でも、マナーとして、以下だけは設定しておきます。
ROBOTSTXT_OBEY
: robots.txtに従うかどうか(robots.txtについては、こちらを参考)DOWNLOAD_DELAY
: ダウンロード間隔(秒)CONCURRENT_REQUESTS_PER_DOMAIN
とCONCURRENT_REQUESTS_PER_IP
: それぞれドメインごと・IPごとのリクエスト並行数
# Obey robots.txt rules ROBOTSTXT_OBEY = True # Configure a delay for requests for the same website (default: 0) # See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay # See also autothrottle settings and docs DOWNLOAD_DELAY = 60 # The download delay setting will honor only one of: CONCURRENT_REQUESTS_PER_DOMAIN = 1 CONCURRENT_REQUESTS_PER_IP = 1
指定したURLからtitleを取得するSpider
それではこのブログのエントリ(例えば https://ohke.hateblo.jp/entry/2018/06/23/230000 )からtitleタグの値を取り出して、テキストファイルに保存するSpiderを実装します。
以下では、scrapy.Spiderを継承したクラスを作成してます。
start_requests()
では、2つのURLへのリクエストを行っています- scrapy.Requestオブジェクトを返すジェネレータとなってます
parse()
では、得られたレスポンス(HTML)をパースして、titleタグで囲まれた値を取得して、ファイルに書き込んでます- scrapy.Response継承クラスのオブジェクトが引数として渡されます
css()
で、CSSセレクタライクにDOMを指定・取得できます (scrapy.selector.SelectListオブジェクトが返されます)- さらに
extract()
やextract_first()
で文字列として取得できます
- さらに
name
はこのあとのシェルコマンドからの実行で必要
import scrapy class EntrySpider(scrapy.Spider): name = 'entry_spider' filename = 'entry_title_list.txt' def start_requests(self): urls = [ 'https://ohke.hateblo.jp/entry/2018/06/23/230000', 'https://ohke.hateblo.jp/entry/2018/06/16/230000' ] for url in urls: yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): with open(self.filename, 'a') as f: f.write(response.css('title::text').extract_first() + '\n')
ちなみにscrapy shell
コマンドを使うと、指定したURLからHTMLをダウンロードされますので、その場でセレクタの動作確認などができます。終了するときはquit
で抜けられます。
$ scrapy shell 'https://ohke.hateblo.jp/entry/2018/06/23/230000' ・・・ In [1]: response.css('title') Out[1]: [<Selector xpath='descendant-or-self::title' data='<title>Python: set にlistやtupleを追加する - け日記'>] In [2]: response.css('title').extract_first() Out[2]: '<title>Python: setにlistやtupleを追加する - け日記</title>'
scrapy crawl
コマンドで、作成したSpiderクラスのname
(ここでは"entry_spider")を指定することで、そのSpiderを起動することができます。
$ scrapy crawl entry_spider
entry_title_list.txtファイルが作成され、2つの記事タイトルが格納されていることがわかります。
Python: setにlistやtupleを追加する - け日記 NumPyを使って線形モデルのパラメータを最小二乗法で推定する - け日記
ページングされたURLを再帰的に巡回するSpider
もう少し難しいこととして、このブログの全エントリのタイトルを取得してみましょう。
先程はエントリのURLを全て指定していましたが、2つの問題点を抱えてます。
- エントリと同じ数だけリクエストが発行される
- 全エントリのURLを事前に知っている必要がある
そこで、各年のアーカイブ(例えば https://ohke.hateblo.jp/archive/2018 )からエントリのタイトルを取得するようにします。
- 1ページ30エントリが表示されて、リクエスト数を最も少なくできると期待できる
- URLも予測可能で、1年に1つしか増えない
ただし、31エントリ以上ある年はページングされます。例えば2017年はhttps://ohke.hateblo.jp/archive/2017とhttps://ohke.hateblo.jp/archive/2017?page=2の2つに分かれてます。これに上手く対応する必要があります。
ここまで踏まえて、Spiderを実装していきます。
start_requests()
を実装せずに、代わりにstart_urls
にURLリストを与えてます- こうするとURLを1つずつ巡回して
parse()
をコールバックしてくれます (上のstart_requests()
の実装と同じことをやってくれます)
- こうするとURLを1つずつ巡回して
parse()
では、次ページへのリンクが有る場合、同じparse()
をコールバックにしたRequestオブジェクトをジェネレートし、次ページのURLにも再帰的にparse()
を実行するようにしてます- 各アーカイブページでは、エントリタイトルを取得してファイルに書き出してます
import scrapy class ArchiveSpider(scrapy.Spider): name = 'archive_spider' start_urls = [ 'https://ohke.hateblo.jp/archive/2015', 'https://ohke.hateblo.jp/archive/2016', 'https://ohke.hateblo.jp/archive/2017', 'https://ohke.hateblo.jp/archive/2018' ] filename = 'entry_title_list.txt' def parse(self, response): next_page_url = response.css('span.pager-next a::attr(href)').extract_first() if next_page_url is not None: yield scrapy.Request(next_page_url, callback=self.parse) with open(self.filename, 'a') as f: f.write('\n'.join(response.css('a.entry-title-link::text').extract()) + '\n')
こちらもscrapy crawl
コマンドで実行することで、全エントリタイトルがentry_title_list.txtに出力されます。
$ scrapy crawl entry_spider
まとめ
Scrapyで簡単なSpiderを実装し、エントリタイトルを取得してファイルに保存するところまでできるようになりました。
参考文献
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2016/12/16
- メディア: 大型本
- この商品を含むブログ (3件) を見る