プログラマでありたい

おっさんになっても、プログラマでありつづけたい

「データを集める技術」の販売開始前に修正箇所のお知らせです

 週明けの11/29が、「データを集める技術」の発売日です。その前に残念なお知らせです。Excelによる一発目のサンプルコードであるAmazonのカテゴリーランキングの取得方法が代わりました。

変更点


 サンプルとしてあげている「クラウド の 売れ筋ランキング」のHTMLのタグが変更されていました。1ヶ月ほど前はタイトルのタグの部分は、zg_titleという解りやすいクラス名で表現されていました。今現在だと、a-link-normalという汎用的なタグ名なので、もう少し上のタグからとって子要素の番号で指定する必要があります。その為、下記のようなスマートでない指定が必要です。

    For Each element In objDoc.getElementsByClassName("a-fixed-left-grid-inner")
        'シートに取得したタイトルの書き込み
        Worksheets(1).Cells(row, 1) = row
        Worksheets(1).Cells(row, 2) = element.Children(1).Children(1).innerText
        row = row + 1
    Next element

f:id:dkfj:20161125204820p:plain

今の気持ち


 「Amazon Web Services クラウドネイティブ・アプリケーション開発技法」の発売を待つばかりという段階で、Cognito User Poolが発表されたりLambdaの仕様やAPI Gatewayの管理コンソールのインターフェースがどんどん変わっていく様を見るしかなかった時の気持ちと同じです。いつか必ず起こると思いながら、今起こるのかよと。つまりまぁ、しゃーないです。
 スクレイピング本というものは、変化する前提で割り切るしかないのです。その為、やり方・考え方を伝えるのがメインにしないといけないですねという良い例でした。サンプルソースのページで出来るだけはフォローしていくようにする方針です。

SBクリエイティブ:『データを集める技術』サンプルファイル

データを集める技術 最速で作るスクレイピング&クローラー

データを集める技術 最速で作るスクレイピング&クローラー

「データを集める技術」という本を執筆しました

 Webからデータを集めるというテーマで、「データを集める技術」という本を書きました。中身は、クローラー/スクレイピング本なのですが、それだと内容が「Rubyによるクローラー開発技法」と丸かぶりになってしまうので、本職プログラマでない人でも読めるような内容を目指して書いています。

データを集める技術 最速で作るスクレイピング&クローラー

データを集める技術 最速で作るスクレイピング&クローラー

Excelでデータ収集をする理由


 メインのターゲットは、ITエンジニアです。ただし前述のとおり、プログラマでない人でも何とかなるように目指しています。ということで、メインのデータ収集環境は、ExcelとGoogleスプレッドシートです。何故、Excelを選んだかというと、技術書で10万冊売れるのはExcelだけだから。環境構築の負荷が極小で、かつ普及しているプラットフォームだからです。我々エンジニアからすると、プログラムを実行する環境を構築することは難しくありません。しかし、一般的にはここのハードルは非常に高いのです。そこをどうやって回避するか考えた結果、Excelを選びました。
 Excelはデータ収集のための実行環境として考えると、何気に優れています。少し設定をするだけでVBAというプログラム言語を利用できます。VBAからIEをコントロールできるので、ブラウザのエミュレートをどうするかという問題も不要です。また集めたデータをシートに保存することも可能ですし、グラフや表にまとめて可視化することも出来ます。使える言語がVBScriptのみということに目をつぶれば、中々に優れた環境です。
 Google スプレッドシートも同様の理由で選んでいます。Windows以外という選択肢も増えている中で、Excelの代替となる可能性のあるものの第一候補はGoogle スプレッドシートだと思います。Google スプレッドシートも、JavaScriptベースのGoogle Apps Scriptを使うことで、自由自在にデータ収集ができます。また、スケジュール実行やイベント駆動も出来るので、スクレイピングという枠に留まらず簡易Botの基盤としても有用なのではないでしょうか。冒頭ではExcelを連呼していましたが、実はExcelよりGoogleスプレッドシートの比重が高いです。

データ収集の方針


 私の人生のモットーは、できるだけ楽をしたいです。ということで、データを集めるにしろ出来るだけ楽をしようと考えながら構成しています。具体的にいうと、プログラムをしなくても既にデータをとれる為の手段があるのであれば、それを使う。自分で頑張る必要がある場合でも、ExcelやGoogleスプレッドシートのセル関数だけで済むものがあれば、それだけで完結される。万策尽きた場合のみ、プログラムするという方針です。
 楽をするために、いろいろなサードパーティ製のサービスを使っています。スクレイピングサービスであるImport.ioや、単なる通知ツールにとどまらないIFTTTやmyThings、連携ツールとしてのSlackなどです。

