下はまともなプログラムではないので、無視して下さい。(2018/4/15)
有名な「ウェブ魚拓」というウェブ・サービスがあって、ウェブページを保存するのに便利だが、ページの内容によってはすぐに削除されてしまうので、自分でつくってみた。使い方は、
ruby webpget.rb URL
とする。メインのページは target.html
として保存される。やっていることは、HTML を読み込んで、必要な CSS と JavaScript の内容及び画像を取得し、相対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)