サイトをクロールしてsitemap.xmlを作るライブラリ

Googleウェブマスターツールやら、Yahoo!サイトエクスプローラーやらのためにsitemap.xmlをつくろうとおもいたった。
動的サイトなら、みんなたぶん自動でsitemap.xmlを作るようにしてるんだろうけど、静的ページがあるとそうはいかない。

いくつかツールはあるけど、

  • サイトルート以下のファイルを検索してファイルを作ってくれるツール →公開したくない場合だってある
  • Webアプリで提供 →大きなサイトだと制限で全部拾えない

ので、簡単につくってみた。
汚いし、やってることは簡単のなので勝手にもってって煮るなり焼くなりどうぞ。ほしいひとだけ。

# sitemap.rb
# サイトマップ構築
require 'digest/md5'
require 'uri'
require 'net/http'
require 'net/https'
require 'parsedate'
require 'rubygems'
require 'hpricot'
require 'builder/xmlmarkup'
class SiteMap

  attr_reader :domains, :links

  def initialize(url, domains = [])
    @site = URI.parse(url)
    @domains = domains.dup
    @domains << @site.host if @domains.empty?
  end

  def build(depth = 10, &block)
    index = @site.to_s
    @links = { index => { } }  # {url => headers}
    @pages = []
    @excepts = []
    crawl(index, depth, &block)
    @excepts.each { |except| @links.delete(except) }
    @links
  end

  def to_s
    xml = Builder::XmlMarkup.new(:indent => 2)
    xml.instruct! :xml, :version => "1.0", :encoding => "UTF-8"
    xml.urlset(
               'xmlns' => 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd',
               'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
               'xsi:schemaLocation' => "http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
               ) do
      @links.each do |page, response|
        time = Time.mktime(*ParseDate.parsedate(response['last-modified'] || Time.now.to_s))
        xml.url do
          xml.loc(page)
          if time > Time.now - 86400 * 2
            xml.changefreq('daily')
          else
            xml.changefreq('weekly')
          end
          xml.priority('0.7')
          xml.lastmod(time.utc.strftime("%Y-%m-%dT%H:%M:%SZ"))
        end
      end
    end
    xml.target!
  end

  def save(path)
    File.open(path, 'w') do |file|
      file.write self.to_s
    end
  end

  private
  # targetで指定したページを開き、含まれるリンクを巡回する
  def crawl(target_url, depth = 10, redirection_limit = 10, &callback)
    return if depth == 0
    raise ArgumentError, 'http redirect too deep' if redirection_limit == 0

    target = URI.parse(target_url)
    http = Net::HTTP.new(target.host, target.port)
    http.use_ssl = true if target.scheme == 'https'
    http.start do |request|
      begin
        callback.call(target_url, depth) if block_given?
        response = request.get(target.request_uri)
        case response
        when Net::HTTPSuccess
          digest = Digest::MD5.hexdigest(response.body)
          if response['content-type'] !~ /text|html/ || @pages.include?(digest)
            @excepts << target_url
            next
          end
          @pages << digest
          response.each { |key, value| @links[target_url][key] = value}
          doc = Hpricot(response.body)
          links = []
          doc.search('a[@href!=""]').each do |linktag|
            uri = target.merge(URI.parse(linktag.attributes['href']))
            domains.each do |domain|
              if uri.host =~ /#{Regexp.quote(domain)}$/
                link = uri.to_s.sub(/#.*$/, '')
                if !@links.keys.include?(link) && !@excepts.include?(link) && !links.include?(link)
                  links << link
                  @links[link] = {}
                end
                break
              end
            end
          end
          links.each do |link|
            crawl(link, depth - 1, redirection_limit, &callback)
          end
        when Net::HTTPRedirection
          @excepts << target_url
          crawl(response['location'], depth, redirection_limit - 1, &callback)
        end
      rescue Exception
      end
    end
  end

end

使い方

require 'sitemap'

sitemap = SiteMap.new('http://www.e-tsuyama.com', ['e-tsuyama.com'])  # ドメインに「e-tsuyama.com」を含む(後方一致)場合同じサイト内と判断
sitemap.build { |url, depth| puts ' ' * (10 - depth) + url }
sitemap.save('sitemap.xml')