GoogleHomeからTwitterの最新ツイートを喋らせる(RaspberryPi + Node-Red)※おまけ編

先日記載した下記記事のおまけ部分を分割＋α記事になります。
https://qiita.com/nyanko5656/items/ab821dc855d7e001d06b

上記を前提にご確認ください。

# 概要

上記記事の「Twitterの最新ツイートを喋らせる編」のままでは、認識している限り下記の課題点があります。

* 検索条件に設定したアカウントがRTしたTweetも喋る
* 検索条件に設定したアカウントがリプライをしたTweetも喋る
* URLもそのままつぶやくため、http://～(以下略)を「えいちてぃてぃぴーころんすらっしゅすらっしゅ(～以下略)」と喋る
* 短時間で連続でTweetされると全てを喋ることができない
* こちらが読ませたい読み方をしない

喋らせる内容次第となりますが、上記については個人的に課題となりました。
そのため、それぞれ下記の対応方針を決め、修正いたしました。

* 検索条件に設定したアカウントがRTした内容も喋る
　 → RTしたTweetは喋らない

* 検索条件に設定したアカウントがリプライをした内容も喋る
　 → リプライしたTweetは喋らない

→ 合わせて、RT＆リプライしたTweetは喋らない

* URLもそのままつぶやくため、http://～(以下略)を「えいちてぃてぃぴーころんすらっしゅすらっしゅ～(以下略)」と喋る
　 → URL情報は喋らない(喋らせる情報から削除する)

* 短時間で連続でTweetされると全てを喋ることができない
　 → 喋らせる情報に間隔を開ける

* こちらが読ませたい読み方をしない
　 → 特定文字列の際、除外／指定した読みに変換し喋らせる

それぞれの対応について記載いたします。

# RTしたTweetは喋らない

RTしたツイートは下記の通り読み込まれます。

```text:例：@TESTアカウントの呟きをRT
RT @TEST: これはRTテストです。http://192.168.10.00:1880/
```
この場合、「リツイート アットマークテスト　これはリツイートテストです。http://192.168.10.00:1880/ 」と読み上げます。

RTした文章は頭が必ず「RT @」となるため、
「switch」ノードを利用し、正規表現に当てはまるものは通過しないようにしました。

改善するために作成したノードのサンプルコードは下記の内容です。

```json:RT編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"RT編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"18a306ef.691769","type":"switch","z":"d1accf23.4ea74","name":"RTを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(RT @[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":380,"y":220,"wires":[["22f89520.ee71ea"]]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["18a306ef.691769"]]},{"id":"35f264bb.32d39c","type":"inject","z":"d1accf23.4ea74","name":"RT + URL","topic":"","payload":"RT @TEST: これはRTテストです。http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":280,"wires":[["18a306ef.691769"]]}]
```

# リプライしたTweetは喋らない

リプライをしたツイートは下記の通り読み込まれます。

```text:例：@TESTアカウントへリプライ
@TEST これはリプライてすとです。 http://192.168.10.00:1880/
```
この場合、「アットマークテスト　これはリプライテストです。http://192.168.10.00:1880/ 」と読み上げます。

リプライした文章は頭が必ず「@」となるため、
「switch」ノードを利用し、正規表現に当てはまるものは通過しないようにしました。

改善するために作成したノードのサンプルコードは下記の内容です。

```json:リプライ編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"リプライ編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["1f698940.aea477"]]},{"id":"35939c9a.b4c364","type":"inject","z":"d1accf23.4ea74","name":"リプライ + URL","topic":"","payload":"@TEST これはリプライてすとです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":280,"wires":[["1f698940.aea477"]]},{"id":"1f698940.aea477","type":"switch","z":"d1accf23.4ea74","name":"リプライを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(@[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":380,"y":220,"wires":[["22f89520.ee71ea"]]}]
```

# 合わせて、RT＆リプライしたTweetは喋らせない

