20200924のNode.jsに関する記事は7件です。

Redisの概要とNode.jsでの使い方

Redisの概要

redisはインメモリのNoSQLのデータベースです。単純なKey-Valueストアのように文字列だけでなく、様々な種類の値をサポートしています。
Webアプリケーションのスケーリングに便利です。

https://redis.io/

環境構築

dockerが利用できるなら、それで環境を作成するのが楽です。

 docker run --name redis -p 6379:6379 -d -v /Users/hoge/dev/node/socketio/data:/data redis:latest

dockerで作成したコンテナでredis-cliを使用するには以下のようにします。

hoge@macpro socketio % docker exec -it redis /bin/bash
root@8f9888c8bb76:/data# redis-cli
127.0.0.1:6379> 

ホストとポートを指定したい場合は以下のようにします。デフォルトではlocal:6379に接続しています。

redis-cli -h localhost -p 6379

Redisのデータについて

Redisは単純なキーと値だけでなくリストやハッシュなどが使えます。
この章は以下を元に記述されていますので英語読める人は以下読んだほうがいいと思います。
https://redis.io/topics/data-types-intro
https://redis.io/topics/data-types

また、この章で記述されているコマンドはredis-cliで実行したものとなります。

キー

Redisのキーはバイナリデータを正しく扱うことができます。"foo"のような文字列からJPEGファイルの中身まで任意のバイナリをキーとして使えます。空の文字列も有効なキーです。
キーに関するルールは以下の通りです。

  • 非常に長いキーはお勧めしません。例えば1024バイトのキーはメモリの点だけでなく、データセット内の検索にコストがかかります。
  • 多くの場合、非常に短いキーはお勧めしません。"user:1000:followers"と記載する代わりに"u1000flw"とかく意味はほとんどありません。前者は読みやすく、使用されるスペースもわずかです。
  • スキーマーに固執するようにしましょう。とえば、"user:1000"のように、 "object-type:id"のような形式にするのは良いアイディアです。  キーの最大は512MBです。

キーに関係するコマンド

コマンド名 概要
DEL キーを削除する
UNLINK 別スレッドで非同期にキーを削除する。DELと同じだが非ブロッキング。
EXISTS キーが存在するか確認する
TYPE キーに格納されている値のタイプを取得する
KEYS 指定のパターンに一致するキーの一覧を取得する
TOUCH キーの最終アクセス時刻を変更する
RENAME キーの名前を変更する
RENAMENX 新しいキーが存在しない場合のみキーの名前を変更する
DUMP 指定されたキーに格納されている値のシリアル化されたバージョンを取得する
OBJECT Redisオブジェクトの内部を検査する
MOVE キーを別のデーターベースに移動する
MIGRATE キーをRediusインスタンスから別のインスタンスに転送する
RANDOMKEY キースペースからランダムなキーを返す
RESTORE 以前にDUMPしたシリアル化された値を使用してキーを作成する
SORT リスト、セット、ソート済セットの要素を並び替える
WAIT 別の接続コンテキストで送信された全ての書き込みコマンドの同期複製を待つ
SCAN キースペースを段階的に反復する

文字列

文字列は最も基本的な種類のRedis値です。Redis文字列はバイナリを正しく扱うことができ、単純な文字列から、JPEG画像など、あらゆる種類のデータを含めることができます。文字列値の最大は512MBです。

単純なサンプル

SETコマンドGETコマンドを使用することで文字列の値を格納、取得することができます。

redis-cliを使用して簡単なキーと値を格納するサンプルを以下に示します。

使用例
 localhost:6379> set mykey somevalue
 OK
 localhost:6379> get mykey
 "somevalue"
 localhost:6379> 

規定ではSETコマンドではキーがすでに存在している場合は、既存のキーに格納されている値を新しい文字列に置き換えることができます。
すでに存在するキーの場合は失敗させる、または存在するキーの場合のみ更新させる場合にはnxオプション、xxオプションを以下のようにして使用します。

使用例
 すでに存在するキーの場合は失敗させる
 localhost:6379> set mykey newval nx
 (nil)

 すでに存在するキーがある場合のみ成功させる
 localhost:6379> set mykey newval xx
 OK

GETコマンドではキーが存在しない場合はnilを返します。また、格納されている値が文字列ではない場合、エラーとなります。

文字列に関係するコマンド

文字列に関係するコマンドは以下の通りです。
https://redis.io/commands/#string

コマンド名 概要
APPEND 指定のキーの値に指定の文字列を追記する
GET 指定のキーの文字列値を取得する
SET キーの文字列値を設定する
SETEX 有効期限を秒で指定してキーの文字列値を設定する
SETNX キーが存在しない場合にキーの文字列値を設定する
PSETEX 有効期限をミリ秒で指定してキーの文字列値を設定する
GETSET キーの文字列値を設定し、古い値を取得する
MGET 指定された複数のキーの値を取得する
MSET 指定された複数のキーと値を設定する
MSETNX キーが存在しない複数のキーと値を設定する
GETRANGE 指定のキーの値の部分文字列を取得する
SETRANGE 指定のキーの値の部分文字列を設定する
STRLEN キーの値の文字列の長さを取得する
APPENDコマンド

指定のキーの値に指定の文字列を追記します。

使用例
127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> append mykey world
(integer) 10
127.0.0.1:6379> get mykey
"helloworld"
GETSETコマンド

GETSETコマンドを使用することで新しい値を設定して、古い値を取得することが可能です。

使用例
 localhost:6379> set mykey test
 OK
 localhost:6379> getset mykey testnew
 "test"
 localhost:6379> get mykey
 "testnew"
MSETとMGETコマンド

複数のキーの値を同時に設定、取得するにはMSET,MGETコマンドを使用します。

使用例
 localhost:6379> mset a 10 b 30 c 30
 OK
 localhost:6379> mget a b c
 1) "10"
 2) "30"
 3) "30"
 localhost:6379> get a
 "10"
 localhost:6379> get b
 "30"
 localhost:6379> get c
 "30"

数値として取り扱うコマンド

rediusの文字列値が整数または浮動小数として変換できる場合は以下のコマンドが使用できます。

コマンド名 概要
INCR キーの整数値を1増やす
INCRBY キーの整数値を指定された数増やす
DECR キーの整数値を1減らす
DECRBY キーの整数値を指定された数減らす
INCRBYFLOAT キーのfloat値を指定された数増やす

これらのコマンドはアトミックな操作となっており、同じキーに対して複数のクライアントが同時に操作しても競合状態にはなりません。例えばINCRコマンドを使用してクライアントAが「10」を読み取り、同時にクライアントBが「10」を読み込んで、両方とも「11」に設定することはなく、最終値は常に「12」になります。

使用例
 localhost:6379> set counter 100
 OK
 localhost:6379> incr counter
 (integer) 101
 localhost:6379> incr counter
 (integer) 102
 localhost:6379> incr counter
 (integer) 103
 localhost:6379> set counter 100
 OK
 localhost:6379> incrby counter 5
 (integer) 105
 localhost:6379> decr counter
 (integer) 104
 localhost:6379> decrby counter 5
 (integer) 99

リスト

Redisリストは、挿入順で並べ替えられた、文字列のリストです。リストに対して先頭(左)または後方(右)に値を追加できます。
リストの最大長は4294967295です。

単純なサンプル

RPUSHコマンドでリストの後方に文字列を追加し、LPUSHコマンドでリストの先頭に文字列を取得します。リストの内容を取得するにはLRANGEコマンドで範囲を指定して取得します。

使用例
127.0.0.1:6379> rpush mylist AA
(integer) 1
127.0.0.1:6379> rpush mylist BB
(integer) 2
127.0.0.1:6379> lpush mylist first
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "AA"
3) "BB"

リストに関係するコマンド

https://redis.io/commands#list

コマンド名 概要
BLPOP リストの最初の要素を削除して取得するまたは、要素が使用可能になるまでブロックする
BRPOP リストの最後の要素を削除して取得するまたは、要素が使用可能になるまでブロックする
BRPOPLPUSH リストから要素をポップして、それを別のリストにプッシュして返す。またはいずれかが利用可能になるまでブロックする
LINDEX インデックスでリストから要素を取得する
LINSERT リスト内の別の要素の前後に要素を挿入する
LLEN リストの長さを取得する
LPOP リストの最初の要素を削除して取得する
LPOS リストの一致する要素のインデックスを取得する
LPUSH リストに1つまたは複数の要素をリストの先頭に追加する
LPUSHX リストが存在する場合のみ、リストの先頭に要素を追加する
LRANGE リストから指定の範囲の要素を取得する
LREM リストから要素を削除する
LSET リスト内の要素の値をインデックスで指定する
LTRIM リストを指定された範囲でトリムします。(指定した範囲外の要素を削除する)
RPOP リストの最後の要素を削除して取得する
RPOPLPUSH リストの最後の要素を削除して別のリストの前に追加して返す
RPUSH リストの末尾に1つまたは複数の要素を追加する
RPUSHX リストが存在する場合にのみ、リストに要素を追加する

セット

文字列の順序付けられていないコレクションです。
コレクション内のメンバーは同じメンバーを許可しないという性質を持っています。
セット内のメンバーの数は最大4294967295になります。

