『7回目の出直し🌻』

好きなことを自分のペースで、のんびり更新

【前編】はてなブログの記事に最終更新日を表示する:GASの新規作成とトリガー設定まで

最近ちょっと使い始めたGASを使って、ブログの記事の最終更新日を表示するスクリプトを作ってみます。

f:id:kanaxx43:20210808151850p:plain

はじめに

インターネットで「はてなブログに最終更新日を表示する方法」を調べるといくつか記事が見つかります。
どうやらメジャーな方法は、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か月以内なら何日前と表示
f:id:kanaxx43:20210809140747p:plain

1か月以上たっている場合は何か月前と表示
f:id:kanaxx43:20210809140750p:plain

1年以上経っている場合は、何年前と表示
f:id:kanaxx43:20210809140749p:plain

これだけです。

作戦会議

GASだけで最終更新日を表示するのは難しいので、二つに分割して考えます。

  1. sitemap_indexとsitemap.xmlから記事のURLと最終更新日を取り出すGASとスプレッドシート
  2. GASをウェブアプリケーション化して、記事の最終更新日を取得するJavaScript

この方法であれば、記事ページを開いたときに1回だけ外部にアクセスするだけで最終更新日がとれます。

前編では、上の1番目の作業を行います。

前編でやること

  • 新しいスプレッドシートを作成する
  • サイトマップへアクセスし記事URLと最終更新日を取り出すスクリプトを書く
  • スクリプトにトリガーを仕掛け定期更新を実現する

1. 記事ごとの最終更新日を蓄えるGASを作る

まずは、GoogleSpreadSheetで記事別の最終更新日を蓄える器と、定期的にデータを取り込む仕組みを用意します。

GASの基本的な設定は、こちらの記事にも書いてあります。 kanaxx.hatenablog.jp

ファイルとスプレッドシートを用意

Google Docsのスプレッドシートに行き、空っぽのファイルを作成します。

f:id:kanaxx43:20210808142808p:plain

やることは2つです。

  • 1.シートを2つ用意します。シート名は「サイトマップ」「データ」です。
  • 2.サイトマップのB1にサイトマップインデックスのURLをいれます。

スクリプトをコピー

スクリプトエディタを開いて、コードをコピーします。

コードはこちらにも置いてあります。
https://github.com/kanaxx/hatenablog-omocha/blob/main/ladtmodifiedtime/getlastmod.gs

「ツール」>「スクリプトエディタ」でスクリプトエディタを開き、 f:id:kanaxx43:20210807164104p:plain

以下のコードをコピーして保存します。

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回前のデータが残るようにするためです。

スクリプトを実行

コードを書いたら保存をしスクリプトを実行します。
f:id:kanaxx43:20210808144943p:plain

いつものように初回だけ「承認ウィンドウ」が出るので承認して進めます。

プログラムが動いて「データ」シートに記事URLと最終更新日が出ればOKです。 f:id:kanaxx43:20210808143840p:plain

スクリプトを実行すると全記事のURLと最終更新日をデータ化するところまで完成です。

2.トリガの設定

ボタンをぽちっと押せば「データ」シートの最終更新日が最新化されるところまでできましたが、ボタンを押さないと更新されないと不便ですね。 この場合は、GASのトリガーを使って、定期的に動くようにします。

トリガーの設定方法

「ツール」>「スクリプトエディタ」でスクリプトエディタを開き、左側にある「トリガー」(時計のアイコン)をクリックします。
f:id:kanaxx43:20210807160931p:plain

右下にある「トリガー追加」ボタンを押します。
f:id:kanaxx43:20210807161050p:plain

トリガー設定の画面が表示されます。ここに必要事項を入力しておきます。
f:id:kanaxx43:20210808144927p:plain

  • 1が定期的に実行したい関数になっていることを確認しておきます。
  • 2は定期的に実行する設定をするところです。「時間手動型」「時間ベースのタイマー」「4時間おき」に設定しました。

設定が終わったら待つだけです。待っていれば定期的に実行され、「データ」シートの内容が更新されます。 f:id:kanaxx43:20210808135519p:plain

ここまでで、定期的にデータを取得してスプレッドシートに記録する仕組みができました。ふぅ、長かった。

まとめ

Google Apps Scriptのトリガー機能を使って定期的に実行するタイマー設定をやってみました。ここまでで半分です。 後半戦は、作ったスプレッドシートをウェブアプリケーションとして公開してAPI化し、記事に更新日を差し込むところまでやり切ります。

後編はこちらから

kanaxx.hatenablog.jp