リアルタイム通知



 本書で重視しているのが、データを発見したときのリアルタイム通知です。データを集める目的として、大量のデータを効率的に集めたいというのと、欲しい情報がWeb上にあがったら即座に知りたいというのがあります。前作については前者を重視していましたが、本作については後者となっています。
 理由としては、そういったニーズがかなりありそうだということです。前作のRubyによるクローラー開発技法についても、エンジニア以外の人でも読んでいますという声をだいぶ頂きました。データを集める目的を聞いてみると、マーケティングや広報・企画といったところで、事前調査や反響を知りたいという目的が多かったです。そういった用途の場合、データを集積するということも大事ですが、なるべく早く知りたいという要求も高くなります。その辺りを考えて、モバイルへのリアルタイム通知については多くのページを割いています。
※本を何冊か書いているうちに、エゴサーチの鬼になったので、そのあたりのニーズがよく解るようになったという事情もあります。

まとめ



 上記のような方針で書いているので、今まで書いてきた本と読者層がだいぶ違うようになります。正直なところ、想定している読者層にちゃんと届くのか、またどんな評価になるのか非常にドキドキしています。わりと配本も多いので、少し大きめの本屋であれば置かれるのではないかなと思うので、本屋で見つけたら是非ペラペラとめくってみてください。

 目次は、こんな感じです。

Chapter1 スクレイピングとクローラーで効率的にデータを集めていく
 1-1 データはWeb上に集まる
  Web上に存在するデータの種類
  本書が対象とするデータ
 1-2 効率的にデータを収集するために行うこと
  データ収集の3つのステップ
 1-3 どうすれば自動化できるのか?
  スクレイピングとクローラー
  半自動化プログラムでできること
  完全自動化プログラムでできること
  本書の進め方
 1-4 Webサイトからデータを集める際の注意事項
  収集したデータの取り扱いと著作権
  Webサイトのリソース圧迫と業務妨害
  クローラーとAPI

Chapter2 Excelとブラウザを連携してWebページからデータを収集する
 2-1 Excel VBAでWebからデータを収集する
  Excelでのデータ収集テクニック
  ExcelでVBAが使えるようにする設定
  データを収集するためのプログラム
 2-2 VBAでIEを操作する仕組み
  宣言部分
  VBAの関数の宣言
  IEの操作
 2-3 取得したHTMLから要素を抽出する仕組み
  HTMLの構造
  HTMLから要素を抜き出すプログラム
 2-4 簡単な方法で要素を特定する
  ChromeでXPathを抽出する方法
  FirefoxでXPathの抽出する方法
 2-5 WEBSERVICE関数とFILTERXML関数によるデータ収集
  WEBSERVICE関数によるデータ収集
  FILTERXML関数によるデータ収集

Chapter3 Googleスプレッドシートを利用して自動的にデータを収集する
 3-1 GoogleスプレッドシートとGoogle Apps Scriptを準備する
  Googleスプレッドシートとは
  Google Apps Scriptとは
  関数とクラス
 3-2 ImportXML関数を使ってWebからデータを収集する
  ImportXML関数の概要
  株価データの取得方法
  セルの値を利用した取得方法
 3-3 Google Apps Scriptを利用してデータを収集する
  Google Apps Scriptの使い方
  Google Apps Scriptによるスクレイピング
  Google Apps Scriptの実行
 3-4 指定した時間に定期的にデータを収集する
  Google Apps Scriptのトリガー
  トリガーの設定方法
Chapter4 スクレイピング専用サービスを利用して手軽にWebからデータを収集する
 4-1 import.ioを利用してWebからデータを収集する
  import.ioのサービス概要
  import.ioの利用方法
 4-2 import.ioでスクレイピングを実行する
  import.ioへのサインアップ
  データの自動抽出
  抽出したデータの取得方法
  取得する要素の任意指定
  APIを使ったデータ抽出
 4-3 収集したデータをシート上に保存する
  Googleスプレッドシートとの連携
  Google Apps Scriptとの連携
  Excelとの連携
Chapter5アプリケーションを利用してTwitterからデータを収集する
 5-1 アプリを利用して収集した結果を通知する
  通知を受け取るためのアプリケーション
  IFTTTの概要
  Slackの概要
  myThingsの概要
 5-2 IFTTTを使ってTwitter上でエゴサーチを行う
  IFTTTの会員登録
  モバイルアプリのインストール
  Twitter検索のレシピ作成
 5-3 Slackを利用して定期的にRSS情報を収集する
  Slackの会員登録
  RSSフィードの収集
  モバイルアプリからの利用
 5-4 myThingsを使ってTwitter上でエゴサーチを行う
  会員登録とモバイルアプリのインストール
  Twitter検索の組み合わせの作成
