RaspberryPiでサーボモーターを動作させてみる

フェリーハッカソン2019(以降ハッカソン)にてスマートロックっぽいものを作ったので、メモっておきます。

基本的にはサーボモーターをRaspberryPiで駆動させて鍵っぽいものを作る形になります。 ハッカソンではトリガーとなる部分はフロントエンド側の担当の方におまかせしたので、 このエントリーではHTTP経由でアクセスされたらCGIpythonのプログラムを動作させてサーボモータを駆動する形としています。 トリガーになる動作がないので部分の処理がないのですが、その当たりはすいませんです。

f:id:ueponx:20190130165017j:plain

サーボモーターを駆動するのは、検索すると結構いっぱいヒットしますが、自分は以下の方の情報を見ながら作業しました。 ありがとうございます。

【参考】 bufferoverruns.blogspot.com

サーボモーターとしてしようしたのはSG90というモーターになります。

eleshop.jp

割と電子工作界隈では有名なサーボモーターのようです。自分は今回はじめてサーボモーターを使用したので、知りませんでした。 モーター駆動はドライバーICなどが必要になると思っていたので躊躇していたのですが、このSG90はGPIO経由(制御3.3V)でも問題なく動作します。 ということで、参考にした情報をもとに動かしてみます。

GPIOのピンからPWM(Pulse Width Modulation)という信号制御を使用して、特定のパルス幅の信号をモーターに与えて制御を行います。 パルスのHI区間とLOW区間の時間の比率であるDuty Cycleを使っているそうです。 今回はGPIOの4pin目を使用してサーボモーターを制御し、17pin目でLEDの点灯制御を行っています。 RaspberryPiのGPIOをpythonから使用するRPiGPIOモジュールを使用します。(多分、Raspbianにはデフォルトでインストールされているかなと思います)

SG90とRaspberryPiの接続は以下のようになります。

  • オレンジ線:PWM信号
  • 赤線:電源
  • ブラウン線:GND

【open.py】

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)

gp_out = 4
gp_led = 17

GPIO.setup(gp_out, GPIO.OUT)
GPIO.setup(gp_led, GPIO.OUT)

GPIO.output(gp_led,GPIO.HIGH)

servo = GPIO.PWM(gp_out, 50) 

servo.start(0.0)

servo.ChangeDutyCycle(2.5)
time.sleep(0.5)

servo.stop()

GPIO.output(gp_led, GPIO.LOW)
time.sleep(5.0)

GPIO.cleanup()

こんな感じで問題ないと思います。

ちょっとだけ説明すると

GPIO.setmode(GPIO.BCM)

ここでGPIOのpin指定のモードを設定します。BCMでGPIOのGPIOの番号で指定するモードになります。 他にもボードのピン番号で指定するモードもあります。

GPIO.setmode(GPIO.BCM)   # GPIO番号指定
GPIO.setmode(GPIO.BOARD)   # ボードピン番号指定

以下の部分でGPIOのピンを出力モードに設定しています。

GPIO.setup(gp_out, GPIO.OUT)
GPIO.setup(gp_led, GPIO.OUT)

これで事前設定は終わりです。 あとは、GPIOのピンにPWMの制御をかけることになります。

servo = GPIO.PWM(gp_out, 50) 

servo.start(0.0)

servo.ChangeDutyCycle(2.5)
time.sleep(0.5)

servo.stop()

GPIO.PWM()の設定のところで引数に50とあるのは50Hzを意味します。この周波数の信号にデューティーサイクルを設定して制御信号を出力します。 SG90は2.5から12.5の値のにデューティーサイクルを与えることで0度~180度の角度で回す事ができるようです。 今回はサーボモータが0度の状態を空いた状態とし、90度の状態を閉まった状態として扱うことにします。 サーボモータは駆動する時間が必要があるのでtime.sleep()でウエイトを与えています。

以上のような感じで制御ができます。

鍵を閉める場合にはサーボモータの角度を90度回転すればいいのでservo.ChangeDutyCycle(7.5)とすれば良い感じです。

【close.py】

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)

gp_out = 4
GPIO.setup(gp_out, GPIO.OUT)
servo = GPIO.PWM(gp_out, 50) 

servo.start(0.0)

servo.ChangeDutyCycle(7.5)
time.sleep(0.5)

servo.stop()
GPIO.cleanup()

これで開け締めはできるようになりました。これをHTTPトリガーで制御できれば良いことになります。

HTTPサーバーの制御

pythonにはHTTPのモジュールがあるのでそれを使用するだけでHTTPサーバを立てる事ができます。 以下のように実行すれば、簡易的なHTTPのサーバとしてすぐに使用することができます。

python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

デフォルトではポートが8000番になります。引数でポート番号を指定する事もできます。 また、オプションで--cgiを指定するとCGIを起動することもできます。CGIはデフォルトでは起動パスに存在するcgi-binというディレクトリに 存在するパスがそれに当たります。

【server.sh】

$ python3 -m http.server --cgi

あるディレクトリにserver.shという形でシェルを作った場合には以下のようにcgi-binディレクトリを作成し、 権限を755などにすればいいかと(実行するpythonで作られたCGIプログラムも実行権限とシバンを記載しておいてください)

ディレクトリ構造】

$ ls -l
合計 24
drwxr-xr-x 2 pi pi 4096  127 09:56 cgi-bin
-rw-r--r-- 1 pi pi  222  126 21:58 close.py
-rw-r--r-- 1 pi pi  343  127 09:00 open.py
-rw-r--r-- 1 pi pi 1240  129 08:27 readNFC.py
-rwxr-xr-x 1 pi pi   29  126 15:17 server.sh
-rw-r--r-- 1 pi pi  208  126 13:56 simpleServer.py

$ ls -l cgi-bin/
合計 8
-rwxr-xr-x 1 pi pi 435  127 09:56 servo_close.py
-rwxr-xr-x 1 pi pi 535  127 09:55 servo_open.py

今回は以下のような感じでCGIのプログラムを作成しています。 servo_open.pyにちょっと変わった部分がありますが、CGIでサーボを駆動させているので、ファイル(ファイル名はlockとしている)による動作のロックをかけています。 他にも方法はあるかなと思いますが、何らかの形でロックの機構をいれていないと制御が微妙になることもあるかと思いますので入れておいたほうが無難です。

ドアをOpenする処理は以下のようになります。

【servo_open.py】

#!/usr/bin/env python3

import os

print('Content-type: text/html; charset=UTF-8\r\n')
print('Open the Door!')

if os.path.exists("lock"):
    exit()

f = open('lock','w')
f.close()

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
gp_out = 4
gp_led = 17