簡単なサンプル

セットを使用するにはSADDコマンドで新しい要素を追加して、SMEMBERSコマンドでセットの要素を取得します。

127.0.0.1:6379> sadd myset 3 1 2 3
(integer) 3
127.0.0.1:6379> smembers myset
1) "1"
2) "2"
3) "3"

この例では重複する要素が登録されていないことをが確認できます。
Redisのセットでは要素の順番については保証がないため、SMEMBERSコマンドは呼び出しのたびに任意の順番で返却されます。

セット用コマンド

https://redis.io/commands#set

コマンド名 概要
SADD セットにメンバーを追加する
SCARD セットのメンバー数を取得する
SDIFF 複数のセットの差を取得する
SDIFFSTORE 複数のセットの差を取得して結果セットをキーに格納する
SINTER 複数のセットを交差させる
SINTERSTORE 複数のセットを交差させ、その結果セットをキーに格納する
SISMEMBER 指定された値がセットのメンバーであるか判定する
SMEMBERS セット内の全てのメンバーを取得する
SMOVE メンバーをあるセットから別のセットに移動する
SPOP セットから1つまたは複数のメンバーをランダムに削除して返す
SRANDMEMBER セットから1つまたは複数のメンバーをランダムに取得する
SREM セットから1つ以上のメンバーを削除する
SUNION 複数のセットを追加する
SUIONSCTORE 複数のセットを追加して、結果のセットをキーに格納する
SDIFFとSINTERコマンドのサンプル

SINTERコマンドは最初に指定したセットと後に指定したセットと同じメンバーを取得します。
SDIFFコマンドでは最初に指定したセットから、共有するメンバーを除いたメンバーを返します。

127.0.0.1:6379> sadd key1 a b c
(integer) 3
127.0.0.1:6379> sadd key2 c d e
(integer) 3
127.0.0.1:6379> sdiff key1 key2
1) "a"
2) "b"
127.0.0.1:6379> SINTER key1 key2
1) "c"

ソート済セット

Redisセットと同様に、文字列の繰り返しのないコレクションです。違いは、並べ替えられたセットのすべてのメンバーがスコアに関連付けられていることです。これは、並べ替えられたセットを最小から最大のスコアまで順番に並べるために使用されます。メンバーは一意ですが、スコアは繰り返される場合があります。

簡単なサンプル

ZADDコマンドを使用してスコアと値を指定してソート済セットに追加します。
ソート済セットの内容はZRANGEコマンドを使用してスコアの小さい順で取得することができます。この際、withscoresオプションを使用するとスコアも同時に取得することができます。
スコアの大きいの順番で取得するにはZREVRANGEコマンドを取得できます。

127.0.0.1:6379> zadd hackers 1940 "Alan Kay"
(integer) 1
127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson"
(integer) 1
127.0.0.1:6379> zadd hackers 1953 "Richard Stallman"
(integer) 1
127.0.0.1:6379> zadd hackers 1949 "Anita Borg"
(integer) 1
127.0.0.1:6379> zadd hackers 1965 "Yukihiro Matsumoto"
(integer) 1
127.0.0.1:6379> zadd hackers 1914 "Hedy Lamarr"
(integer) 1
127.0.0.1:6379> zadd hackers 1916 "Claude Shannon"
(integer) 1
127.0.0.1:6379> zadd hackers 1969 "Linus Torvalds"
(integer) 1
127.0.0.1:6379> zadd hackers 1912 "Alan Turing"
(integer) 1
127.0.0.1:6379> zrange hackers 0 -1
1) "Alan Turing"
2) "Hedy Lamarr"
3) "Claude Shannon"
4) "Alan Kay"
5) "Anita Borg"
6) "Richard Stallman"
7) "Sophie Wilson"
8) "Yukihiro Matsumoto"
9) "Linus Torvalds"
127.0.0.1:6379> zrange hackers 0 -1 withscores
 1) "Alan Turing"
 2) "1912"
 3) "Hedy Lamarr"
 4) "1914"
 5) "Claude Shannon"
 6) "1916"
 7) "Alan Kay"
 8) "1940"
 9) "Anita Borg"
10) "1949"
11) "Richard Stallman"
12) "1953"
13) "Sophie Wilson"
14) "1957"
15) "Yukihiro Matsumoto"
16) "1965"
17) "Linus Torvalds"
18) "1969"
127.0.0.1:6379> zrevrange hackers 0 -1
1) "Linus Torvalds"
2) "Yukihiro Matsumoto"
3) "Sophie Wilson"
4) "Richard Stallman"
5) "Anita Borg"
6) "Alan Kay"
7) "Claude Shannon"
8) "Hedy Lamarr"
9) "Alan Turing"

ソート済セットで使用するコマンド

コマンド名 概要
BZPOPMIN 1つ以上のソート済セットからスコアが最も低いメンバーを削除して取得するまたは、使用可能になるまでブロックする
BZPOPMAX 1つ以上のソート済セットからスコアが最も高いメンバーを削除して取得するまたは、使用可能になるまでブロックする
ZADD ソート済セットに1つ以上のメンバーを追加するか、すでに存在する場合はそのスコアを更新する
ZCARD ソート済セットのメンバー数を取得する
ZCOUNT 指定されたスコアの範囲でソート済セットのメンバー数を取得する
ZINCRBY ソート済のメンバーのスコアをインクリメントする
ZINTERSTORE 複数のソート済セットを交差させ、結果のソート済セットに新しい値を格納する
ZLEXCOUNT 与えられた辞書識範囲内でソート済メンバー数を取得する
ZPOPMAX ソート済セットで最高スコアを持つメンバーを削除して取得する
ZPOPMIN ソート済セットで最低スコアを持つメンバーを削除して取得する
ZRANGE インデックスでソート済セットを取得する
ZRANGEBYLEX 辞書式の範囲でソート済セットのメンバーを取得する
ZREVRANGEBYLEX 辞書式の範囲でソート済セットのメンバーを上位から下位の文字列の順に取得する
ZRANGEBYSCORE スコアの範囲でソート済のセットのメンバーを取得する
ZRANK ソート済セットのインデックスを決定する
ZREM ソート済セットから1つ以上のメンバーを削除する
ZREMRANGEBYLEX 指定された辞書式範囲でソート済セットの全てのメンバーを削除する
ZREMRANGEBYRANK 指定されたインデックス内のソート済セットの全てのメンバーを削除する
ZREMRANGEBYSCORE 指定されたスコア内のソート済セットの全てのメンバーを削除する
ZREVRANGE ソート済セットをスコアの高い順に取得する
ZREVRANGEBYSCORE スコアの範囲内でソート済セットをスコアの高い順に取得する
ZREVRANK ソート済セットのインデックスを決定する。スコアは高い順に並べられる
ZSCORE ソート済セット中のメンバーに関連づけられたスコアを取得する
ZUNIONSTORE 複数のソート済セットを追加して結果のソート済セットに新しいキーを追加する
ZSCAN ソートされたセット要素と関連するスコアを段階的に反復する
辞書式スコア

ZRANGEBYLEXコマンドではソート済セットの要素が全て同じスコアで挿入されたと仮定して、範囲を辞書式で取得することができます。
辞書式の範囲の開始と終了を指定する際、「[」または「(」で始まる必要があります。
「[」の場合、後続の文字列を含んだ値を認めますが、「(」の場合は認めません。
また、「-」を指定すること負の方向で無限である文字列を現し、「+」を指定することで正に無限の文字列を現します。すなわち「ZRANGEBYLEX myzset - +」を指定した場合、全ての要素が同じスコアだとして、全てのメンバーを返します。

127.0.0.1:6379> ZADD myzset 0 a 0 b 0 c 0 d 0 e 0 f 0 g
(integer) 0
127.0.0.1:6379> ZRANGEBYLEX myzset - +
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
127.0.0.1:6379> ZRANGEBYLEX myzset - [f
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
127.0.0.1:6379> ZRANGEBYLEX myzset - (f
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
127.0.0.1:6379> ZRANGEBYLEX myzset [c (f
1) "c"
2) "d"
3) "e"
127.0.0.1:6379> ZRANGEBYLEX myzset (c (f
1) "d"
2) "e"

ハッシュ

ハッシュは、フィールドと値のペアを使用してオブジェクトを表すことができます。

簡単な例

HSETコマンドを使用してキーに紐づくハッシュ値にフィールドを設定します。
HGETALLコマンドを使用して指定のキーのハッシュの全てのフィールドとその値を取得します。
HGETコマンドを使用することでキーとハッシュのフィールドを指定して、その文字列値を取得します。

127.0.0.1:6379> hset user:1000 username "Joe"
(integer) 1
127.0.0.1:6379> hset user:1000 birthyear 1945
(integer) 1
127.0.0.1:6379> hset user:1000 verified 1
(integer) 1
127.0.0.1:6379> hgetall user:1000
1) "username"
2) "Joe"
3) "birthyear"
4) "1945"
5) "verified"
6) "1"
127.0.0.1:6379> hget user:1000 username
"Joe"
127.0.0.1:6379> hget user:1000 birthyear
"1945"
127.0.0.1:6379> hget user:1000 verified
"1"