上記2つはそれぞれ1つの「switch」ノード内に正規表現を記載することで可能としています。
そのため、正規表現を合体させることで、1つの「switch」ノードから題目の機能を実施することが可能です。

```json:RT+リプライ編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"RT+リプライ編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["44b23352.9d0dfc"]]},{"id":"35939c9a.b4c364","type":"inject","z":"d1accf23.4ea74","name":"リプライ + URL","topic":"","payload":"@TEST これはリプライてすとです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":300,"wires":[["44b23352.9d0dfc"]]},{"id":"44b23352.9d0dfc","type":"switch","z":"d1accf23.4ea74","name":"RTとリプライを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(RT @[A-Za-z0-9_]{1,15})|(@[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":390,"y":220,"wires":[["22f89520.ee71ea"]]},{"id":"3fdddbf0.b3fe54","type":"inject","z":"d1accf23.4ea74","name":"RT + URL","topic":"","payload":"RT @TEST: これはRTテストです。http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":260,"wires":[["44b23352.9d0dfc"]]}]
```

# URL情報は喋らない(喋らせる情報から削除する)

TweetではURLもそのまま記載されるため、http://～(以下略)を「えいちてぃてぃぴーころんすらっしゅすらっしゅ(～以下略)」と喋るようになっています。

「function」ノードを利用し、受け取った文字列からURLの正規表現に当てはまるものが合った場合、
該当箇所を削除するようにしました。

```json:URL編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"URL編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["b49b7607.992fe8"]]},{"id":"b49b7607.992fe8","type":"function","z":"d1accf23.4ea74","name":"URLを除外","func":"// var str = new RegExp('(https?(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+))','gi');\nvar str = new RegExp('(https?|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)','gi');\n\nmsg.payload = msg.payload.replace(str, '');\n\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":220,"wires":[["22f89520.ee71ea"]]}]
```

# 喋らせる情報に間隔を開ける

短時間で連続でTweetされると全てを喋ることができません。
そのため、表題の通り喋らせる情報に間隔を開けさせます。

「delay」ノードを利用し流量制限を行います。
Tweetの最大文字数である140文字が何秒になるか、試したところ30秒程度だったため、30秒制限を行っています。
このノードについて、もっと上手いやり方ががありましたらご教示いただけますと幸いです。

```json:遅延編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"遅延編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["a631bc04.7f486"]]},{"id":"e07660f3.ca55b","type":"inject","z":"d1accf23.4ea74","name":"RT + URL","topic":"","payload":"RT @TEST: これはRTテストです。http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":280,"wires":[["a631bc04.7f486"]]},{"id":"a631bc04.7f486","type":"delay","z":"d1accf23.4ea74","name":"連続Tweet制限","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"30","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":360,"y":220,"wires":[["22f89520.ee71ea"]],"info":"短時間で連続で投稿されるとすべてをつぶやくことができなくなるため、流量制限のため入れています。\n\n140文字が何秒になるか、試したところ30秒程度でした。"}]
```

# 特定文字列の際、除外／指定した読みに変換し喋らせる

利用し、判明している限りTweet読み込み時に
「＜」を「& lt;」へ
「＞」を「& gt;」へ
特殊文字を変換しています。
※書き換わってしまうので&の後ろにスペースを入れています。

この場合、読み上げが「あんどれるてぃー」「あんどじーてぃー」となってしまうので、
「function」ノードを利用し、除外処理を行うことにしました。

また、読み方をgoogle-home-notifierで認識できていない文字列は、
想定と違う読み方になりますので、判明次第、
「function」ノードにて文字列を想定する読み方へ変換することにしました。