Chapter6 ターゲットを絞り込んで、さまざまなデータを自在に収集する
 6-1 会社・商品に関する情報を収集する
  特定のキーワードを収集する目的
  収集・保存・通知方法
 6-2 Webから特定の「キーワード」に関する情報を収集・蓄積する
  Googleアラートの設定
  IFTTTの設定
  結果の確認
 6-3 Twitterから特定の「キーワード」を含むツイートを収集・蓄積する
  IFTTTの設定
  結果の確認
 6-4 App StoreからiOSアプリのランキングを収集する
  ランキングのRSSフィード
  ランキングの収集
  結果を日付別シートに保存
  指定した時間に毎日実行する
 6-5 Google PlayからAndroidアプリのランキングを取得する
  import.ioを使ったデータ収集
  結果をGoogleスプレッドシートに保存
  日本語ランキングの取得
 6-6 Amazonのカスタマーレビューを収集する
  import.ioを利用した収集方法
  Googleスプレッドシート+ImportXMLによる収集方法
  Google Apps Scriptを利用した収集方法
 6-7 上場企業の時価総額を収集する
  銘柄コード一覧の取得
  株価に関する情報の収集
  時系列で株価データを収集
 6-8 Amazonのセール情報を取得する
  Amazonのほしい物リスト
  ほしい物リストから商品を取得(Excel版)
  ほしい物リストから商品を取得する(Google Apps Script版)
  Amazonの割引率が高い商品を探す
  商品の割引率とポイント還元率の取得(Excel版)
  商品の割引率とポイント還元率の取得(Google Apps Script版)
Chapter7 データを収集した後の見せ方と使い方のヒント
 7-1 クローラー/スクレイピング開発と関連技術
  認証が必要なページからのデータ収集
  大規模なデータ収集
  スクレイピング/クローラー基盤の将来のアーキテクチャ
 7-2 データを可視化して理解しやすくする
  表とグラフ
  Google My Mapsを利用した地図データとの組み合わせ
  その他の地図データの活用
 7-3 集めたデータを組み合わせて分析する
  分析手法
  機械学習サービス「Amazon Machine Learning」

データを集める技術 最速で作るスクレイピング&クローラー

データを集める技術 最速で作るスクレイピング&クローラー

Amazonのほしい物リストをRSS化するAPIを作ってみた

 執筆上の都合でAmazonのほしい物リストをRSS化するAPIを作ってみました。構成的には、次のような形です。

f:id:dkfj:20161024072647p:plain

 下記のようなURLで取得できます。wishlist_idの部分に公開のほしい物リストのIDを指定したら使えると思います。

https://wishlist-api.takuros.net/prod?wishlist_id=3G4653SB32HMZ

※効果計測の意味を兼ねて、アフィリエイトタグを付けています。

構成上のポイント



 ほしい物リストからNode.jsのLambdaでスクレイプして、リスト内のタイトルとURL・登録日を取得しています。また次のページがある場合は、再帰的に取得できるようにしています。出力は、RSS 2.0形式のXMLで出力しています。それをAPI Gatewayを利用してHTTPSからキックできるようにしています。CloudFrontとAWS Certificate Manager, Route53は、独自ドメインで証明書付で使えるようにする為です。
 問題点としては、API Gatewayのタイムアウトが30秒ということです。Lambdaは300秒まで使えるので、10ページくらいのリストであれば問題なく取得できます。API Gatewayの方の制約で、3〜4ページくらいでタイムアウトすることが多いです。ということで、現状は1ページのみ取得となっています。ここについては、取得済みのページをキャッシュする機構等が必要かなと考えています。

API GatewayとLambdaのバージョニング・エイリアス



 仮に公開するとなると、本番/開発環境とかバージョニングとか考えるよなと思って、導入しています。改めて使ってみると、よく出来た仕組みです。一方で、手動で管理するには、ちょっとつらい領域ですね。最初からCLIでスクリプト化する方が良さそうです。もしくはフレームワークの導入ですね。あと、API Gateway+独自ドメイン+SSLについては、現状AWS Certificate Manager(ACM)が使えません。ここの所は、何とかして欲しいですね。

CloudFront+API Gateway



 ここのところが、よく解りません。キャッシュの設定やヘッダーの転送等で上手く動かない部分があります。いろいろ確認する必要を感じました。一方で、API Gatewayの実装って、もともとCloudFrontに関わっているので、CloudFrontを噛ます意味があるのかという所で疑問も出てきますね。

改善点



 スクレイプのタイミングが要調整です。キャッシュがあればキャッシュを返して、そのリクエストベースでキューに溜めてスクレイプしておくくらいの割り切った実装がよさそうです。スクレイプ結果はS3に保存してキャッシュ代わりにするという形です。

