2019/11/23

計算や検索を行うサイトからデータを取得する



0.はじめに

インターネットを見ると色々なサイトがあります。一般的には内容が表示されている、というのが多いと思いますが、中には複雑な計算をしてくれたり、固有のデータベースを使って検索語に対する答えを出してくれるサイトもあります。
ここでは、そのサイトに行って答えをもらうのではなく、Excelで直接データをもらおう、ということに挑戦したいと思います。

1.入力ページと出力(計算・検索結果の表示)ページの特定

例えば当サイトの「MD5ハッシュ値生成サイト(http://atsumitm.iobb.net/it/test_keisan1.php」を例に考えましょう。(もちろん、ここの説明を行うために作ったものです。また、ハッシュとはデータ改ざん防止等に使われる技術です。)
ちなみに拡張子の「.php」はHTML内にPHPのコードが埋め込まれたページで、クライアントがそのページを要求した際に、Webサーバ側で動的にWebページ(HTML形式)を生成してクライアントに返すものです。他に「.jsp」なども同様の動きをします。

このサイトは2つの機能(入力と出力)が1つの画面で構成されています。入力と出力の画面が分かれているサイトもあります。

2.入力側のソース解析

では、何か文字列を入力して実行ボタンを押した後、画面内で右クリックをし「ソース表示(ブラウザにより異なります)」を選択してみて下さい。サイトを構成しているHTMLが確認できます。その一部を書き出します。
(見易いように行を少し整えてあります)
  1. <body>
  2.  <h1>MD5ハッシュ値発生サイト</h1>
  3.  <form name="out" action="test_keisan1.php" method="POST" target="_self">
  4.   文字列を入れて下さい<br>
  5.   <input type="text" name="moji" size="22" maxlength="20" >
  6.   <br><br>
  7.   <input type="submit" name="exec" value="実 行"><br><br>
  8.  </form>
  9.  <br><br><br><br>
  10.  <table border="1">
  11.   <tr><td>元の文字列</td>
  12.   <td>test</td></tr>
  13.   <tr><td>MD5ハッシュ値</td>
  14.   <td>098f6bcd4621d373cade4e832627b4f6</td></tr>
  15.  </table>
  16.  <br>
  17. </body>
画面上では、「文字列を入れて下さい」に続くボックスに文字列を入力して「実行」ボタンを押すように出来ていますね。これをソースを追って見てみると、
<input type="text" name="moji" ・・・・>
で入力を促していることがわかります。inputタグには文字列(type="text")で入力され「moji」という名前(name="moji")がつけられています。また「実行」と書かれたボタンには送信(type="submit")が指定されています。 そしてそれらは<form ・・・> と </form> で挟まれている、という構造になります。

formタグで挟まれた中では「submitのボタンを押すとformに登録されたページ(action="test_keisan.php")へ名前(name="moji")に紐づけられたデータ(文字列)を送信する」ことができます。その送る方法(method)としてPOSTとGETの2種がありますが、この場合はPOSTということになります。
このサイトの<form>~</form>の間には、nameの付いた項目は1つしかありません(1つしかデータを送っていない)が、もちろん複数の項目を送る場合もあります(name=〇〇 の〇〇が複数ある)。
また、今回は送る先が自分自身(target="_self")であり、「自分を再読み込みする」ということになります。

尚、何も入れずに「実行」ボタンを押すと、結果は表示されません。

3.出力側のソース解析

</form>より下側のソースを見てみます。
画面とソースを見比べてみると、「元の文字列」の右に入力した文字列、「MD5ハッシュ値」の右にハッシュ値があります。 出力値であるこのハッシュ値を拾って来れば目的を達成することが出来ます。
ソースを文字列の形で何らかの手段で取得できたとして、ハッシュ値を得るにはどうすれば良いでしょうか。
ハッシュ値の直前には <td> という文字列、また直後には</td>という文字列がありますのでこれを目安にすれば良いのですが、同じ文字列がその前に一杯ありますのでちょっと工夫が必要です。
そこで、ソース中にある唯一のワードを探し、そのワードの位置から探索をスタートさせる、というやり方です。今回の場合だとソース中の「MD5ハッシュ値」を探し、その先にある <td> のすぐあとにある文字列を見つける というのはどうでしょう。
他にも色々と方法は考えられると思いますが、今回はこの2段階探索でやってみたいと思います。

4.入力ページの入力値を入れる部位名とデータを送る方法の確認

検索するデータ(文字列)をPOST方法でサイト「test_keisan.php」に直接送ってやり、その結果をソース内から探し出す、というプログラムを考えてみましょう。
  1. Sub main()
  2.  Dim st As String   '入力する文字列
  3.  Dim paramStr As String   'ページにデータを渡すPOSTパラメータ
  4.  Dim xmlhttp As Object   'サーバとのHTTP通信を行うための組込みオブジェクト(XMLHttpRequestオブジェクト)
  5.  Dim retCd As String    '結果コード(コード200であれば正常)
  6.  Dim retHtml As String   '結果HTML
  7.  Dim retStr As String    '結果データ
  8.  Dim Position1 As Integer 'ソース中での位置(結果データの手前側)
  9.  Dim Position2 As Integer 'ソース中での位置(結果データの後側)
  10.  Const url As String = "http://atsumitm.iobb.net/it/test_keisan1.php"
  11.  Const StartStr1 As String = "MD5ハッシュ値</td>"  '目的ワードの手前で特徴的な言葉・記号
  12.  Const StartStr2 As String = "<td>"   '目的のワードの直前の記号
  13.  Const EndStr1 As String = "</td>"     '目的のワードの直後の記号
  14.  st=inputbox("文字列を入力して下さい")
  15.   If st = "" Then Exit Sub
  16.  paramStr = "&moji=" & st
  17.  Set xmlhttp = CreateObject("msxml2.xmlhttp")
  18.  xmlhttp.Open "POST", url, False   'url2との接続
  19.  xmlhttp.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"  'HTTPリクエストヘッダ値を設定
  20.  xmlhttp.send (paramStr)   'パラメータを送付
  21.  retCd = xmlhttp.Status   '結果コードを取得
  22.  If retCd <> 200 Then
  23.   retStr= "Error番号:" & retCd
  24.  Else
  25.   retHtml = StrConv(xmlhttp.responseBody, vbUnicode)   '結果HTML取得
  26.   Position1 = InStr(retHtml, StartStr1) + Len(StartStr1)
  27.   Position1 = InStr(Position1, retHtml, StartStr2) + Len(StartStr2)
  28.   Position2 = InStr(Position1, retHtml, EndStr1)
  29.   retStr = Mid(retHtml, Position1, Position2 - Position1)
  30.    if retStr = "" then retStr = "データがありません"
  31.  End If
  32.  MsgBox (st & vbCrLf & retStr)
  33.  Set xmlhttp = Nothing
  34. End Sub
12行目でソース中で唯一のワード、13行目で直前のワード、14行目で直後のワード を指定しています。
16行目でユーザーが入力した文字列を、18行目でPOSTのパラメータとして送付できる形(& + 送るデータの名前 + 「=」 + 「データ」)に整えます。
19行目でHTTPにリクエストするためのオブジェクトの生成を行い、21行目で「test_keisan1.php」に接続。22行目でHTTPリクエストヘッダ値を設定。23行目でパラメータ値を送付します。
その結果として25行目で結果のコード(正常か異常かが番号として得られる)を取得でき、もし正常(正常は200番)以外であればコードを返して終了します(28~29行目)

正常であれば、そのHTML文を30行目で取得します。ここで取得の方法にStrConv関数を使用し、その第一引数として「xmlhttp.responseBody」を指定しています。Variant型でデータを受け取り、Unicodeでテキスト形式に変換し変数retHtmlに代入しています。

ここで、うまく行かなかった人(文字化け)がいるかと思います。実は呼び出しているページ(今回は、test_keisan.php)の文字コードがShift-JISであれば、上記のプログラムで動くのですが、utf-8のページの時には、30行目を以下のように書き換えます。
  1.   retHtml = xmlhttp.responseText   '結果HTML取得

対象のページが「Shift-JIS」か「utf-8」か(その他にもいくつか存在しますが)を確かめるには、ソース表示のところで、<head>~</head>で囲まれた中を見て、<meta ・・・・ charset=〇〇>という行を見つけて下さい。「charset=」の後の〇〇というのが 文字コードになります。
  1. <head>
  2.  <meta http-equiv="Content-Type" content="text/html;charset=Shift_JIS" />
  3.  <meta name="robots" content="noindex" />
  4.  <title>
  5.   テスト
  6.  </title>
  7. </head>
たまに「charsetの明記がされていないページ」もありますが、これはルール違反です。その時は仕方無いので色々試して特定してください。

尚、test_keisan1.phpはshift-JISで作ってありますが、同じ内容でutf-8の文字コードで作ったページ(異なるのは<・・・charset=utf-8" /> のみ)を下記に準備しました。
http://atsumitm.iobb.net/it/test_keisan2.php
コード違いでどうなるか、試してみて下さい。

5.出力された文字列から目的のワードを取り出す

30行目でretHtml変数として取得できたのは、文字列です。この中から欲しい文字の位置を調べ、取り出していきます。
探している文字の位置がどこか、を求める関数としてInStrを使います。この構文としては以下のようになります。

最初に出てきた文字列の位置 = InStr(検索の開始位置,*検索対象の文字列,*検索する文字列,比較モード)  *の引数は必須です。

ここで簡単な例を使って、どういう「文字列の位置」が出てくるか説明しましょう。まず文字列としてABCDEF・・・があるとします。また検索する文字はFGHとします。それを並べてみると、
 1  2  3  4  5  6  7  8
 A   B   C   D   E   F   G   H 
 E   F   G 
となり、検索する文字列「FGH」が合うのは、 検索対象文字列の先頭から6個目の文字= 検索する文字列の1個目の文字 と言う事にあります。これを数式で書くと、

最初に出てきた文字列の位置 =InStr(1,"ABCDEFGH","EFG",vbTextCompare)
で、戻り値として「5」が得られます。

では、次の例はどうでしょう。同じ「EFG」を探すのですが、2組目(13個目~)のEFGの位置を知りたい時は・・・・   
 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16
 A   B   C   D   E   F   G   H  A   B   C   D   E   F   G   H 
 E   F   G 

1つ目の例でやった「最初に出てきた文字列の位置」を使って、最初の引数である「検索の開始位置」を変えてみましょう。   
 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16
 A   B   C   D   E   F   G   H  A   B   C   D   E   F   G   H 
 E   F   G   E   F   G 
まず、InStr(1,"ABCDEFGH","EFG",vbTextCompare) で「最初に出てきた文字列の位置(戻り値=5)」が求められます。次に検索する文字列の長さ を「Len関数」を使って求めます。
検索する文字列の長さ = Len("EFG") Len関数の戻り値は「3」です。E・F・G で3文字ですね。
そして、「最初に出てきた文字列の位置(戻り値=5)」+「検索する文字列の長さ(戻り値=3)」を「検索の開始位置(数値8)」にすれば、2組目の合致する文字列を探し当てる事ができる、という算段です。
この考え方で、求める文字列の位置をコードにしたのが31~33行目です。

最後に、文字を取り出すのには、「Mid関数」を使用しています(34行目)。構文としては下の様になります。

取り出したい文字列 = Mid(*対象となる文字列,*取り出す開始位置,取り出す文字数))  *の引数は必須です。

文字列の位置の数字を足したり引いたりして、目的のワードを挟み込むような感じで取り出しています。

6.エラー処理

但し、予定通りにデータが得られるとは限りません。そのエラー処理として、この例の場合は
(1)アドレスが変わってしまった .... 27~29行目
(2)文字列が入力されなかった ・・・・ 17行目
を行っています。
他には、プログラムコードを修正されたことを想定してInStr関数で目的の文字列が見つからない(戻り値=0)こともエラー処理として必要かもしれません。場合場合に寄って状況が変わるので漏れのないようにして下さい。