がらくた日記

趣味のブログ

VBA-JSONでネストされた深いところにあるJSONデータを取得する

VBAJSONデータのパースは素晴らしいモジュールがあるのでそれを利用させてもらってますが、深い階層のデータ取得方法について書き留めておく。

まあ、開発者のページを読むと書いてあることではありますが。。。

VBA-JSONはここから https://github.com/VBA-tools/VBA-JSON/releases

解析したいJSON

{
    "Dates": {
        "OutboundDates": [
            {
                "PartialDate": "2019-09-03",
                "QuoteIds": [
                    1
                ],
                "Price": 83.0,
                "QuoteDateTime": "2019-08-22T10:43:00"
            },
            {
                "PartialDate": "2019-09-10",
                "QuoteIds": [
                    2
                ],
                "Price": 94.0,
                "QuoteDateTime": "2019-08-24T12:14:00"
            }

VBAでは":"記号の右側にあるデータをitemsプロパティからVariant型で取得でき、複数あるときはFor Each in Next文などでループして取得できるのだが、 いちばん注意したいのは配列の開始を表す"["の記号。

パースされた情報は"["から"]"までを一つの配列として処理しているから、配列"[...]"単位でループをネストさせながら深い階層に入っていく。

上のJSONをパースしたものをresult1に格納したとして、"Dates"直下のデータを取得したいときは

result1("Dates")

もう一つ下の"Dates"の中の"OutboundDates"の階層のデータを取得したいときは、

result1("Dates")("OutboundDates")

のように()で引数を増やして希望の階層まで下がっていき、そこにあるデータをFor Each in Next文でループして取得する。 "PartialDate"、"QuoteIds"、"Price"、"QuoteDateTime"の各データを取得したいときは、

VBA

Sub Deepsea()
'参照設定 Microsoft Scripting Runtime
'参照設定 Microsoft XML. v6.0

Dim objXMLHttp As New MSXML2.XMLHTTP60
Dim result1 As New Scripting.Dictionary
Dim result2 As New Scripting.Dictionary
Dim i As Variant

objXMLHttp.Open  "GET", "http://... ", False
objXMLHttp.send

Set result1 = JsonConverter.ParseJson(objXMLHttp.responseText)

    For Each result2 In result1("Dates")("OutboundDates")
        For Each i In result2.Items
            Debug.Print i
        Next
    Next

ここで、残念ながら"QuoteIds"のデータだけは取得できない。"QuoteIds"はさらにその中が配列"[...]"になっているから。

データは"["記号から次の"["までの間しか取得できないので、"QuoteIds"の中データを取得したいときは

result1("Dates")("OutboundDates")("QuoteIds")

のようにさらに()で引数を増やしてこの配列の中に入りデータを取得する。

【Python】RaspberryPi3で電車の遅延情報をしゃべらせてみる

 だいぶ前に買ったRaspberryPi3、使い方がよく分からなくて、使い道も思いつかないので放置していたんだけど、毎朝出勤前に「OK Google!」とかいちいち呼ばなくてもいつも乗る電車の遅延情報を勝手にしゃべってくれるといいかもと思ったので、Pythonの練習がてら作ってみることにした。

概要

  1. これからはPython2ではなく、3らしいのでPython3を使ってみる
  2. 毎朝設定した時刻にYahoo路線情報から運行情報を検索
  3. 路線名、状況、詳細内容のテキストを取得
  4. 取得したテキストをOpen JTalkで喋らせる
  5. 平常運転のときは黙っててほしい

HTML解析の準備

 HTMLから必要なデータを取り出すのに必要なモジュールを3つLXTerminalからインストール。

pip3 install requests
pip3 install lxml  

 RaspberryPiにPython2とPython3が入っているときは、pipではPython2にインストールされてしまうので、Python3にインストールしたいときはpip3と書く。

運行情報を取得する

 設定できる路線は一つ。Yahoo路線情報で路線を探して、そのアドレスを変数target_urlに入れてHTML解析して情報を取得する。
取得したデータを変数qに代入、この変数の中身がOpen JTalkで喋らせたい言葉になる。
アドレスは末尾の数字で路線が決まってるらしく、うちの地元京葉線は69/0、山手線なら21/0みたいなかんじ。

#Yahoo!路線情報(京葉線)
target_url = 'https://transit.yahoo.co.jp/traininfo/detail/69/0/'
target_html = requests.get(target_url).text
root = lxml.html.fromstring(target_html)
#路線名(かな)
q = root.xpath('string(//span[@class="staKana"])')
#運行情報
q = q + root.xpath('normalize-space(//div[@id="mdServiceStatus"])')
#文字列の編集
q=q.replace('(','\n(')
q=q.replace(':','時')
q=q.replace('頃','分頃')
q=q.replace('00分頃','頃')

print (q)

取得した変数qは「けいようせん [!]運転状況 20時10分頃、●●駅間で発生した●●の影響で、現在も列車に遅れや運休が出ています。」みたいなテキスト文になっている。
路線名を漢字じゃなくふり仮名で取得したのは、Open JTalkが「京成」を「きょうせい」とか読み間違えするから。
さらに、オリジナルのテキスト文をOpen JTalkが喋りやすいようにreplaceを使って少々加工してある。例えば「20:10頃」なら「にじゅう、じゅうごろ」と喋るけど、上のように「20時10分頃」と書き換えてやれば「にじゅうじじゅっぷんごろ」と喋ってくれる。
また、改行(\n)があるとその手前までしか喋らないので、喋ってほしい文中の改行は取り除いたりした(normalize-space)。

Open JTalkを使って音声に変換

OpenJTalkと追加モジュール2つをLXTerminalからインストール。

sudo apt-get install open-jtalk
sudo apt-get install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001

OpenJTalkは、txtファイルに書かれている日本語文章をwavファイルに出力してくれるというもので、例えば初めにDocumentsフォルダ(/home/pi/Documents)にvoice.txtというファイルを作成して、このファイルにしゃべらせたい言葉をかいておき、次のコマンドをLXTerminalで打つと音声合成変換されたtest.wavファイルを同じDocumentsフォルダに作ってくれる。

open_jtalk -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -x /var/lib/mecab/dic/open-jtalk/naist-jdic -ow /home/pi/Documents/test.wav /home/pi/Documents/voice.txt

ちなみに、コマンド前半の

open_jtalk -m /usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice -x /var/lib/mecab/dic/open-jtalk/naist-jdic -ow

の部分は決まり文句らしいのでいじらずに、この後ろに出力したいwavファイルの場所と、入力するtxtファイルの場所を書くだけ。

最終的なコードはこんな感じです。(関数jtalk()とmain()のコードの辺りは借用させていただきました、すいません)。
平常運転のときは黙っててほしいので、変数indexが-1(「事故・遅延に関する情報はありません」という文字列が無い)のときだけ動作するようにした。 作成したプログラムはtest.pyという名前でDocumentsフォルダに保存する。

import lxml.html
import requests
import subprocess

target_url = 'https://transit.yahoo.co.jp/traininfo/detail/69/0/'
target_html = requests.get(target_url).text
root = lxml.html.fromstring(target_html)

q = root.xpath('string(//span[@class="staKana"])')
q = q + root.xpath('normalize-space(//div[@id="mdServiceStatus"])')
q = q.replace('(','\n(')
q = q.replace(':','時')
q = q.replace('頃','分頃')
q = q.replace('00分頃','頃')

index = q.find('事故・遅延に関する情報はありません')

print(index)
print (q)

def jtalk(t):
        app = ['open_jtalk']
        mech = ['-x','/var/lib/mecab/dic/open-jtalk/naist-jdic']
        htsvoice = ['-m','/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice']                                                                  
        outwav = ['-ow','/home/pi/Documents/test.wav']
        cmd = app + mech + htsvoice + outwav 
        c = subprocess.Popen(cmd,stdin=subprocess.PIPE)
        c.stdin.write(t.encode('utf-8'))
        c.stdin.close()
        c.wait()

        aplay = ['aplay','-q','/home/pi/Documents/test.wav']
        wr = subprocess.Popen(aplay)

def main():
        jtalk(q)

if index == -1:
    if __name__ == '__main__':
        main()

平日の朝にだけ喋らせる

 RaspberryPiにプリインストールされているcronを使いました。Windowsのタスクスケジューラみたいなものだと思います。LXTerminalでcrontab -eと打つとエディタが起動するので、一番最後の行(べつに最初の行でもいいけど)に

10,20 8 * * 1-5 python3 /home/pi/Documents/test.py

みたいなかんじで書いて保存する。
書式は「分 時 日 月 曜日 コマンド」なので、この例では「月曜から金曜の8時10分と8時20分にDocumentsフォルダにあるtest.pyファイルをpython3で起動させる」となる。

最後に

 音声出力はRaspberryPiの3.5Φイヤホン端子にダイソーの人気商品300円スピーカーを繋げてみました。いいかんじです。

 かつては風が吹くだけでもすぐ止まると首都圏のJRの中でも悪名高かった京葉線ですが、最近はいろいろ対策されて簡単には止まらなくなったため、なかなかRaspberryPiが喋ってくれなくて寂しいです。