Rubyでスクレイピング方法。http://q.hatena.ne.jp/1455811321など何度か質問を続けて続いている関連。

上記では質問終わって無事解決できています、ありがとうございます。

例えば、今回は関西電力の停電情報です。久しぶりですが他も何とか挑戦しようかと。
http://www.kepco.co.jp/energy_supply/supply/teiden-info/index.php?ScreenName=RK00

ここで、一番右の「発生件数」を左側と合わせて抜き出して他のデータと同列にする方法はどうしたらいいでしょうか。
発生件数のリンク先URLが0から加算されているから、何かをカウントすればいいのかな、と思うところはあるのですが。当然固定ではなく変動しているのでループをどう調整しようか、、

よろしくお願いします。

回答の条件
  • 1人30回まで
  • 13歳以上
  • 登録:2016/09/06 22:23:59
  • 終了:2016/10/06 22:25:03

回答(2件)

id:a-kuma3 No.1

a-kuma3回答回数4412ベストアンサー獲得回数18032016/09/07 22:29:33

ポイント100pt

前の回答のコードを、

  • 質問にあるページをスクレイピングするように変更
  • 一行分のデータをタブ区切りで標準出力に出力

というふうに変えてみました。

require 'open-uri'

class RowData
    def initialize
        @first_line_mode_ = true
        @data_ = []
        @rows_ = []
    end
    def push data, rows
        if @first_line_mode_ then
            @data_ << data
            @rows_ << rows
        else
            i = @data_.index(nil)
            @data_[i] = data
            @rows_[i] += rows
        end
    end
    def clear
        is_remain = false
        (0...@data_.length).each { |i|
            @rows_[i] -= 1
            if @rows_[i] == 0 then
                @data_[i] = nil
            else
                is_remain = true
            end
        }
        unless is_remain then
            @data_ = []
            @rows_ = []
            @first_line_mode_ = true
        else
            @first_line_mode_ = false
        end
    end
    def to_a
        @data_.dup
    end
    def empty?
        @data_.empty?
    end
end


def treat_data data
    puts data.join("\t")
end

#
#   ここまでは、前回と同じです
#

def extract_blackout_information(uri)
    # get HTML

    txt = ""
    http_options = {}

    open(uri, http_options){ |io|
        txt = io.read
    }


    data = RowData.new

    iter = txt.split("\n").to_enum
    loop do
        line = iter.next

        # 通常のデータ
        if line =~ %r|<td([^>]*)>(.*)</td>| then
            td_attr = $1
            txt = $2
            rowspan = 1
            if td_attr =~ %r|rowspan="(\d+)"| then
                rowspan = $1.to_i
            end
            data.push txt, rowspan

        # 地区
        elsif line =~ %r|<td>\s+<span class="nowrap">| then
            txt = ""
            while line =~ %r|<span class="nowrap">(.+)</span>|
                txt += $1
                line = iter.next
            end
            data.push txt, 1

        # 発生件数
        elsif line =~ %r|<td rowspan="(\d+)"><a .* class="modal">| then
            rowspan = $1.to_i
            txt = ""
            while line !~ %r|^\s*</td>\s*$|
                if line =~ %r|<p class="count">(.+)</p>| then
                    txt = $1
                end
                line = iter.next
            end
            data.push txt, rowspan

        # 行の終わり
        elsif line =~ %r|</tr>| then
            unless data.empty? then
                treat_data data.to_a
            end
            data.clear
        end
    end
end


#   2016-9-6 分
extract_blackout_information("http://www.kepco.co.jp/energy_supply/supply/teiden-info/index.php?ScreenName=RK20160906")


データのバリエーションがあるので、2016年9月6日の分を解析するようにしてます(最後の行)。

こんな感じで、標準出力に表示されます

00:01~00:02 和歌山県日高郡 日高町大字池田雷の影響約-軒
00:03~02:36 和歌山県日高郡 美浜町大字和田雷の影響約290軒
00:03~02:36 和歌山県日高郡 日高町大字荊木、大字小池、大字小中、大字志賀、大字高家、大字萩原雷の影響約290軒
00:06~00:07 和歌山県有田郡 湯浅町大字青木、大字別所、大字湯浅雷の影響約-軒
00:06~00:07 和歌山県有田郡 広川町大字名島、大字東中、大字広雷の影響約-軒
00:06~00:07 和歌山県御坊市塩屋町 北塩屋調査中約-軒
00:06~00:07 和歌山県日高郡 美浜町大字吉原調査中約-軒
00:06~00:07 和歌山県日高郡 印南町大字印南原調査中約-軒

  ...