GPIO.setup(gp_out, GPIO.OUT)
GPIO.setup(gp_led, GPIO.OUT, initial=GPIO.LOW)

servo = GPIO.PWM(gp_out, 50) 

servo.start(0.0)
servo.ChangeDutyCycle(2.5)
time.sleep(0.5)
servo.stop()

GPIO.output(gp_led, GPIO.HIGH)
# time.sleep(3.0)

# GPIO.cleanup()

os.remove('lock')

ドアをCloseするのもほぼ同様で記述できます。

【servo_close.py】

#!/usr/bin/env python3

import os

print('Content-type: text/html; charset=UTF-8\r\n')
print('Close the Door!')

if os.path.exists("lock"):
    exit()

f = open('lock','w')
f.close()

import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
gp_out = 4
gp_led = 17

GPIO.setup(gp_out, GPIO.OUT)
GPIO.setup(gp_led, GPIO.OUT, initial=GPIO.LOW)

servo = GPIO.PWM(gp_out, 50) 

servo.start(0.0)
servo.ChangeDutyCycle(7.5)
time.sleep(0.5)
servo.stop()

GPIO.output(gp_led, GPIO.LOW)
# time.sleep(3.0)

# GPIO.cleanup()

os.remove('lock')

ここまで来れば、ブラウザからURIを入力してアクセスすれば動作できます。

f:id:ueponx:20190130083253p:plain 【開く場合】

f:id:ueponx:20190130083320p:plain 【閉じる場合】

【動作ログ】

$ ./server.sh 
Serving HTTP on 0.0.0.0 port 8000 ...
192.168.0.12 - - [30/Jan/2019 08:30:46] "GET /cgi-bin/servo_close.py HTTP/1.1" 200 -
192.168.0.12 - - [30/Jan/2019 08:30:49] code 404, message File not found
192.168.0.12 - - [30/Jan/2019 08:30:49] "GET /favicon.ico HTTP/1.1" 404 -
192.168.0.12 - - [30/Jan/2019 08:32:09] "GET /cgi-bin/servo_open.py HTTP/1.1" 200 -
/home/pi/20190126/cgi-bin/servo_open.py:21: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(gp_out, GPIO.OUT)
/home/pi/20190126/cgi-bin/servo_open.py:22: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(gp_led, GPIO.OUT, initial=GPIO.LOW)

動作ログをみるとWarningが出ていますが、LEDのをつけたままにしておくなどGPIOのCleanupの処理を行っていないので発生しています。 コード内に

GPIO.setwarnings(False)

と記述すればなくなります。気になるようであればコード内に入れておきましょう。

躓いた点

プログラムを作っているときに躓いた点も記載します。

プログラムが実行の度挙動が異なる

最初の数時間これで悩みました。実行する度に挙動が違ったり、RaspberryPiのコンソール接続が切れるという状況になりました。最初はプログラムの設定がおかしいに違いないと思っていたのですが、 電源を変更するとびっくりするほど安定しました。電流値の高いものを使用したほうがいいようです。2.1Aの供給が行われるACアダプタを使用すると正常に動きました。 サーボモーターを動作させる場合にはACアダプターなどはちゃんと確認して使用したほうがいいかなと思います。

個人的には電源に問題があればRaspberryPiがリブートするのではないかと思っていたのですが、結構意外でした。

LEDのOFFしてもなんかついている(なんとなく点灯してる?)

LED制御のGPIOのピンを3にしていたのですが、LEDのOFFをしてもなんとなくついていました。あれ? よくわからなかったのですが、17に変更すると同じことは発生しなかった。時間がなくて、基本LEDのの直結をしていたのも良くないのですが 3と17の違いは…多分プルアップ、プルダウンの関係ではないかと思います。もう少し調べてみたいです。

LED制御のGPIOのピンをGPIO3にしていたのですが、GPIO.cleanup()をする(初期状態)とLEDがなんとなくついていました。暗く点灯している? 制御を行うと完全に点灯と消灯の状態になります。GPIO17に変更すると同じ減少は発生していませんでした。

以下の情報でGPIO.setup()から初期状態を変えることができるらしいので設定すると

GPIO.setup(gp_led, GPIO.OUT, initial=GPIO.LOW)

GPIO.setup()以降は完全消灯していました。ただGPIO.cleanup()ともとに戻ってしまいます。 原因は以下のページにかかれているのですが、GPIO2とGPIO3は内部に抵抗あり、それが原因かと思います。

tool-lab.com

GPIO2とGPIO3は他のGPIOのpinとは違うというのは覚えて置く必要がありそうです。ちょっとスッキリ。

おわりに

f:id:ueponx:20190130165145j:plain

初めてサーボモーターを触りましたがわりと面白いです。これでアームとかできたら違うアイデアうかぶかも。 今回はQRコードからのトリガー部分はつくっていないのでその部分にチャレンジする?またはFelicaからトリガーを飛ばすのはチャレンジしたいかな~。

QRコードだとカメラの処理…。Felicaだとnfcモジュール…(python2.7系との混在)結構時間かかるかも。

フェリーハッカソン2019に参加してきた

f:id:ueponx:20190130133252p:plain

先日、日本一過酷と言われるハッカソン…フェリーハッカソンに参加してきました。

code4osaka.connpass.com

今夏は2回目で、前回のハッカソンの告知が出たときも、告知即応募終了という状況で大人気のイベントではありました。ただ、その後日談をブログで読んでみると 別府の地獄めぐりもびっくりという過酷な内容のイベントだとわかりました。

【参考】 code4.osaka qiita.com togetter.com

自分もハッカソンはよく参加してはいましたし、Code for Oosakaさんにもお世話になっているので今回は告知されてすぐにエントリーすることにしました。

このときの自分の気持ちを書くならこんな感じ

「せっかくだから、俺はこの赤の扉を選ぶぜ! f:id:ueponx:20190130132326p:plain

船上のハッカソンでネットワークにつながらないといっても、瀬戸内海ならなんとかなると鷹をくくっていたのです。

「みんな盛ってるんじゃね?」

ちなみに自分のハッカソンスタイルは随時ネットでググるタイプの開発スタイルなので、ネットワークにつながらない自分は「刺し身タンポポ技術者」程度のクソ雑魚です。 会場入りしてからわかったのですが、エンジニア枠で参加申し込みをしていたようです。いまから思えばなぜその選択肢を選んだ…馬鹿だ。

スケジュール

スケジュールはこんな感じでした。


DAY1 1月25日 (金)

  • 19:55 大阪港発
  • 20:15 船上でアイデアソン開始
  • 22:00 チームビルディング
  • 23:00 船上でハッカソン開始

