かなり煮詰まっているので、Perl,PHP,Rubyなら助かります。プログラミングを教えて頂きたいです。

当方、スクレイピング・スパイダリングを勉強しています。
本やネットで調べ丸ごと取るならばわかるのですが、どうしてもテーブルから取る方法がわかりません。
以下のブログスペースに例を書きます。ここに投稿しようとして文字数から少し削りすぎてしまいましたが。
<tbody>下の<tr>のひとまとめは日によって増減し、<td>7つにそれぞれ日本語表記があります。
<td>それぞれのデータをそれぞれ変数に収めて、そこからMySQLに書き込み記録していきたいです。
1日1回のチェックなど、先方に迷惑かからないように配慮しつつcron使おうかと考えています。
●Mechanize他、追加モジュールは使わないスタンダードな方向でお願いします。
●煮詰まってるスクレイピング部分だけでもいいのですが、詳細に書かれていれば助かるので追加でポイントを贈ります。

http://freez.hatenablog.jp/entries/2015/06/21

回答の条件
  • 1人50回まで
  • 登録:
  • 終了:2015/07/05 00:10:47
※ 有料アンケート・ポイント付き質問機能は2023年2月28日に終了しました。

ベストアンサー

id:a-kuma3 No.1

回答回数4974ベストアンサー獲得回数2154

ポイント3000pt

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
}





追記です。
TEPCO のページから HTML を持ってきて、テーブルからデータを抜き出すところまでやってみました。
ページの charset が Shift-JIS だったので、UTF-8 への変換を入れています(Ruby はデフォルトの文字コードが UTF-8 なので)。

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
}





追記です。
rowspan 対策版です。

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 が入ったときにも対処しているつもりです。

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

仮に見分けるなら、更新時間もあるからいっそ上書きせずに追加しておいて人の目で見比べる方が健全でしょうか。

データにどのようなバリエーションがあるか、と、それをどのように使いたいかによります。
「人の目で見比べる」じゃないと、最初に想定できないことはあります。
でも、一度、パターンが分かってしまえば、プログラムで対応できることもあります。

エラーが出てなくても、想定通りにデータを収集できてないかもしれません。
特に、上書きだと、収集したデータを見ただけでは分からないかも。

自動的にできることはプログラムに任せる、で良いとは思います。
でも、プログラムが期待通りに処理できているかどうかの確認は大切だと思います。
#趣味の範囲だと、ボチボチやれば、ってかんじですけれど。

2015/07/05 00:39:06
id:FREEz

一晩考えて、完全に考え直しました。
変更されたら、全て追加で登録していきたいと思います。
変化がわかるのはすばらしい。
もっとシンプルに求めて、シンプルに考えないといけないと思い詰めると考えすぎてしまうため、改めました。

http://q.hatena.ne.jp/1436077068

新たに質問したため、よろしくお願いします。

2015/07/05 15:20:36

その他の回答1件)

id:a-kuma3 No.1

回答回数4974ベストアンサー獲得回数2154ここでベストアンサー

ポイント3000pt

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
}





追記です。
TEPCO のページから HTML を持ってきて、テーブルからデータを抜き出すところまでやってみました。
ページの charset が Shift-JIS だったので、UTF-8 への変換を入れています(Ruby はデフォルトの文字コードが UTF-8 なので)。

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
}





追記です。
rowspan 対策版です。

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 が入ったときにも対処しているつもりです。

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

仮に見分けるなら、更新時間もあるからいっそ上書きせずに追加しておいて人の目で見比べる方が健全でしょうか。

データにどのようなバリエーションがあるか、と、それをどのように使いたいかによります。
「人の目で見比べる」じゃないと、最初に想定できないことはあります。
でも、一度、パターンが分かってしまえば、プログラムで対応できることもあります。

エラーが出てなくても、想定通りにデータを収集できてないかもしれません。
特に、上書きだと、収集したデータを見ただけでは分からないかも。

