蛇ノ目の記

技術のことも。そうでないことも。

PythonでQRコードを生成してXMLに変換する

PythonQRコードを生成してXMLに変換する話。

qrcodeというパッケージを使う。

pypi.org

8月半ばに作ったWebアプリでXMLに変換したQRコードを使っている。

nao-y.hatenablog.com

QRコードSVGを生成する話は以下のブログが詳しい。

tokibito.hatenablog.com

今回はさらに、SVGXMLにタグに変換してHTMLに埋め込むところまでをやっていく。

コードは以下のGistで公開している。

PythonでQRコードを生成してSVGタグに変換する · GitHub

全体の流れ

def main():
    # QRコードに埋め込むテキストの読み込み
    with open('zen.txt', encoding='utf-8') as f:
        text = f.read()
    # QRコードのSVG作成
    qr = make_qrcode_svg(text)
    # DOMオブジェクトを作成
    dom = create_dom(qr)
    # DOMをXMLに整形
    xml = dom.toprettyxml()

    HTML = f'''
<!DOCTYPE html>
<head>
<title>sample QR</title>
</head>
<body>
{xml}
</body>
</html>
'''
    # HTMLに埋め込み
    with open('sample.html', 'w', encoding='utf-8') as f:
        f.write(HTML)

QRコードSVGを作る

import xml.dom.minidom

import qrcode
import qrcode.image.svg as svg


def make_qrcode_svg(text):
    """
    QRコードのSVGを作る関数
    :param text: QRコードに埋め込みたい文字列
    :return: QRコードのSVG
    """
    factory = svg.SvgPathImage
    img = qrcode.make(
        text,
        image_factory=factory,
        version=1,
        box_size=6,
        error_correction=qrcode.constants.ERROR_CORRECT_L)
    return img

XMLの構造

具体的にXMLにする方法を説明する前に、XMLの構造を簡単にまとめる。

以下が今回、生成するXML

<?xml version="1.0" ?>
<svg height="21mm" viewBox="0 0 21 21" width="21mm" xmlns="http://www.w3.org/2000/svg">
    <path d=". . .">
</svg>

初めにXML宣言がある。<?xml version="1.0" ?> の部分。バージョンや文字コードを定義する。

XMLにおいてタグは要素と呼ぶ(要素ノードとも呼ばれる)。

要素には属性を設定できる(属性もまた、属性ノードとも呼ばれる)。

svg要素のheight属性、というような感じ。

また要素には子要素を設定できる。path要素がsvg要素の子要素にあたる。

XMLを生成する

標準ライブラリのxml.dom.minidomを使う。以下のQiita記事を参考にした。

qiita.com

def create_dom(img):
    """
    QRコードのsvgをXMLとして出力する関数
    :param img: QRコードのsvg
    :return dom: xml.dom.minidom.Document Object
    """
    width = img.width
    items = img.get_image()
    path = img.make_path()

    # DOM
    dom = xml.dom.minidom.Document()
    # svg要素を作成
    elem = dom.createElement('svg')
    # DOMに子要素elemを追加
    dom.appendChild(elem)

    # svgからxmlns属性を取得
    xmlns = items.get('xmlns')

    # svg要素の属性を指定
    values = {
        'width': f'{width}mm',
        'height': f'{width}mm',
        'viewBox': f'0 0 {width} {width}',
        'xmlns': xmlns
    }

    # DOMにsvg要素を設定
    set_dom_attr(dom, elem, values)

    # path要素を作成
    path_node = dom.createElement('path')
    # elem要素に子要素(path_node)を追加
    elem.appendChild(path_node)

    # pathからd属性を取得
    d = path.get('d')
    # pathからid属性を取得
    id_ = path.get('id')
    # pathからstyle属性を取得
    style = path.get('style')

    # path要素の属性を指定
    values = {
        'd': d,
        'id': id_,
        'style': style,
    }

    # DOMにpath要素を設定
    set_dom_attr(dom, path_node, values)
    return dom