ハッシュに関係するコマンド

コマンド名 概要
HDEL 1つい上のハッシュフィールドを削除する
HEXISTS ハッシュフィールドが存在するか確認する
HGET ハッシュフィールドの値を取得する
HGETALL ハッシュの全てのフィールドと値を取得する
HINCRBY ハッシュフィールドの整数値を指定された数だけインクリメントする
HINCRBYFLOAT ハッシュフィールドのfloat値を指定された数だけインクリメントする
HKEYS ハッシュ内の全てのフィールドを取得する
HLEN ハッシュのフィールド数を取得する
HMGET 指定された全てのハッシュフィールドの値を取得する
HMSET 複数のハッシュフィールドを複数の値に設定する
HSET ハッシュフィールドの文字列値を設定する
HSETNX フィールドが存在しない場合のみ、ハッシュフィールドの値を設定する。
HSTRLEN ハッシュフィールドの値の長さを取得する
HVALS ハッシュの全ての値を取得する
HSCAN ハッシュフィールドと関連する値を段階的に反復する

ビットマップ

ビット操作を行うコマンド

redisの文字列値はバイナリデータを取り扱うことができます。いくつかのコマンドはそのバイナリのビットを操作することが可能です。

コマンド名 概要
BITCOUNT 文字列中の1に設定されたビット数を数える
BITFIELD 文字列に対して任意のビットフィールド整数演算を行う
BITOP 文字列間でビット演算を行う
BITPOS 文字列の最初のビットセットまたはビットクリアを見つける
GETBIT キーの文字列における指定のオフセットでのビットを取得する
SETBIT キーの文字列における指定のオフセットでのビットを設定する

Redisキーの有効期限

Redisのキーには有効期限を設定することができます。特定の時間が経過するとキーはDELコマンドを実行した時のように自動的に破棄されます。

コマンド名 概要
EXPIRE キーの存在時間を秒で設定する
EXPIREAT キーの有効期限をUNIXタイムスタンプとして設定する
PERSIST キーから有効期限を削除する
PEXPIRE キーの有効期間をms単位で設定する
PEXPIREAT キーの有効期限をmsで指定されたUNIXタイムスタンプとして設定する
TTL キーの生存時間を秒単位で取得する
PTTL キーの存在時間をms単位で取得する

単純なサンプル

有効期限の設定には以下のようにEXPIREコマンドを使用します。

 localhost:6379> set mykey test
 OK
 localhost:6379> expire mykey 5
 (integer) 1
 5秒以内・・・
 localhost:6379> get mykey
 "test"
 5秒経過・・・
 localhost:6379> get mykey
 (nil)

また同じことはSETコマンドのexオプションで指定することもできます。

 localhost:6379> set mykey sample ex 5
 OK
 localhost:6379> get mykey
 "sample"
 localhost:6379> get mykey
 (nil)

有効期限の残り秒数またはミリ秒を取得するにはTTLコマンドまたはPTTLコマンドを使用します。

 localhost:6379> set mykey sample ex 10
 OK
 localhost:6379> pttl mykey
 (integer) 7982
 localhost:6379> ttl mykey
 (integer) 4

Redis-cli

Redis-cliはRedisにコマンドを送信し、サーバーから送信された応答をターミナルから直接読み取ることができるシンプルなプログラムであるRedisコマンドラインインターフェイスです。

https://redis.io/topics/rediscli

redis-cli 6.0.8ではコマンドラインの引数として以下を設定できます。

引数 説明
-h サーバーのホスト名 (default: 127.0.0.1).
-p サーバーのポート (default: 6379).
-s サーバーのソケット(hostnameとportを上書きする)
-a サーバーに接続するときに使用するパスワード。 REDISCLI_AUTH環境変数を使用して、このパスワードをより安全に渡すこともできる。(両方が使用されている場合、この引数が優先される)
--user ACLスタイル'AUTH username pass'を送信するために使用される.-aが必要.
--pass 新しい--userオプションと一貫性を保つために使用される-aのエイリアス
--askpass STDINからマスク付きのパスワードを入力するようユーザーに強制する。もしこの引数が使われた場合, '-a' と REDISCLI_AUTH環境変数は無視される
-u サーバーURI.
-r 特定のコマンドをN回繰り返す
-i -rオプションを使用した場合にコマンドごとに 秒待機する。次のように1秒未満の時間を指定することが可能。例: 0.1.
-n データベースナンバー
-3 RESP3 protocol modeでセッションを開始する
-x 最後の引数をSTDINから読み込む
-d Multi-bulk delimiter in for raw formatting (default: \n).
-c クラスターモードを有効にする(follow -ASK and -MOVED redirections).
--tls 安全なTLS接続を確立します。
--sni TLSサーバー名を表示する
--cacert 検証するCA証明書ファイル。
--cacertdir 信頼できるCA証明書が保存されているディレクトリ。cacertとcacertdirのどちらも指定されていない場合、デフォルトのシステム全体の信頼されたルート証明書の構成が適用される。
--cert 認証に使用するクライアント証明書
--key 認証に使用する秘密鍵ファイル
--raw 返信に未加工のフォーマットを使用します(STDOUTがttyでない場合のデフォルト)。
--no-raw STDOUTがttyでない場合でもフォーマットされた出力を強制します。
--csv CSV形式で出力する
--stat server: mem, clientsなどの統計情報を出力し続ける
--latency Enter a special mode continuously sampling latency.If you use this mode in an interactive session it runs forever displaying real-time stats. Otherwise if --raw or --csv is specified, or if you redirect the output to a non TTY, it samples the latency for 1 second (you can use -i to change the interval), then produces a single output and exits.
--latency-history --latencyと似ていますが、追跡の遅延は時間とともに変化する。デフォルトの時間間隔は15秒。 -iを使用して変更します。
--latency-dist Shows latency as a spectrum, requires xterm 256 colors.Default time interval is 1 sec. Change it using -i.
--lru-test Simulate a cache workload with an 80-20 distribution.
--replica マスターから受信したコマンドを示すレプリカをシミュレートする
--rdb RDBダンプをリモートサーバーからローカルファイルに転送する。
--pipe raw Redisプロトコルをstdinからサーバーに転送します。
--pipe-timeout --pipeモードでは、すべてのデータの送信後、秒以内に応答が受信されない場合はエラーで中止する。デフォルトのタイムアウト:30。永久に待機するには0を使用する。
--bigkeys 多くの要素(複雑さ)を持つキーを探すRedisキーのサンプル。
--memkeys 多くのメモリを消費するキーを探すRedisキーのサンプル。
--memkeys-samples 大量のメモリを消費するキーを探すRedisキーのサンプル。サンプリングするキー要素の数を定義する
--hotkeys ホットキーを探すサンプルRedisキー。maxmemory-policyが* lfuの場合にのみ機能します。
--scan SCANコマンドを使用した全てのキーのリスト.
--pattern --scan, --bigkeys or --hotkeys オプションを使用した場合のキーのパターン (default: *).
--intrinsic-latency 固有のシステム遅延を測定するテストを実行する。テストは指定された秒数実行される
--eval でLuaスクリプトを使用してEVALコマンドを送信する
--ldb --evalと共に使用すると、Redis Luaデバッガーが有効になる
--ldb-sync-mode --ldbと同様ですが、同期Luaデバッガーを使用します。このモードでは、サーバーはブロックされ、スクリプトの変更はサーバーのメモリからロールバックされない
--cluster [args...] [opts...] Cluster Managerコマンドと引数
--verbose Verboseモード.
--no-auth-warning Don't show warning message when using password on command line interface.
--help ヘルプの出力
--version バージョンの出力

実行例

コマンドを指定してredis-cliを実行する例

root@8f9888c8bb76:/data# redis-cli set mykey test
OK
root@8f9888c8bb76:/data# redis-cli get mykey     
"test"

コマンドを複数回繰り返す例

root@8f9888c8bb76:/data#  redis-cli -r 100 lpush mylist x

コマンドのモニターを行う例
monitorコマンドを実行することでRedisの操作を監視できます。

root@8f9888c8bb76:/data#  redis-cli monitor
OK
1600950437.950595 [0 172.17.0.1:48396] "info"
1600950437.955578 [0 172.17.0.1:48396] "set" "testkey2" "test"
1600950437.961197 [0 172.17.0.1:48396] "get" "testkey2"

node.jsでの使用例

Node Redis

Node Redisを使用することでnode.jsからredisの操作が行えます。
https://github.com/NodeRedis/node-redis

インストール方法

npm install --save redis

サンプルコード
以下のサンプルは文字列値を追加して、それを取得するサンプルです。また、redisの操作についてモニタリングをしています。

test1.js
const redis = require('redis')
const client = redis.createClient(6379, 'localhost')
client.monitor(function(err, res) {
  console.log('client.monitor', err, res);
});
client.on('monitor', function(time, args, rawReply) {
  console.log('on monitor', time, args, rawReply);
});

client.set('testkey2', "test", (err, res)=> {
  console.log(err, res)
  client.get('testkey2', (error, result)=> {
    console.log(error, result);
    client.end(true);
  })
})

