子育てしながらエンジニアしたい

現在 7 歳 女の子の子育て中エンジニアによる、技術系 + 日常系ブログ。

Python: JSON でバイナリを扱う

バイナリデータをネットワーク送受信したい

バイナリデータをネットワーク越しに送受信したいことがあります。
普通にバイナリだけ送れば良いのですが、そのバイナリの属性値などを一緒に送りたいという要望がありました。
いろんな属性値を一度に送るのは JSON が便利なので、バイナリも JSON 形式に載せて送りたいと思ったのですが、ちょっと手こずったので記録しておきます。

テキストを JSON で送るには...

テキスト形式の dict を JSON として送信するのは、json モジュールを使って簡単に実現できます。

import json

# 送りたい dict データ
dictdata = {
  "str": "hoge",
  "num": 2,
}

# json.dumps を使って文字列に変換
strdata = json.dumps(dictdata)

# encode を使ってネットワークで送れるバイナリ形式に変換
bindata = strdata.encode()

# ネットワークに送る

しかし、このやり方でバイナリを送ろうとすると、json.dumps でエラーになります。

import json

# 送りたい dict データ
dictdata = {
  "str": "hoge",
  "bin": b"aa",
}

# json.dumps を使って文字列に変換
strdata = json.dumps(dictdata)

# ここで下記のエラーになる
# TypeError: b'aa' is not JSON serializable

こういうときは、バイナリを文字列に変換してから送ると良いようです。

バイナリを base64 エンコードする

Python ではないですが、以下を参考にしました。

qiita.com

これを Python で実現するには、そのままずばりの base64 モジュールを使います。

import json
import base64

# 送りたい dict データ
dictdata = {
  "str": "hoge",
  "bin": base64.b64encode(b"aa").decode('utf-8'), # base64 エンコード
}

# json.dumps を使って文字列に変換 (エラーにはならない)
strdata = json.dumps(dictdata)

# encode を使ってネットワークで送れるバイナリ形式に変換
bindata = strdata.encode()

# ネットワークに送る

注意しなければならないのは、base64.b64encode の出力はバイト列になるということです。
つまり b64encode の出力は String ではないので、json.dumps で変換できません。
これを解消するために、b64encode したものをさらに .decode('utf-8') することで文字列に変換し、json.dumps で変換できるようにしています。

バイナリと文字列を何度行き来しているんだという感じですが...

受信するときは

上記のようにエンコードされたデータを受信するときは以下のような感じになります。

import json
import base64

# ネットワークから受信したデータ: binrx
# decode を使って、バイナリから文字列に変換
strrx = binrx.decode()

# json.loads を使って dict に変換
dictrx = json.loads(strrx)

# base64 を使って元のバイナリに変換
dictrx['bin'] = base64.b64decode(dictrx['bin'].encode())

ちょっと複雑ですが、これでバイナリを JSON に載せられるようになりました。

Python で自身の IP アドレスを取得したい - Windows 編

[2019/07/17 追記]
本記事は Windows でのみ動作します。
Ubuntu 編を改めて記載しましたので、Ubuntu の方は以下の記事をご覧ください。

edosha.hatenablog.jp


低レイヤーのネットワークプログラミングをしていると、自身の IP アドレスを取得したいことがよくあります。
ところが Python では、これを簡単に取得するということができませんでした。

Stack overflow でもこれが議論になっており、一番簡単なのは外部ライブラリ "netifaces" を使うという結論でした。
stackoverflow.com

でも、移植性を考えると、できれば外部ライブラリは使いたくありません。
なんかないかな~と探していたら、標準ライブラリでいけそうな方法を見つけたので紹介します。

Python socket モジュールで自身の IP アドレスを取得する

実行環境は Python 3.5.3 / Windows 10 です。
標準ライブラリの socket モジュールを使います。

$ python --version
Python 3.5.3 :: Anaconda 4.4.0 (64-bit)

IP アドレスを取得する socket API

以下の 2 種類があります。

  • socket.gethostbyname(hostname)
    • ホスト名を '100.50.200.5' のようなIPv4形式のアドレスに変換します。
  • socket.gethostbyname_ex(hostname)
    • (hostname, aliaslist, ipaddrlist) のタプルを返し、 hostname は ip_address で指定したホストの正式名、 aliaslist は同じアドレスの別名のリスト(空の場合もある)、 ipaddrlist は同じホスト上の同一インターフェースのIPv4アドレスのリスト(ほとんどの場合は単一のアドレスのみ)を示します。

