目を付けた中古車の価格変動をウォッチする方法[GAS] ソース編

開発
画像に alt 属性が指定されていません。ファイル名: image-1024x542.png

↑の画像のように車の基本情報や、車両本体価格を毎日自動で作る仕組み(プログラム)を具体的に説明していきたいと思いまーす
まず、part1としては最初の一歩ってことで、GoogleAppScriptのエディタを開くところから、大まかな処理の説明
最後にソース全体をさらーっといきます。

Step1. Google Drive から スプレッドシートを開く

まず、Googleアカウントを持っている前提になってしまいますが、GoogleDriveを開きましょう
※Googleアカウントを持っていない場合は速攻で作りましょう

Google Drive のページへ入ったら、「新規+」ボタンをクリック

スプレッドシートを選択します

空のスプレッドシートが表示されます。Step1はここまで!

Step2. スクリプト(コード)エディタを開く

スプレッドシートの上部にある「ツール」から「スクリプトエディタ」をクリックしてください

myFunctionのみ書かれているコードが表示されます

Step2 はここまで!一瞬ですねw

Step3. 大まかな処理をイメージする

今回の記事のメインどころです

まずは取得したい項目を整理します。中古車情報サイトから自分が目を付けた車の価格変動を取得するのがメインなので、
「価格」は必須ですね。他にも車種だったり、グレードだったりっていうのが気になってきます。今回取得対象とするのは以下

・車の画像
・車種(車の名前)
・概要(ここにグレードが大体書かれている)
・年式
・走行距離
・車体色
・駆動方式(2WDなのか4WDなのか)
・価格

こんな感じです。

なので、メインメソッド(主な処理)はこんな感じで組みました

function rootFunction() {
  getMySheet();
  let lastCol = sheet.getRange(ROW_URL, 1).getNextDataCell(SpreadsheetApp.Direction.NEXT).getColumn();
  for (wkColumn=WK_START_COL; wkColumn<=lastCol; wkColumn++) {
    //スクレイピングしたいWebページのURLを変数で定義する
    let url = sheet.getRange(ROW_URL, wkColumn).getValue();
    //URLに対しフェッチを行ってHTMLデータを取得する
    let html = UrlFetchApp.fetch(url).getContentText("EUC-JP");

    writeImage(html);     // 画像
    writeCarName(html);   // 車種
    writeOverview(html);  // 概要
    writePrice(html);     // 価格
    writeModelYear(html); // 年式
    writeDistance(html);  // 走行距離
    writeBodyColor(html); // 車体色
    writeDriveSystem(html); // 駆動方式
    writeDataTitle();     // 車種と概要を連結
  }
}

初期状態のスクリプトエディタでは myFunction という名前のメソッドが存在していましたが、
rootFunctionへ変更しています。一番上流のfunctionなので安易に・・・(;’∀’)

で、最初の getMySheet(); ここで、ネットから取得した情報を書き込むスプレッドシート情報を取得します

let lastCol ~ で始まっている処理は、2行目のセルを右方向へチェックした時に、何番目のカラムまで埋まっているか?
をチェックしています。今回のこの処理は「目を付けた車」を処理対象とします
なので、目を付けた車のURLをコピーして、あらかじめスプレッドシートの2行目に貼り付けておきます
URLのみ貼り付けておけば、毎日最新の情報を取得していくという具合です。なので、何列目までURLが埋まっているのかを取得しています

