8. ruby - ウェブスクレイピング - リンク切れをチェック 1ページ

 
8.1 概要
8.2 メイン
8.3 リンク URI を抽出する
8.4 リンク先をチェックする
8.5 # をチェック
8.6 ./ や ../ の編集
8.7 リンク先を取得
8.8 現時点での実行結果

8.1 概要

 さて、前項、前々項の結果を踏まえて、1ページ分はおおよそチェックできるようになったので。  いったんここでメモとして残しておきます。  クラス分けが不適切だったり、ネーミングが不適切だったりするのはご愛敬としてお許しください。

8.2 メイン

 メインは最初に作成したものとの変更はありません。

#!/usr/bin/env ruby

require './checkparent'

tree = { ARGV[0] => 'UNKNOWN' }

parent = CheckParent.new(ARGV[0])
parent.do(ARGV[0], tree)

8.3 リンク URI を抽出する

 リンク先 URI を抽出する部分。  これも大きな変更はありません。

#!/usr/bin/env ruby

require './checkchild'
require 'open-uri'
require 'nokogiri'

class CheckParent
  def initialize(original)
    @check = Check.new(original)
  end

  def do(uri, tree)
    begin

      tree.each do | key, value |
        if (key == uri)
          if (value != 'UNKNOWN')
            return;
          end
        end
      end

      tree[uri] = 'CHECKING'

      originalURI = uri

      uri = URI.encode(uri)
      uri = URI.parse(uri)
      html = open(uri).read

      html.sub!(/^<!DOCTYPE html(.*)$/, '<!DOCTYPE html>')

      doc  = Nokogiri::HTML(html)

      link = {}

      doc.css('a').each do | anchor |
        link[anchor[:href]] = nil
      end

      doc.css('input').each do | anchor |
        if s = anchor[:onclick]
          s = s[/\'.*\'/]
          link[s.delete!("'")] = nil
        end
      end

      doc.css('img').each do | anchor |
        link[anchor[:src]] = nil
      end

      link = Hash[link.sort]

      @check.do(originalURI, doc, link, tree)

    rescue => error
      puts "例外発生"
      puts "#{error.backtrace}"
      puts "[#{self.class.name}][#{error.message}]"
      exit 1
    end
  end
end

8.4 リンク先をチェックする

 リンク先をチェックする部分。

#!/usr/bin/env ruby

require 'open-uri'
require 'nokogiri'
require('./assemble')
require('./searchName')
require('./fetch')

class Check
  def initialize(original)
    protocol, userinfo, domain, other  = URI.split(ARGV[0])
    @top = "#{protocol}" + "://" + "#{domain}"
  end

  def do(parent, doc, child, tree)

    # parent の URI の末尾を加工してインデックスまでを求める

    @index = parent

    split = parent.split('/')
    last  = split.last

    if (last.include?('.'))
      @index = parent[0,parent.length-last.length]
    end

    puts("インデックス = [#{@index}]")

    @parent = parent

    search   = SearchName.new
    assemble = Assemble.new
    fetch    = Fetch.new

    child.each_key do | key |

      full = key
      kind = "                 "
      res  = '未チェック'

      case key[0]
      when '#'
        kind = "ページ内リンク   "
        res = search.do(doc, key)                   # ページ内リンクなので html 内に id=key が存在すること

      when 'h'
        kind = "外部    リンク   "
        res, full = fetch.do(@parent, @top, key)    # 外部リンクなので存在の有無のみ

      when '.'
        kind = "内部  . リンク   "

        # 内部リンクなのでたどりたい

        res, full = fetch.do(@parent, @top, assemble.do(@index, key))

      else
        if (key[-3, 3] == 'png')
          kind = "イメージファイル "
          res, full = fetch.do(@parent, @top, key)  # イメージなので存在の有無のみ

        elsif key[-4, 4] == 'html' || key[-1, 1] == '/'
          kind = "内部    リンク   "
          res, full = fetch.do(@parent, @top, key)  # 内部リンクなのでたどりたい

        else
          kind = "その他のもの     "                # 上記以外なのでチェックしない
        end
      end

      child[key] = { :リンク元 => @parent, :種別 => kind, :リンク => key, :結果 => res, :URI => full }
    end

    child.each_value do | value |
      puts("#{value[:種別]} → #{value[:リンク]} → #{value[:結果]} ")
    end

  rescue => error
    puts "例外発生[#{error.class}]"
    puts "#{error.backtrace}"
    puts "[#{self.class.name}][#{error.message}]"
    exit 1
  end
end

 19~26 行で現在チェック中の URL のインデックス部分を求めています。  これは、インデックス部分と「./」「.//」「#」を組み合わせて、リンク先の URI の文字列を組み立てるためのものです。  36~73 行でリンクを分類して分けたものごとにチェックを行っています。  75~77 行で結果を出力しています。

8.5 # をチェック

 # で始まるリンク先は、ドキュメント内に「id=リンク先」の記述があれば OK とします。

#!/usr/bin/env ruby

#
# # で始まる要素はページ内の特定の場所へのリンク
#   本サイトでは <hx> タグ内に id="" の形式で記述してあるので
#   ドキュメント内での記述をさがす
#
class SearchName
  def initialize()
  end

  # 引数としてドキュメント自身が必要

  def do(doc, id)
    begin

      # とりあえず   h4 のみ探す

      doc.css('h4').each do | anchor |
        if ("#" + anchor[:id] == id)
          return 'OK'
          # 検出したら 'OK' を返す
        end
      end

    rescue => error
      puts "例外発生[#{error.class}]"
      puts "#{error.backtrace}"
      puts "[#{self.class.name}][#{error.message}]"
      exit 1
    end

    return 'NG'
  end
end

8.6 ./ や ../ の編集

 「./」や「../」で始まるリンク先は、リンク元のドキュメントのルートやその上と組み合わせて、URI を再構築します。

#!/usr/bin/env ruby

class Assemble
  def initialize()
  end

  #
  # ./  や ../ で始まる URI を組み立て直す
  #
  def do(index, original)

    uri = original;

    begin
      if (original[0, 2] == '..')                         # ../ インデックスのひとつ上に / 以降を加える
        split = index.split('/')
        uri = index[0, index.length-split.last.length+1] + original[2,original.length-2]

      else                                                # ./  インデックスに / 以降を加える
        uri = index + original[2,original.length-2]
      end
    rescue => error
      puts "例外発生[#{error.class}]"
      puts "#{error.backtrace}"
      puts "[#{self.class.name}][#{error.message}]"
      exit 1
    end

    return uri
  end
end

8.7 リンク先を取得

 ドキュメント内の記述でないリンク先は取得してみます。

#!/usr/bin/env ruby

class Fetch
  def initialize()
  end

  #
  # 外部リンク・ドメイン内リンク・イメージファイルは open してみる
  #
  def do(top, parent, check)

    begin

      sharp = 0

      case check[0]
      when ('#')                            # # で始まっているものはページ内リンクなので親に配下をつける
        sharp = 1
        check = parent + check              # ↑どうもこれでも例外が発生する
      when ('/')                            # / で始まっているものはドメイン内リンクなのでドメインをつける
        check = top + check
      when ('.')
        # ./ や ../ は組み立て直す
      end

      if (sharp)                            # # があるものを encode、parse すると落ちる!
        uri = check
      else
        uri = URI.encode(check)
        uri = URI.parse(uri)
      end

      open(uri)

    rescue OpenURI::HTTPError => error

      return 'NG', check

    rescue => error
      puts "例外発生[#{error.class}]"
      puts "#{error.backtrace}"
      puts "[#{self.class.name}][#{error.message}]"
      exit 1
    end

    return 'OK', check
  end
end

8.8 現時点での実行結果


./ソースファイル名.rb http://freebsd.sing.ne.jp/windows/01/99.html
インデックス = [http://freebsd.sing.ne.jp/windows/01/]
ページ内リンク    → #1 → OK
ページ内リンク    → #2 → OK
ページ内リンク    → #3 → NG
内部  . リンク    → ../ → OK
内部  . リンク    → ../02/01.html → OK
内部  . リンク    → ../02/88.html → NG
内部  . リンク    → ../88/ → NG
内部  . リンク    → ./02/ → OK
内部  . リンク    → ./03.html → OK
イメージファイル  → /img/FreeBSD/adv/small01.png → NG
イメージファイル  → /img/FreeBSD/adv/small03.png → NG
イメージファイル  → /img/FreeBSD/windows/01/01/01.png → NG
イメージファイル  → /img/FreeBSD/windows/01/01/0X.png → NG
イメージファイル  → /img/adv/google01.png → NG
その他のもの      → /php/count.php?df=freebsd&sh=F → 未チェック
内部    リンク    → /tool/ → NG
内部    リンク    → /toor/ → NG
外部    リンク    → http://net-3.blogspot.jp/2013/03/blog-post_16.html → OK
外部    リンク    → http://net-3.blogspot.jp/2013/03/blog-post_99.html → NG