感想



 どんなつまらないものでも、公開するという前提で考えるといい勉強になりますね。

データを集める技術

データを集める技術

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

GoogleスプレットシートのImportFeedのキャッシュの話

 GoogleスプレットシートのImportFeedなど、Import系のタブ関数を使っていると2度目はサクッと動きます。それもそのはずで、Google側でキャッシュをしていて、取りに行っていないからです。

ImportFeedのキャッシュの動作


 例えば、下記のように取得し、隣の列にもう一度貼り付けたとしてもすぐに結果が返ってきます。

=IMPORTFEED("https://itunes.apple.com/jp/rss/topgrossingapplications/limit=25/genre=6015/xml","items title","true")

 自分で建てたサーバ上にXMLを配置し、Googleからのアクセスを確認しても2度目以降はアクセスが来ませんでした。問題は、そのキャッシュ時間がどれくらいか解らないこと。また、コントロールする方法が解らないことです。

f:id:dkfj:20161009203936p:plain

※ちなみに新しいURLを発見したと判断したのか、速攻でGoogleBotが来ますね。

ImportFeedのキャッシュ回避策


 取り敢えずの回避策としては、URLを変えてしまうことです。例えば、下記のように引数に意味のない一意の値をいれると、Googleから都度取得するようになります。

https://itunes.apple.com/jp/rss/topgrossingapplications/limit=25/genre=6015/xml?x=20161009201020

まとめ


 これでいいのかという気がしますが、世の中の人はどうしているのでしょうか?正しい方法あれば、教えてください。

追記


定期的に実行させて、サーバー側のアクセスログを見てみました。
12時間前後くらいキャッシュするっぽいような気がします。
テストパターンとか変えて、試してみればもう少し詳しくわかりそうですね。

