Chienomi

アメブロから更新時に一部を抽出して通知する

プログラミング

  • TOP
  • Old Archives
  • アメブロから更新時に一部を抽出して通知する

私はかなりの頻度でアメブロのとあるブログエントリの更新をチェックしているのだが、ほとんどの場合重要な情報はそのごく一部である。 しかし、アメブロにスマホでアクセスするたびに、くだらない広告や関心のないTV出演者の話に通信量が割かれ、時間も割かれることを大変に腹立たしく思っていた。

そこで、PCからページを取得し、スクレイピングする方針で考えたものである。 スクレイピングした内容はDiscordで通知する。

具体的な内容は差し障るので抽象化する。

コード

#!/usr/bin/zsh
if [[ -e ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry ]]
then
  mv ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry.prev
else
  touch ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry.prev
fi

w3m -dump "$URL" | sed -n -e '/ptn1/ p' -e '/ptn2from/,/ptn2to/ p' -e '/endmark/ q' > ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry

if ! diff -q ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry.prev
then
  curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "content" : "'"$(perl -pe 's/\r?\n/\\n/' ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry)"'" }' "$webhook_uri"
fi

解説

「アメブロから」という難しさ

ユーティリティスクリプトでは「問題自体を可能な限り引き下げ、ゆるく解決する」のが重要である。 開発コストを下げるべきなのだ。

amebloの内容を見ると、独特なエディタから生成されるものだけに、エディタ上での記述はちょっとした違いで大幅に異なるHTMLコンテンツが生成される。 私は投稿者とは連携していないので、ちょっとした記述のブレで抽出すべきコンテンツを見失う可能性がある。

生成されるタグと比べると見た目上の違いは少ない。 言い回しの違いや、スペースの有無程度である。

そこで、w3mを活用する。w3m-dumpオプションは、見た目に近い形でレンダリングした上でテキストにして出力する。 アメブロの執筆者はエディタ上の見た目で書いている可能性が高く、このようにレンダリングしてしまったほうがブレは抑えられる。

その中から情報を抽出する。 ある正規表現に一致する行、またはある正規表現の範囲をアドレスとして出力すれば多くの場合は十分だろう。

続くエントリや広告のゴミ値を拾ってしまわないように、エントリ終了時にはSedを終了させる。 多くの場合、/^AD$/で良いかと思うが、/いいね!した人.*リブログ/というパターンも考えられる。

例えばおやつ工房 ひびのやをdumpした結果からメニューを取得するとすれば、筆者はメニューの表記前に本日のメニューは、という行を置く習慣があるようで、またブログの締めとして本日もよろしくお願いいたしますを置いている。 これに基づけば

/本日のメニューは/,/本日ものよろしくお願いいたします/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

メニューそのものを抽出するならば、で囲むようにしているようなので、そこに限定しても良い。

/*.**/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

しかし、どうもこれは項目であるようだから、メニューとしては成立しないかもしれない。 項目終わりは空行を空けているようなので、この項目から空行まで抽出してみよう。 一応、スペースを入れていた場合にも対応しておく。

/*.**/, /^\s*$/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

しかし次のようなケースが発見された。

*天然酵母パン*
⚫︎食パン1斤→焼き上がりました
自家製カボス酵母、春よ恋使用。
乳製品不使用のシンプルなパンです。国産のお粉の素朴な風味をそのまま味わっていただけます。 

⚫︎レーズンとくるみのパン→焼き上がりました
自家製レモン酵母、春よ恋使用。

⚫︎カンパーニュ →焼き上がりました
自家製ラ・フランス酵母使用。
ライ麦粉と全粒粉を配合。

なので、項目開始だけでなく小項目開始も受け入れることにする。