自動的にできることはプログラムに任せる、で良いとは思います。
でも、プログラムが期待通りに処理できているかどうかの確認は大切だと思います。
#趣味の範囲だと、ボチボチやれば、ってかんじですけれど。

2015/07/05 00:39:06
id:FREEz

一晩考えて、完全に考え直しました。
変更されたら、全て追加で登録していきたいと思います。
変化がわかるのはすばらしい。
もっとシンプルに求めて、シンプルに考えないといけないと思い詰めると考えすぎてしまうため、改めました。

http://q.hatena.ne.jp/1436077068

新たに質問したため、よろしくお願いします。

2015/07/05 15:20:36
id:FREEz

質問者から

FREEz2015/06/21 17:32:39

今は基本的にはPerlで試していますが、RubyやPHPでもわかりますし、そちらに切り替えてもいいので広く教えて頂きたいです。

C++やJavaもいいのですが、今回はスクリプト言語を想定しています。

Pythonは全く触れたことがないです。Pythonでもできるとはわかるので、これを機に学ぶのもアリかもしれませんが。

id:tezcello No.2

回答回数460ベストアンサー獲得回数69

ポイント200pt

> 広く教えて頂きたいです
では 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();
}
id:FREEz

ご返答ありがとうございます。
phpのスクレイピングも確認をしたことはありますが、イメージと違うので戸惑いました。
今回はデータベースがあるからでしょうね。表示するだけならもう少しわかりやすいのだろう、、
詰まっているのが分解して取り出すこととそれを個別にデータベースに書き込むことなので、理屈が理解できて助かります。
しかし思えば一例をあげたことも必要な部分だけだったので、そのHPを自分が紹介した方が全体が見えて回答者に対してよかったかなと、言語別に全体を比較する方がみなが理解できてよかったかなと一考しています。

2015/06/26 15:00:25
id:FREEz

質問者から

FREEz2015/06/30 23:42:28

これ延長できないのか、、残念だなぁ、、、

  • id:TransFreeBSD
    >Pythonでもできるとはわかるので、これを機に学ぶのもアリかもしれませんが。
    Python!Python!
    http://orangain.hatenablog.com/entry/scraping-in-python

    id:a-kuma3さんのはSAXっぽいですね。
    http://www.atmarkit.co.jp/ait/articles/0105/18/news003_2.html
    http://docs.python.jp/3/library/html.parser.html
    まぁ面倒臭いのでおすすめしませんけど
    #追加モジュール使うのがスタンダードだと思うので(てへぺろ
  • id:a-kuma3
    DOM には速度面で泣かされることも多かったので、両刀遣いです :-)
  • id:FREEz
    補足って再編集できないのか驚いた;;
    追加モジュールにした方が全然楽ですよね、全くその通りで。
    ただレンタルサーバ使うとできないんですよねぇ、、有料でも「そのモジュールが」非対応で。
    自鯖立てるかいつものPCに入れるかもできますが、他の面で苦労したりそもそもPC起動していないと動かせなかったり。
    1日1回CRONで手離れて全自動で完結しておいてくれるのがいいなぁ、と迷惑がかからない優しいスクレイピングを模索しています。
    WORDPRESSのモジュールとかでもなかなかいいのがあるのですが、手動でして、、、
    新たに何か作る点ではそういうのと使い分けて、収集で助けてくれるプログラムを結構前から模索して、煮詰まっていました。


  • id:FREEz
    うーん、このRubyの文からどうやって個別に取り出せるのだろう、、
    データベースに書き込むにあたり、とりあえずRubyからの操作方法を勉強しているけど、
    操作するにはとりあえず個別でも全体でも取り出せないと困るんだな、、
    ここまで一覧で見れるのはかなり助かったが、保存ができないと困るw
    うーん先に保存して後で勉強しようと思っていたが、道のりが長い、、

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

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

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

回答リクエストを送信したユーザーはいません