前回GASライブラリのParserを使用して、GASからWebスクレイピングを行う方法について書きました。
あれから幾つかのサイト向けにスクレイピングツールを作成していたのですが、実際使い出すとParserは思った以上に不便で、サイトの構成にもよりますが目的のデータをピンポイントに抽出するというのは意外と難しい事が分かりました。
前回の記事ではAmazonの商品一覧ページから商品名と価格を抜き出しましたが、あの様に綺麗に抜き出せる方が稀かもしれません。そう考えるとAmazonは良いサンプルでしたね。
今回はParserとプラスアルファを組み合わせて、少し取得が難しい(と思われる)データの取り方を実際のコード例とあわせて紹介してみたいと思います。
属性に固有の値が使われているタグから商品名のみを抜き出す
以下は駿河屋の販売商品一覧の画面です。
このページから『商品名』と『販売価格』の2つの値を抽出する処理を組むケースを例にします。まずは該当部分のhtmlソースを見てみます。
<div class="item_box">...</div>
この「item_box」は一覧の中の横1列部分にあたります。
その下に「item」クラスが続いており、こちらに商品一つ一つの情報が記述される形となっています。
ではこのitemの中身を更に掘り下げてみます。
itemクラスの内部はこの様な構造になっています。
ターゲットは赤枠の2箇所です。
まずは商品名から取得していきますが、Parserを使う場合は対象の値の前後の文字を指定する事になりますので、そのまま書くと
fromは「<a href=”/product/detail/185172560″>」
toは「</a>」
といった感じです。
・・・もちろん”この商品の場合”は上記の書き方で問題無いのですが、fromに含まれている「185172560」という数値は明らかに商品IDですよね。という事は商品が変わればこの部分の数値も変わってしまう事になります。
これでは纏めてデータを抽出する事ができないため、別の値を指定しなければいけません。
ではその前後に何か使えそうな文字列が無いかどうかを見てみると・・・
ありますね。
<p class=”title”>
<a href=”/product/detail/185172560″>…</a>
</p>
このtitleのクラスが設定されているPタグあたりが汎用的に使えそうです。
ただこのPタグで抽出してしまうと、Pタグ内に余計なaタグが残ってしまいますので、取り除く処理も追加して以下の様にします。
var titleArray = Parser .data(responseText) .from('<p class="title">') .to('</p>') .iterate(); // RemoveHTMLTagの中身は正規表現でタグを取り除く、 // またはsliceを使って">" ~ "</a>"間の文字列抽出など var newTitleArray = titleArray.map(RemoveHTMLTag);
これで商品名のみの配列を取得する事ができます。
※RemoveHTMLTagの中身は好きな処理で構わないので今回は紹介しません
正規表現で抽出する場合
ちなみに以下の正規表現を使えば、ピンポイントで商品名のみを抽出することが可能です。
<a href=”\/product\/detail\/.*>(.*?)<\/a>
こっちの方がシンプルだし良さそうではあるのですが・・・価格抽出の場合は正規表現だけでは難しいんですよね。
「末尾に”円”を含む0~9の整数とカンマで構成される値」という感じの正規表現を使えば取れる事は取れるのですが、「品切れ」のケースが存在しその場合は整数無しの文字列のみである事、この後実装する買取ページの場合はまた表記が変わることなどなど・・・判定式と組み合わせてあれこれする必要があります。
正規表現で値をうまく取れない商品が一つでも混じっていた場合、抽出した配列の要素数が商品名と価格で一致しないという事になると、出力結果にズレが生じてデータが使えなくなってしまいます。
という訳で、正規表現を使ってもそこまでシンプルにならないので今回は正規表現をメインに使うことはしません。
価格の取得方法
続いては価格の取得です。htmlは以下の通り。
<div class="item_price"> <p class="price_teika"> 中古: <span class="text-red"> <strong>¥ 600</strong> </span> 税込 </p> </div>
単純に見れば「<strong>~</strong>」間を抽出すれば良さそうに見えます。
実際<strong>タグが使用されているのはこの価格の部分だけですので、ピンポイントで価格を抽出する事ができるのですが・・・先ほども書いた「品切れ」表記のケースが存在するので、このままでは抽出漏れが起こってしまいます。
では、
<span class=”text-red”>~</span>
が使えるかと思いきや、「品切れ」の場合はこちらのタグもありません。通常の価格表記と同じく赤文字なんですけどね。
更に、
<p class=”price_teika”>~</p>
もありません。
結局どうするかと言うと、残った
<div class=”item_price”>~</div>
を指定するしか無い訳ですね。
ただここからは簡単です。
商品名と同様に、抽出した文字列から余計な部分を取り除いていくだけですので、それぞれ以下の様な処理を書いていきます。
・「品切れ」という単語が含まれていれば、任意のメッセージを格納する。
・検出できなければ通常の価格表記と判断し、<strong>タグ内の文字を抜き出す。
コードにするとこんな感じ。
function GetSalesPriceList(responseText){ var priceArray = Parser .data(responseText) .from('<div class="item_price">') .to('</div>') .iterate(); for(var i=0; i<priceArray.length; i++){ if(priceArray[i].indexOf("品切れ") > 0){ priceArray[i] = SOLD_OUT_MSG; continue; } priceArray[i] = priceArray[i].slice( priceArray[i].indexOf("<strong>") + "<strong>".length, priceArray[i].indexOf("</strong>")); //¥やカンマを取り除いたり色々して整数に変換 priceArray[i] = ConvertStrToPrice(priceArray[i]); } return priceArray; }
これで価格も無事に抽出する事ができました。
買取情報(価格)の抽出
続いては買取ページから買取価格などの情報を抜き出します。
駿河屋の「あんしん買取」のページに進み、どの様な構成になっているかを確認してみます。
販売ページとはまたデザインが違いますね。新たに抽出処理を作成する必要がありそうです。
早速ソースを見てみましょう。
更に中の構成を見てみます。
商品名をよくみると、
<div class=”title”>~</d>
に囲まれているのが分かると思いますが・・・これどこかで見た事ありますよね?
そうです。最初の方で書いた販売ページの構成と同じなんですね。
なので、実は商品名だけは処理を使い回す事ができます。
さて、問題は価格です。
価格自体はTDタグに囲まれていますが、その他の要素も全てTDタグが使われています。その上の階層のタグとなるとテーブル行のTRタグのみです。
なので・・・
このTRタグ以外使えそうなものは無いので、これを使います。
var priceArray = Parser .data(responseText) .from('<tr class="listap') .to('</tr>') .iterate();
from、toの指定はこんな感じですね。
fromの指定が「<tr class=”listap」と中途半端になっていますが、「<tr class=”listap”>」にしてしまうと「<tr class=”listap bgsell”>」の方が抽出できないのでこの様な指定にしています。
価格の要素はTDタグに囲まれていますので、抽出した各データに対してここから更にTDタグ内の文字を抽出する処理を書きます。
for(var i=0; i<priceArray.length; i++){ var tmpArray = Parser .data(priceArray[i]) .from('<td>') .to('</td>') .iterate();
これで各データ行のTD項目内のデータのみを抽出する事ができました。
ちなみに、priceArray[i]はfromに「<tr class=”listap」を指定したため、「”>」から始まる文字列になっています。
XmlService.parseだとタグが閉じられていなかったり”解析できる形”になっていない場合はエラーになっていましたので、このParserライブラリも何かエラーになるんじゃないの?
・・・と思うかもしれませんが、実は全く問題ありません。
Parserの中身を見てみると分かるのですが、あのライブラリが中で行っているのは
「IndexOfで指定した文字列の開始/終了位置を特定し、substringで切り出す」
という単なる文字列操作ですので、タグ閉じされていないだとかツリー構造がどうだとかそういうのは関係無いんです。「Parser」という名前でつい勘違いしそうですけどね。
結局、価格抽出のために組んだコードはこちら。
var priceArray = Parser .data(responseText) .from('<tr class="listap') .to('</tr>') .iterate(); for(var i=0; i<priceArray.length; i++){ var tmpArray = Parser .data(priceArray[i]) .from('<td>') .to('</td>') .iterate(); for(var j=0; j<tmpArray.length; j++){ // 末尾が「円」、または「」であれば価格データと判定 if(tmpArray[j].slice(-1) == "円"){ priceArray[i] = ConvertStrToPrice(tmpArray[j]); break; // 別途見積もりの表記の場合は「-」をセット }else if(tmpArray[j].indexOf("メールにてお見積") > 0){ priceArray[i] = "-"; break; } } }
・TD内文字列抽出後、末尾1文字を取り出し「円」であれば価格と判定し、整数に変換
・「メールにてお見積」という文字列が含まれていれば価格無し(-)を設定
といった感じ。
※自分用のアプリなので、変数名や構成が雑なのはスルーして下さい
まとめ
今回は自分用に作った駿河屋用販売/買取リスト一覧の抽出処理の中身を紹介してみました。
ただあくまで”抽出”する部分の処理だけですので、スプレッドシート上に貼り付けたり、次のページに移動する処理は別途用意する必要があります。
ちなみにアプリ自体は既に完成しており、今までに10万点以上の商品データを抽出していますが、今の所問題無く抽出する事ができています。
そして気になる実行速度についてですが、100ページ分2400商品で約70秒です。
※実行する日時によって差は出ます
GASの実行上限が6分と考えても、単純計算で500ページ分のデータは取得できますね。量的にも十分だと思います。
GASは使いこなすと本当に便利なので、ぜひ触ってみる事をお勧めします。