Chapter 05無料公開

今日は何の日bot

本章では、一定間隔で「今日は何の日」かをツイートする bot を作ります。

今日が何の日かを得るには Wikipedia の Mediawiki API を利用するのが便利です。

https://ja.wikipedia.org/w/api.php?format=json&action=query&prop=revisions&rvprop=content&exintro&explaintext&redirects=1&titles=クリスマス

この titles に URL エンコードされた日付を埋め込めば良いですね。0埋めされていない日付を得るには date +%-m月%-d日 で得られます。ちなみに %m と書くと0埋めされます。

MediaWiki API は URL エンコードされていないクエリ文字列では動かないので、予め URL エンコードしておきます。

date +%-m%%E6%%9C%%88%-d%%E6%%97%%A5

% 自身は %% と書く必要があります。

URL="https://ja.wikipedia.org/w/api.php?format=json&action=query&prop=revisions&rvprop=content&exintro&explaintext&redirects=1&titles=$(date +%-m%%E6%%9C%%88%-d%%E6%%97%%A5)"

出来上がった URL を curl でアクセスし、jq で整形しながら、目的の項目を抽出していきます。

この時注意すべきは、何度も実行する事で Wikipedia に負荷を掛けない事です。負荷を掛けてしまう可能性があるのであれば、一旦 curl の出力をファイルにリダイレクトしておき、cat result.txt | jq . の様に実行すると良いでしょう。

出来上がったクエリ式は如何になります。

.query.pages[.query.pages|keys[0]].revisions[0]["*"]

オブジェクトのキー一覧 .query.pages|keys から最初のキー [0] を得て、その値を元に .query.pages を参照します。そこから .revisions の最初のアイテムを得てオブジェクトを * というキーでアイテム参照します。

jq に渡して得られるテキストは以下の様になっています。

欲しい部分は == 記念日・年中行事 == から == フィクションのできごと までの、先頭に * が付いいる行の様です。sed で切り取り、HTML コメントの様な部分を捨ててしまいましょう。

curl -fs "$URL" | jq -r "$QUERY" | sed -n '/== 記念日・年中行事 ==/,/== フィクションのできごと ==/p' | sed -z 's/<!--.*\?-->//g' | grep '^* .*の日'

Wikipedia 独特の記述方法なのですが

* [[フグ|河豚]](ふぐ)の日(日本)
* {{仮リンク|世界翻訳の日|en|International Translation Day}}
* [[独立記念日]]({{BWA}})
* 国有化の日({{STP}})<!-- enではAgricultural Reform Dayとなっているが、日本語訳が不明 -->

この様に、色々なパターンがあり得ます。[[ を使ったパターンでは読みと漢字が両方書かれていて、最後に (日本)({{JPN}}) が付いている物もあれば無い物もあります。うまく整形しましょう。順番は、記念日のテキストの範囲抽出、HTML コメントの削除、の日 だけの抽出、後ろのカッコ(())の削除、[[xxx|yyy]] の最後(yyy)だけを残す、{{xxx|yyyの日|zzz}}が書かれている箇所だけ残す、です。

curl -fs "$URL" | jq -r "$QUERY" | sed -n '/== 記念日・年中行事 ==/,/== フィクションのできごと ==/p' | sed -z 's/<!--.*\?-->//g' | grep '^* .*の日' | while read -r LINE; do
  echo "$LINE" |\
      sed -e 's/^\(.*\)([^)]\+)\?$/\1/' \
       -e's/\[\[\([^]|]\+|\)*\([^]]\+\)\]\]/\2/g' \
       -e 's/\({[^日|]\+\|[^日]\+}\||[^日]\+|\)//g' \
       -e 's/[{}|]//g'
done

既にシェルスクリプトのみでやる領域を超えてきています。本来ならば正しいプログラミング言語を選んで正しくフィルタリングすべきだと思います。

筆者はこういったフィルタリングに awk を使わず sed を使っていますが、決して awk が嫌いなわけでも awk が使えない訳でもありません。単なる趣味です。

sed -n '/== 記念日・年中行事 ==/,/== フィクションのできごと ==/p' について解説します。これは検索パターンでレンジを作り p コマンドを実行する事で範囲のみ抽出する様にしています。

sed -z 's/<!--.*\?-->//g' について解説します。通常 sed は行で処理される為、この sed の置換は単一行でしかマッチしません。

ごみ
<!--
はじめ
ほんぶん
おわり
-->
ごみ

この様なテキストの <!-- から --> を削除する為には -z フラグを付けて実行します。

なお -z は GNU 拡張の為、幾らかの環境では使えません。

あとはこれをツイートする様に仕立て上げます。

#!/bin/bash

set -e

URL="https://ja.wikipedia.org/w/api.php?format=json&action=query&prop=revisions&rvprop=content&exintro&explaintext&redirects=1&titles=$(date +%-m%%E6%%9C%%88%-d%%E6%%97%%A5)"
QUERY='.query.pages[.query.pages|keys[0]].revisions[0]["*"]'


RESULT=$(
curl -fs "$URL" | jq -r "$QUERY" | sed -n '/== 記念日・年中行事 ==/,/== フィクションのできごと ==/p' | sed ':l; N; s/<!--.*-->//; b l;' | grep '^* .*の日' | while read -r LINE; do
  echo "$LINE" |\
      sed -e 's/^\(.*\)([^)]\+)\?$/\1/' \
       -e's/\[\[\([^]|]\+|\)*\([^]]\+\)\]\]/\2/g' \
       -e 's/\({[^日|]\+\|[^日]\+}\||[^日]\+|\)//g' \
       -e 's/[{}|]//g'
done
)
RESULT=$(echo -e "$(date +%-m月%-d日は)\n$RESULT")
twty "${RESULT:0:255}"

これで以下の様な内容がツイートされます。

9月29日は
* 発明家の日
* クリーニングの日
* 招き猫の日
* 洋菓子の日
* 接着の日

これを1日1回実行するには crontab で以下の様に設定します。

0 0 * * * /home/mattn/bin/today.sh

簡単ですね。