カテゴリー: Python

  • SESAME3 に対応した Web API の使い方

    いつの間にかしれっと Web API が SESAME3 に対応していたので、実際に使ってみる手順を記録します。

    ※しばらく随時修正しています

    ドキュメント

    SESAME3 の UUID 確認

    公式アプリで操作したい鍵を設定画面を開いて確認します。

    API キーの取得

    ダッシュボードにログインします。
    ログインの仕方はアプリ版と同じで、メールで認証番号が届いて、それを入力するだけです。

    file

    file

    file

    file

    「API_KEY」を開くと確認できます。
    「CLIENT_KEY」は今のところ使いません。

    SESAME3 の状態取得

    Firefox の拡張機能 RESTer で動作確認します。

    file

    成功すると200でJSONが返ってきます。
    パラメータが間違っていたりすると502 Bad Gatewayになります。

    • batteryPercentage: 電池残量
    • batteryVoltage: 電池電圧
    • position: サムターン角度
    • CHSesame2Status: 状態
    • timestamp: 更新された時刻(1970-01-01 00:00:00:000からの経過ms)

    SESAME3 の履歴取得

    Firefox の拡張機能 RESTer で動作確認します。

    file

    成功すると200でJSONが返ってきます。
    パラメータが間違っていたりすると502 Bad Gatewayになります。

    SESAME3 の解錠・施錠操作

    secret key の取得

    公式アプリから鍵のシェアを行って表示されるQRコードを読み取ります。

    iPhone の場合は「ショートカット」アプリで新規ショートカットを作成し、「書類」の「QR/バーコードをスキャン」と「Quick Lookで表示」を追加し、「QR/バーコード」を「テキストとして」にしておくとQRコードの中身をテキストのまま得られます。

    URLは次のような形式になっていると思います。

    ssm://UI?t=sk&sk=AXXXXXXXX省略)XXXXXXXXXXX&l=0&n=%%%%%%%%%%

    この sk= の値をコピーしておいて、Base64 decode した文字列の1-17文字が secret key になります。
    1-17文字といってもindexでの[1:17](JavaScriptだとslice(1, 17))なので、実際は2文字目から17文字目までですね。

    Pythonが使える環境であれば、次のようにして対話モードでサクッと変換できます。

    $ python
    Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    >>> b64 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # skの値(sk=の後から&前まで)
    >>> import base64
    >>> base64.b64decode(b64)[1:17].hex()
    'abcdef0123456789hogehoge'

    動作確認

    CMACの生成が手間だったのでPythonで行っています。
    pipでpycryptodome, requestsをインストールしておきます。

    $ pip install pycryptodome, requests
    import datetime, base64, requests, json
    from Crypto.Hash import CMAC
    from Crypto.Cipher import AES
    
    uuid = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
    secret_key = '0123456789abcdef0123456789abcdef'
    api_key = 'XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx'
    
    # 鍵の操作(toggle/lock/unlock)
    cmd = 82    # 88/82/83 = toggle/lock/unlock
    
    # 履歴に残す内容
    history = 'Locked (Web API)' # 半角21文字/全角不明
    history = base64.b64encode(history.encode()).decode()
    
    # HTTP header
    headers = { 'x-api-key': api_key }
    
    # signの生成
    cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
    message = int(datetime.datetime.now().timestamp()).to_bytes(4, 'little', signed=False)[1:4]
    cmac.update(message)
    sign = cmac.hexdigest()
    
    # 鍵の操作
    url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd'
    
    body = {
        'cmd': cmd,
        'history': history,
        'sign': sign
    }
    res = requests.post(url, json.dumps(body), headers=headers)
    print(res.status_code, res.text)

    参考

  • Python3 で QR コードを作る

    Python3 で QR コードを作る

    パッケージのインストール

    pip で qrcode と内部的に使われている pillow をインストールします。今回は Windows 10 (64bit) 環境を使いました。

    >python --version
    Python 3.7.2
    
    >pip install qrcode pillow
    Collecting qrcode
      Downloading https://files.pythonhosted.org/packages/42/87/4a3a77e59ab7493d64da1f69bf1c2e899a4cf81e51b2baa855e8cc8115be/qrcode-6.1-py2.py3-none-any.whl
    Collecting pillow
      Downloading https://files.pythonhosted.org/packages/40/f2/a424d4d5dd6aa8c26636969decbb3da1c01286d344e71429b1d648bccb64/Pillow-6.0.0-cp37-cp37m-win_amd64.whl (2.0MB)
        100% |████████████████████████████████| 2.0MB 1.7MB/s
    Requirement already satisfied: six in c:\users\user\appdata\roaming\python\python37\site-packages (from qrcode) (1.12.0)
    Requirement already satisfied: colorama; platform_system == "Windows" in c:\users\user\appdata\roaming\python\python37\site-packages (from qrcode) (0.4.1)
    Installing collected packages: qrcode, pillow
    Successfully installed pillow-6.0.0 qrcode-6.1

    qr コマンドで作成

    qrcode をインストールすると qr コマンドが使えるようになるので、コマンドプロンプトで次のように叩くと即座に QR コードが作成できます。

    >qr 'qr code sample text'
     █████████████████████████████
     █████████████████████████████
     ████ ▄▄▄▄▄ █▀▀▄▀ █ ▄▄▄▄▄ ████
     ████ █   █ █ ▀ ▄██ █   █ ████
     ████ █▄▄▄█ ██▄▀▀▄█ █▄▄▄█ ████
     ████▄▄▄▄▄▄▄█ █ █▄█▄▄▄▄▄▄▄████
     ████▄▀▀▀▀█▄▀▄█▀ ▀ ▄▀▀▄  █████
     ████▄▄▄▀█▀▄█  ▄▀ ▀  █   ▀████
     █████▄███▄▄▄  ▀▄ ▄▄▄▄█▀█ ████
     ████ ▄▄▄▄▄ ███▄▄█▄▀█▄█▀▀█████
     ████ █   █ ████ ▀ ▀▀▄▀▄██████
     ████ █▄▄▄█ ██▀▄   ▀ ▄ █▄▄████
     ████▄▄▄▄▄▄▄█▄▄██▄██▄██▄▄█████
     █████████████████████████████
     ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
    qr コマンドで QR コードを作成

    どうやってコマンドプロンプトに画像を表示しているのかと思いましたが、これは色を変えて文字を並べているだけというカラクリでした。おもしろいですね。

    Python から作成

    特にオプションを指定しない最も簡単な作成方法を示します。

    文字列で作成

    import qrcode
    
    src = 'string to convert'
    
    qr = qrcode.make(src)
    qr.save('./qrcode.png')

    qrcode.make() はショートカット関数という扱いですが、エラー訂正符号やサイズを設定しなくてもよい場合はこれで OK です。

    ファイルの内容で作成

    普通の Python のお作法でファイルを読み込むだけで、あとは上記と同じです。encoding にさえ注意すれば日本語を含んでいても問題ありません。

    import qrcode
    
    filename = 'source.txt'
    
    with open(filename, encoding = 'utf-8') as f:
        qr = qrcode.make(f.read())
        qr.save('./qrcode.png')

    参考

  • Python でメールの添付ファイルを zip して送る

    Python でメールの添付ファイルを zip して送る

    Python スクリプトからメールを送るのは smtplib があるおかげで、わりと簡単にできます。ですが、添付ファイルを付けようと思うとちょっと手間でした。

    今回は簡潔に書くため、全て localhost の SMTP サーバを利用しています。Gmail の SMTP サーバで送るときは TLS を使う必要があるのでご注意ください。

    テキストだけのメール

    import smtplib
    from email.mime.text import MIMEText
    from email.header import Header
    from email.utils import formatdate
    
    def mime_message(from_addr, to_addr, subject, body):
    	encoding = 'utf-8'
    
    	m = MIMEText(body, 'plain', encoding)
    	m['Subject'] = Header(subject, encoding)
    	m['To'] = to_addr
    	m['From'] = from_addr
    	m['Date'] = formatdate()
    	
    	return m
    
    def send_message(m):
    	smtp_host = 'localhost'
    	
    	s = smtplib.SMTP(smtp_host)
    	s.sendmail(m['From'], m['To'], m.as_string())
    	s.quit()
    
    to_addr = '[email protected]'
    from_addr = '[email protected]'
    subject = 'メールのテスト'
    body = 'このメールはテストメールです。'
    
    send_message(mime_message(from_addr, to_addr, subject, body))

    smtplib.sendmail() の from と to は RFC を確認せず書いているので、もしかしたら引っかかるかもしれません(“atmark <[email protected]>” みたいな形式)。

    ファイルを zip して添付

    変更するのはメッセージの作成部分だけです。

    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.base import MIMEBase
    from email.mime.text import MIMEText
    from email.header import Header
    from email.utils import formatdate
    from email import encoders
    
    import tempfile, zipfile
    import os.path
    
    def mime_email(to, subject, body, attachment_path = None):
    	encoding = 'utf-8'
    
    	m = MIMEMultipart()
    	m['Subject'] = Header(subject, encoding)
    	m['To'] = to
    	m['Date'] = formatdate()
    
    	# 本文
    	m.attach(MIMEText(body, 'plain', encoding))
    
    	if attachment_path:
    		# 添付ファイルを zip でまとめる
    		temp = tempfile.TemporaryFile()
    		with zipfile.ZipFile(temp, 'w', compression = zipfile.ZIP_DEFLATED) as zip:
    			zip.write(attachment_path, arcname = os.path.basename(attachment_path))
    		temp.seek(0)
    
    		# attach file
    		a = MIMEBase('application', 'zip')
    		a.set_payload(temp.read())
    		encoders.encode_base64(a)
    		m.attach(a)
    		a.add_header('Content-Disposition', 'attachment', filename = date.today().strftime('attachment.zip'))
    	
    	return m

    zipfile.write(filename) は与えられたパスのファイルをアーカイブします。このとき引数 arcname でアーカイブ時のファイル名を指定できます。指定しないとファイル名がそのまま arcname になりますが、無駄なディレクトリ構造を排除したいので os.path.basename() でファイル名を取得して指定しています。同じファイル名が存在する場合は適宜変更してください。

    複数のファイルを zip するときも、次のように zip.write() を続け呼び出すだけです。

    attachment_files = ['path_to_file.txt', 'long_path_to_file.txt']
    for p in attachment_files:
    	zip.write(p, arcname = os.path.basename(p))

    compression = zipfile.ZIP_DEFLATED を指定することで zip 全体を圧縮しています。compression には以下の値を指定できます。

    compression説明依存
    zipfile.ZIP_STORED無圧縮
    zipfile.DEFLATED通常の圧縮zlib
    zipfile.BZIP2BZIP2 圧縮bz2, Python 3.3 以降
    zipfile.LZMALZMA 圧縮lzma, Python 3.3 以降

    compression は zipfile.write() で個別に指定することもできます。

    参考

  • Python でのプロファイリング

    Python でのプロファイリング

    サッと確認

    $ python3 -m cProfile slow_script.py

    時間がかかっている順に並び替え。

    $ python3 -m cProfile -s tottime slow_script.py

    視覚的に確認

    準備

    $ sudo pip3 install pycallgraph
    $ sudo apt install graphviz

    プロファイリング

    $ pycallgraph graphviz -- slow_script.py

    pycallgraph.png が生成されます。

  • Python と SQLite3 で pivot を実現する

    Python と SQLite3 で pivot を実現する

    Excel で集計を行うときに使う「ピボットテーブル」という機能があります。複雑な集計表をマウス操作だけで作れるので、使い方によってはとても便利な機能です。

    使ったことがない方に簡単に説明します。次のようなデータベースを考えます。

    品目産地数量単位
    りんご青森県5
    りんご長野県30
    りんご長野県2
    りんご青森県10
    みかん長崎県7
    みかん静岡県120
    みかん福岡県50

    このデータから、都道府県と品目ごとの数量を集計するとします。ただし、集計するのは単位が「箱」のものだけ。すると次のような表になります。

    単位:箱りんごみかん
    青森県15
    長野県2
    長崎県7

    これを項目を選ぶだけで自動で生成してくれるのが、Excel のピボットテーブルです。

    SQL だけでこのピボットを簡単に作る方法はないのかというと、SQL Server には PIVOT 句があって簡単に実現できます。Oracle にもあるようです。しかし、それ以外の MySQL, MariaDB, PostgreSQL, SQLite3 では PIVOT 句は今のところ無いようでした。無いからといってできないわけではなく、例えば次の記事のように色々と組み合わせればできないこともないようです。

    記事を読まなくても SQL 文を見てもらえばわかると思いますが、よほど SQL だけで解決しないといけない状況でない限り、正直採りたくない手法です。どうせ SQL でデータを取得してからプログラム側で処理するのだから、プログラム側で処理すればいいと思い直したのが今回のお話です。

    説明用に SQLite3 で次のようなデータベースを作成しました。

    CREATE TABLE shipping (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        item TEXT NOT NULL,
        qua INTEGER NOT NULL,
        unit TEXT NOT NULL,
        pref TEXT NOT NULL
    );

    サンプルデータとして次の CSV を予め insert しておきます。

     0,"いちご",10,"箱","岡山県"
    1,"ぶどう",33,"個","長崎県"
    2,"りんご",65,"箱","長野県"
    3,"みかん",25,"箱","福岡県"
    4,"ぶどう",2,"個","長野県"
    5,"ぶどう",42,"個","長野県"
    6,"いちご",31,"箱","岡山県"
    7,"ぶどう",51,"箱","長崎県"
    8,"りんご",79,"箱","福岡県"
    9,"いちご",92,"箱","長崎県"
    10,"ぶどう",40,"箱","岡山県"
    11,"いちご",12,"箱","長崎県"
    12,"いちご",56,"箱","長野県"
    13,"いちご",11,"個","長野県"
    14,"みかん",90,"個","山梨県"
    15,"ぶどう",90,"個","長野県"
    16,"みかん",44,"箱","岡山県"
    17,"いちご",93,"箱","長崎県"
    18,"りんご",13,"個","青森県"
    19,"ぶどう",20,"個","長野県"

    余談ですが、このデータは下記の Python スクリプトで生成させてます。

    import random
    
    item = ('りんご', 'みかん', 'ぶどう', 'いちご')
    pref = ('青森県', '長野県', '山梨県', '岡山県', '福岡県', '長崎県')
    unit = ('箱', '個')
    
    for i in range(20):
    	print('{index},"{item}",{qua},"{unit}","{pref}"'.format(index = i, item = random.choice(item), pref = random.choice(pref), unit = random.choice(unit), qua = random.randrange(1, 100)))

    CSV を SQLite のデータベースに取り込むには、予めテーブルを用意した上でコマンドラインで .import FILE TABLE を実行します。SQLite の標準区切り子は “|” なので、CSV を読み込む前に “,” に変更しておく必要があります。

    sqlite> .separator ,
    sqlite> .import data.csv shipping

    さて、肝心の Python プログラム側ですが、今回は実験も兼ねて SQL は完全にデータの取得だけ、処理は全て Python で行いました。

    import sqlite3
    
    # pivotのフィルタ
    pivot_filter = 'unit'
    pivot_filter_value = '箱'
    
    # pivotする列と行と値
    pivot_col = 'item'
    pivot_row = 'pref'
    pivot_val = 'qua'
    
    # SQLite3からデータ取得
    connect = sqlite3.connect('db.sqlite3')
    connect.text_factory = str
    connect.row_factory = sqlite3.Row
    rows = connect.execute('SELECT * FROM shipping;').fetchall()
    rows = [r for r in rows if r[pivot_filter] == pivot_filter_value]
    
    # pivotの列項目と行項目
    pivot_cols = {r[pivot_col] for r in rows}
    pivot_rows = {r[pivot_row] for r in rows}
    
    # pivot初期化
    pivot = {row : {col : 0 for col in pivot_cols} for row in pivot_rows}
    
    # pivot集計
    for r in rows:
    	pivot[r[pivot_row]][r[pivot_col]] += r[pivot_val]
    
    # pivot出力
    # 列項目
    for c in sorted(pivot_cols):
    	print('\t{}'.format(c), end = '')
    print('')
    # 一度行毎に出力
    for r in sorted(pivot_rows):
    	# 行項目
    	print('{}'.format(r), end = '')
    	# 集計値
    	for c in sorted(pivot_cols):
    		print('\t{}'.format(pivot[r][c]), end = '')
    	print('')

    これを先ほどのデータで実行した結果は次のようになりました。

           いちご ぶどう みかん りんご
    岡山県 41 40 44 0
    福岡県 0 0 25 79
    長崎県 197 51 0 0
    長野県 56 0 0 65

    タブ区切りのテキスト表示なのでちょっと見難いですが、ちゃんとピボットできていますね。

    pivotの出力結果

    この記事はここで終了です、と書こうと思って、試しにデータを 100 万件にしてみたところ、処理がめちゃくちゃ遅いに気付いてしまいました。予想するに SQL で全てしてしまった方が圧倒的に速いのですが、それでは今回の目的である手間を最小にすることから外れてしまいます。なので出来る限り手間を掛けずに高速化できないか、試してみます。

    高速化前

    以下、先に挙げたランダムなデータ生成スクリプトで 100 万件のデータを用意し、SQLite3 に INSERT 済の状態での検証です。

    $ time python3 pivot.py > result.txt

    シェルで上記を 5 回実行し、その平均時間を処理時間とします。

    1. 22.426
    2. 22.441
    3. 22.789
    4. 22.366
    5. 22.335

    高速化前は 22.471 秒となりました。

    高速化 1 – フィルタ処理を SQL で

    まずはすぐにでも思いつきそうな、Python のリスト内包表記で行っているフィルタ処理を SQL の WHERE 句で行う場合。ソースは差分だけ載せます。

    rows = list(connect.execute('SELECT * FROM shipping WHERE {} = ?;'.format(pivot_filter), (pivot_filter_value, )).fetchall())
    1. 13.371
    2. 13.327
    3. 13.226
    4. 13.336
    5. 13.227

    平均 13.297 秒、約 9 秒の高速化。割合にすると 40% ですね。

    高速化 2 – GROUP BY で集計

    これもこの記事を書きながら思いついた方法。純粋にこの方法での影響を調べるために高速化 1 は一度元に戻して検証します。

    rows = connect.execute('''
     	SELECT
     		{0}, {1}, {2}, SUM({3}) AS {3}
    	FROM
     		shipping
    	GROUP BY
     		{0}, {1}, {2};'''.format(
     			pivot_row, pivot_col, pivot_filter, pivot_val)
     		).fetchall()
    1. 17.721
    2. 17.597
    3. 17.565
    4. 17.601
    5. 18.192
    6. 17.792

    平均 17.749 秒、約 5 秒の高速化。約 20% の高速化です。

    高速化 3 – 1 と 2 の組合せ

    単純に高速化 1 と 2 を両方使います。

    rows = list(connect.execute('''
    	SELECT
    		{0}, {1}, {2}, SUM({3}) AS {3}
    	FROM
    		shipping
    	WHERE
    		{2} = ?
    	GROUP BY
    		{0}, {1}, {2};'''.format(
    			pivot_row,
    			pivot_col,
    			pivot_filter,
    			pivot_val),
    			(pivot_filter_value, )
    		).fetchall())
    1. 8.916
    2. 8.853
    3. 8.850
    4. 8.886
    5. 8.807

    平均 8.862 秒、約 13 秒の高速化。60% の高速化です。

    結論

    わかりきったことですが、SQLite は速くて Python は遅いという結果になりました。楽を取るとか速さを取るか、それに尽きます。

    Special Thanks

    実装に当って、れお(@reoreo125)さんの多大なるご協力をいただきました。いつもありがとうございます。

  • reCAPTCHA v3 を Python CGI で使う

    reCAPTCHA v3 を Python CGI で使う

    以前に reCAPTCHA v2 の導入記事を書きましたが、今回は reCAPTCHA v3 です。v2 の画像選択が手間になってきて、よりシンプルに扱える v3 に更新しようと思ったのがきっかけです。

    チェックボックスにチェックする v2 と異なり、v3 はフォームに埋め込むものがありません。JavaScript で token を取得するところまでは一緒なのですが、その token を自分でフォームに埋め込む必要があります。

    reCAPTCHA への登録の仕方は v2 も v3 も同じなので、前回の記事に譲ります。

    クライアントサイド

    head 要素内に次のコードを埋め込みます。

    <script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>

    SITE_KEY の部分は各々発行されたものに置換えてください。

    次に、どこでも良いので次のコードを埋め込みます。head 要素内でも良いですし、body の最後でも問題ありません。

    <script>
    grecaptcha.ready(function() {
      grecaptcha.execute('SITE_KEY', {action: 'action_name'})
      .then(function(token) {
        document.getElementById('g-recaptcha-response').value = token;
      });
    });
    </script>

    ‘action_name’ は名前分けしておくと、admin console で分析が行えるようです。

    最後に、フォーム要素の中に次の隠し要素を埋め込んでおきます。

    <input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">

    先ほどの JavaScript が実行されると、token がこの要素の value に入ります。

    サーバーサイド (Python2)

    今回はお手軽さで cgi.FieldStorage() と urllib を使います。

    import cgi
    import urllib, urllib2
    import json
    
    # フォームの取得
    form = cgi.FieldStorage()
    
    # g-recaptcha-responseの値を取得
    response = form.getfirst('g-recaptcha-response', '')
    
    # APIに照合を行う
    params = urllib.urlencode({'secret': SECRECT_KEY, 'response': response})
    req = urllib2.Request('https://www.google.com/recaptcha/api/siteverify', params)
    result = json.loads(urllib2.urlopen(req).read())

    返ってくる json には次の値が含まれています。

    {u'action': u'action_name', u'score': 0.9, u'hostname': u'kuratsuki.net', u'challenge_ts': u'2018-12-06T06:51:20Z', u'success': True}
    success
    True|False 正しい token が送られたかどうかの判定
    score
    0.0-1.0 bot(0.0) に近いか人間(1.0)に近いかを数値で表します
    action
    string action 名
    challenge_ts
    yyyy-MM-dd’T’HH:mm:ssZZ タイムスタンプ
    hostname
    string reCAPTCHA が行われたホスト名
    error-codes
    [] エラーがあったときのエラーのリスト

    サーバーサイドではこの score を用いて処理を分岐させれば良いようです。

    参考

  • Debian 9 Stretch で locale の変更

    Debian 9 Stretch で locale の変更

    $ localectl
    System Locale: n/a
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105
    $ localectl list-locales
    C.UTF-8
    en_US.utf8
    $ sudo localectl set-locale LANG=en_US.UTF-8
    $ localectl
    System Locale: LANG=en_US.UTF-8
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105