有馬総一郎のブログ

(彼氏の事情)

2020年11月30日 22:45:08 JST - 5 minute read - Google

Google Apps ScriptでWebスクレイピング、Google Formの更新、そして回答をメール送信

Google FormをとあるWebサイトから情報を取得して、それを元に更新させたい!結果は、満足な出来栄えとなった。

スプレッドシート(Google Spreadsheets)でGoogle Apps Scriptをよく使っているが、フォーム(Google Form)でも普通に使える。

Google Apps Script

Google Apps ScriptでのWebスクレイピング

そもそもGoogle Apps ScriptからWebサイトのドキュメントを抽出できるのか?と思いきや、あたり前のよっちゃんイカで出来た。↓参考ページ。

var url = "http://スクレイピングするページ/";
var html = UrlFetchApp.fetch(url).getContentText('UTF-8');

上記ページでこぞってとりあげてるのが Parserというライブラリーだけど、確かにこれは便利だ。

ライブラリーの設定は、リソース -> ライブラリー...

library’s script id バージョン Identifier Development
M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV1 7 Parser 無効
Google Apps Script

Parser 使えば、特定の範囲をガバっと取れる。

<a href="javascript:void(0)" onclick="function('param'); return false;">アンカー</a>
Parser.data(html).from('onclick="function(').to(')').build();

なんて書き方をしても、そのfunction括弧の間のパラメーター'param'とか取れちゃうわけだ。

ただ、

Parser_.prototype.from = function(pattern,offset) {

pattern とあるが、正規表現パターンでなく、ただの文字列なので、そこまで柔軟な抽出が出来るわけではない。

また、どうしてか、

<input type="hidden" id="special_name" value="arimasou16" />

のvalueを抽出しようとして、

Parser.data(html).from('<input type="hidden" id="special_name" value="').to('"').build();

という書き方をしたのだけど、どうしてか上手く行かないことがあった。抽出できないわけでなく、別の部分を抽出してしまった。

またどうしても抽出する要素の隣を取得したい、getNextSibling的なことをしたい場合がある。

<div>nexttodoor</div><input name="purpose" value="abc" />

Google Apps Scriptは document がないのでgetElementsByTagName()なことはできない。

そうなってくると

var ele = Parser.data(html).from('<div>nextodoor</div>').to('/>').build();

という書き方で、<input name="purpose" value="abc" まで取得する。そして、それを自分はXML変換して、そこから属性値なり、テキストノードを取得する力技?で乗り気った。

var xml = XmlService.parse(ele + '/>');
var input = xml.getRootElement();
var value = input.getAttribute('value').getValue();

みたいな。一時期まではXMLのルールで記述されたHTML、XHTMLなんてものが開発されたこともあったので、HTMLをXML化するのはそんなにシンドいわけでもない。一度、XML化してしまえば、 Class Element | Apps Script | Google Developersを見てのとおり、兄弟要素、子要素など取得しやすくなる。

Google Formの取得

そもそも form の取得はフォームのGoogle Apps Scriptとして実装するなら以下↓。

var form = FormApp.getActiveForm();

もしくはスプレッドシートや他のから取得する場合は、ID指定で取得する。IDはフォームの編集ページのURL https://docs.google.com/forms/d/1234567890abcdefghij1234567890klmnopqrst1234/editd//editの間の値。

var form = FormApp.openById('1234567890abcdefghij1234567890klmnopqrst1234');

Class FormApp | Apps Script | Google Developers

Google Formでの質問項目削除や追加など

それからは Class Form | Apps Script | Google Developersを眺めていれば何となくやりたいことは実現できるはず。

自分のケースではフォームの記述を更新、質問項目を更新させたかったので、記述はそのままsetで置き換え、質問項目は一度、削除して再作成した。

form.setDescription(description); // 記述の設定
form.deleteAllResponses(); // 回答を全て削除
var items = form.getItems();
for (var i = 0; i < items.length; i++) {
   // 質問項目を削除
  form.deleteItem(items[i]);
}
// 記述式テキスト(短文回答)
var textItem = form.addTextItem();
textItem.setTitle('タイトル1'); // タイトル
textItem.setRequired(true); // 必須項目
// 記述式テキスト(長文回答)
var paragraphTextItem = form.addParagraphTextItem();
paragraphTextItem.setTitle('タイトル2');
paragraphTextItem.setHelpText('説明文'); // 説明文
Google Apps Script

私はこのsetHelpTextはてっきりHTMLの placeholder かと思いきや、説明文が追記できるんだね。知らなかった。なので、ケースによっては質問(タイトル)を変えずに説明文だけを変えるようにした。

Google Formでの回答の取得

フォームでのGoogle Apps Scriptなのだから、勿論、回答も取得することができる。以下、参考ページ。

  1. FormAppによるフォームの集計と作成(5/5):Google Apps Scriptプログラミング [中級編] - libro
  2. Class FormResponse | Apps Script | Google Developers

良くあるパターンとして 1. にあるとおり、フォームの送信ボタンを押したときに、自動返信も可能。私は、回答結果を集計して身内?に送りたいケースだった。送信毎にメール送信してもいいのだけど、大量のメールを送るのもウザい2ので、ある程度、回答が溜まった時間にトリガーを起動させる形で実装した。

var form = FormApp.getActiveForm();
var description = form.getDescription(); // フォームの記述取得
var formResponses = form.getResponses(); // 回答リストを取得
var list = [];
for (var i = 0; i < formResponses.length; i++) {
  var formResponse = formResponses[i];
  var itemResponses = formResponse.getItemResponses(); // 回答の項目リストを取得
  for (var j = 0; j < itemResponses.length; j++) {
    var itemResponse = itemResponses[j];
    var item = itemResponse.getItem();
    var answer = '';
    // 記述式テキストの場合
    if (item.getType() == 'TEXT' || item.getType() == 'PARAGRAPH_TEXT') {
      var helpText = item.getHelpText(); // 説明文を取得
      answer = itemResponse.getResponse(); // 回答を取得
      list.push(helpTxt + ':' + answer);
      continue;
    }
  }
}
var response = '';
for (var k = 0; k < list.length; k++) {
  response += list[k] + '\n';
}
console.log("回答: " + response);
GmailApp.sendEmail('test@mailaddress.com', 'メールタイトル', response); // メール送信

みたいな。当たり前にこの通りに実装したわけではないので、文法エラーになるかも知れないし、おかしな処理になってるのは御了解いただきたい。

最後に、結局、使わなかったが、日付フォーマットに便利そうなメソッドUtilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss')、将来どこかで使うかも。


  1. 公開しているソースの上部に library_key で記述してある。 ↩︎

  2. 短時間に何度もテスト送信行なったら、メール送信されなくなった。次の日、試したら送れたけど。SPAMメール送りつけてるかのような動きは控えるべき。 ↩︎