DAY2 1月26 日(土)

  • 7:45 別府港
  • 8:30 別府の会場で開発
  • 19:35 別府港
  • 19:35 船上で開発

DAY3 1月27日 (日)

  • 7:35 大阪港着
  • 8:00 大阪の会場で開発
  • 11:00 ハッカソン終了。プレゼン開始
  • 14:00 審査発表

1日目

集合場所のODP大阪デザイン振興プラザまでは新幹線、地下鉄などを乗り継ぎようやく到着。仕事を休んで行きたかったけど 社長にいろいろ説明することがあったので、休めず早退して向かいました。

f:id:ueponx:20190130134736j:plain f:id:ueponx:20190130135941j:plain 【出港前の集合の様子】

開会の挨拶が終わったら、いよいよ出港となります。

以前さんふらわあに乗ったのって前はいつだろう…小さい頃に乗ったような気が

f:id:ueponx:20190130135451j:plain 【いよいよ出港】

他のハッカソンと大きく違ったのは、最初によくあるアイスブレイク&アイデアスケッチの作成をみんなでやらずに各部屋(抽選で選ばれたメンバー)で行うのが少し新鮮。 あと部屋が超微妙なところも・・・。

f:id:ueponx:20190130134930j:plain f:id:ueponx:20190130134633j:plain 【アイデアスケッチをもって食堂に集合】

イデアスケッチを個室で作成したものをもって、フェリー内の食堂に集合し、アイデアをみんなで見てチームビルディングをしていくことになります。 この時点でよる23:30を回っていますが、よくわからないテンションでアイデアのブラッシュアップをしていきます。

自分のチームはIoT系(IはInternetではなくIntranetの意味)なアイデアを実現しようという方向になり、今回はその中でもスマートロックっぽいものを実現するという方向性で進めることになりました。

この日は日本に大寒波が来ていることもあり、船も大揺れで船酔いもプラスされて、みんな生産性だだ下がり。ある程度アイデアが固まったら、チームごとに部屋を割り振り作業開始になります。

あとで聞いた話ですが、去年のチームビルディングの終了は26:00を超えたらしいです。おじいさんな自分には体力的にきつい…

f:id:ueponx:20190130135024j:plainカップ麺タワー】

割り当てられた部屋でアイデアをまとめて就寝。一日目が終わります。 自分の役割はRaspberryPiでスマートロックを作ることになりました。

フェリー乗船後わかりましたが、携帯のLTEなどはかなりの確率で接続できない様子。 キャリア次第というところもありますが、3Gでようやくつながるという感じでしょうか。

2日目

26時に就寝したものの明朝の7時には既に別府に到着。

f:id:ueponx:20190130140314j:plain

前日からの寒波別府でも吹雪くほどの雪が降っていました。(積もらないけど)

f:id:ueponx:20190130140421j:plain 【ここは北国?】

せっかく別府まできているので、朝イチで温泉に行くことにしました。開発の会場が鉄輪(かんなわ)温泉のすぐ近くのかんぽの宿だったので近くにあったひょうたん温泉にいきました。

www.hyotan-onsen.com

広い!でも眠い!そして外は雪!流石に露天風呂にはいけませんでしたが非常にいい温泉でした。 開発の前に温泉に入ったら眠くなるかと思っていたのですが、意外とそのあとは眠くなることもなく開発は進めることはできました。

構成したメンバーが名古屋、大垣、大阪という感じだったので、ひょうたん温泉の由来が秀吉公の千成瓢箪から来ているということもあり、今回のチーム名は「ひょうたん」となりました。

安定したネットワークが使える陸上開発ができるのは2日目がメインなので、ここである程度目処が建てられないと完成はできないなという感じ。結構頑張りました。

今回のハマりどころはまた別エントリーで紹介しますが、簡単にいうと以下の点です。

  • RaspberryPiの電源はちゃんとしたものを使おう(特にサーボモーターを使用するときには)
  • LEDなどをGPIOで使うときにはデフォルトでプルアップ機能のあるピンとないピンがあるので気をつけよう。
  • サーボモーターの制御は適当にやるとモーターが微妙に動きうるさいので、ちゃんと止めよう。
  • RaspberryPiに入っているpythonのVersionが3.5当たりだとワンライナーのhttpサーバーのサンプルが動かないのでその対応はしよう。

今回のプロダクトとしては、httpサーバーをRaspberryPiで作ってCGI経由でpythonのサーボ制御のプログラムを動作させるということで落ち着きました。

f:id:ueponx:20190130143036j:plain

フロントエンド側でQRコードを読み取り、アクセスしてもらうようにしています。RaspberryPiに接続するカメラがあったらRaspberryPiだけで完結したのかなと思いますが、カメラはなかった(共立電子さんの臨時ショップにはあったが、自分はみないことにしたw)ので、フロントエンド側にお願いしてしまいました。スマヌ。

夕方の段階で大体サーボもHTTP系もできるようになったのであとは、なんとかなるかなと。あとは夕方に大阪に向けて乗船となります。乗船前にダイソーによって工作道具などを購入していきました。

深夜、船上の工作でスマートロックのデモ用のドアを作っていました。

f:id:ueponx:20190130142928j:plain 【デモ用のドア】

可愛い感じできた感じです! いろいろやって大体27時ごろ就寝した感じでしょうか。途中寝落ちしてましたが…

3日目

大阪の到着も7:30ごろ。早い。温泉入りたいと思ってももうない… あとは発表資料などをまとめる感じで作業を進めていきました。

発表は各チーム3分で、発表後にタッチアンドトライをして審査へと進みます。

f:id:ueponx:20190130143744j:plain 【タッチアンドトライの展示の様子】

んで、発表の結果!

f:id:ueponx:20190130143632j:plain

ネットワークチーム賞を受賞しました! 頂いた商品はRaspberryPi Zeroです。あー、継続開発なのかも。

f:id:ueponx:20190130145337j:plain

フェリーハッカソンを終わってみて

前回が地獄だったんだな。今回は過酷ではあったもののそこまででもないのかなと。 これは一重に運営の方々や協力・協賛企業の方々の尽力のおかげなのかなと思いました。 特にネットワーク構築のご担当は参加者が移動するたびにネットワーク構築するという 非常に大変な作業を毎回されていたので、本当に頭の下がる思いです。

自分の中では非常に楽しいハッカソンだったなあーという印象でした。 他のハッカソンイベントに比べるとチーム内のコミュニケーションが多かったなあというイメージでしょうか。 「旅」が一つ大きなファクターになっていること、閉鎖空間であることなどが要因にあるのかななんて思いました。

