人力検索はてな
モバイル版を表示しています。PC版はこちら
i-mobile

Rubyでスクレイピング方法。http://q.hatena.ne.jp/1455811321など何度か質問を続けて続いている関連。
上記では質問終わって無事解決できています、ありがとうございます。

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

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

よろしくお願いします。


●質問者: FREEz
●カテゴリ:インターネット ウェブ制作
○ 状態 :終了
└ 回答数 : 2/2件

▽最新の回答へ

1 ● a-kuma3
●100ポイント

前の回答のコードを、

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

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)で行をずらしながら行を取り出すようにしています。


FREEzさんのコメント
早々なご回答ありがとうございました。 私事から対応遅れていますが、あちらのソースコードとこちらのコードと、前のコードと、、いろいろ見て回っています。 コレ大変だなと思ったので見事でした。 しかし、データのバリエーションがあるということは、日付ごとに異なるということですか? 「停電履歴情報はございません。」の場合と、都道府県・市区町村・停電原因の処理が何が理由なのか個別とまとめと別れますね。 全体を疑う感じ、全てで同じようになりうる、と想定する方が筋が通りそう。 これは、例えば全てを同一の規格にするより、そのままコピーを対応できるようにして元データそのままの方がやり方としては普通ですか? 前のように最初にid入れてカウントすると少し目線が変わるかなとも思ったのですが、きっと何かの優先順位でこうなっているからそれを探しています。 一番規模が大きくなりがちな地名が中心にあるかと思ったら、時間な気がしています。 上下内容同じでも時間のずれで別れているような、、

a-kuma3さんのコメント
>> しかし、データのバリエーションがあるということは、日付ごとに異なるということですか? << 回答を書いたときの最新日 9/7 は、行方向のぶち抜き(rowspan > 1)が無かったので、9/6 のデータで検証しました。 想定して作ったわけではないですが、コメントを書いている時点の最新日 9/15 が「停電履歴情報はございません。」ですが、空振りしてくれました <tt>:-)</tt> >> 一番規模が大きくなりがちな地名が中心にあるかと思ったら、時間な気がしています。 上下内容同じでも時間のずれで別れているような、、 << スクレイピングは、取り込むページのデザインが変わると、作り直しってケースもよくあるので、目に見えてないバリエーションまで想定してきっちり作り込むか、今、目の前に見えているページが解析できれば十分だと割り切るかのバランスはあると思います。 今のデータだけ取れれば良い、とか、毎日起動してデータを貯めたい、とか、使い方にもよります。 >> これは、例えば全てを同一の規格にするより、そのままコピーを対応できるようにして元データそのままの方がやり方としては普通ですか? << デザインに合わせて解析の仕方を複数種類そろえて切り替える、ってことですよね。 ふたつ目以降は重複する部分が多いけど、コピペで作っちゃえ、と。 アリだと思いますよ。 自分で使うものだし。 ただ、コピーした部分に手を加える必要が出てきたときに、同じことを複数回やらなくちゃいけなくなります。 この「同じことを複数回やる」ってのが、またなかなかできないもので、注意深くやっているつもりがひとつだけ修正漏れがあってきちんと動かなくなる、なんてことはよくあります。 前の質問では、どれくらいのバリエーションがあるのか分からなかったので、素直に td 要素の rowspan 属性を見て解釈するようにしました。 面白そうだった、ということもあります(後々の自分でも使えるかも、というのもちょっとあった)。

FREEzさんのコメント
回答ありがとうございます。 以前の例と合わせて自分でもいろいろ試してみようと思うのですが、結局はデータまでどう辿るか、どう処理するか、ですよね。 自分としてはこういうので根幹の処理の法則がどうなっているのか、に興味を持ちました。 データを作って配信する側が仕様を変えてそれまでの方法で処理できなくなるというのは普通にあると見聞きしているのですが、そのために処理を簡易にして変更を容易にするか・作りこむか、という判断では前者と本で読みました。 そういう意味で今回の例は、かなりイレギュラー?にも見えたのですが、それで突き詰めすぎてわかりづらいといけないなとふと思いました。 こういうのは、データを得られることを最優先とするのか・ある程度法則を見抜くべきか、という点では、時と場合によるんでしょうか、、

a-kuma3さんのコメント
スクレイピングなので、データが得られないと始まりません <tt>:-)</tt> まず、それが第一。 ただ、この質問にある停電情報を定期的に取得してずーっと貯めこむ、というようなことを考えると、毎日スクリプトをいじるわけにもいかないので、想定できる範囲内で法則というか、データのバリエーションには対応しておかなければならないだろうと思います。 最初に法則を見抜く、といってもなかなか難しいよなあ、という話をひとつ。 ぼくが使ってるスクレイピングのスクリプトで一番古いのは、人力検索の質問・回答状況を抜き出すもので、作ったのは 4年くらい前になります。 何かの質問に回答する為だったか、はてなハイクに投稿するネタにするためだったか、きっかけは忘れてしまいました。 最初にある程度動くものができた後、履歴に残しているやや大きめの変更だけで 25回 手を入れてます。 最初は、直近一年くらいのデータを抜いてたのが、全期間を対象にしてみると、昔のデータに対応できてなかった、とか、抜いたデータを集計してみたら、妙な数字が出てきて、実はデータの抜き出し方が間違っていただとか。 匿名質問ができたときの対応は、一般質問用のメソッドをコピペして、一部を修正して対応したんですが、その後の修正で同じことを2回やらなくちゃいけないので、一本にまとめたりとかしてます。