/*.*\|^⚫/, /^\s*$/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

結果次のように抽出できた。

*マフィン*
プレーン⬇
ポップシュガーに戻しました。
きのこみたい

*シフォンケーキ*
プレーン小

*天然酵母パン*
⚫食パン1斤→焼き上がりました
自家製カボス酵母、春よ恋使用。
乳製品不使用のシンプルなパンです。国産のお粉の素朴な風味をそのまま味わっていた
だけます。 

⚫レーズンとくるみのパン→焼き上がりました
自家製レモン酵母、春よ恋使用。

⚫カンパーニュ →焼き上がりました
自家製ラ・フランス酵母使用。
ライ麦粉と全粒粉を配合。

日付も入れるならば、投稿時タイムスタンプの行を追加しよう。

/[0-9]\{4\}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/ p
/*.**\|^⚫/, /^\s*$/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

なお、サンプルとして「ameblo 本日のメニュー」で検索して最上位に出たおやつ工房 ひびのやさんを使わせていただいた。 埼玉県越谷市にあり、東京スカイツリーライン大袋駅、せんげん台駅が最寄りとなる、パンとお菓子のお店だそうである。11時から18時営業、金曜日休業。

更新されたかどうか

単純に考えれば取得したページをteeで保存して比較したいところだが、 広告などランダムに生成される部分があるため、これは失敗する。

amebloはDateヘッダを含み、Content-Lengthヘッダを含まないため1、ヘッダ部分だけ保存するという方法もある。 curl--dump-headerを使用し、内容は捨てるという方法が良いだろう。

curl -D ~/.cahce/someameblo/header 'https://ameblo.jp/hibino-ya/' > /dev/null

もうひとつはそもそも抽出した内容を比較するという方法がある。 リクエスト回数が減るため合理的だ。

いずれにせよ、前回取得分をローテーションして、今回取得分と比較する。

diff -q ~/.cache/someameblo/header ~/.cache/someameblo/header.prev > /dev/null

通知する

今回の場合、Discordのwebhookを使用している。

curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "content" : "'"$(perl -pe 's/\r?\n/\\n/' ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry)"'" }' "$webhook_uri"

Twitterという手もある。ここではtwを使用する。 セルフDMの送信自体は可能だが、Twitter webや公式アプリでは確認/通知はなくなっている。

tw --yes dm:to=account "$(perl -pe 's/\r?\n/\\n/' ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry)"

LINE Notifyという方法もある。

curl -X POST -H "Authorization: Bearer $TOKEN" -F 'message='"$(perl -pe 's/\r?\n/\\n/' ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry | nkf -WwMQ | tr = %)" https://notify-api.line.me/api/notify

メールが配送できる環境からであればMMS宛というのも悪くない

perl -pe 's/\r?\n/\\n/' ${XDG_CACHE_HOME:-$HOME/.cache}/someameblo/entry | mailx -r "notification@localhost.localdomain" -s "Hooked notification" mms@example.com 

ジョブスケジューラによる定期実行

スクリプトを書いても手動実行するというのはちょっとださい。 ジョブスケジューラによって自動的に実行してほしいところだ。

anacronあたりを使うのが妥当なのだろうけれど、私はSystemd Timer (User)を使用することにした。

まずはserviceユニットを書く。 ここでは~/.config/systemd/someameblo.serviceを作成している。

[Unit]
Description=Get ameblo entry.

[Service]
Type=simple
ExecStart=/bin/zsh "/home/aki/opt/someameblo/getamebloentry.zsh"

[Install]
WantedBy=default.target

次に対応したtimerユニットを書く。 ここでは起動から10分で初回実行し、それ以降4時間ごとに起動している。

[Unit]
Description=Get ameblo entry.

[Timer]
OnBootSec=10min
OnUnitActiveSec=4hour
Unit=someameblo.service

[Install]
WantedBy=timers.target

ユーザーユニットをリロードする。

systemctl --user daemon-reload

タイマーを起動&有効化

systemctl --user start someameblo.timer
systemctl --user enable someameblo.timer

「ゆるい解決」について

このようなユーティリティは基本的に人間を補助するものである。 オートメーションに属するものの、このユーテリティが人間に無断で何かを決断するわけではない。 また、人間にとってこのユーテリティによって与えられる唯一絶対の情報というわけでもない。

できるだけ精度が高ければ嬉しいけれども、もしも例外的ケース(例えば著者が普段とはまるで違う書き方をした場合など)においてうまく機能しなかったとしたならば、それは単に普通にブログを見に行けばいいだけである。 例外的ケースにおいて問題が発生しないように設計し、成功しない可能性については許容する。

同様に情報が常に完璧なフォーマットであることを求める必要もない。 ソースが完璧なフォーマットで書かれない可能性は高いので、完璧を求めることは不毛である可能性が高い。 これもまた、不十分な情報となったときには自らブログを見に行く選択肢もあるのだ。

ただし、十分に機能させるためには、抽出条件はfalse negative(取りこぼしがある)よりはfalse positive(ゴミがはいる)ほうが好ましい。 ゴミがあっても無視すればいいが、取りこぼしがあると必要な情報が損なわれる可能性があるためだ。

プログラミングはプログラムを書くことよりも、どのようにアプローチするかということのほうが重要な場合は少なくない。 プログラミング初級者のうちは設計をないがしろにしがちだが、プログラムの出来や困難性はだいたい設計によって決まる。 今回の主題も、正攻法ではかなり難しい条件を、考え方を変えることで簡単な問題に変換しているということだ。


  1. あるいはcurlがContent-Lengthを含まないのかもしれない↩︎