GAS(Google Apps Script)でスクレイピングを行い、Amazonの商品一覧ページから「タイトル」と「価格」をスプレッドシート上に取得する処理を書いてみたので手順を残しておきます。
対象のページと結果
まず今回作成した処理でどの様な結果が得られるのかを説明します。
処理の対象としたページは、以下の様なAmazonの商品一覧ページ。
上記ページを対象として処理を実行すると、
スプレッドシート上にこの様な形でデータが保存される処理となります。
ひとまず触りだけなので、商品の個別ページまで移動して詳細データを取得してきたり、自動で次のページに移動する処理などは無し。
では早速処理の中身を紹介していきます。
スクレイピング用のライブラリを追加する
GASはJavaScriptがベースとなっているのですが、スクレイピングなどでよく使う「getElementBy~」系のメソッドは用意されていません。
ですので「UrlFetchApp.fetch」を使って目的のページのhtmlを取得し、matchメソッドや正規表現を使って目的の情報を抽出するという方法が一般的の様です。
・・・が、
正規表現はあまり得意ではないのとメンテが面倒臭そうなのでそれはパス。
という事で他に方法は無いかを探してみた所、有志の方々がスクレイピング用のライブラリを作ってくれている様なのでそれを使ってみる事に。
Parser
色んなサイトでも紹介されている定番ライブラリ「Parser」です。
[5分でできるGoogle Apps Scriptを使用した簡単なデータスクレイピング] https://www.kutil.org/2016/01/easy-data-scrapping-with-google-apps.html(英語サイト)
設定方法は非常に簡単です。
上記ページ内に掲載されているライブラリのプロジェクトキーを、自身のスクリプトエディターに以下の手順で登録します。
上記①~④の手順でライブラリを設定する事で、「Parser」が使用可能になります。
記述方法
Parserを使う場合は以下の様に記述します。
var title = Parser .data(content) .from(fromText) .to(toText) .build();
data の引数 content は解析対象のhtmlコードを、
from の引数 fromText は取得対象の直前の文字列を、
to の引数 toText は取得対象の終わりの文字列を、
それぞれ引数として渡します。build()には引数不要。
これだけだと分かり難いと思いますので、公式のイメージ図を添付しておきます。
出典:kutil.org
[Easy data scraping with Google Apps Script in 5 minutes]より
以下のhtmlを例として、
<span class="e-f-ih" title="44,774 users">44,774 users</span>
「44,774 users」
という文字列を抽出しようとした場合、
<span class=”e-f-ih” title=”44,774 users“>
の部分をそれぞれ「from」と「to」に指定するというイメージです。分かり易いですね。
from と to の組み合わせに一致するコードが複数存在した場合についてですが、Parserの最後に「.build();」と記述した場合は一番最初にヒットした情報のみが返ってきます。
一致するコード全てのデータが欲しい場合は、「.build();」の部分を「.iterate();」と記述しましょう。これで一致するデータが配列として返ってきますので、以下の様な形で配列の4番目のデータを取り出す事ができます。
var titleList = Parser .data(content) .from(fromText) .to(toText) .iterate(); Logger.log(titleList[3]);
parser
最初の「Parser」と名前は良く似ていますが、こちらは日本の方が作成された別のライブラリです。当初はこちらを使おうと思っていたのですが、色々試した結果断念しました。
[Google Apps ScriptでHTML・XMLのスクレイピングをするライブラリを公開してみた] https://tadaken3.hatenablog.jp/entry/parser-for-gas
こちらのメリットは何と言っても「getElementBy~」形式で記述できる所。
Parserだと実際のソースをコピペしなければいけない所が手間ですし、「img1」、「img2」・・・といった感じで、対象ソースの一部に連番が含まれていると一気にデータを取れないという不便さがあります。
記述方法
使い方としては以下の様に使うらしいです。
var src = '<doc>' + ' <title id="doc-title">Anime Japan Expo</title>' + ' <chapter class="chapter">' + ' <paragraph class="paragraph">Do you like Anime?</paragraph>' + ' </chapter>' + '</doc>'; function parseXMLById() { var doc = XmlService.parse(src), xml = doc.getRootElement(), title = parser.getElementById(xml, 'doc-title'); Logger.log(title.getValue()); } //出力結果:Do you like Anime?出典:タダケンのEnjou Tech
[Google Apps ScriptでHTML・XMLのスクレイピングをするライブラリを公開してみた]より引用
ふむふむ・・・
では試しにAmazonのデータを取得してみようと思ったのですが、
「XmlService.parse」の所で早速何かエラーが出ました。
The markup in the document preceding the root element must be well-formed.
(ルート要素に先行するドキュメント内のマークアップは整形式でなければなりません。)
らしいです。
原因の箇所ははっきり分かりませんが、とりあえずXMLの整形式に沿っていないと。
つまり『Amazonのソースコードが解析できる形になってませんよ』ってこと?
知らんがな。
・・・
・・・
・・・とりあえず原因となる部分を修正してあげないと読めないってことね。
という事であの膨大なコードを調べて原因を潰すなんて事をわざわざやってられないので、今回こちらのライブラリについては見送る事に。ちなみにYahoo!のニュースサイトでも試してみたのですが、同様に解析段階でエラーとなってしまいました。
こちらは「<link>タグが閉じられてないよ!」って言われてるみたいです。
厳しい!
なまじ動いちゃう分、しっかりタグ閉じしてないサイトってたくさんあると思うんですよ。単純な閉じ漏れも含めて。そういうサイトが全部アウトって考えるとスクレイピングできるサイトってそれなりに限られてしまうのでは?と思う。
それって実用性低くない?このメソッド大丈夫?
・・・って個人的には思うんですが、GAS自体スクレイピングにあんまり興味無さそうなんで、「やりたければ他の方法(言語)でやってくれ」って事なのかもしれませんね。「getElement~」が使えませんしスクレイピング関連のメソッドも少ないですし。
※念のため書いておきますが、これらのエラーの原因はparserライブラリとは関係ありません。
スクリプトを書いてみる
という事でParserライブラリを用いて、冒頭のAmazonの一覧ページからデータを取得する処理を書いてみました。
// 出力先のスプレッドシートIDを記述 var SHT_ID = "***********************************"; function main() { var response = UrlFetchApp.fetch('https://www.amazon.co.jp/(省略)'); var fromText = '<a class="a-link-normal s-access-detail-page s-color-twister-title-link a-text-normal" target="_blank" title="'; var toText = '" href='; var titleList = Parser .data(response.getContentText()) .from(fromText) .to(toText) .iterate(); fromText = '<span class="a-size-base a-color-price s-price a-text-bold">¥ '; toText = '</span>'; var pliceList = Parser .data(response.getContentText()) .from(fromText) .to(toText) .iterate(); var sht = SpreadsheetApp.openById(SHT_ID).getSheetByName("main"); for(var i=0;i<titleList.length;i++){ sht.getRange(i + 2, 1).setValue(titleList[i]); sht.getRange(i + 2, 2).setValue(Number(pliceList[i].replace(",",""))); } }
※使い方の確認なので、最適化はしていません。
上記のコードをそのまま(出力先シートID、URLは要変更)実行すると、冒頭のスプレッドシートの様な出力結果を確認する事ができます。
Parserの書き方は記事中で解説していますし、特に複雑な処理は無いので特に説明はしません。
後はこれを応用して対象サイトや収集する情報を増やす事で、毎日自動的に情報を集めてくれる販売商品のリサーチ用アプリが作れそうですね。