```json:特定文字編.json
[{"id":"d1accf23.4ea74","type":"tab","label":"特定文字編","disabled":false,"info":""},{"id":"22f89520.ee71ea","type":"change","z":"d1accf23.4ea74","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":320,"wires":[["9d693b55.da0768"]]},{"id":"4d6fa7b8.e60428","type":"discovery","z":"d1accf23.4ea74","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["4bc9e9f.0acd718"]]},{"id":"6d4d9f1.64b0b6","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"4bc9e9f.0acd718","type":"function","z":"d1accf23.4ea74","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["6d4d9f1.64b0b6"]]},{"id":"49f64497.f20f4c","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"7e24dc67.5e1cd4","type":"cast-to-client","z":"d1accf23.4ea74","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":700,"y":320,"wires":[["d1ac5dd.a18c0a"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"9d693b55.da0768","type":"change","z":"d1accf23.4ea74","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":320,"wires":[["7e24dc67.5e1cd4"]]},{"id":"d1ac5dd.a18c0a","type":"debug","z":"d1accf23.4ea74","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":860,"y":320,"wires":[]},{"id":"62f929e5.b24578","type":"comment","z":"d1accf23.4ea74","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"265e78b8.e276b8","type":"inject","z":"d1accf23.4ea74","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["94950fea.66e8"]]},{"id":"94950fea.66e8","type":"function","z":"d1accf23.4ea74","name":"特定文字を除外","func":"// < を &lt; > を &gt; と呼ぶため除外\nvar str = new RegExp('(&lt;)|(&gt;)','gi');\n\nmsg.payload = msg.payload.replace(str, '');\n\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":220,"wires":[["22f89520.ee71ea"]]},{"id":"e37282df.ec5ab","type":"function","z":"d1accf23.4ea74","name":"特定文字を変更","func":"var str = new RegExp('○○','gi');\n\nmsg.payload = msg.payload.replace(str, '伏せ字');\n\nreturn msg;","outputs":1,"noerr":0,"x":600,"y":220,"wires":[[]]},{"id":"89fd41fc.abf0c","type":"inject","z":"d1accf23.4ea74","name":"< > 込み発言","topic":"","payload":"&lt;お知らせ&gt; こちらはテストになります。","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":280,"wires":[["94950fea.66e8"]]}]
```

# 総合

ここまでの課題点に対して修正をまとめたものが下記サンプルコードになります。
ご活用いただけますと幸いです。

