ruby - サイトマップ作成 - .xml 形式の更新

 
1. 概要
2. sitemap.xml の形式
3. .ini ファイルの内容
4. メインモジュール
5. .ini ファイルの取得と加工
6. 最終更新日の検出

1. 概要

 前項もどうやらできました(2018年11月12日)。  次にやろうと思っているのが。  現在、ロボット用に「sitemap.xml」を置いています。  これの更新日時を手動で更新しているのですが、うまくやれば、自動で更新日を更新できそうな気がします。  .html 形式のサイトマップ作成をいじろうかと思いましたが、違う方法でやることにします。  下記のサイトが参考になりそうです。
RubyからXMLファイルを動的に編集する - Qiita

2. sitemap.xml の形式

 わたしが設置している sitemap.xml の形式は「HTML - ウェブサーバに設置しておく情報」に掲載しているものの再掲になります(さらにそれは Yahoo Japan の「Sitemapsの記述方法」のほぼ丸写し)が

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

  <url>
    <loc>「URL」</loc>
    <lastmod>「最終更新日」</lastmod>
    <changefreq>「更新頻度」</changefreq>
    <priority>「優先度」</priority>
  </url>

</urlset>
 てな感じになっております。  「最終更新日」の箇所を自動更新したいってことなのです。

3. .ini ファイルの内容

 下記のような内容の .ini ファイルを作成します。

[input]
file = 入力元 XML ファイル名をフルパスで設定

[output]
file = 出力先 XML ファイル名をフルパスで設定

[https://.../]	←	前項の sitemap.xml の「URL」を記述
file =			更新日をチェックするファイル名をフルパスで記述
directory = 更新日をチェックするファイルを再帰的に検索する場合のディレクトリを記述
exclude =	上記で除外するディレクトリがあれば記述
 最後のセクションは sitemap.xml で自動更新の対象としたいものをすべて記述します。  サイトのセクション内の要素はホワイトスペースで区切って複数定義を可能とします。

4. メインモジュール

 1ファイルで記述するには長くなったので分割して掲載します。  実際2つクラスを内包していますが、ファイルを分割するのが面倒だったもので・・・。  まずは、他モジュールの require の箇所。
=begin

  XML 形式のサイトマップを更新する

=end

$LOAD_PATH << File.dirname(File.expand_path(__FILE__))

require('pry')
require('inifile')
require('rexml/document')
require('BaseClass')
require('ExceptLog')

 pry、inifile、rexml/document 以外は、自前のモジュールで、これまでに出現しているものの使いまわしです。  メインの部分
begin
  $parameter = Hash.new

  ini = MyIniFile.new
  $parameter = ini.load(File.dirname(File.expand_path(__FILE__))+'/sitemapXML.ini')

  modified = Modified.new

  $parameter.each do | key, value |
    if ((key == :input) || (key == :output))
      next
    end

    $parameter[key][:modified] = modified.date(value)
  end

  file = File.open($parameter[:input])            # XML ファイル入力
  doc = REXML::Document.new(file)                 # XML ファイル読み込み

  doc.elements.each('urlset/url') do | url |

    key = url.elements['loc'].text

    if ($parameter.has_key?(key))
      if ($parameter[key][:modified] != nil)      # 更新日付が設定されていれば更新日付を設定
        url.elements['lastmod'].text = $parameter[key][:modified]
      end
    end
  end

  File.write($parameter[:output], doc)            # XML ファイル出力

rescue => error
  exeptlog = ExceptLog.new
  exeptlog.execute(self, error)
  exit 1
ensure

end

 142~143行は、ini ファイルの内容を後で使いやすいように加工しているモジュールで後述します。  145~153行は、URI ごとに最終更新日を取得している箇所です。このモジュールについても後述します。  155~156行で、現在の sitemap.xml を取得。  158~167行で、URI ごとに最終更新日を設定し直しています。  169行で更新した内容を出力しています。

5. .ini ファイルの取得と加工

 .ini ファイルを読んで、後で使いやすいように加工しています。
class MyIniFile < BaseClass
  def initialize
  end

  def load(file)
    parameter = Hash.new

    begin
      inifile = IniFile.load(file)                      # 読み込み

      inifile.sections.each do | section |
        if (section == 'input')                         # [input] セクション
          parameter[:input] = inifile[:input]['file']   # file を設定
          next
        end

        if (section == 'output')                        # [output] セクション
          parameter[:output] = inifile[:output]['file'] # file を設定
          next
        end

        # 上記以外はサイトの定義

        parameter[section] = Hash.new

        if (inifile[section]['file'])                   # file アイテム
          parameter[section][:file] = inifile[section]['file'].split(/\s/)
        end

        if (inifile[section]['directory'])              # directory アイテム
          parameter[section][:directory] = inifile[section]['directory'].split(/\s/)
        end

        if (inifile[section]['exclude'])                # exclude アイテム
          parameter[section][:exclude] = inifile[section]['exclude'].split(/\s/)
        end
      end

    rescue => error
      exeptlog = ExceptLog.new
      exeptlog.execute(self, error)
      exit 1
    ensure
      return parameter
    end
  end

end
 26~34行は単純ですね、入出力の XML ファイル名をハッシュにしているだけです。  38~50行は、URI ごとにファイル・再帰検索ディレクトリ・除外ディレクトリを配列の形にして、ハッシュにしています。

6. 最終更新日の検出

 URI ごとに最終更新日を検出して返すモジュールです。
class Modified < BaseClass
  def initialize
  end

  def update(file, date)
    stat = File::Stat.new(file)

    if (date == nil)
      date = stat.mtime
    else
      if (stat.mtime > date)
        date = stat.mtime
      end
    end

    return date
  end

  def date(parameter)
    date = nil
    begin

      parameter.each do | key, value |
        if (key == :file)                               # ファイルの場合
          value.each do | file |
            date = update(file, date)
          end
          next
        end

        if (key == :directory)                          # ディレクトリの場合

          value.each do | directory |
            Dir.chdir(directory)                        # ディレクトリ移動
            join = File.join('**', '*.html')            # 配下の .html ファイルを集める
            glob = Dir.glob(join)

            skip = nil

            glob.each do | bare |                       # 相対パスファイル名
              fullpath = directory + '/' + bare

              if (parameter[:exclude])                  # 除外ディレクトリが存在する場合は
                parameter[:exclude].each do | exclude | # 除外ディレクトリと一致するディレクトリ以下は
                  if (fullpath[0,exclude.length] == exclude)
                    skip = 'true'                           # スキップする
                    break
                  end
                end
              end

              if (skip)
                next
              end

              date = update(fullpath, date)
            end
          end
        end
      end

    rescue => error
      exeptlog = ExceptLog.new
      exeptlog.execute(self, error)
      exit 1
    ensure
      if (date == nil)
        return nil
      else
        return date.strftime("%Y-%m-%d")
      end
    end
  end
end
 68~80行は、ファイル名をフルパスでもらって、新しい日付を返すメソッドです。  初回は、日付に nil が設定されているので、無条件にそのファイルの更新日付を設定します。  86行からがミソかな。  87~92行は、ファイル名が指定されているものは、そのファイルの更新日付を取得します。  94~122行は、ディレクトリしていのあるものの処理です。  97~99行で、ディレクトリ以下の .html ファイルを収集して配列にしています。  104行で、配列に設定しているファイル名の1分にフルパスを設定しています。  106~117行で、除外指定しているディレクトリ以下のファイルであれば、チェックをスキップします。  119行で、チェック対象となっているファイルの更新日付を取得しています。