前回のページとは違って、TABLE のひとつのセルに入ってる情報が HTML のソースでは複数行にまたがっているので、ループが二重になっている、というのが構造的に一番変わっているところです。



追記です。

Ruby にも外部イテレータがあることをさっき知ったので(遅い)、コードをちょっとだけ書き直しました。

「何行目」を表す i を順次増やして配列から取り出していたところを、外部イテレータ(Enumurator)で行をずらしながら行を取り出すようにしています。

他9件のコメントを見る
id:a-kuma3

丸ごと保存するとどういう風に考えるのでしょうか。

「丸ごと」というか、「見たまま」を想定してました。

例えば、9/15 だとこんな感じで。

12:58~12:59和歌山県和歌山市......約-件
16:01~16:02和歌山県西牟婁郡 白浜町......約-件
  西牟婁郡 すさみ町...... 

TD タグがあるところだけ、テキストや数字を保存。
さすがに列がそろってないと後で使いづらいだろうと思うので、列だけは合わせて。

2016/09/16 09:15:59
id:FREEz

失礼しました、以前と同じはこちらですね。こちらのイメージでした。
ページを丸ごと保存して後でローカル環境でも対応するのかと思っていました。
わざわざ丁寧に表示までして頂いたので違いがとても分かりやすく助かりました。。

2016/09/16 11:09:33
id:tobeoscontinue No.2

tobeoscontinue回答回数212ベストアンサー獲得回数522016/09/08 15:40:50

ポイント100pt
<php
require 'open-uri'
require 'nokogiri'

def teiden_parse(html)
  doc = Nokogiri::HTML.parse(html)
  content = doc.css('#content')
  {'archive'=> content.css('p.archive_date').text.gsub(/ |\n|\r/, ''),
   'content'=> content.css("table > tr").inject([]) {|info, tr|
    td = tr.css('td')
    if 0 < td.size
      if td.size == 6 # td.attribute('rowspan').value
        info << [td[0].text.strip, # 発生・復旧時間
                 td[1].text, # 停電地域(都道府県|市区町村|地区)
                [[td[2].text,
                  td[3].text.gsub(/ |\n|\r/, '')]],
                 td[4].text, # 原因
                 td[5].css('p.count').text] # 発生軒数
      else
        info.at(-1)[2] << [td[0].text, td[1].text.gsub(/ |\n|\r/, '')] # 地区
      end
    end
    info
  }}
end

鋸でデータの抽出をして配列で返すようにしてみました。
停電地域の市区町村と地区が複数になることがあるようでこの部分は市区町村と地区のペアーの配列としました。
本来はrowspanを見て処理すべきですが面倒なのでtdの個数で判断するという手抜きをしています。

停電履歴情報はありません。には対応していません。
htmlがどうなっているか分かりませんがteiden_parseが[]を返えすといいのですが。

url='http://www.kepco.co.jp/energy_supply/supply/teiden-info/index.php?ScreenName=RK’+(ARGV.size == 0 ? '00' : ARGV[0])
kepco = teiden_parse(open(url).read)
p kepco['archive']
kepco['content'].each {|info|
puts info[0]
puts info[1]
puts info[2].map {|l| ' '+l[0]+':'+l[1]+"\n" }
puts info[3]
puts info[4]
puts " "
}

id:FREEz

回答ありがとうございます。
ノコギリでやってみたんですね。phpの例もあると見比べられて考えられて楽しいので助かります。
しかしセオリー通りでできましたか?
なんかページごとに違う表示になっているケースがあるので、まずはそこの根幹がどうなっているかを先に見抜かないといけないかなと四苦八苦してまして。
rowspanとtdの両方使用して見分けられるかな、、どう辿っていけばいいんだろう。

2016/09/15 11:01:33
id:tobeoscontinue

検索やマッチングの部分は鋸がやってくれるので記述量が減らせるのが大きなメリットでそれによる可読性の向上が期待できます。しかし全体をパースしDOMを構築するのでリソースを多く消費します。メリット、デメリットはありますが別の方法の提案ということで回答してみました。

>しかしセオリー通りでできましたか?
何のセオリーのことなんでしょう。鋸のセオリーということでしょうか?

