Home > Latest topics

Latest topics > シェルスクリプト(Bash)で作るTwitter bot

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

シェルスクリプト(Bash)で作るTwitter bot - Dec 05, 2016

このエントリはチャットボット Advent Calendarとのクロスポストです。(→Qiitaの方の投稿

チャットボット Advent Calendarをご覧になっているような方々はフレームワークやニューラルネットでディープラーニングなAIといった最新のbot事情に関心の強い方が多いと思うのですが、今日の記事は時代に逆行しまくって、Bashスクリプトで昔ながらの人工無脳botを作りましたというお話です。

この記事では以下のことを書いています。

  • Bashスクリプト製Twitter bot tweetbot.shの使い方説明
  • tweetbot.shの実装の解説
    • マルチプロセス設計
    • 親プロセスが停止させられたら子孫プロセスまでまとめて停止する
    • 一定間隔でREST APIを呼ぶ(ポーリング)
    • 独り言や返事の発言内容の選択
    • Twitter以外の他のサービスへの応用

開発の動機

実際の所、Bash製のTwitter botにも先行実装はいくつかあります(そもそもtweetbot.shの核であるBashスクリプト製Twitterクライアント自体もUSP友の会のTwitter bot用スクリプトの改造に端を発しています)。じゃあなんで自作したのかという話なんですが、端的に言うと技術力のデモです。

というのも、自分が執筆しているシス管系女子はLinuxのコマンド操作やシェルスクリプトの事をマンガ形式で解説しているのですが、「ロクに技術的な知識も無い絵描きが適当なマンガ描きちらかしてんだろwwwww」と思われてはシャクだったので、「Twitter botをスクラッチで書けるくらいの技術力はあるぞゴラァ!!」と示しておきたかったのです。

……というのは半分くらい冗談です。元々、Twitter上で最新情報を時間を変えてツイートしたり言及を拾ったりという地道な広報活動をbotに任せられれば原稿制作の作業に集中できるかな?と思ったのがきっかけだったのですが、既製のbotを使えばいいのにを何をトチ狂ったのか「どうせやるならbotも自作した方が面白いんじゃね?」などと考えてしまい、軽い気持ちで始めたというのが正味の話なのでした。

とはいえ作って放置というわけではなく、実際にここ1年ほどTwitterの宣伝用アカウントの運用に活用しています。 (botで運用中のアカウントのプロフィールのスクリーンショット) 時々動作が怪しくなったりもしますが、概ね安定して動作しているのでそこそこ実用的と言えるのではないでしょうか。

できること

このbotはこんなことができます。

  • キーワードでの検索結果に反応する。
  • メンション、リプライなどに返事をする。
  • フォローされたらフォローし返す。
  • DMでリモート制御する。

Twitterという公開の場に置く以上、イタズラで変な言葉を覚えさせられると困るので、いわゆる学習は行いません(単に、筆者に機械学習とその結果を活用するだけの知識が無いからという理由もありますが……)。

つかいかた

インストール

tweetbot.shは以下のコマンドに依存しています。

  • curl
  • jq
  • nkf
  • openssl
  • git(インストールに使用)

まずaptyumなどで各々のパッケージをインストールし、これらのコマンドを使える状態にしておいてください。

次に、データ類を置くためのディレクトリを用意します。ここでは仮に、~/tweetbot/を使うとしましょう。

$ mkdir ~/tweetbot
$ cd tweetbot
█

ディレクトリができたら、そこにtweetbot.shのリポジトリをcloneします。

$ git clone --recursive https://github.com/piroor/tweetbot.sh.git
█

--recursiveオプションを付けて再帰的にcloneするか、cloneした後でgit submodule update --initしてサブモジュールもすべてダウンロードしておいて下さい。

次は、認証用にapps.twitter.comでアプリケーションを作成します。Webサービスではないので、URLやコールバックは特に指定しなくても大丈夫です。アプリケーションには投稿のための書き込み権限を与えておいてください。また、DMを扱いたい場合はDMの読み書きの権限も必要です。

アプリケーションができたら、認証情報を定義します。以下の内容で~/tweetbot/tweet.client.keyの位置にファイルを作成し、作成したアプリケーションのコンシューマキーとシークレット、アクセストークンとシークレットを記入して下さい。

MY_SCREEN_NAME=投稿に使うアカウントのスクリーンネーム
MY_LANGUAGE=投稿に使うアカウントの言語
CONSUMER_KEY=xxxxxxxxxxxxxxxxxxx
CONSUMER_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ACCESS_TOKEN_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

例えば、スクリーンネームがexampleで言語が日本語なら以下のようになります。

MY_SCREEN_NAME=example
MY_LANGUAGE=ja
...

スクリーンネームと言語は、ストリーミングの受信やツイートの検索、取得したツイートの種類の判別のために使われます。これを忘れると、自分でやったRTを延々RTし続けるみたいなことになってしまうので注意して下さい。

認証情報のファイルを設置したら、今度はbotの挙動を定義するファイルを~/tweetbot/personality.txtの位置に作成します。これはsourceで読み込む前提のファイルで、Bashスクリプトの記法で環境変数として各種の設定を記述します。以下は基本の設定例です。

~/tweetbot/personality.txt:

# 管理者として扱うユーザ。
# これらのユーザからのDMはコマンドとして受け付ける。
ADMINISTRATORS="piro_or, sysadgirl_mint"
# 反応キーワード。
# これらのキーワードへの言及を検出したら反応する。
WATCH_KEYWORDS='sysadgirl, system-admin-girl, シス管系女子, #シス管系女子'

# 独り言の自動投稿の間隔。
MONOLOGUE_INTERVAL_MINUTES=90
# メンションに連続して返事をする最大の回数。
# 延々と同じ返事ばかりを返さないようにする安全装置。
MAX_MENTIONS_IN_PERIOD=10
# 連続して返事をする最大の回数の制限を反映する時間。
# この指定の場合、120分の間に最大で10回までしか返事をしないということ。
MENTION_LIMIT_PERIOD_MIN=120

# フォローする基準:フォローされたらフォローし返す、言及されたらフォローする(RTは除く)。
FOLLOW_ON_FOLLOWED=true
FOLLOW_ON_MENTIONED=true
FOLLOW_ON_QUOTED=true
FOLLOW_ON_RETWEETED=false

# ファボる基準:言及されたらファボる。
FAVORITE_MENTIONS=true
FAVORITE_QUOTATIONS=true
FAVORITE_SEARCH_RESULTS=true

# RT基準:メンション以外はRTする。
RETWEET_MENTIONS=false
RETWEET_QUOTATIONS=true
RETWEET_SEARCH_RESULTS=true

# 言及への言及:リプライかメンションには応答、それ以外は言及しない
RESPOND_TO_MENTIONS=true
RESPOND_TO_QUOTATIONS=false
RESPOND_TO_SEARCH_RESULTS=false

他にも色々と細かい設定が可能です。詳しくはREADMEをご覧下さい。

起動

設定ファイルを置いた~/tweetbotをカレントディレクトリにした状態で、~/tweetbot/tweetbot.sh/watch.shを実行して下さい。

$ cd ~/tweetbot
$ tweetbot.sh/watch.sh
█

これでスクリプトが起動し、その後はCtrl-Cなどで停止するまでスクリプトが動き続ける状態になります。

ただ、この方法だと自分がログアウトできません(botを動かしたままログアウトするには、screentmuxといったいわゆるターミナルマルチプレクサを使う必要があります)。また、サーバーを再起動したらbotは停止したままになってしまいます。init.dを使うなりsystemdを使うなりして、サービスとして自動起動するようにしておくと便利でしょう。

発言データの準備

これだけでもファボやRTはできるのですが、話しかけても全く応答しないbotになってしまうので、できれば応答や発言を用意しておきたい所です。

自動発言(独り言)のデータ

独り言用の発言は、~/tweetbot/monologuesディレクトリの下に置かれたテキストファイル(エンコーディングはUTF-8、改行コードはLF)の1行を1発言としてランダムに選択します。特に重複の除外処理は行っていないため、十分な数の発言データが無いと同じ発言ばかり繰り返してしまいますので、なるべくたくさん用意してあげましょう。例えばこんな感じです。

~/tweetbot/monologues/バレンタイン.txt:

# バレンタイン
# date: *.02.01-*.02.14

バレンタインってなんかワクワクしません?✨おいしいチョコがいっぱい食べれる季節~!😁
この時期、スーパーとかコンビニとか行くとバレンタインソングがすっごいかかってますよねー💦聞きすぎたせいで歌詞覚えちゃいましたよ……😖

見ての通り、Unicode絵文字も使えます。行頭に#がある行は発言にはならずコメントのように扱われますが、DMで発言を追加する際の目印として使う事ができます。

また、特定の範囲の日においてのみ投稿して欲しい独り言のデータも定義できます。日付の範囲はファイルごとに# date: YYYY.MM.DD-YYYY.MM.DDの書式で指定し、ワイルドカードも受け付けます。この例であれば、毎年2月の上旬にだけ流れる発言となります。

自動応答(返事)のデータ

メンションやリプライに対する応答の発言は、~/tweetbot/responsesディレクトリの下に置かれたテキストファイル(エンコーディングはUTF-8、改行コードはLF)の1行を1発言として選択します。このとき、どの発言ファイルを使うかは受け取ったメンションの本文に対するパターンマッチングで判断されます。

~/tweetbot/responses/おはよう.txt:

# good morning
# おはよう
# お早う
# ぐっど? *もーにん
# グッド? *モーニン

おはようございまーす!🙌
おはようございまーす!🙌 今日も一日がんばりましょう🎵

#で始まる行はコメントのように扱われますが、同時に正規表現によるマッチングルールとしても解釈されます。いずれかのルールにマッチすると、そのファイルの中に書かれた発言の中の1つが返事として投稿されます。

マッチングルールだけを定義して発言を定義しない場合、それらのルールにマッチしたメンションには無反応になります。いわゆるNGワードとして使えます。

~/tweetbot/responses/NG.txt:

# 嫌い
# 黙れ

返事の定義ファイルは名前順にマッチングが試行されるため、NGワードを定義するファイルは000NG.txtのように名前順で先頭に来るようにしておくと良いでしょう。

メンションの本文がいずれのマッチングルールにもマッチしなかった場合、

  • _pong.txt(相槌)
  • _connectors.txt(接続)
  • _developments.txt(会話の継続)
  • _topics.txt(新しい話題)

の4つの特別な返事定義ファイルの内容に基づいて、それっぽい応答がランダムに生成されて返されます。これは、以下のような内容で用意します。

~/tweetbot/responses/_pong.txt:

# 相槌

おおっ!
へえー!
ふ~ん!

~/tweetbot/responses/_connectors.txt:

# 接続

そういえば
ところで
それはそうと

~/tweetbot/responses/_developments.txt:

# 会話の継続

いいですね!
すごい!

~/tweetbot/responses/_topics.txt:

# 新しい話題

何かオススメの本ってあります??📖
今日は何をしてるんですか?😃

返事の元になったメンションの内容は考慮されないので、空気を読まない・人の話を聞かない・適当に相槌を打つ、という相当アホの子な返事しか返せません。まあ会話してるっぽい雰囲気だけの古式ゆかしい人工無脳ということで、あしからずご了承ください。

発言ファイルには他にもいくつか機能があります。詳しくはREADMEをご覧下さい。

DMでのリモート制御

設定ファイルで管理者として登録したアカウントからのDMは、リモート操作用のコマンドとして解釈され、コマンドの実行結果がDMの返事として返却されます。以下は代表的なコマンドです。

  • echo 任意のテキスト: 指定されたテキストをそのままDMで返却します。死活確認に使えます。
  • +res キー 返事の内容: 返事のパターンをキー.txtのファイルに追加します。その名前のファイルがない場合は、# キーというコメントがあるファイルに追加します。どちらの場合でもファイルが見つからない場合、新しくファイルを作成します。最初の+-にすると、返事のパターンが削除されます。
  • +キー 独り言の内容: 独り言のパターンをキー.txtのファイルに追加します。その名前のファイルがない場合は、# キーというコメントがあるファイルに追加します。どちらの場合でもファイルが見つからない場合、新しくファイルを作成します。最初の+-にすると、独り言のパターンが削除されます。
  • del ツイートのID: IDで指定したツイートを削除します。

各コマンドの詳細やこれら以外のコマンドについては、READMEをご覧下さい。

……というのがtweetbot.shの大まかな使い方です。

tweetbot.shの実装の解説

では、ここからは実装の解説に入ります。

ただ、全部を解説するとキリが無いので、長時間動き続けるBashスクリプトという形でbotを作る時のキモになりそうな部分だけ解説することにします。

マルチプロセス設計

TwitterのストリーミングAPIのうちUser streamsは完全性が保証されておらず、見落としが発生することが度々あります。なので見落としを防ぐために普通のツイート検索も並行して実行しておきたくなるのですが、普通にやるとストリーミングAPIのレスポンスを待ち受けている間は他のことができません。

また、複雑な処理や外部(Twitter API)と通信する処理は、予期せぬエラーが起こって全体が停止してしまうリスクがあります。

これらの問題を回避するために、tweetbot.shはwatch.shを実行するプロセスをマスタープロセスとして、その配下にストリーミングの待ち受け用やポーリング用などのサブプロセスを従える設計としています。これにより、複数の処理を並行して実行できる上に、サブプロセス側でエラーが発生してプロセスが終了してしまっても、マスタープロセスが生きている間は自動的に処理を復帰できるようになっています。

これは、以下の要領で実装しています。

watch.sh:

#!/usr/bin/env bash

watch_mentions() {
  while true
  do
    # メンション待ち受け処理。正常に動いていればこの行で処理がブロックするため、この先に進むことはない。
    # 異常終了した場合、10秒待ってからまた同じ処理を実行し直す。
    sleep 10
  done
}
watch_mentions &

periodical_search() {
  # 検索APIのポーリング用の処理。
}
periodical_search &

...

wait

通常、関数やコマンドを実行すると実行が終わるまで待ってから次の行に処理が進みますが、コマンド(関数)名の後に&を付けると、子プロセスが作られて関数が非同期に実行されるようになります。この仕組みを使って、並行して動かしたいそれぞれの処理を関数にした上で子プロセスで実行しています。

最後の行にwaitと書いておくのがポイントで、これを書かないと、子プロセスがまだ動いているのにマスタープロセスの方だけ先に最終行に到達してそのまま終了してしまいます。waitを実行すると、子プロセスがすべて終了するまでマスタープロセスの処理がそこで一時停止します。

親プロセスが停止させられたら子孫プロセスまでまとめて停止する

マルチプロセス化とセットでやっておきたいのが、親プロセスの終了と同時に子孫プロセスを自動的に終了させるという事です。これを忘れると、マスタープロセスをCtrl-Cで終了させた後も子孫プロセスが動き続けてしまいます。

これを防ぐには、マスタープロセスが停止した時(正確には、停止要求のシグナルを受け取った時)の処理をtrapコマンドで定義してやります。具体的には以下のようにしています。

watch.sh:

...

kill_descendants() {
  local target_pid=$1
  local children=$(ps --no-heading --ppid $target_pid -o pid)
  for child in $children
  do
    kill_descendants $child
  done
  if [ $target_pid != $$ ]
  then
    kill $target_pid 2>&1 > /dev/null
  fi
}
self_pid=$$
trap 'kill_descendants $self_pid; clear_all_lock; exit 0' HUP INT QUIT KILL TERM

...

コールバックに指定する関数kill_descendantsは「自分の直接の子プロセスを強制停止する処理」を再帰的に実行するようになっていて、これによってマスタープロセスの終了時には末端の子孫プロセスから順番に終了されていきます(プロセスツリーの末端から終了するのは、祖先側から終了すると、万が一親プロセスだけ終了してしまったという場合に子孫プロセスがトラッキング不能な形で取り残されてしまうと思ったのでこうしてみました)。

一定間隔でREST APIを呼ぶ(ポーリング)

前述したようにストリーミングAPIの監視だけでは監視漏れが発生するため、tweetbot.shではそれとは別にツイートの検索やDMの取得のためのポーリング(定期的なAPI呼び出し)も行っています。

ちなみに、ツイート検索専用のストリーミングAPIという物もあり、tweet.shもそれに対応しているのですが、同一IPから複数のストリーミングAPIを同時に使用することは禁止されているため、tweetbot.shでは通常のREST APIを使ってポーリングしています。

watch.sh:

periodical_search() {
  ...
  local last_id_file="$status_dir/last_search_result"
  local last_id=''
  [ -f "$last_id_file" ] && last_id="$(cat "$last_id_file")"
  ...

  while true
  do
    debug "Processing results of REST search API (newer than $last_id)..."
    while read -r tweet
    do
      [ "$tweet" = '' ] && continue
      id="$(echo "$tweet" | jq -r .id_str)"
      ...
      if [ $id -gt $last_id ]
      then
        last_id="$id"
        echo "$last_id" > "$last_id_file"
      fi
      ...
      # 検索結果として得たツイートをここで処理する。
      ...
    done < <("$tools_dir/tweet.sh/tweet.sh" search \
                -q "$query" \
                -c "$count" \
                -s "$last_id" |
                jq -c '.statuses[]' |
                tac)

    if [ "$last_id" != '' ]
    then
      # increment "since id" to bypass cached search results
      last_id="$(($last_id + 1))"
      echo "$last_id" > "$last_id_file"
    fi
    sleep 3m
  done
}

...

if [ "$query" != '' ]
then
  log "Tracking search results with the query \"$query\"..."
  periodical_search &
  periodical_process_queue &
else
  log "No search queriy."
fi

この時大事なのは、前回取得した結果よりも新しい結果だけを取得するようにリクエストするということです。さもないと、同じツイートや同じDMを何度も処理してしまうことになります。

ここではwhileループを二重にしていて、外側が「前回の検索終了時から3分待って次の検索を行う」というポーリングのためのループ、内側が「検索結果として得られたツイート1つ1つを処理する」ためのループです。検索結果の中で最も新しいツイート(=次の検索を行う際に「これより新しい結果のみを返す」という条件に使用するツイート)のIDを保持する変数last_idの値を外側のループと共有したかったので、内側はプロセス置換を使ったwhileループとしています。

tweet.shのsearchコマンドはREST APIで得た検索結果を出力しますが、検索結果は新しいツイートの方が先に返ってきます。しかしストリーミングAPIの補完として使うには古いツイートから処理したいので、そのJSON文字列からjq -c '.statuses[]'でツイートの配列を取り出してtacで逆順に並べ替えています。その出力結果を<(コマンド列)という書き方(Bashのプロセス置換機能)で擬似的なファイルとして扱って、リダイレクトの<で内側のwhileの標準入力に流し込んでいます。これにより、内側のwhileループが外側のwhileループと同じプロセスで実行され、変数last_idの値が共有されるようになります。

内側のwhileループ内で一時ファイルに値を書き出して、それを外側のwhileループで読み込む、という風にすればべつにプロセス置換を使うまでもないのですが、既に値を保持した変数があるのにそれを使えないのは何となく気持ち悪かったので、このようにしました。

内側のループが終了したら、次の検索で「これより新しい結果のみを返す」という条件に使用するツイートのIDをlast_id="$(($last_id + 1))"でインクリメントしています。これは、検索結果が1件も見つからなかった場合にlast_idが変化しないと、次の検索リクエストの内容が前回と同じになってしまってキャッシュされたレスポンスが返ってきてしまう可能性があるためです。

独り言や返事の発言内容の選択

発言を種類毎に管理できるよう、発言のデータは細かくファイルを分けられるようになっていますし、コメント行でマッチングルールの定義や投稿可能日時の範囲の指定もできるようになっています。しかし、実際に発言をする時になってこれらを一気に適切に取り扱うのは結構大変です。

そこで、tweetbot.shではwatch.shの起動時(および、DMのコマンドで動的に発言データが編集されたとき)に発言データファイルを全スキャンし、発言の選択ロジックそのものをシェルスクリプトとして組み立てて、以後はそれを使うようにしています。具体的には、独り言用はgenerate_monologue_selector.sh~/tweetbot/monologue_selector.shというファイルを生成し、返事用はgenerate_responder.sh~/tweetbot/responder.shというファイルを生成します。

自動生成された~/tweetbot/responder.shは、例えば以下のような内容になります。

~/tweetbot/responder.sh:

...

if echo "$input" | egrep -i "good morning|おはよう|お早う|ぐっど? *もーにん|グッド? *モーニン" > /dev/null
then
  [ "$DEBUG" != '' ] && echo "Matched to \"good morning|おはよう|お早う|ぐっど? *もーにん|グッド? *モーニン\", from \"$base_dir/./responses/おはよう.txt\"" 1>&2
  extract_response "$base_dir/./responses/おはよう.txt"
  exit $?
fi

if echo "$input" | egrep -i "^(hello|hey|yo|hi)[ \.,!]|good afternoon|こんに?ちは|ハロー|はろー|ヘイ|やあ|よ[うお]" > /dev/null
then
  [ "$DEBUG" != '' ] && echo "Matched to \"^(hello|hey|yo|hi)[ \.,!]|good afternoon|こんに?ちは|ハロー|はろー|ヘイ|やあ|よ[うお]\", from \"$base_dir/./responses/こんにちは.txt\"" 1>&2
  extract_response "$base_dir/./responses/こんにちは.txt"
  exit $?
fi

...

標準入力で与えられたメンションの本文に対してどのようにマッチングを行いどのファイルから発言を抽出するのか、スクリプトを見れば一目で分かりますし、動作試験も以下のようにスクリプトを実行するだけなので簡単です。常駐プロセスの側をいちいち止めたり再起動したりする必要はありません。

$ cd ~/tweetbot
$ echo "こんにちは" | ./responder.sh
こんにちは~!
$ █

また、今回は実装を見送りましたが、話しかけられた内容を学習したり文脈に応じて会話をしたりといった高度な応答に対応したくなった場合でも、この設計であればresponder.shmonologue_selector.shを差し替えるだけで済むはずです。

Twitter以外の他のサービスへの応用

tweetbot.shはTwitter用botですが、入出力の部分を入れ替えれば、同様のやり方で他の様々なサービス用のbotを作る事もできます。

いずれの場合も、ここで解説したような常駐型スクリプトを作る時の技術が役に立つでしょう。

シェルスクリプトの良い所は、どんなコマンドラインツールも容易に部品として組み込めるという点です。自分の得意な言語用にライブラリが提供されていない場合でも、コマンドラインツールがあればシェルスクリプトで利用できます。また、自分が何かツールを作る時も、コマンドラインツールとして標準入力・標準出力でデータをやりとりできるように設計しておけば、それもまた部品として再利用できます。

様々なコマンドを連携させるグルー(糊)としてシェルスクリプトを活用し、皆さんも普段のちょっとした不満や面倒を解消してみて下さい! (サムアップするみんとちゃん)

宣伝:「シス管系女子」について

最後にまたまたシス管系女子の宣伝です。Qiitaではなくこちらを読まれている方はもう飽き飽きしてますよね……

「シス管系女子」は2011年から日経Linux誌上で連載させて頂いているケーススタディ形式の解説マンガ記事です。

本稿は普通のプログラミングげな事をBashでガッツリやる事例ですが、連載の内容は全くカラーが違っていて、「コマンド操作怖い……」レベルの人が自力でコマンド列を組み立てられるようになるくらいを目指してコマンドやオプションの動作を絵解きで説明する、いたって平易な入門記事となっています。

コマンド一覧を丸暗記しようとして挫折してしまった人、先輩にGUI禁止令を出されて途方に暮れてしまった新人さん、サーバーのトラブルでSSH越しに操作しないといけないのにお手上げになってしまったGUI派の人など、コマンド操作の勘所が分からずお悩みのすべての方にオススメの内容です!

現在は、連載に加筆修正した単行本が以下の2冊リリース済みです。

それ以外にも、Twitterのみんとちゃんbotアカウントイラストや本編に入りきらなかった小ネタを流したり、Webサイトの方にも連載や本では扱わなかったもっと基礎的な話の特別編を置いていたりします。あと、「シス管系女子」をテーマにしたAdvent Calendarも公開中です。

ということで、最後は宣伝で〆てしまいましたがチャットボットAdvent Calendarの5日目でした。6日目はkashira2339さんによる、会社でリアルに良い反響のチャットボットの機能(hubot + GitHub webhook)のお話です。お楽しみに!

分類:Web技術, , , , , 時刻:00:23 | Comments/Trackbacks (0) | Edit

Comments/Trackbacks

TrackBack ping me at


の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2016-12-05_twitter-bot.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2020年11月30日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき