読者です 読者をやめる 読者になる 読者になる

Ruby でウェブ魚拓してみる

有名な「ウェブ魚拓」というウェブ・サービスがあって、ウェブページを保存するのに便利だが、ページの内容によってはすぐに削除されてしまうので、自分でつくってみた。使い方は、ruby webpget.rb URL とする。メインのページは target.html として保存される。

やっていることは、HTML を読み込んで、必要な CSSJavaScript の内容及び画像を取得し、相対URL に替える。また、リンクはすべて絶対URL に直す。原因はよくわからないのだが、うまく取得できないページもある(例えば HTML に埋め込んだ JavaScript で画像を表示している場合)。取得されたページはこんな感じ。

※追記
だいぶ手を入れて、かなりのページが取得できるようになりました。しかし、まだ取れないページも多少あります。よくわからないのだが、ブラウザ以外のアクセスを認めていなかったり、特定のページからしかアクセスを認めていないようなそれが取れないようです。

※再追記
mgurl メッソドを全面的に書きなおしました。これで URL に Unicode 文字が入っていても URI.join と同等のこと(相対アドレスを絶対アドレスに替える)ができます。(11/13 AM2:26)
バグを潰しました。(11/17 AM2:20)

webpget.rb

require "open-uri"
require "./memo"

def quarry_s(html)
  bf = ""
  bf += (chr = html.slice!(0)).to_s
  
  if chr == "<"
    if html[0, 3] == "!--"
      bf += html.slice!(0, 3)
      bf += html.slice!(0) while html[0, 3] != "-->"
      return [bf + html.slice!(0, 3), html]
    else
      bf += chr while !((chr = html.slice!(0)) == ">" or chr.empty?)
      return [bf + chr, html]
    end
  
  elsif !html.empty? 
    bf += chr while !((chr = html.slice!(0).to_s) == "<" or chr.empty?)
    return [bf, chr + html]
  end
  
  [nil, html]
end

def quarry(html)   #HTMLをタグ単位に分割
  ar = []
  while (b = quarry_s(html))[0]
    ar << b[0]
    html = b[1]
  end
  ar
end

def gettag(html_line)    #タグを取得
  st = html_line[1..-1]
  a = b = ""
  b += a until (a = st.slice!(0)) == " " or a == ">"
  b
end

def merge_url(url1, url2)     #相対アドレスを絶対アドレスに変換(Unicode が含まれていても可)
  head = ""
  urljoin = lambda do |ar|
    head + ar.join("/") + "/"
  end
  filter = lambda do |ar|
    ar.delete("..")
    ar.delete (".")
    ar.join("/")
  end
  
  return url1 if !url2 or url2 == "" 
  return url2 if %r|^https?:|.match(url2)

  head = "http://"
  h = 7
  if %r|^https:|.match(url1)
    head = "https://"
    h = 8
  end

  url1 += "-" if url1[-1] == "/"
  
  u = url1
  a = 0
  u = url1[0...a] if (a = url1.index("?"))
  ar1 = u[h..-1].split("/")    #ar1
  ar1.pop if ar1.length > 1
  
  url2 = "/." if url2 == "/"

  u = url2
  ub = ""
  if (a = url2.index("?"))
    u  = url2[0...a].to_s
    ub = url2[a..-1].to_s 
  end
  
  if url2[0..1] == "/?" or url2[0] == "?"
    ar2 = [url2]                #ar2    
  else
    ar2 = u.split("/")
    ar2[-1] += ub
  end
    
  return head + url2[2..-1] if url2[0..1] == "//"
  
  if url2[0] == "/"
    rurl = head + ar1[0] + "/" + filter[ar2]
  elsif ar2[0] == ".."
    ar1.pop
    ar2.shift 
    while (a = ar2.shift) == ".."
      ar1.pop if ar1.length > 1
    end
    ar2.unshift(a)
    rurl = urljoin[ar1] + filter[ar2]
  elsif ar2[0] == "."
    a = (url2 == "." or url2 == "./") ? "" : "-"  
    ar2.shift
    rurl = merge_url(urljoin[ar1] + a, ar2.join("/"))
  else
    rurl = urljoin[ar1] + filter[ar2]
  end

  return rurl
end

def cksuffix(url)
  if (m = /\.(\w+)$/.match(url))
    m[1].to_s
  else
    ""
  end
end

class String
  def my_split
    st = self.dup
    ar =[]
    bf = ""

    while (a = st.slice!(0))
      if a == ";" or a == "\n"
        ar << (bf.empty? ? a : bf)
      else
        bf += a
      end
    end

    ar << bf unless bf.empty?
    ar
  end
end

