前回までのあらすじ
JRA のサイトから出馬表をスクレイピングする、という目標を掲げたものの、肝心の出馬表ページを GET しようとするとパラメータエラーになってしまうことが判明。
URL を確認したところ、別々のページに見えたものがまったく同一の URL になっていましたとさ。
「出馬表 開催選択」画面
「出馬表 レース選択」画面
「出走馬一覧」画面
さて、こういう場合はどうしましょう?
ここで HTTP プロトコルについて、基本をおさらいしておきます。
HTTP プロトコルはいくつかのメソッドをサポートしています。
そのうち最もポピュラーなのは GET と POST でしょう。
GET はサーバにレスポンスを要求するメソッドです。静的な Web ページの取得で用いられるのはもちろんですが、CGI による動的なページをリクエストする場合は、一般的に URI に QueryString(「名前=値」形式のパラメータを & で連結したもの)を付加して使用されます。大量のデータを送信するのには向いていませんが、リクエストデータを単純に URI につないで送信すれば済むため、簡易的な送信フォームなどでよく利用されます。ブラウザのアドレスバーを見ると、エンコードされたリクエストデータを見ることができるのも特徴です。Google 検索すると、検索パラメータがアドレスバーに表示されるのを覚えている方もいるかもしれません。あれが GET です。
一方、POST はその名のとおりデータを投稿するのが目的のメソッドです。大量のデータ送信が可能で、リクエストデータはエンティティボディに格納されるため、ブラウザのアドレスバーを見ても何を送っているのかは まったく分かりません。
このことから、JRA サイトの同一 URL による一連の画面は POST によってリクエストされているのではないか、という推測が成り立ちます。だとすれば、ブラウザのアドレスバーを見ていても分かりませんが、水面下では取得したい画面に応じて異なるリクエストデータが送られているはずです。
では、どうすればリクエストデータを確認できるでしょうか。
Web 開発者が使用するブラウザとしてデファクト スタンダードの地位を保っているのが Firefox です。
Firefox が Web 開発者に重宝される理由にはいろいろあるでしょうが、便利なアドオンが揃っている点は間違いなくその理由のひとつでしょう。
今回は、HTTP リクエストを手軽に覗けるアドオン Live HTTP Headers を使ってみます。
執筆時点での最新バージョンは 0.14 です。このバージョンには UI の日本語サポートも含まれています。
インストールは、Firefox で上記にアクセスし、「Firefox へインストール」ボタンをクリックするだけです。
では早速 使ってみましょう。
JRA トップページを表示し、Firefox の [ツール]-[Live HTTP headers] を選びます。
実際にキャプチャする前に、設定タブの [正規表現による URL 除外] にチェックを入れておきましょう。ここにチェックを入れておかないと、画像や CSS、JavaScript ファイルのリクエストまですべてキャプチャされてしまい、目的の箇所を探すのに苦労することになります。逆に画像等のリクエストの詳細が必要な場合は、ここにチェックを入れてはいけません(あるいは正規表現を修正する必要があります)。この辺は、必要に応じて変更するようにしてください。
さて、設定の確認が済んだら、Live HTTP headers のフローティング ウィンドウが表示されている状態で、「出馬表」のリンクをクリックしてみます。
すると、Live HTTP headers のフローティング ウィンドウに情報がズラズラと出力されるはずです。
これが、ブラウザがサーバに要求したリクエストヘッダと、サーバから返ってきたレスポンスヘッダです。
お目当ては例の URL「accessD.html」のリクエストヘッダです。スクロールしながら探しましょう。
はい、見つかりましたね。
予想通り、POST です。
そして、「cname=pw01dli00%2FF3」というパラメータを送っていることも見て取れます。
このパラメータは、いったいどこから出てきたのでしょうか。
その答えは、いかにも JRA トップページの HTML ソースの中にありそうです。
ソースを表示して目視確認してもいいのですが、せっかく Firefox を使っているのですから、再びアドオンの力を借りるとしましょう。
今度は、これ無しでは開発できない、と多くの開発者に言わしめるほど絶大な人気を誇る開発支援アドオン Firebug を入れることにします。
本稿公開時点での最新バージョンは 1.3.2 (執筆時点での最新バージョンは 1.3.0 のため、スクリーンショットは 1.3.0 ) です。
インストールはやはり、Firefox で上記にアクセスし、「Firefox へインストール」ボタンをクリックするだけです。
では、Firebug をインストールしたら、JRA のトップページへ戻ってみましょう。
「出馬表」のリンク部分に仕掛けがあるのはまず間違いありません。
こんなときは、「出馬表」のリンクの真上で右クリックして、コンテクストメニューから「要素を調査」を選びます。
すると Firebug のウィンドウが開いて、クリックした要素の HTML を整形して表示してくれます。
これを見れば、一目瞭然ですね。
リンクに JavaScript が仕込んであり、引数に URL と 'pw01dli00/F3' というトークンを渡しているのが一目で分かります。
リクエストヘッダとの違いに注目してください。
リクエストヘッダでは「cname=pw01dli00%2FF3」というパラメータが送られていました。
しかし元のトークンは、'pw01dli00/F3'です。
URL エンコードされた結果、トークン中のスラッシュ (/) 記号が %2F に置き換えられて POST されている、という点に注意してください。
今回は固定トークンっぽいので、あらかじめ調べておいた URL エンコード済みの値をそのままセットしてやれば良さそうですが、トークンが動的に生成されるような場合は、自前で URL エンコード処理を行う必要がある状況もじゅうぶん考えられます。これについては今回は深く突っ込みませんが、とりあえず、パラメータをそのまま渡せば良いという状況ばかりではない、ということを頭の片隅に入れておいてください。
本トピックでは以降も Firefox の使用を前提とした説明が出てきますが、これをお読みの方の中には Firefox を入れたくないという方もいらっしゃるかもしれません。
そこで、IE 用のツールをいくつか紹介しておきます。
これらのツールを利用することで、IE でもスクレイピングに必要な情報を簡単に入手できるようになります。
紙幅の都合で、駆け足紹介。
言ってみれば Live HTTP headers の IE 版(機能的には劣りますが)です。
HTTP リクエストヘッダやレスポンスヘッダをキャプチャできます。
言ってみれば Firebug の IE 版です。こちらは MS 純正。
機能的には上の2つを合わせたより上、と個人的には思っているツール。HTTP ロギング、DOM インスペクター、スクリプト デバッガなど。ただし開発は 0.8.5.1 で停止しているっぽい感じ。
Web Development Helper に匹敵する高機能。それも当然で、本来は商用ツールです。個人使用に限りフリーで提供されています。
今回紹介するツールの中で唯一日本語化されており、使い勝手も良いため、個人的には IE での作業なら(あまり機会はありませんが)これをメインにしています。何気に色調査用のスポイトツールまで完備しているあたりがポイント高し。
なお以下は番外としてご紹介するものですが、理由は機能が不十分だからとかではなく、単に YU-TANG が使ったことがないためです。下記の方が適しているとか、性に合うという可能性はじゅうぶんにありますので、選択肢に入れておくとよいでしょう。
まず、IE と Firefox に両対応した HTTP Sniffer HttpWatch Basic Edition というものがあるらしいです。
GIGAZINE さんの紹介を読む限りだとかなり使えそうなので、試す価値アリです。
また、Fiddler Web Debugger - A free web debugging tool というものもあるようです。Proxy サーバとして機能するため、ブラウザの種類を問わず、あらゆる通信を捕捉してデバッグできます。デバッガを謳っているだけあって、ブレークポイントを仕掛けて、ヒットしたところで止めて詳しく調べる、ということも可能な模様。アドオンで機能拡張できるのも特徴のひとつで、単体では実装されていない機能もアドオンを入れることによって対応できたり。
自分はもともと、ローカル起動する JSON エディターを探していて、apeirophobia: Fiddler2のJson Inspectorを追加してエラー経由で知りました。興味のある方はこちらもお試しを。
さて、調査が長くなりましたが、POST に必要なトークンが分かったところで、前回 GET で取得できなかった「出馬表 開催選択」画面をリクエストしてみることにしましょう。
では解説です。前回とダブる部分の解説は割愛します。
POST です! まあ、それだけですが…。
これは Live HTTP headers でキャプチャしたリクエストヘッダにも登場していましたね。
HTML フォームからの URL エンコードされた送信であることを示すメディアタイプ ヘッダですが、WinHTTP は HTML フォームではないので、放っておいてもこのヘッダを付けてくれません(たしか…)。そこで、明示的に追加しておきます。
実際のところ、多くの CGI はこのヘッダが無くても正常に動作しますが(JRA のサイトも、付けなくても動作します)、ASP ではこのヘッダが無いと正常に動作しない場合があるらしいので、転ばぬ先の杖ということで、ちゃんと付けておきましょう。
Live HTTP headers でキャプチャしたリクエストヘッダからコピーした URL エンコード済みデータを Send します。
解説は早々に切り上げて、さっそく実行してみましょう。
……と、実行してみたところ、無事 HTML ソースを取得できたのですが…肝心の開催情報がソース中に見当たりません。
かと言って、パラメータエラーになっているわけでもないようです。
ワケが分からなかったので、ブラウザで JRA のトップページから「出馬表」をクリックしてみました。
ありゃ。。。
これを書いているいま現在は水曜日の夜なんですが、出馬表っていつも載っているわけじゃないんですね。道理でソースに開催情報が見当たらないわけだ。
え。そんなの常識ですか? そうですか。。。
気を取り直して、翌日 再チャレンジ。今度は出馬表が更新されているようです。
HTML ソースを取得して、「出馬表 レース選択」画面の POST に必要な、開催リンクに仕込まれているトークン(と、その前後)を抜き出してみます。
自分は(その方が早そうだったので)ソースを目視して抜き出しましたが、もちろん Firebug を使ってもよいです。
| 開催日 | 開催 | トークン付近 |
|---|---|---|
| 1月17日(土) | 1回中山5日 | 'pw01drl00062009010520090117/A3' |
| 1回京都5日 | 'pw01drl00082009010520090117/37' | |
| 1回中京3日 | 'pw01drl00072009010320090117/07' | |
| 1月18日(日) | 1回中山6日 | 'pw01drl00062009010620090118/91' |
| 1回京都6日 | 'pw01drl00082009010620090118/25' | |
| 1回中京4日 | 'pw01drl00072009010420090118/F5' |
さて、ここからはちょっと推理が必要です。
トークンが固定なら話は簡単ですが、どうもこのトークンは固定ではなさそうです。「2009010520090117」とか、見るからに YYYYMMDD の日付っぽい文字列が入っています。おそらく開催ごとにトークンが異なると見ていいでしょう。ということは、トークンの決め打ちは残念ながら不可です。「出馬表 レース選択」画面を POST で取得するには、先に「出馬表 開催選択」画面からトークンを動的に取得しておく必要がある、ということになります。
一方、トークンの切り出し自体はそれほど難しくはなさそうです。先頭の「pw01drl」あたりは固定っぽい感じですから、そこから次の引用符までを取り出せば良さそうですね。
というわけで、コードを修正してみました。
32〜38 行目が、修正した箇所です。
実行結果(イミディエイト ウィンドウ)は、こうなりました。
pw01drl00062009010520090117/A3 pw01drl00082009010520090117/37 pw01drl00072009010320090117/07 pw01drl00062009010620090118/91 pw01drl00082009010620090118/25 pw01drl00072009010420090118/F5
取れてそうですね。
一応 念を押しておきますが、自分はこのトークンの生成規則を知っているわけではありません。
今回はたまたま欲しい情報が取れましたが、しばらく使っていると、実は先頭の「pw01drl」は固定ではなかった、ということが発覚するかもしれません。そのときは、臨機応変に修正していきましょう。アドホック言語たる VBA の、実はこれこそ正しい使い方なのかもしれないと思ったり思わなかったりする今日この頃。「スクレイピングは永遠にベータ」などと言ってみるテスト。
やや錯乱気味ですが、もう一息。
「出馬表 レース選択」画面用のトークンは取れました。お次は、実際に「出馬表 レース選択」画面の HTML ソース×6を取得して、そこから「全てのレースを表示」リンクに仕込まれている「出走馬一覧」画面用のトークンを取得してみます。
先に「全てのレースを表示」リンクに仕込まれているトークンのパターンに規則性がありそうか、Firebug で見てみましょう。
これを見る限りでは、先頭の「pw01des」を固定とみなして取得できそうですね。
ではコード、行ってみましょう。
だんだんコードが長くなってまいりました。
前のコードではトークンをイミディエイト ウィンドウに出力しているだけでしたが、今回は後ほどリクエストで使用するので、Collection に退避しています。
そうなんですよ。知ってました? くどいですか。そうですか。
Collection に退避しておいたトークンを使って、「出馬表 レース選択」画面をリクエストします。
ここで、トークン内の "/" を "%2F" に置き換えることによって、簡易的に URL エンコードしています。
本来は URL エンコード専用の関数が必要ですが、今回は URL エンコードが必要な文字が "/" 一文字しか無さそうだったので、Replace 関数一発で処理しています。いつもこれで済むわけではないので、ご注意を。
レスポンスからトークンを切り出す部分は前回と同じロジックなので、説明不要でしょう。
似たようなパターンの繰り返しが多くなってきたのでサブルーチン化したい気もしますが、説明用なので、直線的にフローを追えるよう、このままの形で行くことにします。実際に開発するときは、繰り返しは無駄なので関数化するのもよいでしょう。
実行結果(イミディエイト ウィンドウ)は、こうなりました。
pw01drl00062009010520090117/A3 pw01des00062009010520090117/AC pw01des00082009010520090117/40 pw01des00072009010320090117/10 pw01des00062009010620090118/9A pw01des00082009010620090118/2E pw01des00072009010420090118/FE
ちゃんと取れているみたいで、一安心。
いよいよラスボス、「出走馬一覧」画面の取得です。
トークンさえ取れれば、後は繰り返すだけです。
解説は……同じことの繰り返しなので、もう解説するところがありません。
VBA によるスクレイピングがいかに簡単か、これでお分かりいただけたと思います。
さて、HTML ソースは取得できましたが、これはスクレイピングの前半戦でしかありません。
現時点で取得できているのは、HTML ソース丸ごとです。そこには、欲しいデータ以外の HTML タグが散りばめられているわけで、そのままでは再利用が困難なのは火を見るより明らかです。
この文字列の中から欲しい情報を取り出す解析作業が、スクレイピングの後半戦になるわけですが、それはまた次回。
WinHttp.WinHttpRequest または MSXML2.XMLHTTPxx の Send メソッドは、省略可能な引数として Body を取ります。
通常、ここには POST 時などにメッセージボディを指定します。
オブジェクトブラウザで見ると、こんな感じですね。
Sub Send([Body])
見てお分かりの通り、Body 引数は Variant 型です。つまり、厳密にはこの引数は Variant 型でなければなりません。
アーリーバインディングであればデータ型が自動変換されるため、String 型を渡しても問題は起きませんが、レイトバインディングだと、問題が起きます。ただし、ここがちょっとややこしいんですが、文字列リテラルはどちらでも通ります。つまり、今回のサンプルのようなケースです。それじゃどういう場合が問題なのかよく分からないゾという方のために、言い方を変えると、レイトバインディングで String 型の変数を渡した場合がダメです。
この問題の厄介なところは、現象が決まっていないように見えるところです。実行時エラーが発生する場合もあれば、実行時エラーが発生しないまま不正なデータが送信されて、結果的に期待したものと違うレスポンスが返ってきて悩むこともあります。
筆者が JRA のサイトで試したところでは、下記のような実行時エラーが発生したり、かと思えばレスポンス ステータス 200 のみ(ボディなし)が返ってきたり、レスポンス ステータス 200 + HTTP エラー 400 のボディというワケの分からない組み合わせが返ってきたり、支離滅裂でした。
これは JRA のサイトがいけないのでしょうか。それとも String 型変数を処理できない WinHTTP ライブラリがいけないのでしょうか。はたまたデータ型が不正なのを承知でテストしている YU-TANG の性格が悪いのでしょうか。
誰のせいでもない、みんな戦争が悪いのよ (©杉田かおる母)。
Body 引数を変数経由で渡す場合は、次のいずれかの形で記述するとよいでしょう。
混乱するようであれば、要・不要を問わず常に CVar() を付けてしまってもよいと思います。
なお、Send メソッドの Body 引数に渡せるデータ型は他にもあるようです。自分は未検証ですが、下記の魔界の仮面弁士さんのレスに詳しく記載されています。
サンプル MDB のダウンロードは下記からどうぞ。
ZIP 形式で圧縮されています。Windows XP 以降の OS 上であれば、特に外部ソフトを使わず解凍できます。
Windows XP 未満の OS で解凍用のソフトが無い場合は、Vector 等のソフトウェアライブラリから入手する必要があります。
なおコードを見てお分かりのとおり、アーリーバインディングでコーディングしています。
WinHTTP ライブラリが存在しない環境(Windows ME/98/95)で実行する場合は、WinHTTP ライブラリの参照設定を外し、モジュール頭部の
を
に書き換えてから実行してください。