無限スクロールするページをクロールする方法


無限スクロールとは?

FacebookやTwitterのタイムラインなんかで見られる、ページの下までスクロールすると、新しい情報が読み込まれる、あれです。

動機

無限スクロールのページをクロールしようと思った理由は、学校の課題の関係でTwitterの過去ツイートを引っ張ってくる必要があったからです。
え、Twitterは公式APIがあるじゃないか、というそこのあなた。Twitterの公式APIがあまり親切でなく、1週間より前のツイートは取得できない仕様になっています。つまり、それより昔のツイートをとってきたい場合は自分でクロールしなければいけません。そして、Twitterの検索結果は、無限スクロールで表示されるので、無限スクロールするページをクローリングしなければいけません。

無限スクロールをクロールするのが難しい理由

クローラーは基本的に、以下のように動作します:
1. 与えられたurlからHTMLレスポンスを取得し、処理する
2. レスポンスの中からさらにクロールするurlを探し出す
3. 新しいurlで1~2をまた行う

こうして、ネット上から大量のデータを取ってきます。
無限スクロールのページをクロールする時の問題は、ページング(下に「1ページ」、「2ページ」、「次のページ」などのリンクがあって、それを通して検索結果等をナビゲートする方法)を用いるページと違って、ページのHTML上に次の検索結果へのリンクがないということです。
つまり、既存のクローラーフレームワーク(Pythonの場合Scrapyとか)では、太刀打ちができないということです。
今回は、そんな厄介な無限スクロールのページをクロールする方法を、自分のメモも兼ねて紹介していきます。

実例

理論だけ紹介してもあれなので、自分が実際に書いた、Twitterから過去ツイートを引っ張ってくるクローラーを例に説明を進めていきます。
ソースはGithubのレポジトリをご参照ください。
https://github.com/keitakurita/twitter_past_crawler

ちなみに、

$ pip install twitterpastcrawler

でもインストールできます。

手法

無限スクロールの仕組み

では、そもそも無限スクロールとはどういう仕組みで動作しているのでしょうか?
無限スクロールといえども、無限の結果をあらかじめどこかにロードしておくことはデータ量的に無理です。つまり、無限スクロールは、ユーザーが下にスクロールするたびに動的に追加でデータをとってきています。
したがって、無限スクロールが動作するためには、
1. 現在表示されている範囲を知る
2. それを元に、次にとってくるべきデータを知る
ことができなければいけません。
大抵の場合、無限スクロールはなんらかの鍵となるパラメーターがあり、それが現在表示されている範囲を表し、そのパラメーターを使って次の結果をとってきています。

Twitterの場合

実際にどのようにしてこれが実現されているかは、Twitterが裏でどういうリクエストを送っているかを見ることで解析することができます。試しに、qiitaという単語で検索して見ます。
自分はChromeを使っているのですが、どんなブラウザでも、ページの裏で動いているネットワークの状況を見ることができます。Chromeの場合は、「表示」→「開発/管理」→「デベロッパーツール」→ Networkから見れます。開くと以下のような画面が見れるはずです:

何度か下にスクロールしていくと、リクエストの一覧で何度か現れる怪しいURLがあることがわかります:

https://twitter.com/i/search/timeline?vertical=default&q=qiita&src=typd&composed_count=0&include_available_features=1&include_entities=1&include_new_items_bar=true&interval=30000&lang=en&latent_count=0&min_position=TWEET-829694142603145216-833144090631942144-BD1UO2FFu9QAAAAAAAAETAAAAAcAAAASAAAAAAAAQAAEIIAAAAYAAAAAAACAAAAAAAgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAQAAAAAEAAAAAAAAAAAABAAAAAAAAAAAAAIAAAAAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAAAAAEAIACAIQIAAAgAAAAAAAASAAAAAAAAAAAAAAAAAAAAAA

この最後のmin_positionというパラメーターが明らかに怪しいですね。試しにこのレスポンスの結果をダウンロードして見ると、json形式のレスポンスであることがわかります。その中身を見ると、

focused_refresh_interval: 240000
has_more_items: false
items_html: ...
max_position: "TWEET-829694142603145216-833155909996077056-BD1UO2FFu9QAAAAAAAAETAAAAAcAAAASAAAAAAAAQAAEIIAAAAYAAAAAAACAAAAAAAgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAQAAAAAEAAAAAAAAAAAABAAAAAAAAAAAAAIAAAAAAAAAAAAAaAAAAAAAAAAAAAAAAAAAAAAAAEAIACAIQIAAAgAAAAAAAASAAAAAAAAAAAAAAAAAAAAAA"

items_htmlは、ツイートの生のhtmlが入っています。これが求めているtweetの中身です。
注目するべきは、max_positionというパラメーター。先ほどのmin_positionというパラメーターと同様の形式をしていること。試しにこれを、先ほどのurlのmin_positionと交換して改めてリクエストを送ると、同様の形式のレスポンスが得られます。つまり、このmin_positionというのが、求める鍵となるパラメーターということです。

クロールの仕方

ここまでくれば、あとは簡単です。以下の処理を繰り返せば原理的にはクロールできます:
1. 先ほどの形式のurlを、パラメーター(例えばq:クエリ)を調節して、リクエストを送信する
2. 得られたjson形式のレスポンスから、items_htmlmax_positionを取得する。
3. items_htmlの中身を適当に処理する
4. max_positionを先ほどのmin_positionの代わりに代入し、リクエストを送信する
5. 2〜4を繰り返す

twitterpastcrawlerの使い方

自分が作成したパッケージでは、以下のように、クエリを与えるだけで先ほどの処理を自動的に行い、ツイートの情報をcsvファイルに吐き出すようにしています。

sample.py
import twitterpastcrawler

crawler = twitterpastcrawler.TwitterCrawler(
                            query="qiita", # qiitaというキーワードが含まれるツイートを検索する
                            output_file="qiita.csv" # qiita.csvというファイルにツイートの情報を出力する
                        )

crawler.crawl() # クロール開始

最後に

Twitter上から過去のツイートをとってくることができれば、あるイベント(例えば選挙やゲームの発売日とか)の最中、前にどういうツイートがされていたかを調べることができたりして、いろいろ面白いと思います。他にも無限スクロールのページは増えてきているので、無限スクロールするページのクロールは今後用途が広がっていくと思います。