初めてのGASで初めてのWebスクレイピングに挑戦した話


どうも、地方のIT企業に務めるしがないエンジニアです。
初の投稿ですのでどうかお手柔らかに🙏

前々から興味があったWebスクレイピングをやってみようと思います。
スクレイピングといえばPython、というイメージを持ってたので、経験もあるしPythonでやろうかと思ったのですが、そんなときにGoogle Apps Script(GAS)存在を知ったので勉強がてらそっちでやってみることにします。

Google Apps Script(GAS)って?

自分の認識だと「手軽にGoogleのサービスを操作できる便利な言語」って感じ。

リファレンスのGoogle Workspace Servicesってところ見ると、操作できる主要なGoogleサービスが書かれてます。
https://developers.google.com/apps-script/reference

これらのサービスを個別に使おうと思うとそれぞれのAPIを使えるようにする手続きが割と面倒。GASを使うとまとめて利用できるのはとんでもなく便利ですね。

コードの記法はJavaScriptベースっぽいので慣れてる人はすぐ書けそう。自分はJS1ヶ月くらいしかやったことないので割と苦労しました(汗)。

ゴリゴリの初心者なので、間違いや改善できるところがあったらコメントで教えてもらえると助かります><

いざ実装

某ニュースサイトのトップニュース一覧から以下の情報を抽出していきたいと思います。
・タイトル
・配信元のサイト
・記事の投稿時刻

ファイル構成はこんな感じ。

000_app.js
020_Parser.js
025_HTMLParser.js
050_Table.js
060_GDrive.js

最近オブジェクト指向の勉強をした影響で無駄にファイルを分割しまくってます。
なんでファイルの前に数字つけとるんや?、って思った人も多いと思いますが理由ありきでこうしてます。
まぁその詳細はまた今度ってことで。

ソース全体はこっち
https://github.com/brauney1129/collectYJN/tree/master

ちなみにGASでスクレイピングするときにはParserっていう便利なライブラリがあるらしいですが、今回は勉強がてら正規表現を使った自作の関数を使います。

メインで動いてるのはこいつら。

app.gs
/**
 * データを取得する
 */
function getNewData() {
    var html = getHTML("https://news.yahoo.co.jp/");
    var executeDate = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd hh:mm:ss');
    var htmlParser = new HTMLParser();

    var newsTitle = htmlParser.between(html, '<p class="yjnSub_list_headline">', '</p>');
    var newsMedia = htmlParser.between(html, '<span class="yjnSub_list_sub_media">', '</span>');
    var newsDate = htmlParser.between(html, '<time class="yjnSub_list_sub_date">', '</time>');

    var table = new SheetTable();
    table.setColumns(['取得日時', 'title', 'media', 'date']);

    for (i = 0; i < newsTitle.length; ++i) {
        var row = new Array();
        row.push(executeDate);
        row.push(newsTitle[i]);
        row.push(newsMedia[i]);
        row.push(htmlParser.deleteTags(newsDate[i]));

        table.addRow(row);
    }

    return table;
}

/**
 * 新たにデータを取得してシートに追記する
 */
function getNewDataSheet() {
    var table = getNewData()
    var sheet = getCreateSheetByName('yahooNews');
    table.printSheet(sheet, sheet.getDataRange().isBlank());
}

/**
 * 新たにデータを取得してCSVに保存する
 */
function getNewDataCSV() {
    var table = getNewData()
    var csv = table.toCSV();
    var folder = GDrive.prototype.getCreateFolderByName('yahooNews');
    //Blobオブジェクトの作成
    var blob = Utilities.newBlob(csv, MimeType.CSV, makeFileNameWithTime('yahooNews','.csv'));
    //CSVファイルを作成
    folder.createFile(blob);
}

/**
 * 日時をくっつけたファイル名返す
 * @param {string} fileName 
 * @param {string} ext 
 * @returns {string} ファイル名
 */
function makeFileNameWithTime(fileName,ext) {
    var datetime = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyyMMddHHmm');
    return fileName + '_' + datetime + ext;
}

/**
 * シート上のデータをCSVで保存する
 */
function sheetDataToCSV() {
    var csv = SheetTable.prototype.sheetToCSV(getCreateSheetByName('yahooNews'));
    var folder = GDrive.prototype.getCreateFolderByName('yahooNews');
    //Blobオブジェクトの作成
    var blob = Utilities.newBlob(csv, MimeType.CSV, makeFileNameWithTime('yahooNews','.csv'));
    //CSVファイルを作成
    folder.createFile(blob);
}

/**
 * htmlを取得する
 * @param {string} url 
 */
function getHTML(url) {
    var response = UrlFetchApp.fetch(url);
    if (response.getResponseCode() != 200) { return null; }
    return response.getContentText('UTF-8');
}

/**
 * シートを取得する。なければ作成して取得する。
 * @param {string} name 
 */
function getCreateSheetByName(name) {
    //同じ名前のシートがなければ作成
    var sheet = SpreadsheetApp.getActive().getSheetByName(name)
    if (sheet) { return sheet; }

    sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
    sheet.setName(name);
    return sheet;
}

function onOpen() {
    var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    var entries = [{
        name: "新たに取得してCSVで保存",
        functionName: "getNewDataCSV"
    }, {
        name: "新たに取得してシートに追加",
        functionName: "getNewDataSheet"
    }, {
        name: "取得済みのデータをCSVで保存",
        functionName: "sheetDataToCSV"
    }];
    spreadsheet.addMenu("追加機能", entries);
};

実行してみる

スプレッドシートにカスタムメニューを追加したのでそこから実行します。メニューの追加はここでやってます。

function onOpen() {
    var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    var entries = [{
        name: "新たに取得してCSVで保存",
        functionName: "getNewDataCSV"
    }, {
        name: "新たに取得してシートに追加",
        functionName: "getNewDataSheet"
    }, {
        name: "取得済みのデータをCSVで保存",
        functionName: "sheetDataToCSV"
    }];
    spreadsheet.addMenu("追加機能", entries);
};


まるで元からあるメニューかのように溶け込んでてスマートですね。図形描画で作れるボタンは個人的にダサいと思ってるのでこっちが好み。

そしたらメニューからポチッとな。

おぉー。取得できました!
これでスクレイピング童貞卒業です。ググってコピペする作業がバカらしくなりますね笑

本稿では触れませんがCSVでGoogleDriveに保存する機能も作ってみました。やる気があれば今度で紹介します。

GASの勉強とかスクレイピングの勉強とか、色々と試行錯誤した結果、記事にしたい内容がゴチャついているので一旦この辺までで。
後々内容を整理しつつ小出しに投稿していきたいと思ってます。やる気があればね。

とりあえず本稿はこんな感じで。