どちらも引数は hostname です。
つまり、自身の hostname を取得できれば、上記の API に渡すことで IP アドレスを取得できるということです。

hostname を取得する socket API

そのまんまです。

上記を組み合わせて、自身の IP アドレスを取得する

まずは自身の IP アドレスを確認してみます。

$ ipconfig | grep IPv4
ipconfig | grep IPv4
   IPv4 Address. . . . . . . . . . . : 192.168.1.10
   IPv4 Address. . . . . . . . . . . : 192.168.56.1
   IPv4 Address. . . . . . . . . . . : 10.150.2.139

3 つもありました。
1 番上は有線、2 番目は Virtual Box の仮想アダプタ、3 番目は無線です。

では、Python コンソールで socket API を使ってみましょう。

>>> import socket
>>> socket.gethostname()
'edosha_notepc'
>>> socket.gethostbyname(socket.gethostname())
'192.168.1.10'
>>> socket.gethostbyname_ex(socket.gethostname())
('edosha_notepc', [], ['192.168.56.1', '10.150.2.139', '192.168.1.10'])

gethostbyname で有線の IP アドレス、gethostbyname_ex で全ての IP アドレスが取得できました。

私のように 3 つもアドレスを使うのでなければ、gethostbyname でよさそうです。
どれかを選ばせたいときは gethostbyname_ex が有効かもしれません。

Python: マルチプロセスで並列処理をさせるには multiprocessing.Pool が超便利

独立した並列処理を高速化したい

ふつう並列処理といえば、GUI と処理を切り離すなどが思い当たります。
GUI と処理を切り離すといっても、相互にメッセージなどをやり取りして協調動作する必要があります。

今回やりたいのは、そういう協調動作を必要としない、独立した並列処理です。
たとえば、大量のファイルをすべて別個に圧縮するという場合、それぞれのファイルは独立に処理することができます。
これはスレッドを分けても意味が無いので、プロセスを分けて処理する必要があります。
こんなときに、Python の multiprocessing.Pool がとても便利だったので記録として残しておきます。

課題

20 個の処理をマルチプロセスで並列処理したいとします。
CPU がクアッドコアであれば、4 個を同時に走らせることができます。
普通に考えると、プロセスの終了状況を監視して、終了したら次のタスクを割り当てるという流れが必要になります。
実際にこういう方法でプログラムを書いていたのですが、これはけっこう面倒でミスをしやすいプログラムでした。

multiprocessing モジュールの Pool クラスは、なんとこれを 1 行でやってくれます。
はじめから知っておきたかった...

サンプルプログラム

1 秒かけて与えられた数字の 2 乗を返すだけのサンプルプログラムです。
数字は range(20) で与えるようにしていますが、ここはリストなどでも大丈夫です。

出力結果は以下のようになります。

param 0 is being processed
param 2 is being processed
param 4 is being processed
param 6 is being processed
param 1 is being processed
param 3 is being processed
param 5 is being processed
param 7 is being processed
param 8 is being processed
param 10 is being processed
param 12 is being processed
param 14 is being processed
param 9 is being processed
param 11 is being processed
param 13 is being processed
param 15 is being processed
param 16 is being processed
param 18 is being processed
param 17 is being processed
param 19 is being processed
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

プロセスを並列で走らせ、返り値は順番どおりに返してくれる、ほんとに便利なモジュールです!

注意点としては、Pool.map は range(20) の値を 0 から順番に与えるわけではない、ということです。
出力結果を見ると、0,2,4,6 が最初に来ていることがわかります。
この順番を 1 ずつにしたい!という場合は、Pool.map にもう一つ引数を追加する必要があります。

results = pool.map(parafunc, range(20), 1)

この最後の引数 "1" は chunksize と呼ばれる引数で、まとまりを意味するようです。
これを 1 にすると 0,1,2,3,...、2 だと 0,2,4,6,...、3 なら 0,3,6,9... という順番で値が渡されます。
なお、ここを変えたとしても、渡される結果の順番は同一なので安心です。