フェリーハッカソンは大阪ー別府ルート以外でも宮蘭航路でもあったそうなので、また参加してみたいなと思いました。

obnizでLチカしてみた

ようやくobnizが手に入ったのでLチカをやってみました。 Lチカだけで終わらないようにしたい…

詳しくはこちらの動画をみていただくといいかなと思います。

youtu.be

obniz.io

いざ開封

自分はamazonさんから購入しました。

届くと箱に入ってこんな感じ。フリスクケースより一回り小さい感じかなと思います。

f:id:ueponx:20190114190130j:plain

箱開けるとこんな感じになっていました。

f:id:ueponx:20190114190140j:plain

表側(?)にはESP-WROOM32とOLEDが乗っています。

f:id:ueponx:20190114190148j:plain

裏にはメスのピンヘッダーとUSBコネクターがついています。 どのpinからも1A出力できるって本当なんでしょうか?動画ではモータードライバーなしで動いていました。 また、obniz Cloudを経由すればREST経由でもWebSocket経由でもGPIOへアクセスできるようです。 同一LAN内になくてもGPIOへアクセスできるのでわりと敷居が低くなっているのではないかなと思います。 WioNodeとなんとなく似ている感じですが、価格が5倍ぐらい差があるのでそのあたりは作りたいものと時間とのトレードオフかなと思います。

では設定をしていこうと思います。

obnizを設定する

obnizを使うためには、事前にobniz cloudに登録(無料)する必要があります。

まずは以下のサイトにアクセスします。

obniz.io

ウインドウの上の方にある【登録/ログイン】をクリックします。

f:id:ueponx:20190114193410p:plain

すると、認証画面に遷移します。SNSのアカウント経由で認証してもいいですし、メールアドレスでも登録が可能になっています。 今回はGoogleアカウントで登録することにしました。ボタンの中で一番上にある【Sign in with Google】をクリックします。

f:id:ueponx:20190114193847p:plain

複数アカウントを持っている場合には、アカウントを選択するダイアログが表示されるので、使用したいアカウントを選択します。 アカウントをクリックすれば登録完了になり、次の画面に遷移します。

f:id:ueponx:20190114193913p:plain

アカウント登録が終わると今度はobnizの登録を行うことになります。登録されたobnizにはURLが発行されるのでこの処理が必要になってきます。

f:id:ueponx:20190114194316p:plain

画面に表示されている【obnizを追加】ボタンをクリックします。すると次の画面に遷移します。

f:id:ueponx:20190114194638p:plain

obnizに電源を入れてWiFiの設定をするように指示されています。 では、obnizに電源を入れましょう。すると以下のような画面になると思います。

f:id:ueponx:20190114194832j:plain

画面に指示に従ってobnizのハードボタンを押してください。このハードボタンは押し込みと左右の動きができるようになっています。次の画面で認識されたアクセスポイントの一覧が見えていると思います。(アクセスポイントのSSIDは一部しか見えないのですが、選択して時間が立つとスクロールするので十分に確認をしてください) この中からハードボタンで適切なアクセスポイントを選択します。

f:id:ueponx:20190114195457j:plain

アクセスポイントのパスワードを入力します。(ハードボタンを左右に動かし、押し込むことで入力ができます)

f:id:ueponx:20190114195510j:plain

入力が完了したら【END】でハードボタンをプッシュします。 すると以下のように画面が接続処理を行う画面になります。接続が正常に行われると以下のようにQRコードが表示された画面になります。

f:id:ueponx:20190114195954j:plain

QRコードが表示されたらWeb画面に戻り

f:id:ueponx:20190114194638p:plain

【I'm ready】ボタンをクリックします。入力ボックスが表示された画面に遷移するので、

f:id:ueponx:20190114201151p:plain

obnizのOLEDに表示された数字を入力します。入力したら【Next】ボタンをクリックします。 すると以下のような画面に遷移します。

f:id:ueponx:20190114201559p:plain

obnizのOLEDもQRコードの画面から以下のように変化しています。

f:id:ueponx:20190114201735j:plain

OLEDに6桁の数値が表示されているのでWebの入力ボックスにタイプしていきます。

f:id:ueponx:20190114201624p:plain

入力が終わったら【Validate】ボタンをクリックします。

f:id:ueponx:20190114201850p:plain

以下のような画面になればアカウントにobnizが追加されています。

f:id:ueponx:20190114202132p:plain

以下にような画面になれば、事前設定は完了となります。

f:id:ueponx:20190114203851p:plain

Let's Lチカ

では、Lチカのコードを書こうと思ったのですが、初期テンプレートがWeb画面からON/OFFボタンを押すことでLEDの店頭とOLEDの表示を変化させるコードが出てきます…なにも書かなくてもできるとは。 説明は不要かもしれませんが、念の為に新規作成させてみます。

まず、ホーム画面から【プログラムをする】ボタンをクリックします。

f:id:ueponx:20190114211900p:plain

クリックするとデプロイ先の登録済みのobnizのIDの入力が求められるのでQRコードの隣に表示された数字を入力します。

f:id:ueponx:20190114211921p:plain

入力するとチュートリアルをするか尋ねられますが、そのまま【キャンセル】ボタンをクリックします。

f:id:ueponx:20190114210525p:plain

するとコードが開かれます。HTMLにJavaScriptが含まれたものになりますね。 この画面になったら画面上部にある【保存&開く】をクリックすると実行されます。

f:id:ueponx:20190114221802p:plain

実行すると以下のような画面がポップアップします。Web画面に【ON】ボタンと【OFF】ボタンがあります。

f:id:ueponx:20190114210556p:plain

obnizのOLEDにはHello Worldと表示されています。

f:id:ueponx:20190114214953j:plain

【ON】ボタンをクリックするとOLEDにはONの表示が

f:id:ueponx:20190114215030j:plain

【OFF】ボタンをクリックするとOLEDにはOFFの表示が

f:id:ueponx:20190114215057j:plain

GPIOにLEDを指しておけば【ON】ボタンでLチカできます。 (pinpin0がアノード、pin1がカソードになっているので注意してください。)

f:id:ueponx:20190114215121j:plain

ちなみに画面左側のリストにあるblockprogram.xmlのファイルをクリックすると以下の様な画面で

f:id:ueponx:20190114220555p:plain

ブロックプログラムもできるようになっています。

おわりに

ようやくobnizに手を出すことができました。JS使いの人であれば割といい選択肢かなとは思います。 python経由でのIOアクセスに関してもやってみようと思います。

M5Stackもそろそろ触らないとなあ…積み基板が多すぎる。

Twitterのツイートをwordcloudで可視化したい【後編】