IOREDIS

ioredisはredisの機能をフルサポートしており、 Cluster, Sentinel, Streams, Pipeliningなどが使用できます。
またNode RedisではサポートしていないPromisでの記述をサポートしています。

https://github.com/luin/ioredis

2020年9月時点のIOREDISとNode RedisのNPMでのトレンドの比較は以下の通りです。
image.png
https://www.npmtrends.com/ioredis-vs-redis

インストール方法

npm install --save ioredis

サンプルコード

test2.js
const Redis = require("ioredis");
const redis = new Redis();
let monitor = null;
redis.monitor((err, result)=> {
  monitor = result
  monitor.on('monitor', (time, args)=>{
    console.log('on monitor', time, args)
  })
})
redis.pipeline()
     .set('mykey', 'hogehoge')
     .get('mykey', (err, result)=>{
       console.log('get:', err, result);
     })
     .exec((err, result)=> {
       console.log('exec:', err, result)
       monitor.disconnect()
       redis.disconnect()
     })

redisを使用したsocket.ioのスケールアウトの例

Socket.ioサーバをスケールアウトする(Redisを使った複数プロセス間でのブロードキャスト)
https://qiita.com/takehilo/items/8c773d18ec6cfccd6679

下記のモジュールを使用することでredisを使用したsocket.ioのスケールアウトが可能です。
- socket.io-redis
- socket.io-adapter
- sticky-session

以下のコードは複数プロセスで動作しているsocket.ioを使用したチャットプログラムのサンプルになります。このコードはSocket.ioサーバをスケールアウトする(Redisを使った複数プロセス間でのブロードキャスト)を元に作成しています。

package.json
{
  "name": "socketio",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cluster": "^0.7.7",
    "express": "^4.17.1",
    "socket.io": "^2.3.0",
    "socket.io-redis": "^5.4.0",
    "sticky-session": "^1.1.2"
  }
}
server.js
const cluster = require('cluster');
const io = require('socket.io')();
const sticky = require('sticky-session');
const http = require('http');
const redis = require('socket.io-redis');
const express = require("express");
const app = express();
app.use(express.static(__dirname + "/public"));

const server = http.createServer(app);

io.adapter(redis({host: '127.0.0.1', port: 6379}));
io.attach(server);
isWorker = sticky.listen(server, 3000);

if (isWorker) {
  io.on('connection', (socket) => {
    console.log(`worker: ${cluster.worker.id}, connected, id: ${socket.id}`);

    socket.on('message', (user, message) => {
      data = `${message} from ${user}`;
      console.log(data);
      socket.broadcast.emit('message', user, message);
    });

    socket.on('disconnect', () => {
      console.log(`disconnected, id: ${socket.id}`);
    });
  });
}
public/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <meta charset="UTF-8" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <input v-model:value="user"></input>
      <input v-model:value="message"></input>
      <button v-on:click="send">送信</button>
    </div>
    <script src="/socket.io/socket.io.js"></script>
    <script src="/app.js"></script>
  </body>
</html>
app.js
const app = new Vue({
  el: '#app',
  data: {
    socket : {},
    user: 'hoge',
    message : 'message'
  },
  created : function() {
    console.log(window.location.origin);
    this.socket = io.connect(window.location.origin);
    console.log(this.socket);
    this.socket.on("message", (user, message) => {
      console.log(user, message);
    })
  },
  methods: {
    send: function() {
      console.log(this.socket);
      console.log(this.user, this.message);
      this.socket.emit("message", this.user, this.message);
    }
  }
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Product Advertising API 5.0(PA-API v5)をexpressでREST API化する

概要

Amazon Product Advertising API 5.0 というAmazonが提供している商品情報検索APIがあります。
SDKが公式から提供されているのですが、サンプルコードのままだとコード内でASINやキーワードを指定するためAPIを単発でしかコールできないため、expressでREST APIとして動作するようにしました。

リポジトリ:https://github.com/sakatech-jp/paapi5-express-sample

【参考】
Product Advertising API 5.0 Documentation
Product Advertising API 5.0 SDK for NodeJS

前提条件

  • Amazonアソシエイト・プログラムに承認されたアカウントを所持している
  • 認証キーを発行している
  • Node.jsが実行できる

使用方法

実行環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021

$ node -v
v14.1.0

$ docker -v
Docker version 19.03.12, build 48a66213fe

セットアップ

クローンまたはフォークして依存パッケージをインストールします。

$ git clone https://github.com/sakatech-jp/paapi5-express-sample.git
$ cd paapi5-express-sample
$ npm install

config.js内に認証キーとアソシエイトタグを設定します。

module.exports = {
  ACCESS_KEY: '<YOUR ACCESS KEY>', // アクセスキーをセット
  SECRET_KEY: '<YOUR SECRET KEY>', // シークレットキーをセット
  API_HOST: 'webservices.amazon.co.jp',
  REGION: 'us-west-2',
  PARTNER_TAG: '<YOUR PARTNER TAG>', // アソシエイトタグをセット
  PARTNER_TYPE: 'Associates'
}

app.jsを実行することでAPIを立ち上げることができます。

$ node app.js
Listening to PORT: 3000

エンドポイント

以下の4つのエンドポイントがあります。

/searchItems

検索条件から商品データを取得することができます。

/getItems

指定したASINを持つ商品の情報を取得することができます。

/getVariations

指定したASINを持つ商品のバリエーション(サイズや色など)を取得することができます。

/getBrowseNodes

指定した商品カテゴリの情報を取得することができます。

リクエストとレスポンスのサンプル

各エンドポイントへのリクエストのサンプルとそれに対するレスポンスです。

/searchItems

「Node.js」でキーワード検索をしてみます。

$ curl -X GET localhost:3000/searchItems \
-d '{
    "Keywords": "Node.js",
    "Resources": ["Images.Primary.Medium", "ItemInfo.Title", "Offers.Listings.Price"],
    "ItemCount": 1
   }'

「Node.js超入門[第3版]」という書籍の情報が返ってきます。

{
    "SearchResult": {
        "TotalResultCount": 146,
        "SearchURL": "https://www.amazon.co.jp/s?k=Node.js&rh=p_n_availability%3A-1&tag=***&linkCode=osi",
        "Items": [
            {
                "ASIN": "B08HRMTXHB",
                "DetailPageURL": "https://www.amazon.co.jp/dp/B08HRMTXHB?tag=***&linkCode=osi&th=1&psc=1",
                "Images": {
                    "Primary": {
                        "Medium": {
                            "URL": "https://m.media-amazon.com/images/I/51SoAyWCBdL._SL160_.jpg",
                            "Height": 160,
                            "Width": 124
                        }
                    }
                },
                "ItemInfo": {
                    "Title": {
                        "DisplayValue": "Node.js超入門[第3版]",
                        "Label": "Title",
                        "Locale": "ja_JP"
                    }
                },
                "Offers": {
                    "Listings": [
                        {
                            "Id": "rnBB%2BZKboyhEDyET8hzwlpvGBT%2FHZ%2BfRAoEtE4FR6i7%2Bv%2B5YeQ1ap7PjpdIUPxEugDBSHdeZavd6BbESDvgw6Q6zLIAPCpYKOoboMSZgkd6bu1zutQ5byVmg55svmhuJlopqxFxF3WQs4aCICTPDPUWEokDz1%2Fj%2FJgC3VYcQoodSEWsCS1Zu0A%3D%3D",
                            "Price": {
                                "Amount": 3168,
                                "Currency": "JPY",
                                "DisplayAmount": "¥3,168"
                            },
                            "ViolatesMAP": false
                        }
                    ]
                }
            }
        ]
    }
}

/getItems

「B08HRMTXHB」というASINを持つ商品の情報を取得してみます。

$ curl -X GET localhost:3000/getItems \
-d '{
    "ItemIds": ["B08HRMTXHB"],
    "Resources": ["Images.Primary.Medium", "ItemInfo.Title", "Offers.Listings.Price"]
   }'

Node.js超入門[第3版]」という書籍の情報が返ってきます。

{
    "ItemsResult": {
        "Items": [
            {
                "ASIN": "B08HRMTXHB",
                "DetailPageURL": "https://www.amazon.co.jp/dp/B08HRMTXHB?tag=***&linkCode=ogi&th=1&psc=1",
                "Images": {
                    "Primary": {
                        "Medium": {
                            "URL": "https://m.media-amazon.com/images/I/51SoAyWCBdL._SL160_.jpg",
                            "Height": 160,
                            "Width": 124
                        }
                    }
                },
                "ItemInfo": {
                    "Title": {
                        "DisplayValue": "Node.js超入門[第3版]",
                        "Label": "Title",
                        "Locale": "ja_JP"
                    }
                },
                "Offers": {
                    "Listings": [
                        {
                            "Id": "2QzP8U6GS%2BOE3G8ybPwm1FbqJ8V%2Fh6jc24%2Bmw%2FeCFpItFscuzkMg8wE5rhC%2BVJ4bttattXONPnhLtfj%2BI2UQiM3rvFpB7SIcX3YozDc9Gb1UjeyINE%2Btn0Vl%2BdI8hjNwvDExYbcO5QCSnqCcxs%2BwuKUam5dl5tXoc6syYJ3Blv0xxDt6RyoiaA%3D%3D",
                            "Price": {
                                "Amount": 3168,
                                "Currency": "JPY",
                                "DisplayAmount": "¥3,168"
                            },
                            "ViolatesMAP": false
                        }
                    ]
                }
            }
        ]
    }
}

