はしくれエンジニアもどきのメモ

情報系技術・哲学・デザインなどの勉強メモ・備忘録です。

はてなブログの記事の最終更新日を取得

はてなブログの記事の最終更新日を取得

はてなブログの記事では, 最終更新日がデフォでは表示されないので,取得して表示しようというメモ.

最終更新日を追加

記事のhtmlの中には最終更新日が書かれていない

表示はしていないが,メタ情報としてhtmlに書いている場合もある(例えば,headの中にmetaとして設定している場合など)のでhtmlを調べてみたが, どうやら発行日時pubdateのみで,modifieddateといった最終更新日の情報はないようだ.

記事のhtml:pubdateは保存されている

最終更新日を取得する方法

実際に取得する方法を検索したところ以下の2つの方法を発見した.

はてなProユーザでない場合,「サイトマップの利用」しかない.

サイトマップの利用

サイトマップは,基本的に検索エンジンにウェブページがいつ更新されたか教えるインデックスとなっている.

cartman0.hatenablog.com

はてなブログの場合,サイトマップblog_url/sitemap_index.xmlで確認できる. このブログの場合:http://cartman0.hatenablog.com/sitemap_index.xml

サイトマップ1ファイルに100記事分のURLと最終更新日時(lastmod)が保存されている.

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://cartman0.hatenablog.com/</loc>
<lastmod>2017-11-08</lastmod>
</url>
<url>
<loc>http://cartman0.hatenablog.com/about</loc>
<lastmod>2017-11-08</lastmod>
</url>
<url>
<loc>
http://cartman0.hatenablog.com/entry/2017/11/07/schema.org%E3%81%AEArticle%E3%83%A1%E3%83%A2
</loc>
<lastmod>2017-11-08</lastmod>
</url>
....

コード

処理の流れ:

  1. fetchAPIサイトマップxmlを取得(このブログではこのファイルhttp://cartman0.hatenablog.com/sitemap_index.xml
  2. サイトマップxmlから各サイトマップのリスト(URL)の取得
  3. そのリスト(URL)に対してfetchAPIで全XML取得(今回は,Promise.all()を使って全リストに対して非同期処理で取得)
  4. XMLから記事のURLを探索して,最終更新日を取得
  5. 最終更新日のDOMを生成して追加

(ちなみに,サイトマップは同ドメインにあるのでfetchなどのajax通信をしてもCORSに引っかからない.)

JavaScriptコード:gist

<!-- 更新日を追加 -->
<script type="text/javascript">
(function(){
/*
- args: lastmod: str
*/
function addLastmod(lastmod){
  /* ここでlastmodを追加 */
  var time = document.createElement("time");
  time.setAttribute("itemprop", "dateModified");
  time.setAttribute("title", lastmod);
  time.setAttribute("datetime", lastmod);
  time.innerText=lastmod;
  var span = document.createElement("span");
  span.innerHTML = "(LastModified: " + time.outerHTML + ")";
  var date_elements = document.querySelectorAll("div.date")
  Array.prototype.map.call(date_elements, function(e){e.appendChild(span);});
}

var blog_uri = document.querySelector("[data-blog-uri]").getAttribute("data-blog-uri");
var sitemap_url = blog_uri + "sitemap.xml";
var article_url = document.querySelector('[property="og:url"]').getAttribute("content");

function fetch2str(res) {
  return res.text();
}

function str2dom(str) {
  var parser = new DOMParser();
  var dom = parser.parseFromString(str, 'text/xml');
  return dom;
}

/*
return : Promise([URL1, URL2, ...])
*/
function getSitemapURLArrayPromiseFromSitemapXML() {
  return fetch(sitemap_url)
    .then(fetch2str)
    .then(str2dom)
    .then(function(dom) {
      return Array.prototype.map.call(dom.querySelectorAll("sitemap"), function(e) {
        return e.querySelector("loc").innerHTML
      });
    });
}

/*
- return Promise([""<xml >?page=1...</xml>", "<xml >?page=2...</xml>", ...])
*/
function getXMLStrArrayPromiseFromAllSitemapURL(URL_array) {
  return Promise.all(URL_array.map(function(e) {
    return fetch(e).then(fetch2str);
  }));
}

/*
return Promise(url_element in DOMParser)
*/
function find_urlElementMatchedAtriclePromiseFromXMLStrArray(xml_str_arr) {
  var parser = new DOMParser();
  var find_url_e = undefined;
  for (var xml_str of xml_str_arr) {
    var dom = parser.parseFromString(xml_str, 'text/xml');
    var url_elements = dom.querySelectorAll("url");
    find_url_e = Array.prototype.find.call(url_elements, function(e, idx, arr) {
      return e.querySelector("loc").innerHTML === article_url;
    });
    if (find_url_e) break;
  }
  return find_url_e;
}

function getLastmodFrom_urlElement(url_element){
  return url_element.querySelector("lastmod").innerHTML;
}

try{
getSitemapURLArrayPromiseFromSitemapXML()
  .then(getXMLStrArrayPromiseFromAllSitemapURL)
  .then(find_urlElementMatchedAtriclePromiseFromXMLStrArray)
  .then(getLastmodFrom_urlElement)
  .then(addLastmod);
}catch(e){
  console.log(e);
}
}());
</script>

以上のスクリプトを埋め込むと,以下のように,pubdateの隣に追加される.

最終更新日を追加

TODO

  • Promise.all()を使って,100記事を格納している各XMLファイルを一括ですべて取得している

    • よって,記事が多いブログでは容量が必要になる場合がある.
    • また,全取得なので対象がループの最初の記事でも処理時間は同じになる.
  • 解決策1:非同期でやるならば,理想的には最初の成功だけ返すPromise.some()のような処理がほしい.

  • 解決策2:ループの最初の記事の処理を早くしたい場合,同期処理で解決できる. fetchAPIの場合,非同期関数asyncを宣言して中でwaitで同期処理ができる. ただし,現在,GoogleBotでは対応していない.