前回はTwitterのタイムラインを取得するところで終了していましたが、実際に使用してみるうちに改良などをしていきました。

【前回のエントリ】 uepon.hatenadiary.com

修正のポイントを記載してから、wordcloudの処理に進めていこうと思います。(ソース内の*Twitterアプリのキーのため伏せています。)

Twitterのタイムラインを取得に使ったソース完成版】

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import json
from requests_oauthlib import OAuth1Session
from pytz import timezone
from dateutil import parser
import time
import re
import codecs
import emoji

CONSUMER_KEY = '************************'
CONSUMER_SECRET = '************************'
ACCESS_TOKEN = '************************'
ACCESS_TOKEN_SECRET = '************************'
sinceid = -1
total_count = 0
hash_pattern = r'[##]([\w一-龠ぁ-んァ-ヴーa-z]+)'

def removeEmoji(src):
    return ''.join(c for c in src if c not in emoji.UNICODE_EMOJI)

twitter = OAuth1Session(CONSUMER_KEY,
                        CONSUMER_SECRET,
                        ACCESS_TOKEN,
                        ACCESS_TOKEN_SECRET)

url = "https://api.twitter.com/1.1/search/tweets.json"
# query = '#nhk exclude:retweets'
query = '#紅白歌合戦 exclude:retweets'
params = {
    'q': query,
    'count': 200,
    'tweet_mode': 'extended',
    'since_id': sinceid
}

print('----------------------------------------------------')

while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
        search_timeline = json.loads(req.text)
        metadata = search_timeline['search_metadata']
        metasid = metadata['since_id']
        metamid = metadata['max_id']
        total_count += len(search_timeline['statuses'])
        limit = req.headers.get('x-rate-limit-remaining', 0)
        for tweet in search_timeline['statuses']:
            text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", tweet['full_text'])
            text = re.sub('\r', '', text)
            text = re.sub('\n', '', text)
            for hashtag in ((re.findall(hash_pattern, text))):
                r = r'[##]%s' % hashtag
                text = re.sub(r, '', text)
            jst_time = parser.parse(tweet['created_at']).astimezone(timezone('Asia/Tokyo'))

            logData += '"' + str(jst_time)+ '","' + str(tweet['id']) + '","' + \
                      tweet['user']['name'] + '","@' + tweet['user']['screen_name']  + '","' + \
                      text + '"\n'
            # ----------------------------------------------------
    else:
        print("ERROR: %d" % req.status_code)
    params['since_id'] = metamid + 1
    print('******')
    print('total_count::' + str(total_count))
    print('APIlimit::' + str(limit))
    # ----------------------------------------------------
    with codecs.open('./log/log.csv', mode='a', encoding='utf-8') as f:
        print(removeEmoji(logData), file=f, end='')
    # ----------------------------------------------------
    time.sleep(5)

Twitterでタイムラインを収集した際のポイント

先程の完成版で修正などを行ったところをメモしておきます。

Tweetの時刻がUTCとして記録されているので、JSTに変更したい

ツイートに含まれる生成時の時刻create_atUTCで表現されています。そのままだとわかりにくいのでJSTに変更するようにしました。 Pythonタイムゾーンや時刻を扱うにはpytzタイムゾーン処理)、python-dateutil(時刻処理)を使うことが多いようでしたので、 今回はそれを使用しています。

【参考】 qiita.com

【パッケージのインストール】

$ pip install pytz
$ pip install python-dateutil

UTCのDate文字列をJSTのDate文字列に変換する処理

【サンプル】

from pytz import timezone
from dateutil import parser

utc_string = "Sat Mar 19 06:17:57 +0000 2016"
jst_time = parser.parse(utc_string).astimezone(timezone('Asia/Tokyo'))
print(jst_time)
# 出力:2016-03-19 15:17:57+09:00

この処理を組み入れてツイート取得時に含まれる生成時刻create_atを以下のように記述して変換しています。

【プログラム抜粋】

while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
        search_timeline = json.loads(req.text)
【中略】
        for tweet in search_timeline['statuses']:
【中略】
            jst_time = parser.parse(tweet['created_at']).astimezone(timezone('Asia/Tokyo'))
【中略】

140文字以上のツイートを取得する

以前、twitterの書き込みが140文字以上になったというリリースがありましたが、現在はこのような長いツイートがあると全文が表示されず、途中省略されて全文へのリンクが表示されるようになっています。公式ドキュメントを確認すると以下のようになっています。

Tweet updates — Twitter Developers

ドキュメントの表内のExtendedの部分がそれに当たります。 APIアクセス時のパラメータにtweet_mode=extendedを追加し、戻ってきたJSONデータに関しては、これまでのツイート本文であったtextの代わりにfull_textを使えば良いと記載があります。これに合わせてソースを変更します。

【プログラム抜粋】

【中略】
query = '#nhk exclude:retweets'
params = {
    'q': query,
    'count': 200,
    'tweet_mode': 'extended',
    'since_id': sinceid
}
【中略】
while True:
    logData = ''
    req = twitter.get(url, params=params)
    if req.status_code == 200:
【中略】
        for tweet in search_timeline['statuses']:
            text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", tweet['full_text'])

これで140文字以上のツイートも取得できます。

WindowsUTF-8文字列をファイルに書き込もうとするとコードエラーが発生する

取得したデータをcsvファイルに格納しようとするとエンコードエラーが発生してしまいました。 Python3であればUTF-8などの文字コードに関してはそれほど気にしなくてもいいのかなと思っていたのでちょっと驚きました。 更に開発に使用している画面表示に関してはPycharm側のコンソールを使っているのでUTF-8の表示も問題ありませんので、更に気がつくまでに時間がかかりました。

今回の問題はWindows側だけに発生しているようです。以下が参考になりました。

go-journey.club

簡単に言うとPython側で処理した文字列をファイル保存する際にうまく処理がおこわなれないのが原因のようです。 そこでcodecというモジュールを使用し、さらに書き込み時にはwrite()ではなくprint()を使用し、更に引数にfileを指定することで解決ができます。

【サンプル】

import codecs

# 最初に文字コードを指定して「追加」モードでログファイルを開く
f = codecs.open('/var/log/python/app.log', 'a', 'utf-8')
new_words = 'データ'
# print()で書き込みを行う。出力先の引数にfileを指定する
print(new_words, file=f)

これを使用しています。途中removeEmoji()とありますがユーザ定義の関数になりますので、単にここに文字列の変数を入れれば問題ありません。 これが入っている理由はemojiが入っているテキストでは最終目的であるwordcloudで処理ができないためです。

【プログラム抜粋】

