毎朝VOICEROIDに天気を知らせてほしい

## はじめに

タイトル通りです。  
天気予報を確認せずに外出して失敗した経験はありませんか?　私は何度もあります。簡単でいいので毎朝、知らせてくれたらそんな悩みを解決できるかもしれません。

私はプログラミング初心者で、今回が初投稿となりますから少し見辛い点があると思います。ご容赦ください。  

使用言語はpython3.9です。  

## 天気予報を処理


### スクレイプ

今回は[https://tenki.jp/](tenki.jp)のページをスクレイプします。
このスクレイプするプログラムは[こちらの記事](https://kinformation.sakura.ne.jp/20170715-01)を一部編集して引用しています。
そちらのコードをコピペしたファイルscrape_weather.pyをimportします。  

以下がそのコードです。

```python:scrape_weather.py
import re
import requests
from bs4 import BeautifulSoup
import json


def main(url):
    # bs4でパース
    s = soup(url)

    dict = {}

    # 予測地点
    l_pattern = r"(.+)の今日明日の天気"
    l_src = s.title.text
    dict['location'] = re.findall(l_pattern, l_src)[0]
    print(dict['location'] + "の天気")

    soup_tdy = s.select('.today-weather')[0]
    soup_tmr = s.select('.tomorrow-weather')[0]

    # 引用元と異なり変数dictに代入
    dict = forecast2dict(soup_tdy)
    # dict["today"] = forecast2dict(soup_tdy)
    # dict["tomorrow"] = forecast2dict(soup_tmr)

    # JSON形式で出力
    # print(json.dumps(dict, ensure_ascii=False))

    # 引用元と異なり変数dictを返す
    return dict

def soup(url):
    r = requests.get(url)
    html = r.text.encode(r.encoding)
    return BeautifulSoup(html, 'html.parser')

def forecast2dict(soup):
    data = {}

    # 日付処理
    d_pattern = r"(\d+)月(\d+)日\(([土日月火水木金])+\)"
    d_src = soup.select('.left-style')
    date = re.findall(d_pattern, d_src[0].text)[0]
    data["date"] = "%s-%s(%s)" % (date[0], date[1], date[2])
    print("=====" + data["date"] + "=====")

    ## 取得
    weather           = soup.select('.weather-telop')[0]
    high_temp         = soup.select("[class='high-temp temp']")[0]
    high_temp_diff    = soup.select("[class='high-temp tempdiff']")[0]
    low_temp          = soup.select("[class='low-temp temp']")[0]
    low_temp_diff     = soup.select("[class='low-temp tempdiff']")[0]
    rain_probability  = soup.select('.rain-probability > td')
    wind_wave         = soup.select('.wind-wave > td')[0]

    ## 格納
    data["forecasts"] = []
    forecast = {}
    forecast["weather"] = weather.text.strip()
    forecast["high_temp"] = high_temp.text.strip()
    forecast["high_temp_diff"] = high_temp_diff.text.strip()
    forecast["low_temp"] = low_temp.text.strip()
    forecast["low_temp_diff"] = low_temp_diff.text.strip()
    every_6h = {}
    for i in range(4):
        time_from = 0+6*i
        time_to   = 6+6*i
        itr       = '{:02}-{:02}'.format(time_from,time_to)
        every_6h[itr] = rain_probability[i].text.strip()
    forecast["rain_probability"] = every_6h
    forecast["wind_wave"] = wind_wave.text.strip()

    data["forecasts"].append(forecast)

    print(
        "天気              ： " + forecast["weather"] + "\n"
        "最高気温(C)       ： " + forecast["high_temp"] + "\n"
        "最高気温差(C)     ： " + forecast["high_temp_diff"] + "\n"
        "最低気温(C)       ： " + forecast["low_temp"] + "\n"
        "最低気温差(C)     ： " + forecast["low_temp_diff"] + "\n"
        "降水確率[00-06]   ： " + forecast["rain_probability"]['00-06'] + "\n"
        "降水確率[06-12]   ： " + forecast["rain_probability"]['06-12'] + "\n"
        "降水確率[12-18]   ： " + forecast["rain_probability"]['12-18'] + "\n"
        "降水確率[18-24]   ： " + forecast["rain_probability"]['18-24'] + "\n"
        "風向              ： " + forecast["wind_wave"] + "\n"
    )
    return data

if __name__ == '__main__':
    # 世田谷区の一時間ごとの気象情報URL
    URL = 'https://tenki.jp/forecast/3/16/4410/13104/1hour.html'
    main(URL)

```

main関数の末の部分で結果を変数dictで返すよう改変しました。  

### 文字列加工

次に主となるpyファイルgood_morning.pyを作成し先ほどコピペしたscrape_weather.pyと、後でreモジュールも必要なのでimportします。

```python:inform_weather.py
import re

import scrape_weather
```

ここで自分の調べたい場所のURLを指定します。

```python:inform_weather.py
URL = 'https://tenki.jp/forecast/3/16/4410/13101/'
data = scrape_weather.main(URL)
print(data)
```

dataの中身が  
`{'date': '12-12(日)', 'forecasts': [{'weather': '晴のち曇', 'high_temp': '16℃', 'high_temp_diff': '[-1]', 'low_24': '20%'}, 'wind_wave': '南西の風後北西の風'}]}`  
と、辞書とリストが入り組んでいると分かるので扱いやすいよう加工していきます。

```python:inform_weather.py
# 文字列加工
date = data['date']
forecasts = data['forecasts'][0]
rain_probability = forecasts['rain_probability']
# print(date)
# print(forecasts)
date = re.split('[-,(,)]', date.rstrip(')'))
# print(date)
high_temp_diff = int(forecasts['high_temp_diff'].replace('[','').replace(']',''))
low_temp_diff = int(forecasts['low_temp_diff'].replace('[','').replace(']',''))
```

使用した関数やメソッドは[こちら](https://note.nkmk.me/python-str-remove-strip/)を参考してください。  

printは上から  
`12-12(日)`  
`{'weather': '晴のち曇', 'high_temp': '16℃', 'high_temp_diff': '[-1]', 'low_temp': '8℃', 'low_temp_diff': '[+2]', 'rain_probability': {'00-06': '---', '06-12': '10%', '12-18': '20%', '18-24': '20%'}, 'wind_wave': '南西の風後北西の風'}`  
`['12', '12', '日']`  
と、出力されます。  

### 書き込み

続いて、これらを読み上げに最適になるよう整え、テキストファイルに書き込みしていきます。

```python:inform_weather.py
# forecast.txtに気象情報等を書き込み
weather_detail = []
weather_detail.append('おはようございます。')
weather_detail.append('今日は' + date[0] + '月' + date[1] + '日' + date[2] + '曜日、')
weather_detail.append('天気は' + forecasts['weather'] + '、')
# 気温
for est_temp, temp, diff in zip(
    ['最高気温', '最低気温'],
    [forecasts['high_temp'], forecasts['low_temp']],
    [high_temp_diff, low_temp_diff]):
    # '℃'は上手く読まれないため'度'に変更
    weather_detail.append(est_temp + 'は' + temp.rstrip('℃') + '度、')
    if diff > 0:
        weather_detail.append('昨日より' + str(diff) + '度だけ高いです。')
    elif diff == 0:
        weather_detail.append('昨日と同じです。')
    elif diff < 0:
        # str型の気温差の'-(マイナス)'を除去
        weather_detail.append('昨日より' + str(diff).lstrip('-') + '度だけ低いです。')
    else:
        weather_detail.append('昨日との温度差は上手く取得できませんでした。')
# 降水確率
if rain_probability['06-12'] != '---':  # '---'は数値がない時に出力される
    weather_detail.append('12時までの降水確率は' + rain_probability['06-12'] + '、')
else:
    weather_detail.append('12時までの降水確率は上手く取得できませんでした。')
if rain_probability['12-18'] != '---':
    weather_detail.append('18時までは' + rain_probability['12-18'] + 'です。')
else:
    weather_detail.append('18時までの降水確率は上手く取得できませんでした。')

path_txt = './script.txt'
with open(path_txt, mode='w', encoding='utf-8') as f:
    # 要素の末に改行をして書き込み
    f.write('\n'.join(weather_detail))
print('script.txtへの書き込みが完了しました。')
```

するとscript.txtに、以下のように出力されます。

```text:script.txt
おはようございます。
今日は12月12日日曜日、
天気は晴のち曇、
最高気温は16度、
昨日より-1度だけ低いです。
最低気温は8度、
昨日より2度だけ高いです。
12時までの降水確率は10%、
18時までは20%です。
```

これで原稿の完成です。

## VOICEROIDにしゃべらせる

[こちら](https://hgotoh.jp/wiki/doku.php/documents/voiceroid/assistantseika/assistantseika-001)をよく参照してAssistantSeikaのインストールなどをしてください。
[AssistantSeika の説明](https://hgotoh.jp/wiki/doku.php/documents/voiceroid/assistantseika/assistantseika-000)、[SeikaSay2の説明](https://hgotoh.jp/wiki/doku.php/documents/voiceroid/assistantseika/interface/cli/cli-002)を参考にしてください。セットアップ方法やオプションなどが詳しく説明されています。  
それを行った後、[Pythonで呼び出す例](https://hgotoh.jp/wiki/doku.php/documents/voiceroid/assistantseika/interface/cli/cli-005)を参考に以下のように書き加えます。

```python:inform_weather.py
# コマンド入力
import subprocess

cmd = 'seikasay2.exe -cid 2004 -f script.txt'
subprocess.call(cmd, shell=True)
```

このcmdに代入する値は私の場合の例です。

## 定期実行をセット(Windowsのみ)

Windows10に元から入っているタイムスケジューラを使用します。
[こちらの記事](https://atmarkit.itmedia.co.jp/ait/articles/1305/31/news049.html)に詳しい設定方法があるので本記事では省略します。  
ここで、スリープ状態からWindowsを立ち上げる際、セキュリティ上の理由で実行されないことに注意してください。  
以下、それでも実行できるようタスクを設定を変更していきます。  

タイムスケジューラを開いたら、青色に選択されてる本タスクを左クリックしてプロパティを開いてください。

![4a23233e13fa7e83d3d8f93ca9491e929e939b7d1319cea16c16e178512c7a39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1491833/9ec2067e-c74b-59a1-3286-26d88be65b81.png)

全般タグのセキュリティ オプションを以下のようにしてください。

![4e5610e943c33a5381212d00821c3d714c598e055b553f4741c79355ab94be91.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1491833/f71c8fd6-47c0-6760-2e72-a4e4721b2535.jpeg)


条件タグも以下のようにしてください。

![4b5cc8246fa439422b34b5182b66eb115c725aebfee7eb94ee3768793773a006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1491833/12fed1be-8be0-47b0-2378-b6cb57b0ed20.png)


## VOICEROIDを起動

どうやら読み上げるにはAssistantSeikaとVOICEROID2の両方ともが起動していなければならないようです。  
前者は最小化すると左下に目立たない形でほぼバックグラウンドアプリのようになるのですが、~~後者は実行の都度起動した方がよさそうです。~~  

~~起動させるには先ほどimportしたsubprocessモジュールを使います。  
主ファイルの続きに以下を書き加えます。~~  

```python:inform_weather.py
subprocess.Popen('C:/Program Files (x86)/AHS/VOICEROID2/VoiceroidEditor.exe')
```

~~と、自分のアプリのパスを指定してください。~~  

### <font color="Red">**問題が発生しました。** </font> 

主ファイルが終了する際にVOICEROID2を強制終了してしまうようで自身の実行ファイルが壊れてしまうようです。注意してください。

もし破損してしまった場合は、VOICEROID2_Editor.msiを起動しエディタの修復を行ってください。

なので、現状VOICEROID2も起動しっぱなしが一番安定します。

GUI操作系のpythonモジュールで解決しそうですが、自分は面倒臭いので常にアクティブにしています。

## 完成

```python:inform_weather.py
import re
import time

import scrape_weather


URL = 'https://tenki.jp/forecast/3/16/4410/13101/'
data = scrape_weather.main(URL)
print(data)

# 文字列加工
date = data['date']
forecasts = data['forecasts'][0]
rain_probability = forecasts['rain_probability']
print(date)
print(forecasts)
date = re.split('[-,(,)]', date.rstrip(')'))
print(date)
high_temp_diff = int(forecasts['high_temp_diff'].replace('[','').replace(']',''))
low_temp_diff = int(forecasts['low_temp_diff'].replace('[','').replace(']',''))

# forecast.txtに気象情報等を書き込み
weather_detail = []
weather_detail.append('おはようございます。')
weather_detail.append('今日は' + date[0] + '月' + date[1] + '日' + date[2] + '曜日、')
weather_detail.append('天気は' + forecasts['weather'] + '、')
# 気温
for est_temp, temp, diff in zip(
    ['最高気温', '最低気温'],
    [forecasts['high_temp'], forecasts['low_temp']],
    [high_temp_diff, low_temp_diff]):
    # '℃'は上手く読まれないため'度'に変更
    weather_detail.append(est_temp + 'は' + temp.rstrip('℃') + '度、')
    if diff > 0:
        weather_detail.append('昨日より' + str(diff) + '度だけ高いです。')
    elif diff == 0:
        weather_detail.append('昨日と同じです。')
    elif diff < 0:
        # str型の気温差の'-(マイナス)'を除去
        weather_detail.append('昨日より' + str(diff).lstrip('-') + '度だけ低いです。')
    else:
        weather_detail.append('昨日との温度差は上手く取得できませんでした。')
# 降水確率
if rain_probability['06-12'] != '---':  # '---'は数値がない時に出力される
    weather_detail.append('12時までの降水確率は' + rain_probability['06-12'] + '、')
else:
    weather_detail.append('12時までの降水確率は上手く取得できませんでした。')
if rain_probability['12-18'] != '---':
    weather_detail.append('18時までは' + rain_probability['12-18'] + 'です。')
else:
    weather_detail.append('18時までの降水確率は上手く取得できませんでした。')

path_txt = './script.txt'
# utf-8でエンコードすると読み上げの際都合がいいです
with open(path_txt, mode='w', encoding='utf-8') as f:
    # 要素の末に改行をして書き込み
    f.write('\n'.join(weather_detail))

# コマンド入力
import subprocess

subprocess.Popen('C:/Program Files (x86)/AHS/VOICEROID2/VoiceroidEditor.exe')
# 起動に時間がかかるので機体の性能に合わせて休止する秒数を設定
time.sleep(5)

cmd = 'C:/Users/lemon/Documents/assistantseika20211205u/SeikaSay2/SeikaSay2.exe -cid 2004 -f script.txt'
subprocess.call(cmd, shell=True)
```

## 終わりに

初投稿で拙い説明になってしまいましたが、参考にしていただければ幸いです。
SeikaSay2を用いた実装にあたって何度か不具合が起きましたが、公式の説明が詳しくて助かりました。  
誤字が多いですね。気軽に指摘していただければありがたいです。