/getVariations

いろはすのペットボトルのASINである「B0026IAWMU」で情報を取得してみます。

$ curl -X GET localhost:3000/getVariations \
-d '{
    "ASIN" : "B0026IAWMU"
   }'

24本と48本の2種類が返ってきます。

{
    "VariationsResult": {
        "Items": [
            {
                "ASIN": "B0026IAWMU",
                "DetailPageURL": "https://www.amazon.co.jp/dp/B0026IAWMU?tag=***&linkCode=ogv&th=1&psc=1",
                "ItemInfo": {
                    "Title": {
                        "DisplayValue": "コカ・コーラ い・ろ・は・す 天然水 555mlPET×24本",
                        "Label": "Title",
                        "Locale": "ja_JP"
                    }
                },
                "VariationAttributes": [
                    {
                        "Name": "size_name",
                        "Value": "1) 555ml×24本"
                    }
                ]
            },
            {
                "ASIN": "B007B9T4UK",
                "DetailPageURL": "https://www.amazon.co.jp/dp/B007B9T4UK?tag=***&linkCode=ogv&th=1&psc=1",
                "ItemInfo": {
                    "Title": {
                        "DisplayValue": "I LOHAS(い・ろ・は・す) いろはす 555ml×24本×2ケース",
                        "Label": "Title",
                        "Locale": "ja_JP"
                    }
                },
                "VariationAttributes": [
                    {
                        "Name": "size_name",
                        "Value": "555mlx48本"
                    }
                ]
            }
        ],
        "VariationSummary": {
            "PageCount": 1,
            "VariationCount": 2
        }
    }
}

/getBrowseNodes

商品カテゴリID「2275256051」の情報を取得してみます。

$ curl -X GET localhost:3000/getBrowseNodes \
-d '{
    "BrowseNodeIds": ["2275256051"]
   }'

「Kindle本」というカテゴリ情報が返ってきます。

{
    "BrowseNodesResult": {
        "BrowseNodes": [
            {
                "ContextFreeName": "Kindle本",
                "DisplayName": "Kindle本",
                "Id": "2275256051",
                "IsRoot": false
            }
        ]
    }
}

指定できるパラメータについて

上記のサンプルで指定しているもの以外にもパラメータは多く定義されています。
詳しくは公式ドキュメントをご覧ください。

searchItems

https://webservices.amazon.com/paapi5/documentation/search-items.html

getItems

https://webservices.amazon.com/paapi5/documentation/get-items.html

getVariations

https://webservices.amazon.com/paapi5/documentation/get-variations.html

getBrowseNodes

https://webservices.amazon.com/paapi5/documentation/getbrowsenodes.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【NestJS】ヘルスチェック(v7版)

【NestJS】ヘルスチェック(v6版) を NestJS 7 へのバージョンアップに伴い、変更した内容になります。

環境

NestJS 6 時代は、godaddy/terminus が必要でしたが、7 から依存が排除されています。

インストール

yarn add @nestjs/terminus --no-optional

// or

npm install --save @nestjs/terminus --no-optional

実装

  • ほとんど、↓のページに書いている通りです。
  • const dbConfig = config.get('database') とそれに関連する箇所は、各環境に応じて変更してください。
  • src/health/health.module.ts に置いていますが、場所は任意です。
src/health/health.module.ts
import config from '@config'
import { Controller, Get, Module } from '@nestjs/common'
import {
  HealthCheck,
  HealthCheckResult,
  HealthCheckService,
  HealthIndicatorResult,
  TerminusModule,
  TypeOrmHealthIndicator,
} from '@nestjs/terminus'
import { TypeOrmModule } from '@nestjs/typeorm'

@Controller('healthz')
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly db: TypeOrmHealthIndicator
  ) {}

  @Get()
  @HealthCheck()
  healthCheck(): Promise<HealthCheckResult> {
    return this.health.check([
      // Set the timeout for a response to 300ms
      (): Promise<HealthIndicatorResult> =>
        this.db.pingCheck('database', { timeout: 300 }),
    ])
  }
}

const dbConfig = config.get('database')

@Module({
  imports: [TerminusModule, TypeOrmModule.forRoot(dbConfig)],
  providers: [],
  controllers: [HealthController],
})
export class HealthRestModule {}

上記以外の設定

【NestJS】ヘルスチェック(v6版) を参考にしてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twitter広告APIを利用してキャンペーンを作ってみる その5~Twitter広告APIでキャンペーン作成編~

経緯

私が所属している会社では待ラノという小説投稿サイトを運営しています。
待ラノではオススメ小説のランキング上位5作を定期的にTwitterの公式アカウントで紹介しています。
紹介された小説をTwitter広告のキャンペーンを利用してプロモーションをしようってなりました。

そもそもTwitter広告のキャンペーンって何?

Twitter広告のキャンペーンですが、簡単いうと1日にかける予算や期間内にかける総予算を指定して、Twitterに広告を出す機能です。

Twitter広告APIでキャンペーンを作る理由

1つのキャンペーンで複数のツイートをプロモーションする場合、1日にかける予算を一気に消化されてしまいます。
しかもどのツイートにどれだけ予算が消化されているかがわかりません。
そのため、1つのツイートに1キャンペーンを紐付けることで消化される予算の見える化を行うことになりました。

ただ手動でTwitterの広告コンソールから、1ツイートに1キャンペーンを毎回作ることになると結構手間です。
というわけで、Twitter広告APIを利用して動的にキャンペーンを作成することになりました。

Twitter広告APIでキャンペーンを作成するためには

以下の手順が必要です。

  1. Tiwtterアカウントを作成する(省略)
  2. Tiwtterアカウントにメールアドレスと電話番号を設定する(省略)
  3. TiwtterAPIの利用申請をする
  4. TiwtterAPIのAPIキーとトークンを取得する
  5. TiwtterAPIを利用してツイートをする
  6. Tiwtter広告APIの利用申請をする
  7. Tiwtter広告APIでを利用してツイートを使ったキャンペーンを作る ←イマココ

今回は5の【Tiwtter広告APIでを利用してツイートを使ったキャンペーンを作る】について説明していきます。

動作環境

  • 使用端末:Mac
  • Node.js:v12.18.2
  • yarn:v1.22.4
  • gem:v3.0.3

使用するライブラリ

仕様

サーバーのURLにアクセスすると【その5】で作成したツイートも利用したキャンペーンを作成する。

1.Twitterの連携アプリを認証する

1-1. Twurlをインストールする

$ sudo gem install twurl

1-2. Twurlコマンドで認証URLを取得する

【その2】で取得した「APIキー」「APIシークレットキー」を使って以下のコマンドを叩きます。
すると、認証URLが出力されます。
さらにPINコードの入力も求められていますが、下記の【1-4】までそのままにしてください。

$ twurl authorize --consumer-key [APIキー] --consumer-secret [APIシークレットキー]
Go to https://api.twitter.com/oauth/authorize?oauth_consumer_key=xxx&oauth_nonce=yyy&oauth_signature=zzz&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1600840260&oauth_token=fff&oauth_version=1.0 and paste in the supplied PIN

1-3. ブラウザで認証URLにアクセスしてPINコードを取得する

【1-2】で取得した認証URLにブラウザでアクセスします。
以下のようにページが開かれるので「連携アプリを認証」ボタンをクリックします。
するとPINコードが表示されます。
スクリーンショット 2020-09-23 15.05.48.png

1-4. 取得したPINコードをコマンドライン上にペーストして実行する

成功すると「Authorization successful」と表示されます。

$ twurl authorize --consumer-key [APIキー] --consumer-secret [APIシークレットキー]
Go to https://api.twitter.com/oauth/authorize?oauth_consumer_key=xxx&oauth_nonce=yyy&oauth_signature=zzz&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1600840260&oauth_token=fff&oauth_version=1.0 and paste in the supplied PIN
111111 ←PINコードを入力してEnterを押す
Authorization successful

1-5. アクセストークンを再生成する

開発ポータルにアクセスして、対象のアプリの「鍵」ボタンをクリックしてください。
スクリーンショット 2020-09-23 15.24.07.png

次にAccess Token & Secretの「Regenerate」ボタンをクリックします。
スクリーンショット 2020-09-23 15.26.54.png

確認画面が表示されますので「Yes, regenerate」ボタンをクリックします。
スクリーンショット 2020-09-23 15.29.44.png

再生成された「Access token」「Access token secret」をどこかにメモしておきます。
スクリーンショット 2020-09-23 15.33.01.png

2.Twitter広告APIのバージョンを確認する

Twitter広告APIのバージョンが掲載されているページにアクセスします。
有効期限が細く設定されているので、確認してください。
※2020年9月23日現在ではバージョンは「8」となってます。
スクリーンショット 2020-09-23 15.49.41.png