>なんかページごとに違う表示になっているケースがあるので
具体的なページのソースがあれば対応可能かもしれません。
例えば東北電力や東京電力では以下のかんじになります。
def teiden_tepco(html)
doc = Nokogiri::HTML.parse(html, nil, "Shift_JIS")
content = doc.css('table.bo_lv4')
doc.search('br').each {|br| br.replace(',') }
{'archive'=> doc.css('select[@name="day"] > option[@selected]').text,
'content'=> doc.css('table.bo_lv4 tr').inject([]) {|info, tr|
td = tr.css('td')
if 0 < td.size
if td.size == 7 # td.attribute('rowspan').value
info << [td[0].text, # 発生・復旧時間
td[1].text, # 停電地域(都道府県|市区町村|地区)
td[2].text,
td[3].text[0..-2] ,
td[5].text, # 原因
td[4].text] # 発生軒数
else
info.at(-1)[2] << [td[0].text, td[1].text[0..-2 # 地区
end
end
info
}}
end

def teiden_touhoku_epco(html)
doc = Nokogiri::HTML.parse(html, nil, "Shift_JIS")
{'archive'=> doc.css('table.LayoutTable6 option[@selected]').text.strip,
'content'=> doc.css('table.LayoutTable5 tr').inject([]) {|info, tr|
td = tr.css('td')
if 0 < td.size
if td.size == 5 # td.attribute('rowspan').value
td[0].search('br').each {|br| br.replace('-') }
info << [td[0].text, # 発生・復旧時間
td[1].text, # 停電地域(都道府県|市区町村|地区)
[td[2].text.split(/\u3000/)],
td[4].text, # 原因
td[3].text] # 発生軒数
else
end
end
info
}}
end
関数の返す配列はどの電力会社でもほぼ同様になっています。

rowspanは市区町村と地区が複数ある場合のためのものでしょう。
rowspanを見て対処することは可能ですがrowspanが無い場合など本来の処理以外にも対処が必要となるのでteiden_parseではtdのサイズ6以外は市区町村と地区として処理しています。

2016/09/16 15:43:03
  • id:a-kuma3
    >一番右の「発生件数」を左側と合わせて抜き出して他のデータと同列にする方法
    「件数情報」のボタンを押して表示される停電件数を、時刻とか都道府県とかとひもづけたい、ってことですか?

    「件数情報」は見た目は A タグですが、クリックして表示する内容は、HTML 的にすぐ次に書いてあるので、それほど難しくないと思います。
    どちらかというと、時刻や都道府県が行をぶち抜いてる(rowspan > 1)ことがある方が、処理としては面倒です。
  • id:FREEz
    早速のお返事ありがとうございます。お久しぶりです。
    ポイント贈ろうと思ったんですがエラーでできなくなってしまったんですね;;

    おっしゃる通りで、抜き出した他のデータと同じく紐づけて保存したいという意味です。
    ほぼ同じため後で調整するかもしれないですが項目や順番が違うためまずは会社ごとに別のデータベースにすべきと思いますが。

    >どちらかというと、時刻や都道府県が行をぶち抜いてる(rowspan > 1)ことがある方が、処理としては面倒です。

    変形というか見づらいものは入力する際はどういう処理しているのだろうと、毎回思います、、、
    他に何か容易に考える方法はありますでしょうか。Mechanizeとか使えば容易ですか?
    今までのを応用しようかと思うところもあったのですが、とりあえず、、まず質問してみました。。


  • id:a-kuma3
    >Mechanizeとか使えば容易ですか?
    id とか、「意味」を持たせた class がついてると多少は簡単になります。
    でも、行をぶち抜いた(rowspan > 1)なところは簡単にならないので、どっちもどっちというところでしょうか。

    ただ、これは個人的な感想だろうなあ、という気はします。
    古くから java で XML を扱ってきた人にとっては、SAX と DOM という選択肢がありました。
    - SAX : 制約が厳しいけど速い
    - DOM : 自由度があるけど遅い

    文字列ベースで正規表現なんかでマッチさせるやり方は SAX に近くって、Mechanize とかを使うのは DOM に当たります。
    javascript とかから入った人だと、そもそも SAX って何、という感じのはずですし、なんでこんな面倒なソースを書くんだろう、という感覚を持っても不思議ではないです。

この質問への反応(ブックマークコメント)

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

絞り込み :
はてなココの「ともだち」を表示します。
回答リクエストを送信したユーザーはいません