【中略】
    with codecs.open('./log/log.csv', mode='a', encoding='utf-8') as f:
        print(removeEmoji(logData), file=f, end='')
【中略】

ちなみにemojiを扱うパッケージは、そのままemojiとなりますのでインストールもそのままでOKです。

【インストール】

$ pip install emoji

これを使用して以下のようにemoji削除の関数を作成しています。

def removeEmoji(src):
    return ''.join(c for c in src if c not in emoji.UNICODE_EMOJI)

長時間動作中にKeyErrorエラーが発生する

プログラムを長時間動作させていると極稀にKeyErrorが発生することがあります。 エラーに該当している部分、取得したJSONのHeaderの中にキーが含まれないことがあることに起因します。むしろそんなことがあるのか?と驚きましたがあるようです。

そこで取得する方法をget()を使うことでキーが存在しなければ、指定したデフォルト値を返すという方法に変更しました。

【変更前】

        limit = req.headers['x-rate-limit-remaining']

【変更後】

        limit = req.headers.get('x-rate-limit-remaining', 0)

これで長時間動作も可能になりました。(エラーは数時間やって一回程度のものです。)

そろそろWordCloudが使いたい…

ようやくデータが取得できたので目的であるWordCloudを使う準備をしてみたいと思います。

パッケージのGitHubのサイトは以下になります。

github.com

テストされているPythonのバージョンは、2.7、3.4、3.5、3.6、3.7のようです。

WordCloudのライブラリ

$ pip install wordcloud

依存するパッケージも同時にインストールされるので、素の状態であれば以下のパッケージがインストールされます。

  • pillow
  • numpy
  • wordcloud

このパッケージをインストールすると、CLI環境で使用できるwordcloud_cliというツールも同時にインストールされます。 こちらを使ってもテストができます。

【使用例】

$ wordcloud_cli --text mytext.txt --imagefile wordcloud.png

GitHubにあるサンプルを使用して、実験を行ってみます。

【alice.txt】 https://raw.githubusercontent.com/amueller/word_cloud/master/examples/alice.txt

【今回の実行例】

$ wordcloud_cli --text alice.txt --width 640 --height 480 --imagefile wordcloud.png

特に大きなエラーも発生せず、問題なく生成されています。

f:id:ueponx:20190101195212p:plain

では、このまま日本語のテキストもやってみようと思います。 以下のようなテキストを準備します。単語と単語の間にスペースを開ける必要があるようなのでこのようにしてみました。

【Jsamplet.txt】

今日 明日 明後日 晴れ 雨 しかし

以下のように実行してみると…

$ wordcloud_cli --text Jsample.txt --width 640 --height 480 --imagefile wordcloud.png

おや?文字が出ません。

f:id:ueponx:20190101201049p:plain

ググってみると日本語ではフォントファイルの設定をしないと表示できないということがかかれていたので --fontfileオプションでフォントファイルを設定して以下のように実行してみました。

$ wordcloud_cli --text Jsample.txt --fontfile C:\Windows\Fonts\meiryo.ttc --width 640 --height 480 --imagefile wordcloud.png

これでうまく日本語が表示されました。

f:id:ueponx:20190101202337p:plain

これからはPythonのプログラムと連携させて生成するようにしていきます。

(注)やっていてわかったのですが、wordcloudは画像を生成する際に?内部でmatplotlibモジュールを使用しているようなので、 もしインストールを行っていない場合には事前にインストールを行う必要があるようです。ほんと?

matplotlibモジュールのインストール】

$ pip install matplotlib

【wordcloudの画像生成】

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from wordcloud import WordCloud

contents = open('Jsample.txt', encoding="utf-8").read()
# contents = '今日 明日 明後日 晴れ 雨 しかし'

fpath = 'C:\\Windows\\Fonts\\meiryo.ttc'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(contents)
wordcloud.to_file("./wordcloud_sample.png")

実行するとこんな感じに生成されます。今回は背景色を白に指定しています。

f:id:ueponx:20190101213721p:plain

あとはTwitterのタイムラインとこのプログラムを結合することになります。 ただ、wordcloudの入力に与えられるテキストデータは単語をスペースを開けた形にする必要があるのでデータの整形が必須となります。

入力データの整形を行う

今回収集しているデータはCSVなのでそのうちのツイートを取り出し、更にテキストの分かち書きを行うことになります。 分かち書きは簡単に言うと「文章を品詞分解を行い、単語ごとに取り出す」ことになります。この分かち書きには形態素解析エンジンを使用します。

以前のエントリではMecabという形態素解析エンジンを使用していましたが今回は違うアプローチをしたほうが面白いと思ったのでjanomeという 形態素解析エンジンを使用することにします。

Python形態素解析エンジンjanome

Janomeは、Mecabと比べると実行速度は劣りますが、Pythonのみで実装されていて辞書も内包されている点が特徴となります。 pipコマンドだけでインストールできる容易さも魅力です。詳細は以下をお読みください。

janomeドキュメント】

Welcome to janome's documentation! (Japanese) — Janome v0.3 documentation (ja)

インストールは以下でOKです。

$ pip install janome

まずは、形態素解析のサンプルを作ってみます。janome公式のドキュメントを見ながら作ってみると以下のようになります。

【サンプル】

from janome.tokenizer import Tokenizer

Sentence='ことしも熱い戦いをありがとう'

t = Tokenizer()
tokens = t.tokenize(Sentence)

for token in tokens:
    print("表層形:",token.surface,"\n"
          "品詞:",token.part_of_speech.split(',')[0],"\n"
          "品詞細分類1:",token.part_of_speech.split(',')[1],"\n"
          "品詞細分類2:",token.part_of_speech.split(',')[2],"\n"
          "品詞細分類3:",token.part_of_speech.split(',')[3],"\n"
          "活用型:",token.infl_type,"\n"
          "活用形:",token.infl_form,"\n"
          "原形:",token.base_form,"\n"
          "読み:",token.reading,"\n"
          "発音:",token.phonetic)
    print('-'*32)

【サンプルの実行結果】