3.アプリIDを取得する

開発ポータルにアクセスして、対象のアプリの「設定(歯車)」ボタンをクリックしてください。
スクリーンショット 2020-09-18 15.04.49.png

「アプリID」が表示されます。
スクリーンショット 2020-09-18 15.05.09.png

4.package.jsonを作成する

以下のコマンドでpackage.jsonを作成します。
対話式で質問を聞かれるので基本的に全てEnterで問題ありません。

$ yarn init
question name (twitter-api-post-tweet):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:

2.ライブラリをインストールする

以下のコマンドでライブラリをインストールする

$ yarn add express twitter-ads

3.ソースファイルを作成する

$ touch index.js

4.ソースコードを書く

今回使用しているTwitter広告APIに関しては、「キャンペーンの作成 - 詳細な手順」を参考にしております。

※ちなみに今回使用してる「twitter-ads」async/awaitに対応していないため、コールバック地獄になってるのであしからず

index.js
// Server settings
const express = require('express')
const server = express()
const port = 3000

// Twitter settings
const TwitterAdsAPI = require('twitter-ads')

// 【その2】で取得したAPIキー
const API_KEY = 'AAAAAAAAAA'
const API_SECRET_KEY = 'BBBBBBBBBB'
// 【1-5】で取得したトークン
const ACCESS_TOKEN = 'CCCCCCCCCC'
const ACCESS_TOKEN_SECRET = 'DDDDDDDDDD'
// 【2】で取得したトークン
const VERSION = '8'
// 【3】で取得したアプリID
const APP_ID = 'XXXXXXX'
// 【その3】で取得したツイートID
const TWEET_ID = '1309021433616633862'

const twitterAdsClient = new TwitterAdsAPI({
  consumer_key: API_KEY,
  consumer_secret: API_SECRET_KEY,
  access_token: ACCESS_TOKEN,
  access_token_secret: ACCESS_TOKEN_SECRET,
  sandbox: false,
  api_version: VERSION
})

// Routes
server.get('/', (req, res, next) => {
  res.send('server is up')
})

// http://localhost:3000/campaign にアクセスすると自動でキャンペーンを作成する
server.get('/campaign', (req, res, next) => {
  // 1.アカウントIDを取得します
  // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/accounts#get-accounts」を確認してください
  twitterAdsClient.get('accounts', null, (accountError, accountResponse, accountBody) => {
    if (accountError) {
      console.log(accountError)
      res.send(accountError)
      return
    }

    if (accountBody.data.length > 0) {
      const accountId = accountBody.data[0].id

      // 2.お支払いIDを取得します
      // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/funding-instruments#get-accounts-account-id-funding-instruments」を確認してください
      const fundingInstrumentsUrl = 'accounts/' + accountId + '/funding_instruments'
      twitterAdsClient.get(fundingInstrumentsUrl, null, (fundingInstrumentError, fundingInstrumentResponse, fundingInstrumentBody) => {
        if (fundingInstrumentError) {
          console.log(fundingInstrumentError)
          res.send(fundingInstrumentError)
          return
        }

        if (fundingInstrumentBody.data.length > 0) {
          const fundingInstrumentId = fundingInstrumentBody.data[0].id

          // 3.キャンペーンを作成し、お支払い方法に関連付けます
          // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/campaigns#post-accounts-account-id-campaigns」を確認してください
          const createCampaignUrl = 'accounts/' + accountId + '/campaigns'
          const createCampaignParams = {
            funding_instrument_id: fundingInstrumentId, // お支払いID
            name: 'キャンペーン01', // キャンペーン名
            start_time: '2020-10-01T00:00:00Z', // キャンペーン開始時間(ISO 8601表記)
            end_time: '2020-10-07T23:59:59Z', // (ISO 8601表記)
            total_budget_amount_local_micro: 1000000, // キャンペーンに割り当てる合計予算額(1000000で1円扱い)
            daily_budget_amount_local_micro: 1000000, // キャンペーンに割り当てる日別予算額(1000000で1円扱い)
            entity_status: 'PAUSED' // ステータス
          }
          twitterAdsClient.post(createCampaignUrl, createCampaignParams, (createCampaignError, createCampaignResponse, createCampaignBody) => {
            if (createCampaignError) {
              console.log(createCampaignError)
              res.send(createCampaignError)
              return
            }

            const campaignId = createCampaignBody.data.id

            // 4.キャンペーンに関連付けた行項目(広告グループ)を作成します
            // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/line-items#post-accounts-account-id-line-items」を確認してください
            const createLineItemUrl = 'accounts/' + accountId + '/line_items'
            const createLineItemParams = {
              campaign_id: campaignId, // キャンペーンID,
              name: '広告グループ01', // 行項目(広告グループ)名
              objective: 'WEBSITE_CLICKS', // キャンペーン目的
              placements: 'ALL_ON_TWITTER', // 配置場所
              product_type: 'PROMOTED_TWEETS', // プロモ商品のタイプ
              automatically_select_bid: true, // 自動入札最適化フラグ
              entity_status: 'PAUSED' // ステータス
            }
            twitterAdsClient.post(createLineItemUrl, createLineItemParams, (createLineItemError, createLineItemResponse, createLineItemBody) => {
              if (createLineItemError) {
                console.log(createLineItemError)
                res.send(createLineItemError)
                return
              }

              const lineItemId = createLineItemBody.data.id

              // 5.行項目にツイートを関連付けます
              // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/promoted-tweets#post-accounts-account-id-promoted-tweets」を確認してください
              const relatePromotedTweetUrl = 'accounts/' + accountId + '/promoted_tweets'
              const relatePromotedTweetParams = {
                line_item_id: lineItemId, // 行項目ID,
                tweet_ids: TWEET_ID // ツイートID
              }
              twitterAdsClient.post(relatePromotedTweetUrl, relatePromotedTweetParams, (relatePromotedTweetError, relatePromotedTweetResponse, relatePromotedTweetBody) => {
                if (relatePromotedTweetError) {
                  console.log(relatePromotedTweetError)
                  res.send(relatePromotedTweetError)
                  return
                }

                /**
                 * 6.行項目に関連付けるターゲティングプロフィールを作成します
                 */
                // 6-1.行項目に関連付けるターゲティング(地域)を取得します
                // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-options#get-targeting-criteria-locations」を確認してください
                const getTargetLocationsUrl = 'targeting_criteria/locations?location_type=COUNTRIES&q=Japan'
                twitterAdsClient.get(getTargetLocationsUrl, null, (getTargetLocationsError, getTargetLocationsResponse, getTargetLocationsBody) => {
                  if (getTargetLocationsError) {
                    console.log(getTargetLocationsError)
                    res.send(getTargetLocationsError)
                    return
                  }

                  if (getTargetLocationsBody.data.length > 0) {
                    const locationTargetValue = getTargetLocationsBody.data[0].targeting_value

                    // 6-2.行項目にターゲティング(地域)を関連付けます
                    // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-criteria#post-accounts-account-id-targeting-criteria」を確認してください
                    const relateTargetLocationUrl = 'accounts/' + accountId + '/targeting_criteria'
                    const relateTargetLocationParams = {
                      line_item_id: lineItemId, // 行項目ID,
                      operator_type: 'EQ', // 比較演算子(EQ/NE/GTE/LTE)
                      targeting_type: 'LOCATION', // ターゲティング種別
                      targeting_value: locationTargetValue // ターゲティング値
                    }
                    twitterAdsClient.post(relateTargetLocationUrl, relateTargetLocationParams, (relateTargetLocationError, relateTargetLocationResponse, relateTargetLocationBody) => {
                      if (relateTargetLocationError) {
                        console.log(relateTargetLocationError)
                        res.send(relateTargetLocationError)
                        return
                      }

                      // 6-3.行項目に関連付けるターゲティング(言語)を取得します
                      // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-options#get-targeting-criteria-languages」を確認してください
                      const getTargetLanguagesUrl = 'targeting_criteria/languages?q=Japanese'
                      twitterAdsClient.get(getTargetLanguagesUrl, null, (getTargetLanguagesError, getTargetLanguagesResponse, getTargetLanguagesBody) => {
                        if (getTargetLanguagesError) {
                          console.log(getTargetLanguagesError)
                          res.send(getTargetLanguagesError)
                          return
                        }

                        if (getTargetLanguagesBody.data.length > 0) {
                          const languageTargetValue = getTargetLanguagesBody.data[0].targeting_value

                          // 6-4.行項目にターゲティング(言語)を関連付けます
                          // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-criteria#post-accounts-account-id-targeting-criteria」を確認してください
                          const relateTargetLanguageUrl = 'accounts/' + accountId + '/targeting_criteria'
                          const relateTargetLanguageParams = {
                            line_item_id: lineItemId, // 行項目ID,
                            operator_type: 'EQ', // 比較演算子(EQ/NE/GTE/LTE)
                            targeting_type: 'LANGUAGE', // ターゲティング種別
                            targeting_value: languageTargetValue // ターゲティング値
                          }
                          twitterAdsClient.post(relateTargetLanguageUrl, relateTargetLanguageParams, (relateTargetLanguageError, relateTargetLanguageResponse, relateTargetLanguageBody) => {
                            if (relateTargetLanguageError) {
                              console.log(relateTargetLanguageError)
                              res.send(relateTargetLanguageError)
                              return
                            }

                            // 6-5.行項目にターゲティング(年齢)を関連付けます
                            // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-criteria#post-accounts-account-id-targeting-criteria」を確認してください
                            const relateTargetAgeUrl = 'accounts/' + accountId + '/targeting_criteria'
                            const relateTargetAgeParams = {
                              line_item_id: lineItemId, // 行項目ID,
                              operator_type: 'EQ', // 比較演算子(EQ/NE/GTE/LTE)
                              targeting_type: 'AGE', // ターゲティング種別
                              targeting_value: 'AGE_20_TO_49' // ターゲティング値(詳細は「https://developer.twitter.com/ja/docs/ads/general/overview/enums#targeting-age」を参照)
                            }
                            twitterAdsClient.post(relateTargetAgeUrl, relateTargetAgeParams, (relateTargetAgeError, relateTargetAgeResponse, relateTargetAgeBody) => {
                              if (relateTargetAgeError) {
                                console.log(relateTargetAgeError)
                                res.send(relateTargetAgeError)
                                return
                              }

                              // 6-6.行項目にターゲティング(キーワード)を関連付けます
                              // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/targeting-criteria#post-accounts-account-id-targeting-criteria」を確認してください
                              const relateTargetKeywordUrl = 'accounts/' + accountId + '/targeting_criteria'
                              const relateTargetKeywordParams = {
                                line_item_id: lineItemId, // 行項目ID,
                                operator_type: 'EQ', // 比較演算子(EQ/NE/GTE/LTE)
                                targeting_type: 'PHRASE_KEYWORD', // ターゲティング種別
                                targeting_value: 'キーワード' // ターゲティング値
                              }
                              twitterAdsClient.post(relateTargetKeywordUrl, relateTargetKeywordParams, (relateTargetKeywordError, relateTargetKeywordResponse, relateTargetKeywordBody) => {
                                if (relateTargetKeywordError) {
                                  console.log(relateTargetKeywordError)
                                  res.send(relateTargetKeywordError)
                                  return
                                }

                                // 7.行項目の一時停止を解除します
                                // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/line-items#put-accounts-account-id-line-items-line-item-id」を確認してください
                                const updateLineItemUrl = 'accounts/' + accountId + '/line_items/' + lineItemId
                                const updateLineItemParams = {
                                  entity_status: 'ACTIVE' // ステータス
                                }
                                twitterAdsClient.put(updateLineItemUrl, updateLineItemParams, (updateLineItemError, updateLineItemResponse, updateLineItemBody) => {
                                  if (updateLineItemError) {
                                    console.log(updateLineItemError)
                                    res.send(updateLineItemError)
                                    return
                                  }

                                  // 8.最後に、キャンペーンの一時停止を解除します
                                  // APIの詳細は「https://developer.twitter.com/en/docs/ads/campaign-management/api-reference/line-items#put-accounts-account-id-line-items-line-item-id」を確認してください
                                  const updateCampaignUrl = 'accounts/' + accountId + '/campaigns/' + campaignId
                                  const updateCampaignParams = {
                                    entity_status: 'ACTIVE' // ステータス
                                  }
                                  twitterAdsClient.put(updateCampaignUrl, updateCampaignParams, (updateCampaignError, updateCampaignResponse, updateCampaignBody) => {
                                    if (updateCampaignError) {
                                      console.log(updateCampaignError)
                                      res.send(updateCampaignError)
                                      return
                                    }

                                    // TwitterAdsAPIの結果をそのまま返す
                                    res.send(updateCampaignBody)
                                  })
                                })
                              })
                            })
                          })
                        } else {
                          res.send(new Error('Not found targeting languages.'))
                        }
                      })
                    })
                  } else {
                    res.send(new Error('Not found targeting locations.'))
                  }
                })
              })
            })
          })
        } else {
          res.send(new Error('Not found funding instrument.'))
        }
      })
    } else {
      res.send(new Error('Not found account.'))
    }
  })
})

