最近ちょっと使い始めたGASを使って、ブログの記事の最終更新日を表示するスクリプトを作ってみます。
はじめに
インターネットで「はてなブログに最終更新日を表示する方法」を調べるといくつか記事が見つかります。
どうやらメジャーな方法は、jQueryを使ってsitemap_index.xmlとsitemap.xmlを見に行き、表示中の記事URLと一致するlastmodの日付を取り出すというスクリプトです。
このスクリプトは、記事ページが表示されるたびに
- sitemap_index.xmlをとりに行く
- sitemap_periodical.xmlをとりに行く
- sitemap_periodical.xmlの中に記事URLがあるか確認する。無かったら次のsitemap_periodical.xmlをとりに行く
- 見つかったら最終更新日を表示する
という流れになっています。
実はブラウザで記事ページを開くたびに、見えないところでHTTPリクエストがたくさん発生しています。運がよくても2回、運が悪いと(古い記事だと)10回以上のHTTPアクセスが発生します。
裏側でアクセスをしているので大きな問題はないですが、せっかくなので作り直ししてみます。
作ったもの
記事ページに最終更新日を差し込むスクリプトです。最近覚えたGoogle Apps Scriptとスプレッドシートを使って作りました。
1か月以内なら何日前と表示
1か月以上たっている場合は何か月前と表示
1年以上経っている場合は、何年前と表示
これだけです。
作戦会議
GASだけで最終更新日を表示するのは難しいので、二つに分割して考えます。
- sitemap_indexとsitemap.xmlから記事のURLと最終更新日を取り出すGASとスプレッドシート
- GASをウェブアプリケーション化して、記事の最終更新日を取得するJavaScript
この方法であれば、記事ページを開いたときに1回だけ外部にアクセスするだけで最終更新日がとれます。
前編では、上の1番目の作業を行います。
- 新しいスプレッドシートを作成する
- サイトマップへアクセスし記事URLと最終更新日を取り出すスクリプトを書く
- スクリプトにトリガーを仕掛け定期更新を実現する
1. 記事ごとの最終更新日を蓄えるGASを作る
まずは、GoogleSpreadSheetで記事別の最終更新日を蓄える器と、定期的にデータを取り込む仕組みを用意します。
GASの基本的な設定は、こちらの記事にも書いてあります。 kanaxx.hatenablog.jp
ファイルとスプレッドシートを用意
Google Docsのスプレッドシートに行き、空っぽのファイルを作成します。
やることは2つです。
- 1.シートを2つ用意します。シート名は「サイトマップ」「データ」です。
- 2.サイトマップのB1にサイトマップインデックスのURLをいれます。
スクリプトをコピー
スクリプトエディタを開いて、コードをコピーします。
コードはこちらにも置いてあります。
https://github.com/kanaxx/hatenablog-omocha/blob/main/ladtmodifiedtime/getlastmod.gs
「ツール」>「スクリプトエディタ」でスクリプトエディタを開き、
以下のコードをコピーして保存します。
const sitemapNamespace = "http://www.sitemaps.org/schemas/sitemap/0.9"; const cellOfSitemapIndex = "B1"; const startCellOfSitemap = "D1"; const startCellOfBlogEntry = "A1"; const sitemapWorksheetName = "サイトマップ"; const dataWorksheetName = "データ"; const httpMaxRetry=5; function bootstrap() { let ss = SpreadsheetApp.getActiveSpreadsheet(); let sitemapSheet = ss.getSheetByName(sitemapWorksheetName); sitemapSheet.getRange('D:D').clearContent(); let sitemapIndexUrl = sitemapSheet.getRange(cellOfSitemapIndex).getValue(); let time = Utilities.formatDate( new Date(), 'Asia/Tokyo', 'yyyyMMdd-HHmmss'); let newSheet = ss.insertSheet(time); newSheet.getRange('D1').setValue(time); let result = fetchSitemaps(sitemapIndexUrl, sitemapSheet, newSheet); if(result){ let dataSheet = ss.getSheetByName(dataWorksheetName); if(dataSheet){ ss.deleteSheet(dataSheet); } newSheet.setName(dataWorksheetName); }else{ ss.deleteSheet(newSheet); } } function fetchSitemaps(sitemapIndexUrl, sitemapSheet, dataSheet){ console.info('サイトマップインデックスを取る') let response; response = fetch(sitemapIndexUrl) if(!response){ return false; } let sitemapIndexXml = response.getContentText(); let sitemapIndexDoc = XmlService.parse(sitemapIndexXml); let xmlProtocol = XmlService.getNamespace(sitemapNamespace); let sitemaps = sitemapIndexDoc.getRootElement().getChildren('sitemap', xmlProtocol); console.info('サイトマップインデックスからサイトマップURLを取る') let sitemapLocations = []; sitemaps.forEach(function(sitemap, s){ let loc = sitemap.getChild('loc', xmlProtocol).getText(); console.log(loc); sitemapLocations.push(loc); sitemapSheet.getRange(startCellOfSitemap).offset(s, 0).setValue(loc) }); console.info('サイトマップからブログエントリーのURLを取る') let rng = dataSheet.getRange(startCellOfBlogEntry); for (let s in sitemapLocations) { let sitemapLoc = sitemapLocations[s]; console.log('%s/%s %s',s+1, sitemapLocations.length, sitemapLoc); response = fetch(sitemapLoc); if(!response){ return false; } let sitemapXml = response.getContentText(); let sitemapDoc = XmlService.parse(sitemapXml); let urls = sitemapDoc.getRootElement().getChildren('url', xmlProtocol); for(let url of urls){ let entryLoc = url.getChild('loc', xmlProtocol).getText(); let lastmod = url.getChild('lastmod', xmlProtocol).getText(); let lastmodJst = Utilities.formatDate(new Date(lastmod), "JST", "yyyy-MM-dd HH:mm:ss"); rng.setValue(entryLoc).offset(0, 1).setValue(lastmodJst); rng = rng.offset(1,0) } } return true; } function getLastMod(url){ let lastmod ; let dataSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(dataWorksheetName); let targetRange = dataSheet.getRange("A:A"); let finder = targetRange.createTextFinder(url).matchEntireCell(true); let res = finder.findNext(); if(res !== null) { if(!res.offset(0,1).isBlank()){ // lastmod = Utilities.formatDate(res.offset(0,1).getValue(), 'Asia/Tokyo', 'yyyy-MM-dd 00:00:00'); lastmod = new Date(res.offset(0,1).getValue()); } } return lastmod; } //たまに失敗するので5回リトライするfetch function fetch(url){ let options = { muteHttpExceptions: true }; let httpcount = 0; let response = null; while(httpcount < httpMaxRetry ){ try{ response = UrlFetchApp.fetch(url, options); let code = response.getResponseCode(); console.log('[%s][%s] %s',httpcount, code, url); if(code == 200){ return response; } }catch(e){ console.log(e); console.log('[%s][error] %s',httpcount, url); } httpcount++; } return null; } // for testing function testLastMod(){ let url = 'https://kanaxx.hatenablog.jp/entry/typec-cable'; let lastmod = getLastMod(url); console.log(lastmod); }
やっていることを簡単に
最初に新規のシート(日付と時刻の名前のシート:20210807-123456)を作ります。
「サイトマップ」シートのB1
のサイトマップインデックスのURLへアクセスしXMLデータを取り出します。
サイトマップインデックスを読み取りサイトマップのURLを取り出します。
サイトマップURLへアクセスし、記事のURLと最終更新日を取り出します。
作成したシートに記事URL(A列)と最終更新日(B列)を記録していきます。
データ取得時に何も問題がなかったら、もともとあった「データ」シートを削除し、作成したシートを「データ」シートに名前を変えます。途中でエラーが起きても1回前のデータが残るようにするためです。
スクリプトを実行
コードを書いたら保存をしスクリプトを実行します。
いつものように初回だけ「承認ウィンドウ」が出るので承認して進めます。
プログラムが動いて「データ」シートに記事URLと最終更新日が出ればOKです。
スクリプトを実行すると全記事のURLと最終更新日をデータ化するところまで完成です。
2.トリガの設定
ボタンをぽちっと押せば「データ」シートの最終更新日が最新化されるところまでできましたが、ボタンを押さないと更新されないと不便ですね。 この場合は、GASのトリガーを使って、定期的に動くようにします。
トリガーの設定方法
「ツール」>「スクリプトエディタ」でスクリプトエディタを開き、左側にある「トリガー」(時計のアイコン)をクリックします。
右下にある「トリガー追加」ボタンを押します。
トリガー設定の画面が表示されます。ここに必要事項を入力しておきます。
- 1が定期的に実行したい関数になっていることを確認しておきます。
- 2は定期的に実行する設定をするところです。「時間手動型」「時間ベースのタイマー」「4時間おき」に設定しました。
設定が終わったら待つだけです。待っていれば定期的に実行され、「データ」シートの内容が更新されます。
ここまでで、定期的にデータを取得してスプレッドシートに記録する仕組みができました。ふぅ、長かった。
まとめ
Google Apps Scriptのトリガー機能を使って定期的に実行するタイマー設定をやってみました。ここまでで半分です。 後半戦は、作ったスプレッドシートをウェブアプリケーションとして公開してAPI化し、記事に更新日を差し込むところまでやり切ります。
後編はこちらから