$ python.exe janome_sample.py
表層形: ことし 
品詞: 名詞 
品詞細分類1: 副詞可能 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: ことし 
読み: コトシ 
発音: コトシ
--------------------------------
表層形: も 
品詞: 助詞 
品詞細分類1: 係助詞 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: も 
読み: モ 
発音: モ
--------------------------------
表層形: 熱い 
品詞: 形容詞 
品詞細分類1: 自立 
品詞細分類2: * 
品詞細分類3: * 
活用型: 形容詞・アウオ段 
活用形: 基本形 
原形: 熱い 
読み: アツイ 
発音: アツイ
--------------------------------
表層形: 戦い 
品詞: 名詞 
品詞細分類1: 一般 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: 戦い 
読み: タタカイ 
発音: タタカイ
--------------------------------
表層形: を 
品詞: 助詞 
品詞細分類1: 格助詞 
品詞細分類2: 一般 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: を 
読み: ヲ 
発音: ヲ
--------------------------------
表層形: ありがとう 
品詞: 感動詞 
品詞細分類1: * 
品詞細分類2: * 
品詞細分類3: * 
活用型: * 
活用形: * 
原形: ありがとう 
読み: アリガトウ 
発音: アリガトー
--------------------------------

Process finished with exit code 0

形態素解析の結果でwordcloudで使用したいのは今回使用するのは表層系品詞ぐらいでしょうか。 助詞や接続詞などが多く出てきても面白くないですしね。(ツイッターでは比較的短文が多いので、それらの品詞あまり数はないと思いますが)

では次は取得したデータを使って分かち書きの出力を行おうと思います。

ツイートデータから分かち書きファイルを生成する

取得したツイートのデータから形態素解析を行って分かち書きを行ってみます。

取得したツイートデータの形式は

  • 書き込み時刻
  • TweetしたユーザID
  • Username(ユーザ名)
  • Screenname(@のついたユーザ名)
  • Tweetテキスト

となっているのでCSVの第4フィールドを取り出して形態素解析を行い、スペースで分かち書きした形式で出力を行います。

import csv
from janome.tokenizer import Tokenizer
import codecs

wakachi = ''

with codecs.open('./wakachi_log.txt', mode='w', encoding='utf-8') as fw:
    None