server.listen(port, function () {
  console.log('Listening on port ' + port)
})

5.サーバーを立ち上げる

以下のコマンドを叩いてサーバーを起動させる。

$ node index.jp

6.自動ツイートするURLにアクセスする。

ブラウザで「http://localhost:3000/campaign」にアクセスする。
アクセスするとキャンペーンが作成されます。
差育成されているかを確認する場合はTwitterの広告コンソールをご確認ください。


■「Twitter広告APIを利用してキャンペーンを作ってみる 」シリーズ

  1. その1~TwitterAPI申請編~
  2. その2~TiwtterAPIのAPIキーとトークンの取得編~
  3. その3~TwitterAPIでツイート編~
  4. その4~Tiwtter広告APIのAPI申請編~
  5. その5~Twitter広告APIでキャンペーン作成編~ ←イマココ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『このExcelのA列のファイルのB列の文字列をC列の文字列にしてクレメンス』

「ちな、7,000件な!」
さすがに手入力はやってられん。という事で、Node.js で自動化してみました

※ 完成品はこちら

テスト環境の下準備

Node.jsはインストール済みで
最低限のjs知識がある方を前提としています

最終的なディレクトリ構造はこんな感じを想定してます
image.png

プロジェクトの作成

任意のディレクトリを作成してnpm initしましょう
入力項目は任意で決めて大丈夫です

bash
mkdir excel-replaceer
cd excel-replaceer
npm init

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

今回使用するモジュールはfsxlsxの2つです

fs : https://www.npmjs.com/package/fs
xlsx : https://www.npmjs.com/package/xlsx

bash
npm install fs
npm install xlsx

テスト用のExcelファイルの作成

読み込んでくるtest.xlsxファイルを作成します
今回はプロジェクト直下に/testというディレクトリを作成し
そこに読み込んでくるExcelファイルと置換するhtmlファイルを設置します

今回、Excelのデータ内容は下記の様な感じで作成します

A B C
1 test01.html dummy test01
2 test02.html dummy test02
3 test03.html dummy test03

テスト用のhtmlファイルの作成

今回のテストで置換するhtmlファイルを作成します
『A列』に記載がある通りの名前でhtmlを3つ作成してください
ディレクトリはExcelファイルと同様/test配下に保存します

htmlファイルの中身は任意で決めてもらえばよいですが、
今回は『B列』の「dummy」を置換するのでどこかしらにdummyを記述してください

test01~3.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>dummy</title>
</head>
<body>

</body>
</html>

これでテストを行う為の下準備は完了です

モジュール作成

次は実際にExcelから情報を取得して、置換処理を行うまでの解説です

index.jsの作成

プロジェクト直下にindex.jsを作成してください
ここに実際の置換処理を書いていきます

node moduleの読み込み

下準備の段階でインストールを行ったfsxlsxを使用するので
この2つを呼び出して、いつでも使えるように変数に格納します

index.js
const xlsx = require('xlsx');
const utils = xlsx.utils;
const fs = require('fs');

utilsxlsxの機能の一つで、こちらも毎回呼び出す手間を省く為に
変数に格納しています
これでモジュールを使う準備ができました

Excelファイルの読み込み

Excelファイルの操作はxlsxモジュールを使用して行います

index.js
// 上部 略

const testX = xlsx.readFile('./test/test.xlsx');
const sheet = testX.Sheets['Sheet1'];

readFileを使用することによってExcelファイルの読み込みができます
読み込んだExcelの内容をtextXに格納し、その中のSheet1の情報を
sheet変数に格納しています

セルの値を取得する為にはsheet['A1']というように
シート情報からセル名を指定して取得してきますが、
今回は入力されている範囲を取得して、ループを回してデータを取得します

範囲の取得

index.js
// 上部 略

const range = sheet['!ref'];
const rangeN = utils.decode_range(range);

