PerlのWeb

今更ながらにPerlWeb::Scraperを触っていてなにか目的意識がほしいと言うことでTwitterのボットを作ってみました。

ネタとしては「福岡市の新型インフルエンザに関する情報をポストするボット」と言うことで、http://www.city.fukuoka.lg.jp/pflu/から「福岡市内における新型インフルエンザ患者の発生状況」を引っ張ってきてポストすると言う単純なものです。

@fukuoka_influ

とはいっても、http://plagger.g.hatena.nh.jp/noboruhi/20070620/1182346881で公開されていたものをほぼコピー。変えたのはスクレイピングする対象の部分とFeedのEntryを入れなおすところくらい。特段といってはなんですが、あまり考えずに作っています。上手く設定できていれば1時間に一回チェックをかけに行くはずです。

いくつかの反省点

実行権限

スクレイピングするために、Web::Scraperを使ったプログラムを書いたわけですが、実行権限をつけるのを忘れてしばらくはまってました。

てっきりモジュールみたいに呼び出すのかと思っていましたが、実行結果を受け取るような感じなのですね。確かにモジュールの書き方じゃないのでつまるところではないはずなんですが、まぁご愛嬌といったところ。

入れ子構造の取得

今回対象にしたWebページの構成が以下のような感じで面倒くさかった。本当は内容のspanタグのところもWeb::Scraperを入れ子にして対応すればよかったんでしょうけど、いまいち動ききらなかったのでtdまるごと引っ張ってきて、その後に正規表現で分割しています。ちょっとかっこ悪い。

-trタグ
--td: 日付
--td:
---span: 内容1
---span: 内容2
--td: 日付
--td:
---span: 内容1
---span: 内容2
---span: 内容3

日付のフォーマット

対象のページから何日付の情報かという部分を抜き出してきたわけですが、残念なことに年に関する情報がありませんでした。で、手抜きということで直接2009と書いてしまっています。来年までこのプログラムが動き続けていたら、破綻します。2010年問題です。

さっさと実行した年を入れればいいんですが、面倒くさいので放置しました。放置した結果この記事のこの段が増えてしまっているので、もっと面倒くさいことになったわけです。この段を書いている間に書けば良いのにと思います。

気になるところ

怒られないかなぁ

今回は勝手にスクレイピングしてTwitterに転載しているわけです
一応行政がやっていることなので怒られたりしないとは思うのですが、大丈夫なんでしょうかね。多分大丈夫だと思います。それ以前に気づかれることもなく役目を終えることになると思いますし。

このページのHTML手書きだよなぁ

拙い見識からすると多分このページはHTML手書き、もしくはかなり手作業で作っているよなぁっと思っています。作っている中の人にはお疲れ様といいたいところですが、いつHTMLの構造が変わるか分からないのは怖いことです。

明日の更新で急にtableを使った構造から変わるかもしれません。
急に予算がおりて、どこかの業者さんが本気を出したデザインになるかもしれません。そういうのを追尾していくのは非常に面倒くさいなぁっと思います。

せめてRSS

手書きで書いていると予想していて言うのは酷ですが、こういうタイムリーに更新するものについては、せめてRSSを提供していただければなぁっと思います。Twitterでやるのは費用対効果的にお勧めはしませんが、RSSくらいならバチはあたらないのではないかと。

まぁRSS自体も一般的に利用されているとは思いませんし、その辺は勘案のしどころです。そもそもRSSが使われていたら今回の「Web::Scraperを使う」という目的に合致しなくなるので、本末転倒です。RSSが提供されてなかったから今回作ったのです。

追記 更新される場所がよく分からない

思いついてよく調べずにさっさと作ってしまったのでよく分からないのですが、このページって多分時間ごとに情報を一気に登録しているんだよなぁっと思ったり。今の想定だと新しい情報が出た場合の掲載方法は以下の2パターンだと前提にしているけど、違った場合の重複チェックがすり抜けるなぁ。

-tr要素ごと増える
-td要素内のspan要素が下に追加される

ということで、ダミーでEntryのLinkを作る際にもう一手間かけて、抜き出した文章のMD5値を追加するようにしました。おそらくこれで大丈夫だと思う。はず

作ったもの

fukuokaInflu.pl

基本的には、http://plagger.g.hatena.nh.jp/noboruhi/20070620/1182346881で公開されていたものの焼き直し。

|perl|

!/usr/bin/perl -w

use strict;

use URI;

use HTTP::Cookies;
use LWP::UserAgent;
use HTTP::Request::Common qw (POST);
use YAML::Syck;
use Web::Scraper;
use utf8;
use DateTime::Format::Japanese;
use Encode;

更新情報の重複チェックのために、ハッシュ値を使うために呼び出す

use Digest::MD5 qw/md5_hex/;

my $uri = URI->new("http://www.city.fukuoka.lg.jp/pflu/index.html");

日毎に取り出す

my $s = scraper {
process 'table[class="marginL20"] tbody tr',
"list[]" => scraper {
process 'td span',
"date" => 'TEXT';
process 'td + td ',
"text" => 'HTML';
result 'date','text';
};
result 'list';
};

my $scr = $s->scrape($uri);

my $scr = $item->scrape($content);

my $feed = {
title => "福岡市の新型インフルエンザに関する情報",
link => $uri->as_string,
};

for my $menu (@{ $scr}) {
if ($menu->{date} =~ m/.*(\d+)月(\d+)日/ ) {
my $month = $1;
my $day = $2;
$month =~ tr/0-9/0-9/;
$day =~ tr/0-9/0-9/;

# 日付のフォーマットをあわせる  
#  2010年問題を内包  
my $date  = sprintf("%4d-%02d-%02d", 2009, $month, $day);

for my $entry ($menu->{text} =~ m|<span[^>]*>([^<]+)<|g) {  
  # 対象にしないエントリーを取り除く  
  #  空のspamと記者会見資料  
  next if( $entry =~ /^[\s\r\n]*$/ );  
  next if( $entry =~ /^記者会見資料/ );

  # 文字数を50文字までにに制限する  
  $entry =~ s/^(.{50}).+$/$1/;

  # module: Dedupedを使う際にLinkを見るような気がするので無理やり作る  
  #  ダミーのlinkをユニークにするために、抜き出したテキストを元にしたmd5値を埋め込む  
  #  URL + 日付 + md5値  
  push @{$feed->{entries}}, {  
    title => $entry,  
    link  => $uri->as_string . "#" . $date . "-" . md5_hex(encode_utf8($entry)),  
    body  => $date . " " . $entry . " " . $uri->as_string,  
    date  => $date,  
    #date  => $menu->{date},  
  };

}  

}
}

print Dump($feed);

||<

fukuokaInflu.yaml

|yaml|
include:
- /path/to/bash.yaml
# 上のbash.yamlの場所を指定

plugins:
- module: CustomFeed::Script
- module: Subscription::Config
config:
feed:
- script:/path/to/fukuokaInflu.pl

  • module: Aggregator::Simple

# 重複エントリを POST しない
- module: Filter::Rule
rule:
module: Deduped
path: /path/to/fukuoka_influ.log

# 新しい発言が上になるよう逆転させる
- module: Filter::Reverse

||<