t = Tokenizer()
with open('log.csv', 'r', encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        tweet = row[4]

        word_list = []
        tokens = t.tokenize(tweet)
        for token in tokens:
            word = token.surface
            word_base = token.base_form
            partOfSpeech = token.part_of_speech.split(',')[0]
            if token.base_form in ["ある", "なる", "こと", "よう", "そう", "これ", "それ", "する", "いる", "いい"]:
                continue
            if partOfSpeech in ["形容詞", "動詞", "名詞", "代名詞", "副詞"]:
                if (partOfSpeech == "名詞"):
                    if (token.part_of_speech.split(',')[1] in ["数", "接尾", "助数詞", "非自立"]):
                        continue
                elif (partOfSpeech == "動詞"):
                    if (token.part_of_speech.split(',')[1] not in ["自立"]):
                        continue
                elif (partOfSpeech == "形容詞"):
                    if (token.part_of_speech.split(',')[1] not in ["自立"]):
                        continue
                elif (partOfSpeech == "副詞"):
                    if (token.part_of_speech.split(',')[1] in ["助詞類接続"]):
                        continue
                word_list.append(word_base)
                print("表層形:", token.surface, "\n"
                    "品詞:", token.part_of_speech.split(',')[0], "\n"
                    "品詞細分類1:", token.part_of_speech.split(',')[1], "\n"
                    "品詞細分類2:", token.part_of_speech.split(',')[2], "\n"
                    "品詞細分類3:", token.part_of_speech.split(',')[3], "\n"
                    "活用型:", token.infl_type, "\n"
                    "活用形:", token.infl_form, "\n"
                    "原形:", token.base_form, "\n"
                    "読み:", token.reading, "\n"
                    "発音:", token.phonetic)
            print('-' * 8)

        wakachi = " ".join(word_list)
        with codecs.open('./wakachi_log.txt', mode='a', encoding='utf-8') as fw:
            print(wakachi, file=fw)

いろいろノイズが多いので品詞で絞ったり、あってもよくわからないNGワードは抜いています。 出来上がったファイルは以下のようになりました。

【wakachi_log.txt】

f:id:ueponx:20190103163505p:plain

あとは出力されたファイルをwordcloudの処理にかければOKです。 前述のファイルを少し変えて以下のようにしました。

【wc.py】

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
from wordcloud import WordCloud

contents = open('wakachi_log.txt', encoding="utf-8").read()

fpath = 'C:\\Windows\\Fonts\\meiryo.ttc'
wordcloud = WordCloud(background_color="white", font_path=fpath, width=900, height=500).generate(contents)
wordcloud.to_file("./wordcloud.png")

箱根駅伝直後の数分のデータで生成すると以下のようになりました。

f:id:ueponx:20190103163941p:plain

割といい感じになっているような気がします。

ようやくお楽しみの時間へ

いくつかの番組をwordcloudの処理をかけてみました。

NHK紅白歌合戦 12/31 19時頃放送】

NGワード調整前

f:id:ueponx:20190103180840p:plain

いろいろ抽象的なものが多かったので、除外ワード追加調整後

f:id:ueponx:20190107080322p:plain

ぐるナイ おもしろ荘 12/31 24:30頃放送】

f:id:ueponx:20190103193948p:plain

フットンダ 12/31 26:00頃放送】

f:id:ueponx:20190103170050p:plain

【第95回箱根駅伝 往路 1/2 8:00頃放送】

f:id:ueponx:20190103200926p:plain

【第95回箱根駅伝 復路 1/3 8:00頃放送】

f:id:ueponx:20190103203314p:plain

おわりに

割と面白い結果がでてきますね。 それにしてもデータの取得さえしておくといろいろ楽しめます。

Twitterのツイートをwordcloudで可視化したい【前編】

まえまえから思っていたんですが、まったく手をつけられていませんでした。ただ、年末という非常にツイートデータが大量に手に入る時期なのでやるのは今だと思ってやります。ただいろいろやっている人もいるので、大した違いはないかなと思います。

Twitterアプリの作成

【注意】もしすでに作成済みのアプリがあるようであれば、そちらを使ったほうが時間がかなり短縮できるのでオススメです。

以前にもTwitterアプリを作成したのでそれが参考になるかなと思います。

【参考】 uepon.hatenadiary.com

ただ、UIがちょっと変わっているかなと思いますので久しぶりに作成してみます。

Twitterにログインしている状態で以下のURLへいくと新規アプリ作成(アプリ一覧)ページに遷移します。 過去エントリの参考のURLからでも行けるようになっていますが、URLは今後変わるので新しいものを記載しています。

https://developer.twitter.com/en/apps

ページにはこれまでに作成したTwitterアプリの一覧が表示されます。

f:id:ueponx:20181230121049p:plain

画面右上にある【Create an app】ボタンをクリックすると

f:id:ueponx:20181230122030p:plain

確認のダイアログが表示されるので【Apply】ボタンをクリックします。

f:id:ueponx:20181230122147p:plain

以前はアカウントに携帯電話番号の登録することなくアプリの作成ができましたが、現在ではできないようなので以下の画面が出た場合には登録する必要があります。 【Add a valid phone number】ボタンをクリックして登録を行います。

f:id:ueponx:20181230122746p:plain

クリックするとダイアログが表示されるので【Country/region】と【Phone/number】のプルダウンご自身の設定値を適切に入力して【Next】ボタンをクリックします。

f:id:ueponx:20181230123419p:plain

確認用コードの送信ダイアログになります。この時点で携帯電話のSMS経由で確認用コードが送信されるので、そこで表示された数字列を【Verification code】の入力ボックスに 入力して【Verify】ボタンをクリックします。

f:id:ueponx:20181230123432p:plain

確認が成功すると以下のような通知が表示されます。

f:id:ueponx:20181230123355p:plain

携帯での本人確認が終了したら画面表示がもとに戻るので【Continue】ボタンをクリックして作業を続けます。 ここからアプリの設定になります。

f:id:ueponx:20181230124619p:plain

Who are you requesting access for?の質問には【I am requesting access for my own personal use】のラジオボタンにチェックし、 残りの項目へ入力していきます。

f:id:ueponx:20181230124945p:plain

f:id:ueponx:20181230125726p:plain

f:id:ueponx:20181230125816p:plain

設定が完了したら、画面下の方にある【Continue】ボタンをクリックします。

f:id:ueponx:20181230125947p:plain

続くページでも作成の理由などを詳細に書く必要があります。 特に自由記述の文面を各部分もありハードルは上がっています。

f:id:ueponx:20181230130619p:plain

f:id:ueponx:20181230130739p:plain

設定が終わったら【Continue】ボタンをクリックします。

f:id:ueponx:20181230130908p:plain

画面が利用許諾のページになるので、文面を確認してチェックボックスにチェックを入れます。 ※許諾の文面をスクロールさせてすべて閲覧しないとチェックボックスが押せないので注意です。

f:id:ueponx:20181230131008p:plain

チェックができると【Submit application】ボタンがアクティブになるので、クリックします。

f:id:ueponx:20181230131218p:plain

これで設定が終わったので、確認用のメールが送信されるので【Confirm your email】をクリックしてください。

f:id:ueponx:20181230131520p:plain

ここからレビューして作成が可能になるとのことです。

f:id:ueponx:20181230131836p:plain

アプリ作成で手順がかなり増えたような… これでメールが返送されてきたらアプリの作成作業に入ります。

以下のURLをクリックして https://developer.twitter.com/en/apps

アプリ一覧の画面に遷移して画面上の方にある【Create an app】ボタンをクリックします。

f:id:ueponx:20181230180343p:plain

アプリ名などを入力する詳細設定の画面に遷移するのでデータを入力していきます。

f:id:ueponx:20181230181059p:plain

f:id:ueponx:20181230181305p:plain

必要な項目に入力をしたら、画面下方にある【Create】ボタンをクリックします。

f:id:ueponx:20181230181428p:plain

クリックするとダイアログが表示されるかもしれませんが、内容に問題がなければ【Create】ボタンをクリックします。

f:id:ueponx:20181230181544p:plain

うまく処理が行われるとブラウザに通知が行われます。

f:id:ueponx:20181230181714p:plain

アプリができたら

以下のURLからアプリ一覧を選択して

https://developer.twitter.com/en/apps

該当のアプリの【Details】ボタンをクリックします。

f:id:ueponx:20181230182024p:plain

するとアプリの詳細設定画面に移るので【Keys and tokens】をクリックします。

f:id:ueponx:20181230182521p:plain

f:id:ueponx:20181230182219p:plain

すると、APIのアクセスに必要な

4つのキーを表示する画面になります。Access tokenAccess token secretの値が表示されていない場合には【Create】ボタンをクリックして生成をおこなってください。

f:id:ueponx:20181230142708p:plain

この4つの値がコーディングに必要になるのでこれをメモしておきます。

コーディング

ようやくコーディングになります。長かった…

ツイッターでキーワードサーチをしてデータを収集するプログラムを作成します。

これまではTweepyを使用していましたが、今回は新しい取り組みとしてrequests_oauthlibを使用してみます。(Tweepyでも処理はあんまり変わらないのですが新しいものを使ってみたかったのでこちらにしています。)詳細は以下のページを御覧ください。

TweepyではTwitterに特化した処理ですが、requests_oauthlibは他のOAuth認証を使った処理でも使用可能なので、これを使えるほうがいいことがありそうです。

【参考】 pypi.org

というかもう結構まとまっている記事がありましたので、そちらを参考にしてコードを組みました。

【参考】 qiita.com

先程メモしておいた - API key - API secret key - Access token - Access token secret の値に関しては*で伏せています。

下準備

パッケージのインストール

$ pip install requests_oauthlib
$ pip install pytz
$ pip install python-dateutil

サンプルのコーディング

このサンプルではハッシュタグ#nhkリツイートではないものを最大20件取得するものを作っています。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import json
from requests_oauthlib import OAuth1Session
from pytz import timezone
from dateutil import parser

CONSUMER_KEY = '****************'
CONSUMER_SECRET = '****************'
ACCESS_TOKEN = '****************'
ACCESS_TOKEN_SECRET = '****************'

twitter = OAuth1Session(CONSUMER_KEY,
                        CONSUMER_SECRET,
                        ACCESS_TOKEN,
                        ACCESS_TOKEN_SECRET)

url = "https://api.twitter.com/1.1/search/tweets.json"
query = '#nhk exclude:retweets'
params = {
    'q': query,
    'count': 20
}

print('----------------------------------------------------')

req = twitter.get(url, params = params)

if req.status_code == 200:
    search_timeline = json.loads(req.text)
    for tweet in search_timeline['statuses']:
        print('name::\n' + tweet['user']['name'])
        print('text::\n' + tweet['text'])
        jst_time = parser.parse(tweet['created_at']).astimezone(timezone('Asia/Tokyo'))
        print('date::')
        print(jst_time)
        print('----------------------------------------------------')
else:
    print("ERROR: %d" % req.status_code)

このコードはそんなに難しくないのですが、返されるJSONに含まれる値のcreate_at(ツイートがされた時刻)がタイムゾーンUTCで扱われているので、 ローカルタイムゾーンに合わせるためpytzdateutilのパッケージを使用しています。

終わりに

2018年の年末まで1日しかありませんが、出来上がるのでしょうか。 すくなくともデータの取得までは完成させてwordcloudへの出力が遅れるのもやむなしかなと思ってがんばります。

/* -----codeの行番号----- */