def set_dom_attr(dom, elem, values):
    """
    属性ノードを生成して要素を設定する関数
    :param dom: xml.dom.minidom.Document Object
    :param elem: 要素
    :param values: 属性
    :return:
    """
    for attr, value in values.items():
        # 属性ノードを生成
        subnode_attrs = dom.createAttribute(attr)
        # 属性ノードの値を設定
        subnode_attrs.value = value
        # 属性ノードを要素ノードにセットする
        elem.setAttributeNode(subnode_attrs)

以下のようにHTMLが出力される(path要素の中身は長すぎるので省略。

<!DOCTYPE html>
<head>
<title>sample QR</title>
</head>
<body>
<?xml version="1.0" ?>
<svg height="21mm" viewBox="0 0 21 21" width="21mm" xmlns="http://www.w3.org/2000/svg">
    <path d=". . .">
</svg>
</body>
</html>

出力されたHTMLは以下のようになる。 f:id:Nao_Y:20180923203817p:plain

Webアプリで使う場合はこのQRコードの部分(今回はxml変数)にテンプレートエンジンのフィルター(Jinja2ではsafeフィルター)を掛ければHTML埋め込むことができる。

PyCon JP 2018とBP入社一周年

PyCon JP 2018

9/15~9/18にかけてPyCon JP 2018が開催された。

pycon.jp

去年に引き続きスタッフとしての参加。今年はコンテンツチームに所属して、トークの採択やコミュニティブースの企画に関わった。去年作った領収書発行サイトの更新もやったりした(デプロイはシステムチームの方にお願いしたのだけど)。

コミュニティブースはPythonコミュニティが活動を紹介する企画で、南はPyCon Kyushu、北は札幌Pythonまで日本各地から6つのコミュニティが参加した。

コンテンツチームのタスクにはトークセッションの進行も含まれている。そういうわけで今年はあまりトークが聴けていない。とはいえ、いい感じにシフトを組んでくれたおかげで聴きたいトークの進行に割り当てられることもあった。以下、進行をしながら聴いたトーク

Day1

  • Webアプリケーションの仕組み - Takayuki Shimizukawa

  • あなたと私いますぐパッケージン - Atsushi Odagiri

  • Interactive Network Visualization using Python 〜 NetworkX + BokehでPEPの参照関係を可視化する - Tomoko Furuki

  • How to Data Wrangling? Tips for using python libraries for big-data analysis including scikit-learn. - 松岡光

Day2

  • HomeSecurity with Python - Yuki Takino

  • 複数アプリケーションのプロセスとログを管理するための新しいツールと手法- 谷津真樹/Masaki Yatsu

  • From Data to Web Application: Anime Character Image Recognition with Transfer Learning - Iskandar Setiadi

  • Django を Zappaで構築してServerless Python のベストプラクティスを探る - 向山 裕介 (Yusuke Mukoyama)

実のところ、来年は一般枠で参加しようと思っていたけどコンテンツチームをやってみて、改善したいことや新しくやってみたいことが少し出てきたので来年もまたスタッフTシャツを着ていそうな気がする。

BP入社一周年

PyCon JP 2018のカンファレンス前日準備の9/16がビープラウド入社一周年だった。

nao-y.hatenablog.com

この記事にある通り、3連休だったので初出社したのは9/19だったのだけど。

この1年でPythonistaとしてどれだけ成長できたのか、あまり自分ではわかっていない。仕事でPythonを使ったことがなかった人間がconnpassやPyQ、受託案件に関わってなんとかやってこれたのでは多少なりとも成長できたのだと思いたい。

入社した頃はまだ手が届かなかったと思っていた書籍執筆はSoftware Design9月号の特集記事の一つを書くという形で体験できた。書籍レビュー・監修に至っては「Pythonプロフェッショナルプログラミング第3版」「スラスラ読める Pythonふりがなプログラミング」「Pythonで学ぶあたらしいデータ分析の教科書」と3冊も関わることができた。

うまい具合に機会を拾ってやってこれた。あれ、それなりに成長してるのでは。とはいえWebもデータも全然まだまだなのでやれること・やりたいことをきちんとやっていきたい。

ただただエモいだけの話になってしまったが、次の1年もBPで頑張っていく。

Python3.7でDiscord.pyを動かすときの躓きどころと対策

近頃、またDiscord botを作っている。

Python.3.7に上げたことによってDiscord.pyが動かないという現象に見舞われたので対策をメモ。

発生したエラー

このissueと同様のエラーが発生した。

github.com

対策

This library does not support 3.7. This error in particular is caused by async becoming a reserved keyword.

Please use Python 3.4-3.6.

async予約語になったからっぽい。Python3.4-3.6を使って、どうぞ。」

Python3.7からasync予約語になったことが原因のようだ。

バージョンを下げたくなかったのでさらに読み進めてみた。

https://github.com/Rapptz/discord.py/issues/1249#issuecomment-412256277

によれば

Your options are the following:

  • separately upgrade aiohttp and websockets to the latest versions after installing the async branch from GitHub
  • downgrade to Python 3.6
  • switch to the rewrite branch
  • aiohttpとwebsocketsを別々にアップグレードしてから、asyncブランチをインストールする

  • Python3.6にダウングレードする

  • rewriteブランチを使う

とのこと。一番手っ取り早そうなrewriteブランチを使う方法でやってみる。

ちなみに、rewriteブランチを使うという対策は以下のQiitaでも紹介されている(エラーの原因については触れられていない)。

qiita.com

以下のコマンドでDiscord.pyのrewriteブランチをインストール。

pip install git+https://github.com/Rapptz/discord.py.git@rewrite

rewriteブランチでの注意点

これまではbotに発言させる際にclient.send_messageメソッドを使っていたが、rewriteブランチではmessage.channel.sendメソッドを使う。ググって出てくるDiscord.pyの使い方は前者で書かれていることが多いので気をつけよう。

Software Design2018年9月号にて「データ分析にPythonが選ばれる理由」を執筆しました

というわけで記事を執筆しました。とうとう著者デビューしたので著者ページ作ってみました。著者画像も経歴もまだないけど。

Amazon.co.jp: 横山 直敬:作品一覧、著者略歴

さて、この記事はPython初学者向けを意識して、以下の4つに焦点を当てています。

  • データ分析におけるPythonの強み

  • データ分析のための環境構築

  • 手軽に入手できるデータセットの紹介

  • ボストン市地域別平均住宅価格データの分析を体験

データ分析といえばAnacondaが便利ですが、condaとpipが混在しているせいでハンズオンで躓いたという事例をよく聞くので、Anacondaを使わずPython3.6.6とvenvを使った環境構築を説明しました(執筆中に3.6.6と3.7が出たときは少し焦った)。

データセット紹介ではscikit-learn付属データセットとKaggle Datasetsについて触れています。データ分析体験では、線形回帰を使って住宅の部屋数から住宅価格を予測します。

Pythonによるデータ分析を手軽に体験できる記事に仕上がっていると思います。ぜひお手にとってみてください。

Special Thanks

  • レビューしてくれたBPのみなさん

  • 技術評論社の編集者さん

  • 告知ツイートをRT,いいねしてくれた人たち

  • これから雑誌を買ってくれる・買ってくれた人たち

tagmineをリリースしました-酷暑のイベントの緊急時に役立つWebアプリ

tagmineをリリースしました。

f:id:Nao_Y:20180809010509p:plain

tagmineとは

熱中症などで倒れたとき、医療処置に役立つ情報を埋め込んだQRコードを作成するWebアプリです。

QRコードに埋め込める主な情報

  • 氏名
  • 血液型
  • 既往歴
  • 緊急連絡先

QRコードは名刺サイズのカードとして表示されるので、印刷して使うことができます。財布の中やネックストラップの内側に入れるといざというときに見つけてもらいやすいでしょう。

少し前にこんなtogetterまとめを見て、QRコード版ドッグタグを作ってみようと思ったのがきっかけです。

togetter.com

コミケが8/10から始まります。今年も暑くなりそうですので、活用してもらえたら嬉しいです。

tagmineの中身の話はまた後日。

herokuのProcfileに書くgunicornのコマンドにハマった話

herokuで動かすWebアプリがどんなプロセスを使うかを定義するProcfile

Procfileはドキュメントルートに置き、例えばFlask + gunicornを使うときには web: gunicorn app:appとする。古事記にもそう書いてある(heroku公式ドキュメントやいろいろなブログ)

heroku公式ドキュメント: Deploying Python Applications with Gunicorn | Heroku Dev Center

その教えに従ってProcfileを作成したが一向に動かない。

Failed to find application object 'app' in 'myapp'

と表示されるばかり。

ディレクトリ構成はこんな感じ。

flask_app
├ Procfile
├ requirements.txt
├ runtime.txt
└ myapp
  ├ myapp.py
  ├ static
  └ templates

一度myappディレクトリに移動する必要があるのでは、と考えて

web: gunicorn --chdir myapp myapp:app

と書き換えたところアプリが正常に動いた。

作ったWebアプリの話はまた後日。

追記

アプリのディレクトリを分けずに以下のようなディレクトリ構成にすることで、Procfileにディレクトリ移動を書かなくてよくなる。アプリのディレクトリを分けているとローカルでの実行やパッケージングで不便になる、と@shimizukawaからアドバイスをもらった。ありがとうございます。

flask_app
├ Procfile
├ requirements.txt
├ runtime.txt
├ myapp.py
├ static
├ templates

また、herokuの環境変数PYTHONPATHをPYTHONPATH=myappsと設定すると、アプリのディレクトリを分けていてもProcfile実行時のディレクトリ移動が必要なくなる。と@shimizukawaからアドバイスをもらった。ありがとうございます。

ユニットテストで躓いたところ - mock.patch()

単純なスクリプトユニットテストを書いていて躓くことがあったので、解決法をメモ。

例えばこんな、おみくじをするだけの簡単なスクリプトがあるとする。

import random


fortunes = {
    1: '凶',
    2: '吉',
    3: '大吉'
}

number = random.choice([1, 2, 3])

fortune = fortunes[number]

print(fortune)

変数numberに1〜3までのランダムな値が入って、対応する運勢が出力される 。random.choice()の部分をパッチして、期待通りの運勢が出力されることをテストする。

import unittest

from importlib import import_module
from unittest.mock import patch


class FortuneTestCase(unittest.TestCase):

    @patch('random.choice', lambda x: 1)
    def test_bad(self):
        module = import_module('fortune')
        expect = ('凶')
        self.assertEqual(module.fortune, expect)

    @patch('random.choice', lambda x: 3)
    def test_good(self):
        module = import_module('fortune')
        expect = ('大吉')
        self.assertEqual(module.fortune, expect)

さて、このテストを実行するとどうなるか。

(env) $ python3 -m unittest tests.test
凶
.F
======================================================================
FAIL: test_good (tests.test.FortuneTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py", line 1191, in patched
    return func(*args, **keywargs)
  File "/tests/test.py", line 21, in test_good
    self.assertEqual(module.fortune, expect)
AssertionError: '凶' != '大吉'
- 凶
+ 大吉


----------------------------------------------------------------------
Ran 2 tests in 0.029s

FAILED (failures=1)

test_goodメソッドのアサーションで例外が発生する。パッチした値が反映されていない。つまり初めに実行されたtest_badメソッドでパッチした値がそのまま使われている。

値がパッチされっぱなしなのではれば、モジュールを読み込み直してみる。importlib.reload()を使う。

Python3 公式ドキュメント importlib - reload

import unittest

from importlib import import_module, reload
from unittest.mock import patch


class FortuneTestCase(unittest.TestCase):

    @patch('random.choice', lambda x: 1)
    def test_bad(self):
        module = import_module('fortune')
        expect = ('凶')
        self.assertEqual(module.fortune, expect)

    @patch('random.choice', lambda x: 3)
    def test_good(self):
        module = import_module('fortune')
        reload(module)
        expect = ('大吉')
        self.assertEqual(module.fortune, expect)
(env) $ python3 -m unittest tests.test
凶
凶
.大吉
.
----------------------------------------------------------------------
Ran 2 tests in 0.025s

OK

無事テストが通った。テストメソッド実行ごとにパッチされると思っていただけにこの動きにだいぶ悩むこととなった。

公式ドキュメントに以下の注釈があるためtest_goodメソッドでだけreloadを使った。

注釈 :いろいろなテストが実行される順序は、文字列の組み込みの順序でテストメソッド名をソートすることで決まります。

https://docs.python.jp/3/library/unittest.html#organizing-test-code