2019-05-04

シェルで lastmod changefreq 付きの sitemap.xml を作る

こんにちは、今回は内容の濃い記事が書けそうでワクワクしています😀  このサイトでは、PHP を使わず静的な html でコンテンツを配信しています。 せっかく作っている Web サイト、この存在をクローラに気づかせるためには sitemap.xml の設置が重要となります。 今回は、sitemap.xml を自動的かつこだわりをもって(?)生成する方法を紹介します。

このサイトの sitemap.xml
2019/05/04 sitemap.xml を生成するコマンドを作りました!GitHub

    sitemap.xml の構造

    sitemap.xml は、そのサイトのサイトマップを表す xml ファイルです。 一般的にドキュメントルートに設置して、robots.txt で sitemap.xml の場所を指定します。

    robots.txt の例

    User-agent: *
    Sitemap: https://apps.bluehood.net/sitemap.xml
    

    sitemap.xml はさらに別の xml ファイルに分割することもできるのですが、 1ファイルにすべてのページの情報を書き込むとするとこのような形式になります。

    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    	<url>
        	<loc>https://apps.bluehood.net/</loc>
            <lastmod>2019-04-26T00:00:00+09:00</lastmod>
            <changefreq>weekly</changefreq>
        </url>
    	<url>
        	<loc>https://apps.bluehood.net/articles/</loc>
        	<lastmod>2019-04-29T19:40:41+09:00</lastmod>
        	<changefreq>hourly</changefreq>
        </url>
        …
    </urlset>
    

    url タグで1ページ分の情報として列記していきます。 url タグ内の情報を以下のようにまとめました。

    • loc: ページの URL(必須)
    • lastmod: ページの最終更新日時(任意)
    • changefreq: ページの更新頻度(任意、hourly, daily, weekly, monthly, yearly など)
    • (priority): そのページの重要度(任意、今回は省略)

    最低限の sitemap.xml を作るのであれば、loc だけを書いて簡易的に済ませることもできます。 しかし、できれば lastmodchangefreq も書いてサイトの情報を詳しく伝えたいところです。 ちなみに priority はページの重要度を 0.0~1.0 の範囲内で指定できるのですが、Google はこの値を無視しているそうです。 まあ、私としても priority は主観的であいまいなデータですし、あまりあてにならないかなと思います。 もちろん全部のページを priority=1.0 とかやっても意味がないですからね!

    sitemap.xml を手動で作成することもできるのですが、サイト更新のたびに修正するのも手間がかかります。 そこで、シェルスクリプトを使って loc を書いていき、さらにこだわりとして lastmodchangefreq も書いてくれるプログラムを作りました。

    lastmodchangefreq 生成の仕組み

    まず、lastmodchangefreq を生成するには、現在のサイトコンテンツだけでなく、その1つ前のバージョンのコンテンツも必要になります。 ……たとえば、現在のサイトコンテンツが old ディレクトリ内に入っているとして、サイトを更新するときは old ディレクトリを html ディレクトリにコピーして作業するものとします。

    sitemap.xml を作成するときに、html ディレクトリ内の A.html と、old ディレクトリ内の A.html の比較をします。 もしファイル間の差異があれば「そのページは更新した」と判断できます。 そして、サイトを実際に更新したら、いまある old ディレクトリを削除して html ディレクトリが新たな old ディレクトリとなります。

    ここで、どのファイルにもタイムスタンプ(最終更新日時)を OS が記録しています。 もし old/A.html と html/A.html が同じ内容であれば、old/A.html のタイムスタンプを html/A.html にコピーします。 すると、A.html が更新されない限り、最終更新日時は old/A.html に保持され続けます。

    lastmod は html/A.html のタイムスタンプをもとに生成するとして、changefreq は現在時刻と old/A.html の時間差から計算することにします。 たとえば時間差が1時間以内であれば hourly、24時間以内であれば daily、……となります。

    とりあえず完成形

    今回は説明が難しくて長くなりそうなので、先に完成形のシェルスクリプトを貼らせてください🙇  ちなみにこのスクリプトは、crawl.sh としてこのサイトの運営にも組み込まれています。

    #!/bin/bash
    
    # URL 先頭部分
    domain="https://apps.bluehood.net"
    
    # sitemap.xml 登録リストの生成
    sitemaps=`find html/ -name *.html`
    sitemaps=${sitemaps/html\/404.html/}
    sitemaps=${sitemaps/html\/50x.html/}
    
    # sitemap.xml の作成
    cat << EOS > html/sitemap.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    EOS
    for path in ${sitemaps}
    do
      page=${path#html/}
      old=old/${page}
      page=${page/index.html/}
    
      # 変更チェック
      if [ -e ${old} ]; then
        prevmod=`date "+%s" -r ${old}`
    
        if diff ${path} ${old} > /dev/null 2>&1; then
          # 変更なしの場合、古い更新日時を継承する
          mv ${old} ${path}
        fi
      else
        # 新規ファイルの場合、前回の更新時間は1970年1月1日とみなす
        prevmod=0
      fi
    
      # <loc> の生成
      loc=`echo "${domain}/${page}" | recode utf8..html`
    
      # <changefreq> の生成
      elapsed=$((`date "+%s"` - ${prevmod}))
      if [ ${elapsed} -le $((60*60)) ];then
        changefreq="hourly"
      elif [ ${elapsed} -le $((60*60*24)) ];then
        changefreq="daily"
      elif [ ${elapsed} -le $((60*60*24*7)) ];then
        changefreq="weekly"
      elif [ ${elapsed} -le $((60*60*24*28)) ];then
        changefreq="monthly"
      else
        changefreq="yearly"
      fi
    
      # <lastmod> の生成
      # W3C Datetime: https://www.w3.org/TR/NOTE-datetime
      lastmod=`date "+%Y-%m-%dT%H:%M:%S%:z" -r ${path}`
    
      # <url> の登録
      echo "<url><loc>${loc}</loc><lastmod>${lastmod}</lastmod><changefreq>${changefreq}</changefreq></url>" >> html/sitemap.xml
    done
    echo "</urlset>" >> html/sitemap.xml
    

    今回のプログラムは長めですね。 順を追って解説していきたいと思います。

    sitemap.xml 登録リストの生成

    sitemaps=`find html/ -name *.html`
    sitemaps=${sitemaps/html\/404.html/}
    sitemaps=${sitemaps/html\/50x.html/}
    

    まずは sitemap.xml に登録する html ファイルを find コマンドで取得します。 その後、sitemap.xml に登録しないファイルを、bash の変数展開で除去しています。 今回の例では、404.html と 50x.html を除去しています。

    sitemap.xml のヘッダー書き込み

    cat << EOS > html/sitemap.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    EOS
    

    bash のヒアドキュメントで sitemap.xml のヘッダーを書き込みます。 注意点としては、echo コマンドではなく cat コマンドを使用しているところです。 EOS で囲まれたヒアドキュメントは標準入力として扱われるため、cat コマンドで標準入力→標準出力に流してあげる必要があります。

    各 html ファイルのループ処理

    for path in ${sitemaps}
    do
      page=${path#html/}
      old=old/${page}
      page=${page/index.html/}
    
      # 各 html ファイルのループ処理
    
    done
    

    変数 sitemaps に各 html ファイルのパスが入っていますので、これを for 文でループ処理します。 パスの先頭は必ず html/ から始まるため、これを除去して変数 page に入れています。 そして、新たに old/ を先頭に付けて、1つ前のバージョンのパス old を作ります。 また、sitemap.xml に登録するときは「index.html」を除去して URL を正規化することにします。

    変更チェック

      # 変更チェック
      if [ -e ${old} ]; then
        prevmod=`date "+%s" -r ${old}`
    
        if diff ${path} ${old} > /dev/null 2>&1; then
          # 変更なしの場合、古い更新日時を継承する
          mv ${old} ${path}
        fi
      else
        # 新規ファイルの場合、前回の更新時間は1970年1月1日とみなす
        prevmod=0
      fi
    

    html/ と old/ で html ファイルの変更があるかを判定します。 まず、if [ -e ${old} ]; で old/ の html ファイルがあるかを判定します。 ない場合は新規ページとみなせるため、前回の更新日時 prevmod を1970年1月1日にセットします。 プログラムでは prevmod=0 としていますが、これは時間を UNIX 時間で扱っているためです。

    old/ の html ファイルが存在する場合は、diff コマンドでファイル内容の変更チェックをします。 変更がない場合は、old/ の html ファイルを html/ に移動(上書き)することで、古い更新日時を継承します。 prevmod は、date コマンドで old/ の html ファイルから取得します。

    この処理での出力は、前回の更新日時 prevmod と、html/ にある html ファイルの更新日時です。

    <loc> の生成

      loc=`echo "${domain}/${page}" | recode utf8..html`
    

    ドメイン名と html ファイルのパスをつなぎ合わせて loc タグを作ります。 なお、sitemap.xml では < や > などの特殊文字をエンコードする必要があるため、recode utf8..html を挟んでいます。

    <changefreq> の生成

      # <changefreq> の生成
      elapsed=$((`date "+%s"` - ${prevmod}))
      if [ ${elapsed} -le $((60*60)) ];then
        changefreq="hourly"
      elif [ ${elapsed} -le $((60*60*24)) ];then
        changefreq="daily"
      elif [ ${elapsed} -le $((60*60*24*7)) ];then
        changefreq="weekly"
      elif [ ${elapsed} -le $((60*60*24*28)) ];then
        changefreq="monthly"
      else
        changefreq="yearly"
      fi
    

    前述の処理で求めた prevmod をもとに、更新頻度 changefreq を求めます。 まずは date &quot;+%s&quot; で現在の UNIX 時間を取得し、prevmod と引き算をすることで経過時間 elapsed を計算します。 elapsed は秒単位となりますが、これが 60x60=3600 秒、つまり1時間以内であれば changefreq=&quot;hourly&quot; としています。 同様に daily、weekly、monthly、yearly の判定を if 文で記述していますが、monthly の判定は 28 日としました。

    <lastmod> の生成

      # <lastmod> の生成
      # W3C Datetime: https://www.w3.org/TR/NOTE-datetime
      lastmod=`date "+%Y-%m-%dT%H:%M:%S%:z" -r ${path}`
    

    html/ にある html ファイルから lastmod を求めますが、sitemap.xml には W3C Datetime という形式で書き込む必要があります。細かい時分秒で出力するにはタイムゾーンの情報も必要になります。

    最後に <url> を書き込んで完成

      # <url> の登録
      echo "<url><loc>${loc}</loc><lastmod>${lastmod}</lastmod><changefreq>${changefreq}</changefreq></url>" >> html/sitemap.xml
    done
    echo "</urlset>" >> html/sitemap.xml
    

    頑張って作った loc, changefreq, lastmodurl タグにまとめて sitemap.xml に追記します。 そして、各 html ファイルのループ処理後にフッター </urlset> を書き込めば完成です。

    アルゴリズムでオリジナリティを出す

    さらにこだわるとしたら、priority を計算するアルゴリズムを考えて実装するところでしょうか? これで、Google にもサイトの評価をしてもらえそうです。 せっかく書いている記事、多くの人に見てもらいたいので🌿

    2019/05/04 sitemap.xml を生成するスクリプトをコマンド化しました。GitHub

    この記事の執筆者

    hato6502

    hata  

    中学生の頃に 6502 という CPU からプログラミングの世界に入りました。 C/C++ で1から作ることも好きですが、最近は変化の激しい IT 業界をみて Web 系にシフトしつつあります。