FREEzさんのコメント
あれ、Enumerator てどこかありますか? リンク先でどう使うのかを見ているのですが、、

a-kuma3さんのコメント
>> あれ、Enumerator てどこかありますか? << 以下のコードで、String#split の戻り値が Array 。 Array#to_enum の戻り値が Enumerator です。 >|ruby| iter = txt.split("\n").to_enum ||< >|ruby| a = ["a", "b", "c"] a.each { |item| puts item } e = a.to_enum loop do item = e.next puts item end ||< これは、同じ結果になります(サンプルとしては、つまらない)。 回答のコードでは、例えば、取得した HTML が 100行あったとして、外側のループで 100回まわすのではなくて、内側のループでもずらしたくて、外と内で合わせて 100回まわしたいので Enumerator を使っています。 >|ruby| iter = ... # Enumerator loop do line = iter.next ... while ... # ここでも数回ループする line = iter.next ... end end # Enumerator#next は、合わせて 100回よばれる ||<

FREEzさんのコメント
コレ大変ですね、テーブルごとに表示変わると、、、 東電の時にも同じようなことはあったんだが、さてどうしていたかを確認して見比べていますw 今見たところ、なるほど以前は時間を基軸として別れたところは両方に記載し件数も二重に両方に計測していました。 都道府県や理由など文字データはそれでいいんだが、よく見たら数字はアレだ二重になってる、、最終的に内訳わからないのでそれはそれでいいんですけど。 関電の方がややこしいと思っていましたが数字は最後だから重なることもなく、文字を両方にすればいい、、と考えれば、、、まだいいのかな?w 以前のやり方でこの辺は対応できませんか。どこが欠けるか、でどうしようかと随分まいってるんですが。

a-kuma3さんのコメント
関電の方も、複数行にわたって「軒数情報」のボタンがひとつとかある(9/15 とか)ので、東電と同じですね。 >> 以前のやり方でこの辺は対応できませんか。どこが欠けるか、でどうしようかと随分まいってるんですが。 << 東電のときと抜き出したデータの形式は同じになっているつもりなんですが。 抜き出したデータを、どう使いたいかで変わってくるかもしれません。 どう使いたいかは後で変わるかもしれない、とすれば、以下のどちらかでしょうか。 - 見えてるままの形で保存しておく(DB にしづらい?) - ばらして保存して(今回の回答ように)、必要があれば後でくっつける 情報量を落としてしまうと、後で復元することが難しくなるので。 ざっと見た感じでは、発生・復旧時刻が同じで、別の行になっているデータはなさそうなので、後でくっつけるとしたら、それを使うか、別にひとつの行だったという情報を加えておくか。 きっと、元のデータは「停電の案件」でひとつのレコードになっているのだと思います。 ・停電情報 |停電案件キー|発生日時|復旧日時|発生件数|原因| <br> ・エリア情報(ひとつの停電案件に、複数のエリアコード) |停電案件キー|エリアコード|プライマリキー| <br> ・エリアマスタ(住所単位ではなく、送配電の設備の単位があると思います) |エリアコード|地区コード|市町村区コード|都道府県コード| <br> で、コードと名称などと対応付けたマスタがあるとか、じゃないのかなあと想像します。 ただ、ここまでばらす必要があるかなあ、という気がしなくもなく。

FREEzさんのコメント
以前同様に、 >ばらして保存して(今回の回答ように)、必要があれば後でくっつける そのまま丸ごと保存よりはこちらで、ただし以前と同一にする必要はないため時間などはそのままでもいいかと、思ったのですが、丸ごと保存するとどういう風に考えるのでしょうか。後でローカルでスクレイピング?確かにいっそ丸ごとが楽ぽいですがページごと保存もしておいた方がいいってことなんでしょうか。 確かに今の段階で以前のように発生時と復旧時に分けることができれば便利ですね。 ただそこまでやれるなら丸ごと保存するような理由はなくなってしまいますねw 個人的に思うのは、住所以外の理由で細かく表示されているのだと思います。 例えば管轄の事業所の位置や、送配電網の都合です。あれらは住所とは別ですので。 時代とともに土地の事情が変わっていってもインフラって変わらないですからw あとは停電の理由の大小や、社内システムの影響じゃないでしょうかね、、

a-kuma3さんのコメント
>> 丸ごと保存するとどういう風に考えるのでしょうか。 << 「丸ごと」というか、「見たまま」を想定してました。 例えば、9/15 だとこんな感じで。 |12:58?12:59|和歌山県|和歌山市|...|...|約-件| |16:01?16:02|和歌山県|西牟婁郡 白浜町|...|...|約-件| |&nbsp;|&nbsp;|西牟婁郡 すさみ町|...|...|&nbsp;| TD タグがあるところだけ、テキストや数字を保存。 さすがに列がそろってないと後で使いづらいだろうと思うので、列だけは合わせて。

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

2 ● tobeoscontinue
●100ポイント
<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 " "
}


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

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以外は市区町村と地区として処理しています。
関連質問

●質問をもっと探す●



0.人力検索はてなトップ
8.このページを友達に紹介
9.このページの先頭へ
対応機種一覧
お問い合わせ
ヘルプ/お知らせ
ログイン
無料ユーザー登録
はてなトップ