66.249.77.25 - - [16/Oct/2016:04:24:52 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.32 - - [16/Oct/2016:13:58:21 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.33 - - [16/Oct/2016:23:52:48 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.32 - - [17/Oct/2016:13:52:24 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.71.6 - - [18/Oct/2016:04:33:14 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.77.26 - - [18/Oct/2016:18:00:33 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.32 - - [19/Oct/2016:08:19:01 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.83 - - [19/Oct/2016:20:43:56 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.79.73 - - [20/Oct/2016:10:38:22 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.75.168 - - [21/Oct/2016:00:41:56 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.65.52 - - [21/Oct/2016:15:12:07 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"
66.249.65.52 - - [22/Oct/2016:06:06:53 +0000] "GET /test.xml HTTP/1.1" 200 358999 "-" "FeedFetcher-Google; (+http://www.google.com/feedfetcher.html)"

Google PlayからAndroidアプリのランキングを取得する

 前回、手軽にAppStoreからiOSアプリのランキングを取得する方法を紹介しました。Androidの場合は、どうしたら良いのでしょう?幾つか方法を考えてみます。対象は売上トップのAndroidアプリとして、URLは次の通りです。
https://play.google.com/store/apps/collection/topgrossing?hl=ja

  1. import.io + Google スプレッドシートのあわせ技
  2. Google スプレッドシートのIMPORTXML関数で何とかする
  3. Google Apps Scriptで頑張る
  4. みんな大好きExcelを使う

import.io + Google スプレッドシートのあわせ技



 まず試すのが、import.ioとGoogle スプレッドシートのあわせ技です。これで実現できれば、一番ラクです。

import.ioでログインし、New ExtractorでURLを指定します。すると下記のように、自動的に項目を抽出してくれます。
f:id:dkfj:20161008101245p:plain

ただ、何か辺です。どう見ても日本のランキングには見えません。恐らくIPアドレスを元に実行元の国のランキングを出しているっぽいです。残念です。

 ちなみに、取得結果をGoogle スプレッドシートに吐き出すのは簡単です。作成したEcxtactorからIntegrateタグを選び、Google Sheetsを選ぶとImportData形式の関数を吐き出してくれます。それをスプレッドシートに貼るだけです。

f:id:dkfj:20161008101257p:plain

 簡単なので、国内のデータが取れないのが残念です。
※別解として、AWSのAPI Gatewayを日本に用意して、HTTP Proxyとして返すという手はあるかも。

Google スプレッドシートのIMPORTXML関数で何とかする


 それでは、次にGoogle スプレッドシートのImportXML関数です。先程の実行元IPがあるので、そもそもGoogle スプレッドシートがどこで実行されているか確認する必要があります。httpサーバーを立ててアクセスし、ログからIPアドレスを取得します。その結果を、IPアドレスの素性が解るサイトで検索します。

f:id:dkfj:20161008105657p:plain

 見事にアメリカのカリフォルニアからのアクセスです。残念ながら、何とかなりませんでした。こちらも、Proxy的なモノを用意してとかなら何とか出来るかもしれませんが、一般的では無くなるので、止めておきます。

Google Apps Scriptで頑張る


 次に、Google Apps Scriptで頑張れるか確認してみます。前回と同様に、IPアドレスの素性を調べます。

f:id:dkfj:20161008110017p:plain

 今度は、アメリカのヴァージニア州でした。こちらは、GAEで動いているんだなと、変に納得してしまいます。と言う事で、Google Apps Scriptでも頑張れませんでした。

みんな大好きExcelを使う



 クラウド上のサービスが駄目なら、ローカルから実行すれば良いじゃないか。ということで、みんな大好きなExcelを使ってみましょう。Excelから外部データを取得するには、大きく2つの手段があります。

1. WebService関数を使う
2. VBA+IEでスクレイピング機能

 まず簡単なWebService関数を使ってみましょう。この関数は、URLを指定するだけというお手軽な関数です。

f:id:dkfj:20161008113245p:plain

 エラーが出ています。この関数の弱点として、文字数制限があります。元々Excelのセルの制限に、32,767文字というものがあります。それに引きづられて、関数の取得できる文字列のMaxも32,767文字に制限されています。最近のHTMLだと、殆どの場合が超えてしまいます。RSSとか取得するには、良いんですけどね。今回は使えません。残念。

 仕方がないので、VBA+IEで行ってみましょう。処理の概要としては、VBAからInternetExplorerを立ち上げ、HTMLElementCollectionで必要な部分のHTML要素を抜き出すという方法です。この方法の詳細は、こちらをご参照ください。

Excel VBA+IEでのスクレイピング


 取得方法としては、"card no-rationale square-cover apps small"というclass名指定するとランキングのリストが取得します。そこから、子のオブジェクトを指定するという形になります。その結果をシートに保存します。面倒くさいですね。

Option Explicit
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Private objIE As InternetExplorer

Sub main()
    Set objIE = New InternetExplorer
    Dim url
    Dim baseUrl
    
    baseUrl = "https://play.google.com"
    url = baseUrl & "/store/apps/collection/topgrossing?hl=ja"
    objIE.Visible = True
    objIE.Navigate2 (url)
    
    '読み込み完了待ち
    While objIE.readyState <> READYSTATE_COMPLETE Or objIE.Busy = True
        DoEvents
        Sleep 100
    Wend
    Sleep 100
    
    Dim objDoc As HTMLElementCollection
    Set objDoc = objIE.document
    Dim element As IHTMLElement
    Dim title
    Dim path
    Dim row
    row = 1
    
    For Each element In objDoc.getElementsByClassName("card no-rationale square-cover apps small")
        title = element.Children(0).Children(2).Children(1).innerText
        path = element.Children(0).Children(2).Children(1).getAttribute("href")
    
        Worksheets(1).Cells(row, 1) = title
        Worksheets(1).Cells(row, 2) = baseUrl & path
        
        row = row + 1
    Next element
    
    objIE.Quit
    Set objIE = Nothing
End Sub

取得結果は、次のような形になります。
f:id:dkfj:20161008132611p:plain

まとめ


 まさかのExcelを使う羽目になってしまいました。クラウド上のWebサービスは便利なのですが、実行元の制限ができません。クライアント側のIPアドレスによって、国を判別するという処理には極めて弱くなります。AWSのAPI GatewayのHTTP Proxyを使うとか、回避方法は色々あるのですが本筋ではないので省略します。とりあえずIPアドレスを元に判別という処理は、なくしてほしいですねと勝手な願いで終わりとさせて頂きます。

See Also:
AppStoreからiOSアプリのランキングを取得する
環境構築レスでAmazonの商品レビューを取得する
Google スプレッドシートの関数でWebからデータを取得する

AppStoreからiOSアプリのランキングを取得する

 スマホアプリの人気の動向を知りたい場合は、AppleのAppStoreとGoogle Playを見ると思います。今回は、それを自動的に取得する方法を紹介します。

AppStoreのRSS Feed GeneratorからURL取得



 AppStoreのランキングは、RSS Feedで配信されています。また、いつから存在するのか解らないのですが、このRSS Feedを生成するGeneratorサイトがあり、任意の国/カテゴリー/データ種別用のRSSを指定できるようになっています。

f:id:dkfj:20161007073726p:plain

RSS Generator

例えば、日本での有料のiOSアプリのトップ25を取得する場合は、次のようなURLとなります。
https://itunes.apple.com/jp/rss/topgrossingapplications/limit=25/genre=6015/xml


 取得できるXMLは、次のような形です。
f:id:dkfj:20161007074216p:plain

Google スプレッドシートで取得する



 それでは、取得したデータをGoogle スプレッドシート取得してみましょう。Google スプレッドシートには、幾つか外部データを取得するセル関数が存在します。今回は、ImportFeed関数が良いでしょう。関数の詳細については、この辺りを見てください。


Google スプレッドシートの関数でWebからデータを取得する

 ImportFeedの構文は、次のようになっています。

IMPORTFEED(URL, [クエリ], [見出し], [アイテム数])

 必須はURLのみで、後はオプションです。まずは、URLのみでの取得をしてみましょう。
f:id:dkfj:20161008092711p:plain

 デフォルトでは、Title,Author,URL,Summaryの4つの項目を取得します。しかし、このデータの場合はSummaryが大きすぎて見づらいです。そこで、項目指定で取得することにします。項目は、引数"クエリ"で指定します。欲しい値は、TitleとURLです。その場合、"items title"と"items url"といったように指定します。

=IMPORTFEED("https://itunes.apple.com/jp/rss/topgrossingapplications/limit=25/genre=6015/xml","items title","true")
=IMPORTFEED("https://itunes.apple.com/jp/rss/topgrossingapplications/limit=25/genre=6015/xml","items url","true")


※本当は、1つの関数で取得したいのですが、複数項目を指定する方法が解りませんでした。誰か教えてください。

 A1,A2のセルに上の二つの関数を並べると、次のような形になります。スッキリ!!
f:id:dkfj:20161008093831p:plain

まとめ



 AppStoreのランキングについては、RSSで配信されているので簡単に取得できます。一方で、Google PlayについてはRSSが提供されていません。次回、どうやったら楽に取れるか考えてみます。

See Also:
環境構築レスでAmazonの商品レビューを取得する
Google スプレッドシートの関数でWebからデータを取得する

環境構築レスでAmazonの商品レビューを取得する

 世の中、ひょんなことから思いもかけないようなデータが必要になる場合があります。そんな時に備えて、クローラー/スクレイピングのノウハウを持っているのは当たり前の時代です。(大嘘)
 そんな訳で、Webから簡単にデータを取ってくる方法を紹介します。取得する為に、サーバーやクライアントPCの実行環境を構築すると言った瞬間、8割の人が去っていきます。そこで、環境構築レスでデータを収拾する方法を考えてみます。また、ちょっと癖があるAmazonの商品レビューを例に考えてみます。

 今回の対象は、この2冊の本のデータを取得するとしましょう。
Amazon Web Services パターン別構築・運用ガイド
Rubyによるクローラー開発技法

ポイントとしては、次のとおりです。

  • 複数の本を引数指定で取ってこれるようにしたい
  • レビュー数が10件以上あるので改ページが必要。

 取得は、出来るだけ楽にしたいです。その為、サーバの構築といったことはなく出来るだけ誰でも使えるような環境を考えています。

  1. GoogleスプレットシートのImportXML関数を利用する
  2. ImportIOを利用する
  3. Google Apps Scriptを利用する
  4. AWS Lambdaを利用する

GoogleスプレットシートのImportXML関数を利用する


 お手軽という点でGoogleスプレットシートがまず考えられるのですが、今回の用途では、残念ながら使えません。何故なら、Amazonさんの方で対策されていて、データが返ってこないからです。下記は、curlでImportXML関数の
ユーザーエージェントを指定した場合です。200 O.K.が返ってくるもののデータは返してくれません。Amazonさんも色々あって困っているのでしょう。察してあげてください。

$ curl --head -H 'User-Agent: Mozilla/5.0 (compatible; GoogleDocs; apps-spreadsheets; +http://docs.google.com)' https://www.amazon.co.jp/product-reviews/4797382570/ref=cm_cr_getr_d_show_all?pageNumber=1

HTTP/1.1 200 OK
Server: Server
Date: Sun, 02 Oct 2016 03:44:39 GMT
Content-Type: text/html;charset=UTF-8
Content-Length: 336708
Connection: keep-alive
X-Frame-Options: SAMEORIGIN
cache-control: no-cache
pragma: no-cache
expires: -1
Content-Language: en-US
Vary: Accept-Encoding,User-Agent
Set-Cookie: session-id=351-5722232-0993809; Domain=.amazon.co.jp; Expires=Tue, 01-Jan-2036 08:00:01 GMT; Path=/
Set-Cookie: session-id-time=2082787201l; Domain=.amazon.co.jp; Expires=Tue, 01-Jan-2036 08:00:01 GMT; Path=/

 ということでGoogle スプレッドシートのセル関数は諦めます。
使い方については、こんな感じです。ちょっとしたことに、便利ですよ。

Google スプレッドシートの関数でWebからデータを取得する

ImportIOを利用する



 次にImportIOを利用する場合です。これは、良くもあり悪くもありです。サインアップして、Create New Extractorで新規の抽出を作成を開始します。この時は、URLを指定するだけでほぼ望みどおりのデータを取得できます。

f:id:dkfj:20161002124947p:plain

 1つの商品だけを取得する場合は、これでも良いかもしれません。問題点としては、複数のURLを取得する場合です。ImportIOの場合、同じサイト・構造のURLであれば複数のURLを指定できます。ただし、設定で指定する必要があります。また、吐き出されるデータは、全てのURLの収拾結果を1つのレスポンスで返すという形になります。その為、データを受けた方で更に切り分け処理が必要となります。また、改ページ等の対応も必要となります。
 ImportIOの有料版を利用すると解決出来ることが増えるのですが、$240/月からとなるのでかなりハードルが高いんですよね。

Google Apps Scriptを利用する



 Google スプレッドシートのセル関数がダメだとしたら、Google Apps Script(GAS)も考えてみましょう。GASのデータ取得の場合は、UrlFetchAppが便利です。Googleスプレッドシートで問題となったユーザーエージェントは、次のような形になります。

Mozilla/5.0 (compatible; Google-Apps-Script)

 これでアクセスすると、Amazon CAPTCHAが出てきて人間かどうか疑われます。GASでこれをクリアーしようとすると、哲学的に大変になります。

<html class=""a-no-js"" lang=""jp""><!--<![endif]--><head>
<meta http-equiv=""content-type"" content=""text/html; charset=Shift_JIS"">
<meta charset=""utf-8"">
<meta http-equiv=""X-UA-Compatible"" content=""IE=edge,chrome=1"">
<title dir=""ltr"">Amazon CAPTCHA</title>
<meta name=""viewport"" content=""width=device-width"">
<link rel=""stylesheet"" href=""https://images-na.ssl-images-amazon.com/images/G/01/AUIClients/AmazonUI-3c913031596ca78a3768f4e934b1cc02ce238101.secure.min._V1_.css"">

 Google Apps ScriptのUrlFetchAppは、ヘッダー情報も送れるのでユーザーエージェントの偽装ができます。

  // 1.HTMLの取得
  var url = "https://www.amazon.co.jp/product-reviews/4797382570/ref=cm_cr_getr_d_show_all?pageNumber=1";

  var postheader = {
    "useragent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
    "accept":"gzip",
    "timeout":"20000"
  }
  
  var parameters = {
    "method":"get",
    "muteHttpExceptions": true,
    "headers": postheader
  }
  
  var response = UrlFetchApp.fetch(url,parameters);
  //var content = response.getContentText("UTF-8");
  var content = response.getContentText();

 ここまでは順調ですが、GASを利用時の最大の問題点があります。デフォルトの機能では、XPath指定でのデータ取得が出来ません。サードパーティ製のツールを使うか、正規表現で抽出する必要があります。これが、超絶面倒くさいです。色々な方が検討しているのですが、HTMLをXML形式に変換して扱うのがベターのようです。ただし、問題点としては全てのHTMLが上手くXMLに変換できる訳ではないという点です。幾つか試行錯誤した所、Xml.parseして更にXmlService.parseすると比較的成功率は上がります。なんだ、このノウハウは。。。
 また無事、XML形式にしたとしてもエレメント操作をするのは非常に面倒くさいです。ということで、idやclass、或いはAttribute指定で取得する関数を作るのが吉です。この辺りのブログを参考にさせて頂きました。

Google Script Api で Webサイトスクレイピング - shohu33's diary
HTML/XMLをパースする - 技術のメモ帳

function myFunction() {
  // 1.HTMLの取得
  var url = "https://www.amazon.co.jp/product-reviews/4797382570/ref=cm_cr_getr_d_show_all?pageNumber=1";

  var postheader = {
    "useragent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36",
    "timeout":"50000"
  }
  
  var parameters = {
    "method":"get",
    "muteHttpExceptions": true,
    "headers": postheader
  }

  var content = UrlFetchApp.fetch(url, parameters).getContentText();
  Logger.log(content);
  var doc = Xml.parse(content, true);
  var bodyHtml = doc.html.body.toXmlString();
  doc = XmlService.parse(bodyHtml);
  var root = doc.getRootElement();

  // 2. シートの用意
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet=ss.getSheetByName("sheet1");
  writeSpreadsheet(sheet, 1, 1, '評価');
  writeSpreadsheet(sheet, 1, 2, 'レビュー日');
  writeSpreadsheet(sheet, 1, 3, 'レビュー内容');

  // 3. レビューリストの取得
  var reviewList = getElementsByClassName(root, 'a-section review');
  var row = 2;
  for each(var review in reviewList) {
    // 4. 個々のレビュー要素の抜き出し
    var rate = getElementByAttribute(review, 'data-hook', 'review-star-rating' ); //評価(レート)
    var review_date = getElementByAttribute(review, 'data-hook', 'review-date' ); //レビュー日
    var review_body = getElementByAttribute(review, 'data-hook', 'review-body' ); //レビュー内容
    
    // 5. シートに記載
    writeSpreadsheet(sheet, row, 1, rate.getValue());
    writeSpreadsheet(sheet, row, 2, review_date.getValue());
    writeSpreadsheet(sheet, row, 3, review_body.getValue());
    row++;
  }
}

function writeSpreadsheet(sheet, row, column, value) {
  sheet.getRange(row, column).setValue(value);
}

function getElementByAttribute(element, attributeToFind, valueToFind) {
  var descendants = element.getDescendants();
  for (var i in descendants) {
    var elem = descendants[i].asElement();
    if ( elem != null) {
      var e = elem.getAttribute(attributeToFind);
      if ( e != null && e.getValue() == valueToFind) return elem;
    }
  }
}

function getElementById(element, idToFind) {
  var descendants = element.getDescendants();
  for (var i in descendants) {
    var elem = descendants[i].asElement();
    if ( elem != null) {
      var id = elem.getAttribute('id');
      if ( id != null && id.getValue() == idToFind) return elem;
    }
  }
}

function getElementsByClassName(element, classToFind) {
  var data = [], descendants = element.getDescendants();
  descendants.push(element);
  for (var i in descendants) {
    var elem = descendants[i].asElement();
    if (elem != null) {
      var classes = elem.getAttribute('class');
      if (classes != null) {
        classes = classes.getValue();
        if (classes == classToFind) {
          data.push(elem);
        } else {
          classes = classes.split(' ');
          for (var j in classes) {
            if (classes[j] == classToFind) {
              data.push(elem);
              break;
            }
          }
        }
      }
    }
  }
  return data;
}

function getElementsByTagName(element, tagName) {
  var data = [], descendants = element.getDescendants();
  for(var i in descendants) {
    var elem = descendants[i].asElement();
    if ( elem != null && elem.getName() == tagName) data.push(elem);
  }
  return data;
}

 複数のURL指定とか、10件以上のコメント取得については、力尽きたので別エントリーで書きます。記録するシート別けて、総レビュー数からページ数を算出するだけなので、比較的簡単には作れます。
※これ誰か、エレガントに書いてくれませんかね。。。

 実行結果は、こんな感じです。なかなかいい感じでまとめられると思います。ただ、この行数を書けと言われると、ヤダと言う人が多いかもしれません。
f:id:dkfj:20161004005401p:plain

AWS Lambdaを利用する



 Lambdaを使う場合は、Node.jsかPythonを利用したらさっくりと書けます。例によって力尽きたので、取得部分だけ書きます。保存は、DynamoDBでも利用したら楽だと思います。

'use strict';
let client = require('cheerio-httpcli');
client.headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36';

exports.handler = (event, context, callback) => {
  client.fetch('https://www.amazon.co.jp/product-reviews/4797382570/ref=cm_cr_getr_d_show_all?pageNumber=1', { q: 'node.js' }, function (err, $, res) {
    console.log(res.headers);
    console.log($('title').text());
    let list = '';
    let reviewList = $("div[class='a-section review']");
    reviewList.each( function (idx) {
      let review = $(this);
      console.log(review.find("span[data-hook='rate']").text());
      console.log(review.find("span[data-hook='review-date']").text());
      console.log(review.find("span[data-hook='review-body']").text());
    });
    callback(null, review);
  });
};

 ほぼcheerio-httpcliの使い方だけの問題です。lambdaのアップロード手順とかは、この辺りみてください。ブラウザだけでも頑張れば何とかなるけど、基本的にはクライアント側にnode.jsをインストールして、cheerio-httpcliも取ってこないといけないので、非エンジニアにはハードル高いですね。
AWS Lambda+Node.jsのモジュールcheerio-httpcliでWebスクレピングをする

まとめ



 個人的には、Lambdaが一番ラクです。でも、Lambdaの場合は、AWSアカウント作成とかIAM関係のところとか、万人向けかというと残念ながらそうでもないです。悩ましいですね。そして冒頭で2つのURL使うとか言いながら、放置しておりました。また次回、Google Apps Script編で詳しく書きます。

データを集める技術 最速で作るスクレイピング&クローラー (Informatics&IDEA)

データを集める技術 最速で作るスクレイピング&クローラー (Informatics&IDEA)

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例