for (wkColumn ~ の部分から、繰り返し処理が始まります
先程求めたx列目まで「指定されたURLを使って、ネットから情報を取得してスプレッドシートへ書き込む」という動作を繰り返します

let url ~ と let html ~ の部分はセルからURL(文字列)を取得して、Fetchという機能を使って指定したURLで取得できるhtmlを文字列として受け取ります
ここでポイントが一つあります

取得するページの文字コードに注意

ソースを見ると getContext(“EUC-JP”); と書かれている部分があります
なぜ、EUC-JPを指定しているのかというと、goo-netのページに使用している文字コードがEUC-JPだからです
どこでそれを確認したかというと?

上の画像の黄色枠線部分です
ここに書かれている文字コードと異なる文字コードを指定してHTMLを取得すると、激しく文字化けを引き起こします

なので、取得した結果がよくわからない文字列になっている場合はここを疑いましょう!

残りの処理は、取得したHTMLテキストから、必要な情報を引っこ抜いてスプレッドシートへ書き込むという処理を実装しています
ざっくり考えれば簡単そうでしょ?

4.ソース全体

最初に、ソースはgithubへupしてあるので、そちらも参考に

goo-net-scraping/Main.js at main · sdoi883/goo-net-scraping
Contribute to sdoi883/goo-net-scraping development by creating an account on GitHub.

処理を上から説明していきますー

定数・変数宣言

  
const ID = '<スプレッドシートID>';
const SHEET_NAME = '<シート名>';
const DATE_CELL_START = 9;
const ROW_IMAGE = 1;
const ROW_URL = 2;
const ROW_CAR_NAME = 3;
const ROW_OVERVIEW = 4;
const ROW_MODEL_YEAR = 5;
const ROW_DISTANCE = 6;
const ROW_BODY_COLOR = 7;
const ROW_DRIVE_SYSTEM = 8;
const ROW_NAME_OVERVIEW = 9;
const WK_START_COL = 2;
var sheet;

var wkColumn;  // 作業用列番号を保持する

最初に、ソースの中で使用する固定値を宣言しています
小難しいものはありません

1行目2行目に記載している ID と SHEET_NAME はグーネットから取得した情報を
保存するスプレッドシートの情報になります。

ID:スプレッドシートを開いた際の、URLから取得できます
SHEET_NAME:文字通り、シート名を指しています

次いきましょう

rootFunction

ここが一番最初に動くメソッドになります
メインメソッドですね

function rootFunction() {
  getMySheet();
  setFormat();
  let lastCol = sheet.getRange(ROW_URL, 1).getNextDataCell(SpreadsheetApp.Direction.NEXT).getColumn();
  for (wkColumn=WK_START_COL; wkColumn<=lastCol; wkColumn++) {
    //スクレイピングしたいWebページのURLを変数で定義する
    let url = sheet.getRange(ROW_URL, wkColumn).getValue();
    //URLに対しフェッチを行ってHTMLデータを取得する
    let html = UrlFetchApp.fetch(url).getContentText("EUC-JP");

    writeImage(html);     // 画像
    writeCarName(html);   // 車種
    writeOverview(html);  // 概要
    writePrice(html);     // 価格
    writeModelYear(html); // 年式
    writeDistance(html);  // 走行距離
    writeBodyColor(html); // 車体色
    writeDriveSystem(html); // 駆動方式
    writeDataTitle();     // 車種と概要を連結
  }
}

このメインメソッドから、各メソッドを呼び出して処理を進めていく形で作りました
ここのポイントは前述したので、次いきましょう

getToday

文字通り、今日(日付)を取得します
フォーマットを指定してるところぐらいかな

function getToday() {
  let today = new Date();
  return Utilities.formatDate(today, "JST", "YYYY/MM/dd");
}

getMySheet

スプレッドシートを取得してますね
IDを指定してブックを、さらにSHEET_NAMEでどのシートに書き込むのかを指定

え?コメント書かなくても見ればわかる?

function getMySheet() {
  var spreadsheet = SpreadsheetApp.openById(ID);
  sheet = spreadsheet.getSheetByName(SHEET_NAME);
}

setFormat

ここは今思うと、メソッド名が微妙かもだけど。。。

スプレッドシートに設定する固定文言(ラベル的なもの)を書いてます
あと、ちょっと背景色いじってるぐらい

function setFormat() {
  sheet.getRange(ROW_URL, 1).setValue('URL');
  sheet.getRange(ROW_CAR_NAME, 1).setValue('車種');
  sheet.getRange(ROW_OVERVIEW, 1).setValue('概要');
  sheet.getRange(ROW_MODEL_YEAR, 1).setValue('年式');
  sheet.getRange(ROW_DISTANCE, 1).setValue('走行距離');
  sheet.getRange(ROW_BODY_COLOR, 1).setValue('車体色');
  sheet.getRange(ROW_DRIVE_SYSTEM, 1).setValue('駆動方式');
  sheet.getRange(DATE_CELL_START, 1).setValue('日付');
  sheet.getRange(DATE_CELL_START + ':' + DATE_CELL_START).setBackground("#c9daf8");
}

writeImage

車の情報を取得してくるといっても、さすがに文字情報だけだとどの車かわからなくなるので、、、(俺だけ?
画像のURLを取得してきて、その画像をIMAGE関数に与えてやることで、スプレッドシート上に画像を表示させています

function writeImage(html) {
  let imageURLs = html.match(/<div class="item image"><img src="[^""]+"/g);
  imageURL = imageURLs[0].replace(/<div class="item image"><img src="/g,"").replace(/"/g,"");
  console.log(imageURL);
  sheet.getRange(ROW_IMAGE, wkColumn).setValue('=IMAGE("' + imageURL + '")');
}

rootFunctionで取得したhtmlから、画像を表示する為のタグを html.match~ の部分で取得しています
match関数で引っこ抜いた文字列から、さらに不要な文字列をreplace関数で削り落として
最終的にキレイになったURLをIMAGE関数の引数に設定してセルに書き込む という処理ですね

match関数:正規表現に該当した文字列を取得する
replace関数;正規表現に該当した文字列を置換する

↑今回のソースで頻繁に出てきます

writePrice

車両価格を取得して書き込みます
グーネットさんの場合、車両本体価格と乗り出し価格(支払総額)の表示があるので、どちらも取れるように考えていたのですが、、、
取得した金額をグラフ化することを考えた時に、微妙だなーと思って車両本体価格のみ取得するようにしています
match の中に書いている span class=”num” <- 最後のダブルクォーテーションがポイントです。これを外すと総額も取ります

function writePrice(html) {
  let priceList = html.match(/<td><span class="num".*>/g);

  let price = "";
  for (i=0; i<priceList.length; i++) {
    tmp = priceList[i].length > 0 ? tagCutter(priceList[i]) : "-";
    price = price + (i==1 ? '/' : '') + tmp;
  }
  sheet.getRange(getTodaysRow(), wkColumn).setValue(price.replace('万円',''));
  return;
}

writeCarName

車の名前は必要っすよねー

車名はページの上部にデカデカと書いてあるので、そこから取得します

function writeCarName(html) {
  let carNames = Parser.data(html).from('<p class="tit">').to("</p>").iterate();
  let carName = tagCutter2(carNames[0]);
  sheet.getRange(ROW_CAR_NAME, wkColumn).setValue(carName);
}

さっきから ちょいちょい登場してるんですが tagCutter っていう関数を作って、不要なタグを削っています これは後ほど

writeOverview

車の名前はわかった
でも、もう一つ大事なことがあるよね?そう!グレードっす!

いくら安かろうが、グレードが低ければそりゃそうだろうと
で、グーネットさんの場合、グレードが明記されていません
何故か概要欄に記載されています。よくわからん

ほぼほぼこの概要欄の先頭にグレードが書いてあることが多いので、そこから取得するようにしています。

function writeOverview(html) {
  let overview = tagCutter(html.match(/<p.*class="hdBlockTop_txt".*>/g,"")[0]);
  sheet.getRange(ROW_OVERVIEW, wkColumn).setValue(overview);
  return;
}

writeModelYear

年式はちょっと難しくてですね。。。Parserというライブラリを別途使用しています
今まで使っていたmatchは同一行内に収まっている文字列は取得できるんですが
改行が入ったりすると、その改行でちょん切れます

function writeModelYear(html) {
  let modelYears = Parser.data(html).from('年式</th>').to('</td>').iterate();
  let modelYear = tagCutter(modelYears[0]);
  sheet.getRange(ROW_MODEL_YEAR, wkColumn).setValue(modelYear);
}

改行を無視して取得するならこのParserを使いましょう
fromで指定した位置からtoで指定した位置までを抜き出します

writeDistance

走行距離をゲットしてます
年式を取得するところと大差ないですね

function writeDistance(html) {
  let distances = Parser.data(html).from('走行</th>').to('</td>').iterate();
  let distance = tagCutter(distances[0]);
  sheet.getRange(ROW_DISTANCE, wkColumn).setValue(distance);
}

writeBodyColor

車体色をゲット
これも同様ですな。。。つまらんな

function writeBodyColor(html) {
  let bodyColors = Parser.data(html).from('車体色</th>').to('</td>').iterate();
  let bodyColor = tagCutter(bodyColors[0]);
  sheet.getRange(ROW_BODY_COLOR, wkColumn).setValue(bodyColor);
}

writeDriveSystem

駆動方式をゲット
え、マジ同じソースやん

function writeDriveSystem(html) {
  let driveSystems = Parser.data(html).from('駆動方式</th>').to('</td>').iterate();
  let driveSystem = tagCutter(driveSystems[0]);
  sheet.getRange(ROW_DRIVE_SYSTEM, wkColumn).setValue(driveSystem);
}

共通関数化した方がよかったかな。。。

writeDataTitle

これは金額の変遷をグラフ化する際に、グラフのラベルに使いたかっただけ
お好みで変更してください

function writeDataTitle() {
  let title = "";
  title = title + sheet.getRange(ROW_CAR_NAME, wkColumn).getValue();
  title = title + sheet.getRange(ROW_OVERVIEW, wkColumn).getValue();
  sheet.getRange(ROW_NAME_OVERVIEW, wkColumn).setValue(title);
}

tagCutter

不要なタグをカットする記述が沢山出てきちゃったので関数化したものですね
基本的には < ~ > の中身を消す!

/**
 * 不要なタグを削って返す
 */
function tagCutter(arg) {
  return arg.replace(/<[^>]+>/g,"").trim();
}
/**
 * 不要なタグを削った返す part2
 */
function tagCutter2(arg) {
  return arg.replace(/<.*>/g,"").replace(/\s/g,"");
}

getTodaysRow

日々、価格情報をため込んでいくので、行数がどんどん増えていきます
今日の価格を書き込むのは一番下~♪ って鼻歌まじりで人間が書くのは簡単ですが
これをプログラムで実行しなくちゃなりません

function getTodaysRow() {
  let today = getToday();
  let range = getDateRange();
  let dateRange = range.getValues();
  for (i=1; i<dateRange.length; i++) {
    if (Utilities.formatDate(new Date(dateRange[i]), "JST", "YYYY/MM/dd") == today) {
      return DATE_CELL_START + i;
    }
  }
  sheet.getRange(DATE_CELL_START + dateRange.length, 1).setValue(today);
  return DATE_CELL_START + dateRange.length;
}

ポイントはこの後説明する getDateRange メソッドですね

getDateRange

今日の価格を記載する行数を特定する為のメソッドです

ポイントはアクティブセルから下方向にどれだけ入力セルが続いているかを取得する
getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
この部分です

ただ、このメソッド、弱点が一つあります

アクティブセルのすぐ下が空白だと、スプレッドシートの最下行まですっ飛んでいきます
そうなるとマズイので、アクティブセルの下が空白だったらっていうif文を差し込んでいます

function getDateRange() {
  let lastRow = DATE_CELL_START;
  if (sheet.getRange(DATE_CELL_START + 1,1).getValue() != '') {
    lastRow = sheet.getRange(DATE_CELL_START, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
  }
  let range = sheet.getRange(DATE_CELL_START,1,lastRow - (DATE_CELL_START - 1));
  return range;
}

最後

ざっくりですが、一通りソース説明してみました
細かい関数の使い方等はググってもらうなりすればわかるかと思います

この要領でamazonからも商品価格を取得してみました
これはまた別の記事で書きます

ではまた!