2019-09-15   4218fd24

シェルで 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 ディレクトリとなります。

    もし 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から作ることも好きですが、最近は Web 系にシフトしつつあります。