```json:おまけ編.json
[{"id":"c24d16a7.123938","type":"tab","label":"おまけ","disabled":false,"info":""},{"id":"b6d6a5c8.da69e8","type":"function","z":"c24d16a7.123938","name":"URLを除外","func":"// var str = new RegExp('(https?(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+))','gi');\nvar str = new RegExp('(https?|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+)','gi');\n\nmsg.payload = msg.payload.replace(str, '');\n\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":240,"wires":[["a19b3d2e.58943"]]},{"id":"da17fd5d.3ed0a","type":"change","z":"c24d16a7.123938","name":"出力変換","rules":[{"t":"set","p":"message","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":400,"wires":[["8d6f51c7.01498"]]},{"id":"a342b59f.6473f8","type":"switch","z":"c24d16a7.123938","name":"RTとリプライを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(RT @[A-Za-z0-9_]{1,15})|(@[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":410,"y":300,"wires":[["b6d6a5c8.da69e8"]]},{"id":"89eddde1.e7656","type":"discovery","z":"c24d16a7.123938","name":"","topic":"","service":"_googlecast._tcp","x":140,"y":100,"wires":[["29d5c7b4.a4d418"]]},{"id":"79df36a7.66b818","type":"debug","z":"c24d16a7.123938","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":100,"wires":[]},{"id":"29d5c7b4.a4d418","type":"function","z":"c24d16a7.123938","name":"add to flow.castdevice","func":"const name = msg.payload.txtRecord.fn;\nconst ip = msg.payload.addresses[0];\nmsg.name = name;\nmsg.ip = ip;\nvar castdevice = flow.get(\"castdevice\");\nif (castdevice === undefined) {\n   castdevice = {};\n}\ncastdevice[name] = ip;\nflow.set(\"castdevice\", castdevice);\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":100,"wires":[["79df36a7.66b818"]]},{"id":"4fccf840.3c7548","type":"comment","z":"c24d16a7.123938","name":"GoogleHomeの名前とIPのペアをflow.castdeviceに入れる","info":"","x":250,"y":60,"wires":[]},{"id":"21e4c290.d9b76e","type":"cast-to-client","z":"c24d16a7.123938","name":"Cast端末","url":"","contentType":"","message":"","language":"","ip":"","port":"","volume":"","x":780,"y":400,"wires":[["e5acb692.693ab8"]],"icon":"node-red-contrib-cast/google-home-mini2.svg"},{"id":"8d6f51c7.01498","type":"change","z":"c24d16a7.123938","name":"ファミリールーム","rules":[{"t":"set","p":"ip","pt":"msg","to":"castdevice[\"ファミリー ルーム\"]","tot":"flow"},{"t":"set","p":"language","pt":"msg","to":"ja","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":400,"wires":[["21e4c290.d9b76e"]]},{"id":"e5acb692.693ab8","type":"debug","z":"c24d16a7.123938","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"message","targetType":"msg","x":940,"y":400,"wires":[]},{"id":"e41ae919.ce5938","type":"comment","z":"c24d16a7.123938","name":"GoogleHomeを喋らせる","info":"","x":150,"y":160,"wires":[]},{"id":"ca4d1198.2c50e","type":"switch","z":"c24d16a7.123938","name":"RTを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(RT @[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":380,"y":220,"wires":[["b6d6a5c8.da69e8"]]},{"id":"6d5d9c86.a78884","type":"inject","z":"c24d16a7.123938","name":"通常 ＋ URL","topic":"","payload":"これは通常つぶやきテストです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":220,"wires":[["ca4d1198.2c50e"]]},{"id":"82dcf04c.1580a","type":"inject","z":"c24d16a7.123938","name":"RT + URL","topic":"","payload":"RT @TEST: これはRTテストです。http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":280,"wires":[["34200124.98c01e"]]},{"id":"271ad392.10769c","type":"inject","z":"c24d16a7.123938","name":"リプライ + URL","topic":"","payload":"@TEST これはリプライてすとです。 http://192.168.10.00:1880/","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":340,"wires":[["ca4d1198.2c50e"]]},{"id":"34200124.98c01e","type":"switch","z":"c24d16a7.123938","name":"リプライを読まない","property":"payload","propertyType":"msg","rules":[{"t":"regex","v":"^(?!(@[A-Za-z0-9_]{1,15})).*","vt":"str","case":false}],"checkall":"true","repair":false,"outputs":1,"x":400,"y":260,"wires":[["b6d6a5c8.da69e8"]]},{"id":"941b2b0e.afc528","type":"delay","z":"c24d16a7.123938","name":"連続Tweet制限","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"30","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1040,"y":240,"wires":[["da17fd5d.3ed0a"]],"info":"短時間で連続で投稿されるとすべてをつぶやくことができなくなるため、流量制限のため入れています。\n\n140文字が何秒になるか、試したところ30秒程度でした。"},{"id":"a37d1a06.e94308","type":"inject","z":"c24d16a7.123938","name":"< > 込み発言","topic":"","payload":"&lt;お知らせ&gt; こちらはテストになります。","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":400,"wires":[["ca4d1198.2c50e"]]},{"id":"a19b3d2e.58943","type":"function","z":"c24d16a7.123938","name":"特定文字を除外","func":"// < を &lt; > を &gt; と呼ぶため除外\nvar str = new RegExp('(&lt;)|(&gt;)','gi');\n\nmsg.payload = msg.payload.replace(str, '');\n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":240,"wires":[["941b2b0e.afc528"]]},{"id":"3ef8aa78.fe1f66","type":"function","z":"c24d16a7.123938","name":"特定文字を変更","func":"var str = new RegExp('○○','gi');\n\nmsg.payload = msg.payload.replace(str, '伏せ字');\n\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":280,"wires":[[]]}]
```