• 14
    いいね
  • 0
    コメント

チャットボット Advent Calendarをご覧の皆様、初めまして。Piroといいます。普段はJavaScriptやRubyでのソフトウェア開発やFirefoxの法人向け技術サポートなどをしつつ、日経Linux誌シス管系女子という連載マンガを描かせて頂いております。

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

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

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

開発の動機

実際の所、Bash製のTwitter botにも先行実装はいくつかあります1。じゃあなんで自作したのかという話なんですが、端的に言うと技術力のデモです。

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

とはいえ作って放置というわけではなく、実際にここ1年ほどTwitterの宣伝用アカウントの運用に活用しています。
mintbot.png
時々動作が怪しくなったりもしますが、概ね安定して動作しているのでそこそこ実用的と言えるのではないでしょうか。

できること

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

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

Twitterという公開の場に置く以上、イタズラで変な言葉を覚えさせられると困るので、いわゆる学習は行いません3

つかいかた

インストール

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などで停止するまでスクリプトが動き続ける状態になります。

ただ、この方法だと自分がログアウトできません4。また、サーバーを再起動したら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は「自分の直接の子プロセスを強制停止する処理」を再帰的に実行するようになっていて、これによってマスタープロセスの終了時には末端の子孫プロセスから順番に終了されていきます5

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

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

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の値が共有されるようになります7

内側のループが終了したら、次の検索で「これより新しい結果のみを返す」という条件に使用するツイートの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を作る事もできます。

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

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

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

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

最後にシス管系女子の事も改めてご紹介します。

自分は2011年から日経Linux誌上で「シス管系女子」というケーススタディ形式の解説マンガ記事を連載させて頂いています。この記事でやっているような「Bashでガッツリプログラミング」という物ではなく、「コマンド操作怖い……」レベルの人が自力でコマンド列を組み立てられるようになるくらいを目指した内容です。

ブログ等で紹介されているコマンド列をコピペ実行はできても、そのコマンド列をゼロから自分で組み立てられないのでは、ちょっと前提が変わっただけでお手上げになってしまいます。また、理解が怪しい状態では、Webサービスの開発や運用でサーバーにSSHでログインしてコマンドで操作しないといけないという場面でも、何が起こるかよく分からなくて物怖じしてしまうのは致し方ないでしょう(筆者もそうでした)。

なので「シス管系女子」では、「個々のパーツのはたらきや仕組みをちゃんと理解して、必要に応じて手順や要素などのパーツを入れ換えたり調整したりといった応用ができる」というレベルになる事を意識して、コマンドやオプションの動作を絵解きで説明することを心がけています。上記のような不安を持っていた方から「実際に参考になった」という感想を頂くこともあり、また、上司や先輩の方が新人や後輩のための1冊としてご紹介して下さっている事例も結構あると伺っています。

コマンド一覧を丸暗記しようとして挫折してしまった人、先輩にGUI禁止令を出されて途方に暮れてしまった新人さん、サーバーのトラブルでSSH越しに操作しないといけないのにお手上げになってしまったGUI派の人など、コマンド操作の勘所が分からずお悩みのすべての方にぜひ見て頂きたい内容なので、もし良かったら、お近くの大きめの書店でシス管系女子の本や掲載誌を手に取ってチラ見して頂けましたら幸いです。

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

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


  1. そもそもtweetbot.shの核であるBashスクリプト製Twitterクライアント自体もUSP友の会のTwitter bot用スクリプトの改造に端を発しています 

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

  3. 単に、筆者に機械学習とその結果を活用するだけの知識が無いからという理由もありますが…… 

  4. botを動かしたままログアウトするには、screentmuxといったいわゆるターミナルマルチプレクサを使う必要があります。 

  5. プロセスツリーの末端から終了するのは、祖先側から終了すると、万が一親プロセスだけ終了してしまったという場合に子孫プロセスがトラッキング不能な形で取り残されてしまうと思ったからです。 

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

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

  8. これまでのイラスト一覧