蛇ノ目の記

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

Ubuntu 16.04でLet's Encryptを使ってサーバ証明書を取得する

BeProud Advent Calender 2018 15日目の記事です。

adventar.org

仕事でサーバーにSSL証明書を入れる機会があった。

できればワイルドカード証明書使いたい、という気持ちがあった。そこで、手軽に使えてワイルドカード証明書にも対応しているというLet's Encryptを採用。

Let's Encryptのクライアントならワイルドカード証明書が取得できる、そう思っていた時期がありました(´・ω・`)

環境

tl;dr

certbotではなくletsencrypt

Let's Encryptユーザーガイドを見ると、certbotコマンドを使っているが、Ubuntu 16.04ではletsencryptコマンドを使う。

インストール方法: Ubuntu 16.04 (xenial)に従って、

sudo apt-get install letsencrypt python-letsencrypt-apache

とするだけでいい。apacheプラグインが不要な場合は python-letsencrypt-apacheは省略する。

Ubuntu 16.04にインストールする方法はあるが、リポジトリを追加する必要がある。

www.digitalocean.com

オーケストレーションの際にリポジトリを追加する手間が増えるので、今回は採用しなかった。ワイルドカード証明書は必須の要件ではなかったし。

ワイルドカード証明書

ワイルド証明書とは、一枚の証明書で同じドメインに属しているサブドメインSSL化ができる証明書のこと。

証明書を取得する際に、*.example.comのようにドメイン名の左側にアスタリスクをつける。

参考: ワイルドカード証明書について | JPRS

ワイルドカード証明書を取得するには、Let's EncryptのパッケージがACME v2 APIに対応していなければいけない。ACMEとは証明書発行のプロトコルのこと。

参考: Let's Encrypt を支える ACME プロトコル - Block Rockin’ Codes

Let's Encryptでのワイルドカード証明書取得については以下が詳しい。

ACME v2 とワイルドカード証明書の技術情報 - Let's Encrypt 総合ポータル

ワイルドカード証明書と ACME v2 へ対応 - Let's Encrypt 総合ポータル

ACME v2 APIに対応したクライアントは、ACME Client Implementations - Let's Encrypt - Free SSL/TLS Certificatesで一覧できる。そしてここにletsencryptの名前はない

サーバ証明書の取得

Webサーバが動作している環境では、webrootプラグインを使う。

$ letsencrypt certonly --webroot {webroot-path} -d certificated.example.com

{webroot-path}はWebサーバのドキュメントルートディレクトリ。

Webアプリが/home/www/myapp/であれば、{webroot-path}/home/www/myapp/になる。

このコマンドを実行すると{webroot-path}/.well-known/acme-challengeにワンタイムトークンが作られる。Let's Encryptの認証サーバーがexample.com/.well-known/acme-challenge/{token}に対してHTTPリクエストすることで、ドメインの認証を行う仕組みになっている。

今回の案件ではドキュメントルートディレクトリではなく/home/www/letencrypt/を指定した。

この場合、HTTP通信(80番ポート)を通したcertificated.example.comへのリクエストに対して、/home/www/letencrypt/マッピングしておく。

# /etc/nginx/nginx.conf
server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

コマンドを実行するとどうなる?

過去に証明書を取得したことがない場合は、鍵の紛失時の連絡や期限切れ通知に使うメールアドレスを入力するTUIが起動する。メールアドレスを入力すると利用規約への同意が求められる。これも初回のみ。

nginxにSSL通信の設定をする

サーバ証明書が取得できたので、nginxの設定をしていく。

サーバ証明書秘密鍵/etc/letsencrypt/live/{ドメイン名}に置かれている。

# /etc/nginx/nginx.conf
server {
    listen 443;
 server_name certificated.example.com
    ssl on;
    ssl_certificate /etc/letsencrypt/live/certificated.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/certificated.example.com/privkey.pem;

    location / {
    . . .
    }
}

server {
    listen 80;
    server_name certificated.example.com;

    location / {
        root /home/www/letsencrypt;
    }
}

Djangoのtemplateでdefaultdictを使えない件について

BeProud Advent Calender 2018 9日目の記事です。

adventar.org

案件でDjangoを使っていたときにdefaultdictがtemplateにレンダリングできないことに気づいたので検証してみた。

案件で使っているバージョンは1.9.3。

おしながき

ディレクトリ構成

Django 1.9.3, Django 2.1.4, jinja2で確かめるということで変則的なディレクトリ構成になっている。 mysite以下はDjangoプロジェクトの通常の構成。

advent_cal_2018
  ├ django_1.9.3
  │ ├ mysite
  │ └ env
  ├ django_2.1.4
  │ ├ mysite
  │ └ env
  └ jinja2
    ├ env
    └ defaultdict_survey.py

検証用のWebアプリ

localhost:8000 に繋ぐと辞書の中身を表示するだけのWebアプリをでっち上げた。

# mysite/myapp/views.py
from collections import defaultdict, OrderedDict

from django.shortcuts import render


def index(request):

    # dictionary
    normal_d = {
        'foo': 'foo',
        'bar': 'bar',
        'baz': 'baz',
    }

    # defaultdict(list)
    default_d_1 = defaultdict(list)
    s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

    for k, v in s:
        default_d_1[k].append(v)

    # defaultdict(str)
    default_d_2 = defaultdict(int)
    keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']

    for k in keys:
        default_d_2[k] += 1

    # OrderedDict
    ordered_d = OrderedDict()
    s = [(0, 'zero'), (1, 'one'), (2, 'two')]
    for k, v in s:
        ordered_d[k] = v

    context = {
        'normal': normal_d,
        'default_d_list': default_d_1,
        'defaukt_d_int': default_d_2,
        'ordered_d': ordered_d,
    }

    return render(request, 'myapp/index.html', context)
<!--mysite/myapp/temaplates/myapp/index.html-->
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in normal.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_list.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_int.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in ordered_d.items %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

</body>
</html>

Django 1.9.3で確認する

$ pwd
/Users/user/Python/works/advent_cal_2018/django_1.9.3
$ ls
env                     mysite                  requirements.txt
$ . env/bin/activate
(env) $ cd mysite/
(env) $ pip list
Package    Version
---------- -------
Django     1.9.3  
pip        10.0.1 
setuptools 39.0.1 
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(env) $ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2018 - 05:46:05
Django version 1.9.3, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

表示された画面 f:id:Nao_Y:20181209144824p:plain

確かにdefaultdictはレンダリングされていない。

Django 2.1.4で確認する

$ pwd
/Users/user/Python/works/advent_cal_2018/django_2.1.4
$ ls
env                     mysite                  requirements.txt
$ . env/bin/activate
(env) $ cd mysite/
(env) $ pip list
Package    Version
---------- -------
Django     2.1.4  
pip        10.0.1 
pytz       2018.7 
setuptools 39.0.1 
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(env) nao-Mac:mysite nao$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
December 09, 2018 - 05:52:18
Django version 2.1.4, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

表示された画面 f:id:Nao_Y:20181209145345p:plain

2.1.4でもdefaultdictはレンダリングできないっぽい。

Jinja2で試してみる

Jinja2ではテンプレートにdefaultdictを埋め込んで、HTMLを出力することで確認する。

# jinja2/defaultdict_survey.py
from collections import defaultdict, OrderedDict

from jinja2 import Template

template = """
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in normal.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_list.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>
    
    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in default_d_int.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            {% for k, v in ordered_d.items() %}
            <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
            {% endfor %}
        </table>
    </div>

</body>
</html>
"""

template = Template(template)

# dictionary
normal_d = {
    'foo': 'foo',
    'bar': 'bar',
    'baz': 'baz',
}

# defaultdict
default_d = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

for k, v in s:
    default_d[k].append(v)

# defaultdict(list)
default_d_1 = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

for k, v in s:
    default_d_1[k].append(v)

# defaultdict(str)
default_d_2 = defaultdict(int)
keys = ['ham', 'spam', 'eggs', 'ham', 'spam', 'spam', 'eggs']

for k in keys:
    default_d_2[k] += 1

# OrderedDict
ordered_d = OrderedDict()
s = [(0, 'zero'), (1, 'one'), (2, 'two')]
for k, v in s:
    ordered_d[k] = v

context = {
    'normal': normal_d,
    'default_d': default_d,
    'ordered_d': ordered_d,
}

rendered = template.render(
    normal=normal_d,
    default_d_list=default_d_1,
    default_d_int=default_d_2,
    ordered_d=ordered_d)

print(rendered)
$ cd 
$ pwd
/Users/user/Python/works/advent_cal_2018/jinja2
$  ls
defaultdict_survey.py   env                     requirements.txt
$ python defaultdict_survey.py > rendered.html
<!--jinja2/rendered.html-->
<html>
<head>
    <title>BP Advent Calendar 2018</title>
</head>
<body>
    <div class="normal">
        <h1>dictionary</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>foo</td><td>foo</td></tr>
            
            <tr><td>bar</td><td>bar</td></tr>
            
            <tr><td>baz</td><td>baz</td></tr>
            
        </table>
    </div>

    <div class="defaultdict-list">
        <h1>defaultdict(list)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>yellow</td><td>[1, 3]</td></tr>
            
            <tr><td>blue</td><td>[2, 4]</td></tr>
            
            <tr><td>red</td><td>[1]</td></tr>
            
        </table>
    </div>
    
    <div class="defaultdict-int">
        <h1>defaultdict(int)</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>ham</td><td>2</td></tr>
            
            <tr><td>spam</td><td>3</td></tr>
            
            <tr><td>eggs</td><td>2</td></tr>
            
        </table>
    </div>

    <div class="OrderedDict">
        <h1>OrderedDict</h1>
        <table>
            <tr><th>key</th><th>value</th></tr>
            
            <tr><td>0</td><td>zero</td></tr>
            
            <tr><td>1</td><td>one</td></tr>
            
            <tr><td>2</td><td>two</td></tr>
            
        </table>
    </div>

</body>
</html>

Jinja2では問題なくdefaultdictを使える。

Djangoでdefaultdictを使いたいとき

単純にdefaultdictをdictionaryに変換してあげるのが一つ。

テンプレートエンジンにJinja2を設定できるのでそれを利用する手もある。

https://docs.djangoproject.com/en/2.1/topics/templates/#django.template.backends.jinja2.Jinja2

ただ、すべてのテンプレートをJinja2に合わせる必要があるので、基本的にはdictionaryに変換することになる、のかなぁ。

なぜdefaultdictとOrderedDictで扱いが違うのか。

OrderedDict

通常の dict メソッドをサポートする、辞書のサブクラスのインスタンスを返します。

defaultdict

新しいディクショナリ様のオブジェクトを返します

collections --- コンテナデータ型 — Python 3.7.1 ドキュメント

このあたりに理由がありそう。

OrderedDictが 辞書のサブクラスであるのに対してdefaultdictは 新しいディクショナリ様のオブジェクト

字面だけ見るとdefaultdictは辞書のようで実際は辞書でない、と読める。Djangoのテンプレートエンジンは辞書とそのサブクラスには対応できるが、「ディクショナリ様のオブジェクト」には対応していない、のかな。

原因を把握するためにはdefaultdictとOrderedDictの実装、Djangoのテンプレートエンジンでの辞書の扱いを理解する必要がありそう。

プログラミングの面白さを感じながらITエンジニア目指してほしいよねっていうエモい話

ここ数年でプログラミングを学ぶハードルは格段に下がった。無料の動画コンテンツはたくさんあるし、少しお金を出せばUdemyなんかでさらに踏み込んだ内容を学ぶことだってできる。あとは実際に手を動かして学べるコンテンツも充実してきている。

「ITエンジニアになれば給料が上がる」とか「未経験でもITエンジニアになれる」という言葉も目にするようになってきた。

そんな中でおととい、Twitterのタイムラインでこのブログ記事を目にした。この1件だけでこういうことを言うのは憚れるけど、「未経験でもITエンジニアになれる」という向きには違和感を覚える。

30sman.com

プログラミングスクール上がりの新人の教育にとても苦労したというSEの体験談なのだけど、この新人が悪いとかプログラミングスクールが悪いとか言うつもりはなくて、ただ「仕事に就く」という目的だけで、それ以外のモチベーションが無いまま勉強していたがために起きた不幸なことなのかなと思った。

未経験の状態からでも十分な教育を受ければ実践で活躍できるようになれると信じているけど、なによりその前にプログラミングを面白いと感じられるかどうかが重要。プログラミングを教える側としては面白さに気づける仕組みやコンテンツを作りたい。一方、ITエンジニアを目指して勉強する側は「仕事に就く」ことだけにとらわれずに、プログラミングでできるいろいろな面白いことにも目を向けて、いざ仕事に就いたときに楽しくやれるようになってほしい。

周りが猛者ばかりだと生存者バイアスみたいなもので気づきにくいけど、プログラミングを面白いと感じられない人はいるはず。結構大変な話だけど、教える側はそこに気づいたら、フォローしたり、時にはITエンジニア以外の道を示すこともあっていいのかなと思う。そもそも面白く感じない人がなんでITエンジニア目指すんだよってなるけど、「未経験でも」っていう発想だとどうせ仕事だし、ってことでまあまあありそう。

ちなみに「未経験でもITエンジニアになれる」という言い方は仕事を下に見ているような感じがしてあまり好きじゃない。「未経験だけどプログラミングって面白そうだからITエンジニアになりたい」と思う人が増えたらいいなと願っている。あ、給料が上がるらしいっていうのはモチベーションがぶち上がっていいと思います。

Dockerコンテナ上でPythonスクリプトを動かしてみる

仕事でDjango製のWebアプリのテスト環境をDockerで作る、という試みをやったのでその入り口になる話を書いてみる。

Docker Hubを探せばPython3.7のイメージはあるけど、ここではUbuntu16.04のイメージを使う。

Ubuntu 16.04のイメージにPython3.7をインストールする

Dockerfile中にコメントを書いている通り、Ubuntu16.04にPython3.7をインストールする方法は以下の記事を参考にした。

medium.com

# Dockerfile
FROM ubuntu:16.04

# refs. Install Python3.7 in ubuntu 16.04
# https://medium.com/@manivannan_data/install-python3-7-in-ubuntu-16-04-dfd9b4f11e5c
RUN apt update && apt-get install -y \
    build-essential \
    checkinstall \
    libreadline-gplv2-dev \
    libncursesw5-dev \
    libssl-dev \
    libsqlite3-dev \
    tk-dev \
    libgdbm-dev \
    libc6-dev \
    libbz2-dev \
    zlib1g-dev \
    openssl \
    libffi-dev \
    python3-dev \
    python3-setuptools \
    wget \
    && mkdir /tmp/Python37
WORKDIR tmp/Python37
RUN wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz \
    && tar xvf Python-3.7.0.tar.xz
WORKDIR /tmp/Python37/Python-3.7.0
RUN ./configure --enable-optimizations \
    && make altinstall \
    && mkdir /usr/local/Python
WORKDIR /usr/local/Python

Linux力が全然無いので、Python3.7を入れるだけでこんなにパッケージが必要なのか(小並)となった。

docker-compose.ymlを書く

次にdokcer-compose.ymlを用意する。

Pythonスクリプトを置いたディレクトリをReadOnlyでマウントしてから、ファイルを実行する。

# docker-compose.yml
version: "2"
services:
  python:
    build: .
    container_name: run_python
    volumes:
      - ./src:/usr/local/Python/:ro 
    command: "python3.7 sample.py"

以下がそのスクリプト

# sample.py
print('spam ham eggs')

マウントしたスクリプトを実行する

docker-compose build でサービスをビルドしてから、docker-compose upでサービスを実行する。

スクリプトがファイルを出力するとき

docker-compose.ymlの中でマウントすると、コンテナ上で作成されたファイルはローカルにも作られる。

このとき、ReadOnlyを外しておかないと怒られる。

# sample.py
zen_en = 'zen_en.txt'
zen_ja = 'zen_ja.txt'

with open(zen_en, 'w', encoding='utf-8') as f:
    f.write('Beautiful is better than ugly.')

with open(zen_ja, 'w', encoding='utf-8') as f:
    f.write('醜いよりも美しい方が良い。')

これを書いて、サービスを実行するとローカルに2つのファイルが作られる。

ローカルに影響を与えたくないのであれば、DockerfileCOPYを追加する。

このとき、docker-compose.ymlからvolumesは消しておく。

# Dockerfile
.
.
.
WORKDIR /tmp/Python37/Python-3.7.0
RUN ./configure --enable-optimizations \
    && make altinstall \
    && mkdir /usr/local/Python
WORKDIR /usr/local/Python
COPY ./src /usr/local/Python/
# docker-compose.yml
version: "2"
services:
  python:
    build: .
    container_name: run_python
    command: "python3.7 sample.py"

スクリプトに変更があったときはサービスをビルドし直してから、実行する必要があってひと手間増えてしまう(´・ω・`)

sample.pyにコンテナ上にファイルが作られたか確認するためにファイルの中身を確認する処理を追記した。

# sample.py
zen_en = 'zen_en.txt'
zen_ja = 'zen_ja.txt'

with open(zen_en, 'w', encoding='utf-8') as f:
    f.write('Beautiful is better than ugly.')

with open(zen_ja, 'w', encoding='utf-8') as f:
    f.write('醜いよりも美しい方が良い。')

with open(zen_en, encoding='utf-8') as f:
    for line in f:
        print(line)

with open(zen_ja, encoding='utf-8') as f:
    for line in f:
        print(line)
$ docker-compose build
.
.
.
$ docker-compose up
Recreating run_python ... done
Attaching to run_python
run_python | Beautiful is better than ugly.
run_python | 醜いよりも美しい方が良い。
run_python exited with code 0

まとめ

「Dockerコンテナ上にスクリプトを置いて、コマンドを実行する」という流れを応用して、「Webアプリを置いてテストコマンドを実行」とすればDockerを使ったテスト環境を作れる。

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の使い方は前者で書かれていることが多いので気をつけよう。