当方、スクレイピング・スパイダリングを勉強しています。
本やネットで調べ丸ごと取るならばわかるのですが、どうしてもテーブルから取る方法がわかりません。
以下のブログスペースに例を書きます。ここに投稿しようとして文字数から少し削りすぎてしまいましたが。
<tbody>下の<tr>のひとまとめは日によって増減し、<td>7つにそれぞれ日本語表記があります。
<td>それぞれのデータをそれぞれ変数に収めて、そこからMySQLに書き込み記録していきたいです。
1日1回のチェックなど、先方に迷惑かからないように配慮しつつcron使おうかと考えています。
●Mechanize他、追加モジュールは使わないスタンダードな方向でお願いします。
●煮詰まってるスクレイピング部分だけでもいいのですが、詳細に書かれていれば助かるので追加でポイントを贈ります。
http://freez.hatenablog.jp/entries/2015/06/21
Ruby だと、こんな感じでしょうか。
def treat_data data p data end txt = ... # 対象ページのデータがまるっと入ってるとして in_table = in_tbody = in_tr = false data = [] txt.each_line { |line| line.chomp! if ! in_table && line =~ %r|<table.*class="lv4"| then in_table = true elsif line =~ %r|</table>| then in_table = false elsif in_table && line =~ %r|<tbody>| then in_tbody = true elsif line =~ %r|</tbody>| then in_tbody = false elsif in_tbody && line =~ %r|^<tr>| then in_tr = true elsif in_tr && line =~ %r|</tr>| then treat_data data data = [] in_tr = false end if in_tr && line =~ %r|<td[^>]+>(.*)</td>| then data << $1 end }
OpenURI とかを使って対象ページの内容を文字列として取り込んだ後の処理です。
行単位で読み進めていって、table や tbody の内側に入っているかを変数に持たせて、その内側にある td を拾っています。
対象のページに table がひとつだけしかないのであれば、以下のような感じでも十分です。
def treat_data data p data end txt = ... data = [] txt.each_line { |line| line.chomp! if line =~ %r|<td[^>]+>(.*)</td>| then data << $1 elsif line =~ %r|</tr>| then unless data.empty? then treat_data data data = [] end end }
require 'open-uri' # get HTML uri = "http://teideninfo.tepco.co.jp/day/day003-j.html" txt = "" http_options = {} open(uri, http_options){ |io| txt = io.read } # encode to UTF-8 txt.force_encoding("Shift_JIS") txt = txt.encode("UTF-8") def treat_data data data.each_with_index { |x, i| puts "#{i} : #{x}" } puts '-' * 72 end data = [] txt.each_line { |line| line.chomp! if line =~ %r|<td[^>]+>(.*)</td>| then data << $1 elsif line =~ %r|</tr>| then unless data.empty? then treat_data data data = [] end end }
require 'open-uri' # get HTML uri = "http://teideninfo.tepco.co.jp/day/day002-j.html" txt = "" http_options = {} open(uri, http_options){ |io| txt = io.read } # encode to UTF-8 txt.force_encoding("Shift_JIS") txt = txt.encode("UTF-8") 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 data.each_with_index { |x, i| puts "#{i} : #{x}" } puts '-' * 72 end data = RowData.new txt.each_line { |line| line.chomp! 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|</tr>| then unless data.empty? then treat_data data.to_a end data.clear end }
元の処理ループをあまり変えずに、表の意味的な一行を表すクラスを作って対応してみました。
DB に書き込むということなので、rowspan の指定で TD が抜けているところは、前の行の値を引き継ぎます。
テストは甘いですが、rowspan が 2よりも大きいときや、別の列に rowspan が入ったときにも対処しているつもりです。
Ruby だと、こんな感じでしょうか。
def treat_data data p data end txt = ... # 対象ページのデータがまるっと入ってるとして in_table = in_tbody = in_tr = false data = [] txt.each_line { |line| line.chomp! if ! in_table && line =~ %r|<table.*class="lv4"| then in_table = true elsif line =~ %r|</table>| then in_table = false elsif in_table && line =~ %r|<tbody>| then in_tbody = true elsif line =~ %r|</tbody>| then in_tbody = false elsif in_tbody && line =~ %r|^<tr>| then in_tr = true elsif in_tr && line =~ %r|</tr>| then treat_data data data = [] in_tr = false end if in_tr && line =~ %r|<td[^>]+>(.*)</td>| then data << $1 end }
OpenURI とかを使って対象ページの内容を文字列として取り込んだ後の処理です。
行単位で読み進めていって、table や tbody の内側に入っているかを変数に持たせて、その内側にある td を拾っています。
対象のページに table がひとつだけしかないのであれば、以下のような感じでも十分です。
def treat_data data p data end txt = ... data = [] txt.each_line { |line| line.chomp! if line =~ %r|<td[^>]+>(.*)</td>| then data << $1 elsif line =~ %r|</tr>| then unless data.empty? then treat_data data data = [] end end }
require 'open-uri' # get HTML uri = "http://teideninfo.tepco.co.jp/day/day003-j.html" txt = "" http_options = {} open(uri, http_options){ |io| txt = io.read } # encode to UTF-8 txt.force_encoding("Shift_JIS") txt = txt.encode("UTF-8") def treat_data data data.each_with_index { |x, i| puts "#{i} : #{x}" } puts '-' * 72 end data = [] txt.each_line { |line| line.chomp! if line =~ %r|<td[^>]+>(.*)</td>| then data << $1 elsif line =~ %r|</tr>| then unless data.empty? then treat_data data data = [] end end }
require 'open-uri' # get HTML uri = "http://teideninfo.tepco.co.jp/day/day002-j.html" txt = "" http_options = {} open(uri, http_options){ |io| txt = io.read } # encode to UTF-8 txt.force_encoding("Shift_JIS") txt = txt.encode("UTF-8") 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 data.each_with_index { |x, i| puts "#{i} : #{x}" } puts '-' * 72 end data = RowData.new txt.each_line { |line| line.chomp! 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|</tr>| then unless data.empty? then treat_data data.to_a end data.clear end }
元の処理ループをあまり変えずに、表の意味的な一行を表すクラスを作って対応してみました。
DB に書き込むということなので、rowspan の指定で TD が抜けているところは、前の行の値を引き継ぎます。
テストは甘いですが、rowspan が 2よりも大きいときや、別の列に rowspan が入ったときにも対処しているつもりです。
仮に見分けるなら、更新時間もあるからいっそ上書きせずに追加しておいて人の目で見比べる方が健全でしょうか。
データにどのようなバリエーションがあるか、と、それをどのように使いたいかによります。
「人の目で見比べる」じゃないと、最初に想定できないことはあります。
でも、一度、パターンが分かってしまえば、プログラムで対応できることもあります。
エラーが出てなくても、想定通りにデータを収集できてないかもしれません。
特に、上書きだと、収集したデータを見ただけでは分からないかも。
自動的にできることはプログラムに任せる、で良いとは思います。
でも、プログラムが期待通りに処理できているかどうかの確認は大切だと思います。
#趣味の範囲だと、ボチボチやれば、ってかんじですけれど。
一晩考えて、完全に考え直しました。
変更されたら、全て追加で登録していきたいと思います。
変化がわかるのはすばらしい。
もっとシンプルに求めて、シンプルに考えないといけないと思い詰めると考えすぎてしまうため、改めました。
http://q.hatena.ne.jp/1436077068
新たに質問したため、よろしくお願いします。
今は基本的にはPerlで試していますが、RubyやPHPでもわかりますし、そちらに切り替えてもいいので広く教えて頂きたいです。
C++やJavaもいいのですが、今回はスクリプト言語を想定しています。
Pythonは全く触れたことがないです。Pythonでもできるとはわかるので、これを機に学ぶのもアリかもしれませんが。
> 広く教えて頂きたいです
では PHPで書いてみます。
やっている事は先の方と同じですが...
try { // DB に接続 $dbh = new PDO($dsn, $user, $password); // エラー時は例外を投げるようにしておくべきでしょうね(省略) // プリペアドステートメントの発行 $sth = $dbh->prepare('INSERT INTO ... (略)'); $src = '....'; // 目的のテーブルを含む取得したデータ // 抜き出したいデータ部分を特定 // ソース中の最初の tbody 部だと仮定 // テーブルが例示の一つだけなら $tbody = $src でもOKでしょう $pos1 = strpos($src, "<tbody>\n<tr>\n") + 13; $pos2 = strpos($src, "</tr>\n</tbody>", $pos1); // 目的部分を抜き出し $tbody = substr($src, $pos1, $pos2-$pos1); // 1行毎に分解して各列データを取り出す foreach(explode("</tr>\n<tr>", $tbody) as $row) { if (preg_match_All('!<td[^>]*>(.*)</td>!', $row, $mch)) { // ここでプリペアドステートメントを実行する // 指定したSQL に依るが例えばこんな感じ $sth->execute($mch[1]); } } } catch (PDOException $e) { echo 'Connection failed: ' . $e->getMessage(); }
ご返答ありがとうございます。
phpのスクレイピングも確認をしたことはありますが、イメージと違うので戸惑いました。
今回はデータベースがあるからでしょうね。表示するだけならもう少しわかりやすいのだろう、、
詰まっているのが分解して取り出すこととそれを個別にデータベースに書き込むことなので、理屈が理解できて助かります。
しかし思えば一例をあげたことも必要な部分だけだったので、そのHPを自分が紹介した方が全体が見えて回答者に対してよかったかなと、言語別に全体を比較する方がみなが理解できてよかったかなと一考しています。
データにどのようなバリエーションがあるか、と、それをどのように使いたいかによります。
2015/07/05 00:39:06「人の目で見比べる」じゃないと、最初に想定できないことはあります。
でも、一度、パターンが分かってしまえば、プログラムで対応できることもあります。
エラーが出てなくても、想定通りにデータを収集できてないかもしれません。
特に、上書きだと、収集したデータを見ただけでは分からないかも。
自動的にできることはプログラムに任せる、で良いとは思います。
でも、プログラムが期待通りに処理できているかどうかの確認は大切だと思います。
#趣味の範囲だと、ボチボチやれば、ってかんじですけれど。
一晩考えて、完全に考え直しました。
2015/07/05 15:20:36変更されたら、全て追加で登録していきたいと思います。
変化がわかるのはすばらしい。
もっとシンプルに求めて、シンプルに考えないといけないと思い詰めると考えすぎてしまうため、改めました。
http://q.hatena.ne.jp/1436077068
新たに質問したため、よろしくお願いします。