はじめに
- 社内でAdventarを使って今年のアドベントカレンダー記事をリレーしていたところ、記事がアップされたらSlackに通知するともっと読む人が増えるのではという意見をもらい、なるほどと思い取り掛かってみたが思った通りにいくまで意外と大変だったので備忘で残す。
前提
- 対象サイト: Adventar
- 軽く書いて軽くトリガー設定をしたいのでGASで完結したい。
失敗した方法
- Adventarのサイトを見てみても特にWebAPIなどの提供はないようなのでサイトをスクレイピングする方法をまず思いつく。
- AdventarはSPAではなくレンダリング済みのHTMLを返してくれるのでCSSセレクタやXPathを使えば簡単にいけそうに思えた。
<ul data-v-06066d55="" data-v-a70d413c="" class="EntryList">
<li data-v-06066d55="" class="item">
<div data-v-06066d55="" class="head">
<div data-v-06066d55="" class="date">12/1</div>
</div>
<div data-v-06066d55="" class="article">
<div data-v-06066d55="" class="left">
<div data-v-06066d55="" class="link"><a data-v-06066d55="" href="https://example.com">https://example.com</a></div>
<div data-v-06066d55="">記事のタイトル</div>
</div>
</div>
</li>
</ul>
Parser
- Parser
- GASのHTMLパーサー用ライブラリ。
- GASでスクレイピングしようと検索するとこればかり引っかかる。
- ソースも公開されているが、CSSセレクタやXPathを使うのではなく下記のように正規表現で当てにいくパーサー。
Parser
.data(content)
.from(fromText)
.to(toText)
.build();
- 一般的なサイトであれば十分用途が足りるがAdventarは
<div data-v-06066d55="" class="article">
などHTMLタグに動的な属性が入るため無理そうだった。 - GASでCSSセレクタを使ったライブラリの作成を頑張っている人を見かけたがGASのスペックだと難しいようだ。
スプレッドシートのIMPORTXMLと組み合わせる
- IMPORTXML
- URLとXpathでHTMLの中身を簡単に取得できるので、スプレッドシートで必要な値を抽出して、GASでそれを拾って通知しようと考えた。
=IMPORTXML("https://adventar.org/calendars/xxx", "//div[@class='article']//a")
- しかし、IMPORTXMLはリアルタイムでHTMLを取得してくれるわけではなく、ブラウザの自動リロードプラグインなども入れてみたが、リロードされても更新されるのはローカルの情報だけで内部的には更新されないみたい。
- GAS内でIMPORTXMLを直接叩く方法もあるようだったが見るからにソースが複雑で、IMPORTXMLの本来の使い方と大きくズレそうに思えたのでこれ以上深追いをやめて断念。
上手くいった方法
裏技を発見する
- なんとAdventarのソースはOSSでGithubにMITライセンスで公開されていた。
- 相談する手順は書いてなかったのでとりあえずissueを立てて相談してみたところ、RSSの取得方法とバックエンドのAPIを常識の範囲内で自由に使って良いと教えてもらい感謝しかない。
RSS
- Slackで
/feed カレンダーのURL.rss
とコマンドを打つだけでお手軽に通知を受け取れる。 - お手軽で良いのだが、誰かがAdventarのコメントなどを少し修正しただけでも通知が流れてしまいノイジーなのと、Slack通知は単一Botからで統一したかったので断念。
- 個人で特定のカレンダーをウォッチしたいという用途であればピッタリかも。
APIをポーリング
- 裏APIを叩くとこんな感じでJSONで任意のカレンダーの記事一覧が取れる。
$ curl -s -X POST 'https://api.adventar.org/adventar.v1.Adventar/GetCalendar?calendar_id=3860' | jq | head -n 30
{
"calendar": {
"id": "3860",
"owner": {
"id": "1",
"name": "hokaccha",
"iconUrl": "https://lh3.googleusercontent.com/a-/AOh14Giiag-jureIwCmScFirY0iJ4yzihfXzLNsT_z6M_w=s96-c"
},
"title": "Adventarを支える技術",
"description": "今年はAdventarのシステムを、gRPC-Web、Nuxt.js、サーバーレスアーキテクチャによる SSR、Firebase Authentication、AWS、ECS などで刷新しました。システムを構築するうえでの利用した技術の詳細、工夫した点、得られた知見などについてできる限り書いてみます。また、今年からオープンソースにしたので、こち
らも合わせてどうぞ。\n\nhttps://github.com/adventar/adventar",
"year": 2019,
"entryCount": 25
},
"entries": [
{
"id": "81662",
"owner": {
"id": "1",
"name": "hokaccha",
"iconUrl": "https://lh3.googleusercontent.com/a-/AOh14Giiag-jureIwCmScFirY0iJ4yzihfXzLNsT_z6M_w=s96-c"
},
"day": 1,
"comment": "Adventar 2019 の技術構成概",
"url": "https://hokaccha.hatenablog.com/entry/2019/12/01/221358",
"title": "Adventar 2019 の技術構成概要 - hokaccha memo",
"imageUrl": "https://img.adventar.org/img/422c3049a18fdbe64389264ff55a0e84bdadbd86?url=https%3A%2F%2Fogimage.blog.st-hatena.com%2F8454420450094485628%2F26006613474657252%2F1575206038"
},
{
"id": "70516",
"owner": {
- 後は常識の範囲内で一定間隔でポーリングし、スプレッドシートで投稿情報を管理して未投稿ユーザーが投稿したらSlackに通知するようにして完成。
- 下記はAdvenarから記事情報を取得して投稿情報を管理して通知メッセージを作成する部分のGASのソース。(一ヶ月使い捨てソースなので雑)
- Slack通知方法はググるとたくさん情報があるので割愛。
function createNewAirticleMessage() {
const sheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
// シート内のデータを全取得
const startrow = 1;
const startcol = 1;
const lastrow = sheet.getLastRow();
const lastcol = sheet.getLastColumn();
const sheetdata = sheet.getSheetValues(startrow, startcol, lastrow, lastcol);
// 対象ACの全情報を取得
const options =
{
"method" : "post"
}
const res = UrlFetchApp.fetch(AC_URL, options)
const resJson = JSON.parse(res.getContentText())
const entries = resJson["entries"]
// まだ通知前の投稿があれば通知する
for (let i = 0; i < lastrow; i++) {
for (let entry of entries) {
if (sheetdata[i][3] == entry["day"]) {
if (!sheetdata[i][2] && entry["url"]) {
// 通知済みであることを保存する
sheet.getRange(i + 1, 2 + 1).setValue(1) // getRangeは0始まりなので1を加えている
let message = ''
message += 'アドベントカレンダーに `' + Moment.moment(sheetdata[i][0]).format('MM/DD') + '` の `' + sheetdata[i][1] + 'さん' + '` の記事が投稿されました:ablobcheer: \n'
const comment = entry["comment"]
if (comment) {
message += '`' + comment + '`\n'
}
message += entry["url"]
return message
}
}
}
}
return null
}
完成
まとめ
- 苦戦はしたけど想定通りの仕組みが作れて満足。
- GASは意外とライブラリが揃ってないので、何でもできるとは思わない方が良いことを学んだ。