次にシートの記載がある範囲を取得します
先ほどシートの情報を格納したsheet変数から['!ref]を指定すると
範囲が取得できるので、こちらをrengeの変数に格納します
今回の場合、rangeには『A1:C2』が入っています

範囲を取得することはできましたが、今取得してきた範囲は
テキストデータになっている為、A列からC列までループ処理は回せません
これを解決するためにutilsを使用していきます

utilsdecode_range()を使用して、先ほどとってきたテキストデータの
範囲を渡してあげる事で、詳細な範囲データを返してくれます
これをrangeNに格納しています 中身は下記の様になっています

{ s: { c: 0, r: 0 }, e: { c: 2, r: 1 } }

それぞれ情報内容はこんな感じ

key 情報
s スタートのセル情報
e エンドのセル情報
c 列 (A, B, C ~~) の情報
r 行 (0, 1, 2 ~~) の情報

この情報を利用すれば、ループ分を回して行数分実行したり
列分実行したり、全レコード分実行したりできます

今回は『B列』に元データ『C列』に変更後データがある状態を想定して作るので
行数分、置換が実行できれば良いことになります

ループ・置換処理

今回は『A列』に置換したいファイル名が記載されており
『B列』に変更前の文字列、『C列』に変更後の文字列が記載されているので
行数分、置換処理を実行すれば良いことになります

for文で行数分ループするようにしましょう

index.js
// 上部 略

for (let r = rangeN.s.r; r <= rangeN.e.r; r++) {
    let address = utils.encode_cell({c:0, r:r});
    let cell = sheet[address];
}
for (let r = rangeN.s.r; r <= rangeN.e.r; r++)

rangeN.s.rのスタートの行番号から、
rangeN.e.rのエンドの行番号までループする処理を書きます

次にセルの情報を取得してきます
『A列』に置換したいファイル名が記載されているので
列のインデックス番号であるcc:0で固定して編集するファイルを取得しましょう

let address = utils.encode_cell({c:0, r:r});
let cell = sheet[address];

utilsencode_cell()を使用することでcrのプロパティで
指定されたセルを、テキストベースに変換してくれます

let address = utils.encode_cell({c:0, r:0});
の場合は、addressに「A1」が入ります

これで、cellに『A列』のデータ(ファイル名)が入るように設定できました
指定のファイルを読み込んでみましょう

// fs.readFile([読み込むファイルのパス], [読み込む文字コード], [コールバック関数]); 
fs.readFile('./test/' + cell.v, 'utf-8', (err, data) => {
    // エラー処理
    if (err) {
        console.log(`【 ${cell.v} 】ファイル読み込みエラー`);
        throw err;
    }
});

ファイルの読み書きにはfsモジュールを使用します
ファイルの読み込みではreadFile()を使用します
上記のように、簡単なエラー処理も書いておきましょう
このコールバック関数内で、置換の処理を行っていきます
コールバック関数で渡しているdataには取得してきた内容が入っています

// 変更前の内容を取得
let B_address = utils.encode_cell({c:1, r:r});
let B_cell = sheet[B_address];

// 変更後の内容を取得
let A_address = utils.encode_cell({c:2, r:r}); 
let A_cell = sheet[A_address];

// 置換
const beforeTxt = data;
const afterTxt = beforeTxt.replace(new RegExp(B_cell.v,"g"), A_cell.v);

今回は『B列』に変更前の文字列、『C列』に変更後の文字列が
入っていることがわかっているので、それぞれ『A列』の情報を
とってきた時と同様に、cプロパティを固定して情報を取ってきます

あとはおなじみのreplace()を使用して置換をおこない
afterTxtに格納しておきます

// fs.writeFile([書き込むファイルパス], [書き込む内容], コールバック関数)
fs.writeFile('./test/' + cell.v, afterTxt, (err) => {
    if (err) {
        console.log(`【 ${cell.v} 】ファイル置換エラー`);
        throw err;
    }

    console.log(`【 ${cell.v} 】success !`);
});

最後にファイルの上書きを行っていきます
ファイルの書き込みにはwriteFile()を使用します
こちらにも簡単なエラー処理を書いてあげましょう

index.jsの完成形はこんな感じ

index.js
// モジュールのインストール
const xlsx = require('xlsx');
const utils = xlsx.utils;

const fs = require('fs');

// エクセルファイルの読み込み
const testX = xlsx.readFile('./test/test.xlsx');

// シートの読み込み
const sheet = testX.Sheets['Sheet1'];

// セルの範囲の取得
const range = sheet['!ref'];
// console.log(range);

// セルの範囲を数値化
const rangeN = utils.decode_range(range);

// ループ処理
for (let r = rangeN.s.r; r <= rangeN.e.r; r++) {
    // ファイル名取得
    let address = utils.encode_cell({c:0, r:r});
    let cell = sheet[address];

    // htmlの読み込み
    fs.readFile('./test/' + cell.v, 'utf-8', (err, data) => {
        // エラー処理
        if (err) {
            console.log(`【 ${cell.v} 】ファイル読み込みエラー`);
            throw err;
        }

        // 置換処理
        // 変更前の内容を取得
        let B_address = utils.encode_cell({c:1, r:r});
        let B_cell = sheet[B_address];

        // 変更後の内容を取得
        let A_address = utils.encode_cell({c:2, r:r}); 
        let A_cell = sheet[A_address];

        // 置換
        const beforeTxt = data;
        const afterTxt = beforeTxt.replace(new RegExp(B_cell.v,"g"), A_cell.v);

        // ファイルの上書き
        fs.writeFile('./test/' + cell.v, afterTxt, (err) => {
            if (err) {
                console.log(`【 ${cell.v} 】ファイル置換エラー`);
                throw err;
            }

            console.log(`【 ${cell.v} 】success !`);
        });
    });
}

これで処理は完成しました!

処理の実行

最後に下記で実行して置換が行われるか試してみてください

node index.js
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebSocket のサーバー・クライアントをコマンドラインで簡単に実行する(npx と wscat)

はじめに

こちらのツイートをきっかけに、ふと思いついて試した内容の第二弾のメモです。

上記を見て「MQTT.js で実現できそうかも」という話をコメントして、その際に試した内容を以下の記事に書きました。

●MQTTクライアントをコマンドラインで簡単に実行する(npx と MQTT.js) - Qiita
 https://qiita.com/youtoy/items/edeaa9cc316d5f47c0e8

そして、このときもう 1つ思いついていたのが「wscat も似たような、便利な使い方ができるのでは?」ということでした。

npx で wscat を実行する

IoTな作品を作ろうとしたときに、デバイス間の通信などで MQTT の他に使っていたものの 1つに WebSocket がありました。そして、その検証時に活用できそうなツールを調べ、見つけて実際に利用していたものの 1つに wscat があります。

wscat を知ったのは、情報を調べていて以下の記事にたどり着いたことがきっかけでした。

●WebSocket の検証では wscat が便利だった - Qiita
 https://qiita.com/toshihirock/items/4dfaca29058c9e744529

以下のような手順で利用でき、自分はあまり利用してないですが様々なオプションもあるようです。

  1. インストール: npm install -g wscat
  2. サーバーとして実行(ポート番号は 8000 を指定): wscat -l 8000
  3. クライアントとして実行: wscat -c ws://【サーバーのURL】:【ポート番号】

websockets_wscat_usage.jpg

通常の利用方法は上記のとおり、グローバルインストール後にコマンドを実行する形ですが、npx を利用することで、インストールのステップを踏むことなく wscatコマンドでのサーバー・クライアントとしての実行を行うことができました。

  • サーバーとして実行: npx wscat -l 【ポート番号】
  • クライアントとして実行: npx wscat -c ws://【サーバーのURL】:【ポート番号】

Mac のターミナル上で実行すると、以下のようになります。

・サーバー
wscat_server.jpg

・クライアント
wscat_client.jpg

あとは、サーバー側でもクライアント側でも、どちらかで文字を入力してみると、他方にその入力された文字が表示される、という挙動が確認できます。

おわりに

WebSocket を使った開発を行う際、簡単に環境を準備できて便利に使えそうです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

グローバルインストールせずにMQTTをCLIで試す

JavaScriptやNode.jsを書いてる人はnpmコマンドなどは通常装備だと思うのでnpxでmqttを利用する方法のメモです。
JSerやNoder向けIoTって感じですね。とはいえMQTTをとりあえず試すってときの話です。

npmが入ってない人は素直にmosquittoをインストールでも良いかも。

MQTTクライアントライブラリ(MQTT.js)を利用

mqttをcliで利用できるかつnpmにレジストリされてるツール探してました。

探しても見つからないなぁと思ってたらmqtt.jsのCLI利用のドキュメントにありました。 @youtoyさんありがとうございます :)

subscribe側

$ npx mqtt sub -h ホスト名 \
-t トピック名 \

これで待ち受けてくれます。npxは一回だけインストールして実行してくれるのでグローバルインストール(npm i -g ~~)したく無い時に使えます。

publish側

$ npx mqtt pub -h ホスト名 \
-t トピック名 \
-m '{"content":"Hello!!! from MQTT!"}'

PubNubの場合

PubNubで5分でリアルタイムWebこと初め + MQTTでデバイス連携も など最近PubNub触ってるのでこちらもメモ。mosquittoのCLIとオプションが一緒なのでコピペで大丈夫でした。

subscribe側

$ npx mqtt sub -h mqtt.pndsn.com \
-t pub-c-xxxxxxxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxxxxxx/トピック名 \
-i pub-c-xxxxxxxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxxxxxx/クライアントID

publish側

$ npx mqtt pub -h mqtt.pndsn.com \
-t pub-c-xxxxxxxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxxxxxx/トピック名 \
-i pub-c-xxxxxxxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxxxxxx/クライアントID \
-m '{"content":"Hello!!! from MQTT!"}'

所感

subscribe側は一度実行したら実行しっぱなしというのが多いと思うので、npxでも良さそうだけど、publish側は確認で何回も実行すると思うのでnpxでの実行だと毎回インストールされてだるいかもしれませんね。

npm i -g mqttでグルーバルインストールしてしまってもよいかもしれません。

追記: 同じタイミングで記事書いてくれてたという笑

@youtoyさんも記事を書かれていたので、リンクを掲載します。

MQTTクライアントをコマンドラインで簡単に実行する(npx と MQTT.js)

同じタイミングで......わろ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む