def check_css(fname, url)     #CSS の中で url(●●) で指定されるファイルがあれば取得
  data = fname1 = url2 = "" 
  save = lambda do
    puts "" + data.sub!(/url\("?(.+?)"?\)/i, %Q|url("#{fname1}")|)
    save_file(url2, fname1)
    $counter += 1
  end

  css = bf = ""
  fl = false
  open(fname, "r") {|io| css = io.read }

  css.my_split.each do |d|
    data = d
    data.gsub!(/'/, '"')

    if (m = /url\("?(.+?)"?\)/i.match(data))
      fl = true
      url2 = merge_url(url, m[1].to_s).to_s

      case cksuffix(url2)
      when "css", "CSS"
        fname1 = "file#{$counter}.css"
        save.call
        check_css(fname1, url2)
      else
        fname1 = "file#{$counter}#{url2.imgsuffix}"
        save.call
      end

    end rescue puts "encode error"

    bf += data
  end

  if fl
    File.delete(fname)
    open(fname, "w") {|io| io.write(bf)}
  end
end

def getpage(page_url)
  html_line = fname = u2 = ""
  save = lambda do |regexp, st|
    html_line.sub!(regexp, st)
    puts html_line
    save_file(u2, fname)
    $counter += 1
  end
  
  html = ""
  dir = URI(page_url).host
  Dir.mkdir(dir)
  Dir.chdir(dir)
  open(page_url).each {|f| html += f}
  
  a = 0
  page_url = page_url[0, a] if (a = page_url.index("?"))
  rewrited_html = ""
  flscript = false
  
  quarry(html).each do |h|
    html_line = h  
    if html_line[0] != "<" or html_line[0, 2] == "<!"
      rewrited_html += html_line
      next
    end
    if flscript
      if html_line.downcase == "</script>" or html_line == "</noscript>"
        flscript = false
      end
      rewrited_html += html_line
      next
    end
    
    case gettag(html_line)
    
    when "link", "LINK"
      html_line.gsub!(/'/, '"')
      if %r|type="text/css"|i.match(html_line) or %r|rel="stylesheet"|i.match(html_line)
        u1 = page_url
        u3 = html_line[/href="(.*?)"/i, 1]
        u1 = u3 if /http/.match(u3)
        next unless u3
        u2 = merge_url(page_url, u3)
        fname = "file#{$counter}.css"
        save.call(/href=".*?"/i, %Q|href="#{fname}"|)
        check_css(fname, u1)
      else
        u1 = html_line[/href="(.*?)"/i, 1]
        next unless u1
        u3 = merge_url(page_url, u1)
        html_line.sub!(/href=".*?"/i, %Q|href="#{u3}"|)
        puts html_line if PrintFlag
      end
       
    when "script", "SCRIPT"
      html_line.gsub!(/'/, '"')
      if /src/i.match(html_line)
        u1 = html_line[/src="(.*?)"/i, 1]
        next unless u1
        u2 = merge_url(page_url, u1)
        fname = "file#{$counter}.js"
        save.call(/src=".*?"/i, %Q|src="#{fname}"|)
      else
        flscript = true
      end
    
    when "img", "IMG"
      html_line.gsub!(/'/, '"')
      reg = [/src="(.*?)"/i, /src=(.*?)( |>)/i]
      u1 = ""
      if (m = reg[0].match(html_line))
        fl = 0; u1 = m[1]
      elsif (m = reg[1].match(html_line))
        fl = 1; u1 = m[1]
      else
        puts "match error: line='#{html_line}'"
        rewrited_html += html_line
        next
      end
      next unless u1
      u2 = merge_url(page_url, u1)
      fname = "file#{$counter}#{u1.imgsuffix}"
      fname1 = %Q|src="#{fname}"|
      fname1 += " " if fl == 1
      save.call(reg[fl], fname1)
    
    when "a", "A"
      html_line.gsub!(/'/, '"')
      reg = [/href="(.*?)"/i, /href=(.*?)( |>)/i]
      u1 = ""
      fl = 0
      if (m = reg[0].match(html_line))
        u1 = m[1]
      elsif (m = reg[1].match(html_line))
        u1 = m[1]; fl = 1
      end
      next unless u1
      u2 = merge_url(page_url, u1)
      u = "href=\"#{u2}\""
      u += " " if fl == 1
      html_line.sub!(reg[fl], u)
      puts html_line if PrintFlag
    
    when "body", "BODY"
      html_line.gsub!(/'/, '"')
      if (m = /background="(.*?)"/i.match(html_line))
        u1 = m[1].to_s
        u2 = merge_url(page_url, u1)
        fname = "file#{$counter}#{u1.imgsuffix}"
        save.call(/background=".*?"/i, %Q|background="#{fname}"|)
      end
    end
    
    rewrited_html += html_line
  end
  open("target.html", "w") {|io| io.write(rewrited_html)}
end

PrintFlag = false
url = ARGV[0]
$counter = 1
getpage(url)

memo.rb

def save_file(url, filename, max=0)
  count = 0
  begin
    open(filename, 'wb') do |file|
      open(url) {|data| file.write(data.read)}
    end
    true 
  rescue
    puts "ファイル入出力エラー: " + $!.message.encode("UTF-8")
    count += 1
    return false if count > max  #max回までリトライする
    puts "リトライ: #{count}"
    sleep(1)
    retry
  end
end

class String
  def imgsuffix
    /(\.\w+)$/.match(self)
    case $1
    when ".jpg", ".gif", ".png", ".jpeg", ".bmp", ".JPG", ".GIF", ".PNG", ".JPEG", ".BMP"
      $1
    else
      ""
    end
  end
end

こんがらがったコードだなあ。よくも動いていると思う。しかしこれは僕が悪いだけでなく、文法どおりに HTML や CSS を書かない連中のせいでもある。既存のブラウザはよく処理しているものだと、つくづく感心する。まあ当り前だけれど。さて、もっと技量が上がったらこれをクリーンアップできるのだろうか。

※追記
できるだけコードが見やすくなるように書き直しました。(2016/5/1)