#stapy #glnagano みんなのPython勉強会 in 長野#2に参加しました

3/21にギークラボ長野で開かれた「みんなのPython勉強会 in 長野 #2」に参加しました。
みんなのPython勉強会 in 長野 #2 - connpass

今回は

  • Web
  • データ解析
  • ハードウェア/IoT
  • ビギナー

の分野の開発スプリントでした。

また、#1同様、みんなのPython勉強会から @akucchan_world さん・@tsjshgさん・@NaoY_pyさんがお越しになり、メンターをしていただきました。ありがとうございます。

 
自分はビギナーのメンターとして参加しました。

Python Boot Camp in 長野八ヶ岳に参加されていた方や、プログラム自体初めての方、これから授業などでプログラムなどに触れる方などとお話しつつ時間を過ごしました。

うまいことメンターができたか分かりませんが、引き続きプログラムに興味を持っていただければ嬉しいです。

また、会場の雰囲気を見て、「最近はインプットばかりだけれどアウトプットも増やしたい」という気持ちになりました。

 
自分は予定があったため途中で抜けましたが、スプリントの結果発表やLT大会、懇親会も盛り上がったようです。

 
最後になりましたが、企画・運営・参加をされたみなさま、ありがとうございました。

デブサミ2018の二日目に参加しました #devsumi

2/16にデブサミ2018の2日目に参加しました。
Developers Summit 2018

セッション資料は以下で公開されています。
デブサミ2018、講演関連資料まとめ:CodeZine(コードジン)

ここでは自分の感想やメモを残しておきます。誤りがあったらすみません。

 
目次

 

【16-E-1】もはや定番!?Kotlinの概要再確認と2018年の使い方!

長澤 太郎 氏[エムスリー]

社内でKotlinもくもく会が開かれたこともあり、Kotlinの概要が知りたくて参加しました。

Kotlinの概要、バージョンアップとそので重要だと思われる項目がピックアップされていて、分かりやすかったです。

印象に残ったのは、Nullまわりの型に関してです。Kotlinでは

  • Nullable
  • NotNull
  • プラットフォーム型

の順で、安全側に倒せるのでおすすめとのことでした。

 

【16-A-2】ヤフーを支える社内システム

伊藤 康太 氏[ヤフー]
資料:ヤフーを支える社内システム #devsumi 16-A-2

情シスに関する話を聞く機会はなかなか無く、気になったので参加しました。

印象に残ったのは以下です。

  • 文化
    • 社内システムを自分たちで作る文化
    • 必要なら自分たちで作ってしまう文化
  • 社内システムの歴史
    • ファイル/DBでの連携から、REST APIでの連携へ
  • アカウント認証はSSO
    • 社員・部署情報はAPI化されており、必要な情報はすぐ取れる
      • 社内システムを作りやすい
  • 社内システム例:エアコン管理システム
    • ブラウザからエアコンの温度制御ができて便利
    • ただし、良いことだけではない
      • 移転前の18倍の使用料金になっていた
    • APIを使ったbotを作って、自分のまわりを暖かくするなど、bot同士の戦いもある
  • Slackのような社内チャットツールもある
    • ソースコードは社内に公開
    • 改善が必要だと思えばがプルリクを投げられる
  • 現在の社屋は、情シスがCADを使って図面から設計
  • 新しい技術をいち早く使える
    • 技術に関する知見を社内に溜め込める
  • 使うだけではなく貢献する
    • 自分たちが作ったいいものは、社外の情シスとも情報共有する

 
講演の中でも触れられていましたが、ベンチャー気質のある情シスであり、やりがいがありそうだと感じました。

まずは一つ作ってみませんかということで、何か作ろうかという気持ちになりました。

 

【16-B-L】もしSIerのエンジニアがSRE本を読んだら

安藤 知樹 氏[エーピーコミュニケーションズ]
資料:もしSIerのエンジニアがSRE本を読んだら

SRE本の概要を知りたくて参加しました。
O'Reilly Japan - SRE サイトリライアビリティエンジニアリング

印象に残ったのは以下です。

  • 50%ルール
    • 運用業務は50%以下、残りは生産性向上のための勉強にあてる
  • 業務が変わらない時のリスク
    • メンバーがくさる
      • 特に変えていきたいと思っている人
  • SREは、当たり前の業務に気づきを与えてくれる
    • 自分の現状や経験を照らし合わせると、新しいものが見えてくる

 
講演を聞いて、やはりSRE本は良さそうだと感じ、読破したいなと感じました。

 

【16-E-3】加速するフロントエンドとPWA

竹馬 光太郎 氏
資料:加速するフロントエンドとPWA // Speaker Deck

最近話題になっていたPWA(Progressive Web Apps)がどのようなものかを知りたくて参加しました。

講演の目的が「パラダイムが変わるイメージだけを持ち帰ってもらう」であったおかげもあり、図などを使い分かりやすい説明でした。そのため、フロントに詳しくない自分でもイメージが浮かべることができました。また、フロントエンドに関して調べる時の良いとっかかりにもなりました。

また、PWAに関連して、Service Workerの話もありました。名前だけ知っていたので、Service Workerについてのイメージを持てました。

他に、講演の中で以下の書籍がおすすめされていました。
超速! Webページ速度改善ガイド ──使いやすさは「速さ」から始まる:書籍案内|技術評論社

Webページ速度については「ページが重いのは機能の重みであり、自分に必要な速度は何かを考え、削ってはいけないものを削らないようにする」と話していたのが印象的でした。

いろいろと技術的な話題が詰め込まれていたので、また資料を読み返して理解を深めようと思います。

 

【16-B-4】大規模サービスにおける価値開発の“これまで”と“将来” ~新たな“じゃらん”のチャレンジに関して~

坂東 塁 氏[リクルートライフスタイル]

「今までは開発に携わる社員は少なく、外部パートナーにお願いしていた」という環境に転職し、プロダクトオーナーとしてリーン開発を取り入れてどう変わっていったかの講演でした。

印象に残ったのは以下です。

  • リーン開発化後
    • 現場の決定を重視し、権限を与えた
      • そのために、決裁者と年間計画について合意
    • 削れるものは徹底して削った
      • 日々の業務でも、自動化・フォーマット化を徹底
    • エンジニアがパフォーマンスを発揮する方法を追求した
  • 転職直後の誰も助けてくれないぼっち感の状況
    • それでも少しずつやっていった
    • メンバーが助けてくれるようになった
    • マネージャーがフォローしてくれた
    • 外部の人のサポートを得られた

 

【16-D-5】貴方のサービスを守る知財とは?~弁理士から見たAzure IP Advantageの考察~

原田 貴史 氏[原田国際特許商標事務所]

普段生活では弁理士の話を聞く機会がないことから、せっかくなので参加しました。

主に特許に関する講演で、「特許とは」から特許の係争事例などが豊富に紹介されました。

印象に残ったのは以下です。

  • 特許とは、国から与えられる権利
    • 真似されると活力が無くなり、その結果国家が衰退
    • それを防ぐために、国が保護するための仕組み
  • アメリカ:トリプル賠償
    • 制裁的な意味も含まれ、損害の3倍が請求される
  • 特許はシンプルのほうが強力:回避しづらくなる
  • 特許の取得企業
    • 日本は0.03%の大企業が、特許全体の90%近い割合を占める
    • 中国やアメリカとは事情が真逆
  • 特許を取得することについて
    • 大規模企業にとっての特許:市場の排他効果
    • 小規模企業にとっての特許:ブランディング
      • 体外的な広報や、相見積もりされる機会が減る等
  • OSSだと著作権がOKになるけれど、特許はまた別の話
    • 特許侵害のリスクもある

 
また、MicrosoftのAzure IP Advantageについても紹介がありました。

Azure上でアプリを作れば、

  • 係争となったとき、Microsoftが弁護士費用を保証
  • Azure上の顧客は、係争となったらMicrosoftの特許(1万件)を使用可能

など、特許侵害のことを考えず、サービス開発に注力できるとのことでした。

知財面からクラウドサービスを選択するというのも面白い視点だなと感じました。

 

【16-C-6】The Amazon Way~Amazonのソフトウェア開発~

西谷 圭介 [アマゾン ウェブ サービス ジャパン]

Amazonの文化についての講演でした。Our Leadership Principlesという文化が根づいているのが伝わってきました。

印象に残ったのは以下です。

  • フィードバックを早くするため、チームの人数を絞る
    • Two-Pizza Team
  • DevOpsを分けずに面倒を見る
    • QAはチームの中で対応
    • オンコールは順番で、24時間対応をする
    • チームの中には役割があるものの、専任のOpsはいない
  • 社内標準はあまりない
    • 高い水準で維持するために、トレーニングや共有を細かく行う
  • テストで重要なことは自動化すること
    • テストはやればやるほど上達する
  • 社内勉強会として、ランチボックスを持ち寄って技術を共有している
  • 安易に妥協しないが、決定には全面コミットする

いろいろと学ぶことが多い文化でした。

文化に合わない人は採用しないということもあり、文化の質が維持できているのが感じられました。

 

【16-C-7】子育て・介護に向き合うエンジニアが技術に取り組み続けるために

阿佐 志保 氏[TIS]/横路 隆 氏[freee]/竹下 康平 氏[ビーブリッド]

近年、自分の身近でも幸不幸があったので、IT技術者視点での事例・知見を知りたくて参加しました。

自分の中では今回のデブサミで一番印象に残る講演であり、参加して良かったです。

 

阿佐 志保 氏

資料:リターンシップって知っていますか? // Speaker Deck

最近自分の時間をコントロールできていないことが多いので、共感するとともに、とても参考になりました。

印象に残ったのは以下です。

  • 育児・介護などを担う、自分で自分の時間をコントロールできない社員 = 多様性のもと
  • 多様性のある職場を目指しても、既存の仕組みを変えないことには、多様性のもととなる社員を活かせない
  • 育児・介護には不幸のスパイラルがある
    • 自分で自分の時間をコントロールできないと、難易度の低い、専門性の低い仕事につきがち
      • 出産前までは働けていたのに、出産後はできなくて申し訳ないとしか言えない
      • 働かせてもらえるんだから、好きなことをなんて言ってられない
    • 出産前は自分のなりたいキャリアプランがあった
      • 出産後は、自分のキャリアを後回し、自分のキャリアより会社に言われるままに過ごすことにもなる
    • 仕事楽しくない、何となくダメになるのでは感
    • こじらせると、詐欺師症候群になる
      • 自己評価が著しく低くなる
      • 仕事で評価されても、ただ運が良かっただけと感じる
    • ごめんなさい言い過ぎ症候群
  • 育児・介護の問題は、在宅・裁量労働でほぼ解決する
    • 自分の時間を自分でコントロールできない人は、難易度が高いものをすべき
  • リターンシップには雑談が大事
    • 上司になんでも言う、家の事・仕事、関係なく言う
      • 上司「成果は数字できちんと出せ、必要な物があれば相談しろ」
      • 部下「ムリなものはムリ」と言う
  • 時間がないなら、なるべく得意なことをやって、短い時間で効率良くやる
    • キャラを目立たせるため、強み・弱みをアピール、専門性を高める
    • 市場価値を高めることを怠らない
  • 古いやり方を撤廃する
    • マネジメントできる体制を作る
      • 監視型マネジメントからより、難易度・重要度の高いマネジメントに変化させることが必要
    • リモートワーク、Slack、Skypeを徹底的に使いこなす

 

横路 隆 氏

講演の途中で寝かしつけスケジュールのアラートが出ましたが、育児が現在進行形なのだとはっきり分かる出来事であり、それ以降の講演に重みが出たように感じました。

印象に残ったのは以下です。

  • 一人で生後二週間の子どもを育てた
  • 時間がなくなるので、色々と積極的に諦め、やらないことをしっかり決める
    • 自炊
    • 母乳
    • ルンバや自動感想洗濯機、水回りはアウトソース
  • スマートスピーカー便利
    • だっこすると何もできないので、話すだけでOKなのはよい
    • 授乳とかの生活ログを残しやすい
  • 職場の雰囲気で育休が取れない現実もある
    • freeeは取得している
      • 特に、男性は積極的に取得している
  • 育児の社内コミュニティもある
  • freeeに遊びに行けば、育休取った人の話も聞けるとのこと

 

竹下 康平 氏

前のお二人が育児というキラキラした方面であったのに対し、こちらは介護という重いテーマでした。

実際、自分も介護については詳しく理解できていないので、講演の最後にあった「介護とはなんだろう、と調べることから始める」をしようと感じました。

印象に残ったのは以下です。

 

 
最後になりましたが、今年もいろいろな話を聞くことができました。デブサミの関係者の皆様、ありがとうございました。

Django + django-localflavorで、フォームの郵便番号入力や都道府県選択を作成してみた

Djangoのフォームで

  • 郵便番号入力
  • 都道府県選択

を簡単に作る方法を調べたところ、 django-localflavor がありました。

 
Web上を調べたところ、Django 1.6以降削除されたの記事を見かけたため、使えないのかなと思いました。

ただ、公式ドキュメントのChangeLogを読むと、Django2.0に対応しているとのことでした。
Changelog — django-localflavor 2.0 documentation

 
そこで、Django2.0で試してみました。

 
目次

 

環境

 

環境構築

Djangoアプリ作成まで

いつも通りなので省略します。

# Djangoアプリを作るまで
$ python -m venv env364
$ source env364/bin/activate
$ pip install django
$ django-admin startproject myproject
$ cd myproject/
$ python manage.py startapp myapp

 

django-localflavorのインストール

PyPIにあるため、pipでインストールします。
django-localflavor 2.0 : Python Package Index

$ pip install django-localflavor
...
Successfully installed django-localflavor-2.0

 

django-localflavor向けの設定(settings.py)

settings.pyのうち、

  • INSTALLED_APPS
  • LANGUAGE_CODE
  • USE_I18N

を修正します。 (USE_I18Nは、デフォルトで True かもしれません)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # アプリ
    'myapp',
    # django-localflavor用
    'localflavor',
]

LANGUAGE_CODE = 'ja'

USE_I18N = True

 

アプリの中身を作成

テンプレート(templates/form.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>MyForm</title>
</head>
<body>
    <form action="" method="post">
        {% csrf_token %}
        {{ form.as_p }}
    </form>
</body>
</html>

 
プロジェクトのurls.py

from django.urls import path
from django.views.generic import FormView
from myapp.forms import MyForm


urlpatterns = [
    path('', FormView.as_view(
        template_name = 'form.html',
        form_class = MyForm
    ))
]

 
あとは、myapp.forms.MyFormに、django-localflavorを使った実装を書いていきます。

 

django-localflavorを試す

公式ドキュメントを見ると、以下の機能が含まれていました。
http://django-localflavor.readthedocs.io/en/latest/localflavor/jp/

  • localflavor.jp.forms.JPPostalCodeField
  • localflavor.jp.forms.JPPrefectureCodeSelect
  • localflavor.jp.forms.JPPrefectureSelect

 
それぞれ試してみます。

 

JPPostalCodeField

Formに実装してみます。

# MyForm
from django import forms
from localflavor.jp.forms import JPPostalCodeField


class MyForm(forms.Form):
    my_postal_code = JPPostalCodeField()

すると、テンプレートには

<p>
  <label for="id_my_postal_code">My postal code:</label>
  <input type="text" name="my_postal_code" required id="id_my_postal_code" />
</p>

と出力されます。

 
また、バリデーション機能もあるため、 a のような誤った値を入力すると

<ul class="errorlist">
  <li>XXXXXか、XXXXX-XXXXの形式で郵便番号を入力してください。</li>
</ul>
<p>
  <label for="id_my_postal_code">My postal code:</label>
  <input type="text" name="my_postal_code" value="a" required id="id_my_postal_code" />
</p>

とエラーが出ます。

 

JPPrefectureCodeSelect

同じくFormに実装してみます。

注意点としては、

  • JPPrefectureCodeSelectは、widget( django.forms.fields.Select を継承)であること
  • CharFieldのwidgetに設定すること
    • ChoiceFieldだと、optionタグが表示されない

です。

from django import forms
from localflavor.jp.forms import JPPrefectureCodeSelect


class MyForm(forms.Form):
    my_pref_code_ng = forms.ChoiceField(widget=JPPrefectureCodeSelect)
    my_pref_code_ok = forms.CharField(widget=JPPrefectureCodeSelect)

 
フォームを表示してみると

<!-- ChoiceFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_code_ng">My pref code ng:</label>
  <select name="my_pref_code_ng" id="id_my_pref_code_ng"></select>
</p>

<!-- CharFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_code_ok">My pref code ok:</label>
  <select name="my_pref_code_ok" id="id_my_pref_code_ok">
  <option value="01">北海道</option>
  <option value="02">青森県</option>
  ...
</p>

となります。

 
なお、都道府県の並び順に関しては、ソースコード

Prefectures ordered to conform with the Japanese entry of ISO-3166.
This ordering is widely used in Japan.
See:
http://en.wikipedia.org/wiki/ISO_3166-2:JP

# https://github.com/django/django-localflavor/blob/master/localflavor/jp/jp_prefectures.py

とありました。ISO-3166準拠のようです。

 

JPPrefectureSelect

JPPrefectureCodeSelectとほぼ同じですが、違いはoptionタグのvalue属性の値です。

from django import forms
from localflavor.jp.forms import JPPrefectureSelect


class MyForm(forms.Form):
    my_pref_ng = forms.ChoiceField(widget=JPPrefectureSelect)
    my_pref_ok = forms.CharField(widget=JPPrefectureSelect)

として表示してみると

<!-- ChoiceFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_ng">My pref ng:</label>
  <select name="my_pref_ng" id="id_my_pref_ng">
  </select>
</p>

<!-- CharFieldのwidgetとして設定したもの -->
<p>
  <label for="id_my_pref_ok">My pref ok:</label>
  <select name="my_pref_ok" id="id_my_pref_ok">
  <option value="hokkaido">北海道</option>
  <option value="aomori">青森県</option>
  ...
</p>

となります。value属性に都道府県名のアルファベット表記が設定されます。

 

JPPrefectureCodeSelectとJPPrefectureSelectの初期表示を変更する

引数 initial を使います。

それぞれコードとアルファベット表記を指定します。

class MyForm(forms.Form):
    my_pref_code_default = forms.CharField(widget=JPPrefectureCodeSelect, initial='20')
    my_pref_default = forms.CharField(widget=JPPrefectureSelect, initial='nagano')

 
なお、initialに指定できる値は、localflavor/jp/jp_prefectures.py にある

  • JP_PREFECTURES
  • JP_PREFECTURE_CODES

に記載されています。

 
表示してみます。

<!-- JPPrefectureCodeSelect版 --> 
<p>
  <label for="id_my_pref_code_default">My pref code default:</label>
  <select name="my_pref_code_default" id="id_my_pref_code_default">
  <option value="01">北海道</option>
  ...
  <option value="20" selected>長野県</option>
</p>

<!-- JPPrefectureSelect版 --> 
<p>
  <label for="id_my_pref_default">My pref default:</label>
  <select name="my_pref_default" id="id_my_pref_default">
  <option value="hokkaido">北海道</option>
  ...
  <option value="nagano" selected>長野県</option>
</p>

 

JP_PREFECTURESとJP_PREFECTURE_CODES

以下のような定義となっています。

# https://github.com/django/django-localflavor/blob/master/localflavor/jp/jp_prefectures.py

JP_PREFECTURES = (
    ('hokkaido', _('Hokkaido'),),
...
)

JP_PREFECTURE_CODES = (
    ('01', _('Hokkaido'),),
...
)

 

テーブルに含まれている都道府県だけ表示したい場合

今までの方法は、全部の都道府県を表示するものでした。

しかし、場合によっては、都道府県のうちトランザクションテーブルに含まれるものだけ表示したいことがあるかもしれません。

とはいえ、django-localflavorではそのような機能がないため、自分で実装します。

 
以下のModelがあるとします。

# models.py
from django.db import models

class RingoProductingArea(models.Model):
    pref = models.CharField('都道府県名', max_length=10)
    ratio = models.PositiveSmallIntegerField('割合', default=0)

 
また、RingoProductingArea Modelのデータ(fixture)は以下であり、ここに含まれる「青森県・長野県・山形県」だけを表示したいとします。
出典:日本国内のりんご生産量|りんご大学

[
  {
    "model": "myapp.RingoProductingArea",
    "pk": 1,
    "fields": {
      "pref": "青森県",
      "ratio": 58
    }
  },
  {
    "model": "myapp.RingoProductingArea",
    "pk": 2,
    "fields": {
      "pref": "長野県",
      "ratio": 18
    }
  },
  {
    "model": "myapp.RingoProductingArea",
    "pk": 3,
    "fields": {
      "pref": "山形県",
      "ratio": 5
    }
  }
]

 
この場合、Formの __init__() の中でchoicesの値を作成・設定します。

あまりきれいなロジックではないので、何かいい方法をご存知の方がいらっしゃいましたら、ご指摘ください。

from django import forms
from django.db.models.aggregates import Count
from localflavor.jp.jp_prefectures import JP_PREFECTURES
from localflavor.jp.forms import JPPrefectureCodeSelect, JPPrefectureSelect, JPPostalCodeField
from localflavor.us.forms import USPSSelect, USZipCodeField
from .models import RingoProductingArea


class MyForm(forms.Form):
    limit_pref = forms.ChoiceField(label='Modelに存在する都道府県', choices=[('', '')])

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 都道府県が登録されているデータを取得する
        q = RingoProductingArea.objects.values_list('pref', flat=True) \
            .annotate(count_status=Count('pref')) \
            .filter(count_status__gt=0).distinct()
        # choicesの形(tupleのlist)にしておく
        product_prefs = [(pref, pref) for pref in q]

        # product_prefsと比較しやすいよう、JP_PREFECTURESを日本語表記の都道府県tupleのlistにしておく
        # 元々は、(ローマ字表記, 日本語表記)のtuple
        all_prefs_by_jp = [(pref_by_jp, pref_by_jp) for pref_by_en, pref_by_jp in JP_PREFECTURES]

        # 都道府県が登録されているデータだけにする
        exists_prefs = [pref for pref in all_prefs_by_jp if pref in product_prefs]
        # 先頭にメッセージを入れる
        exists_prefs.insert(0, ('', '都道府県を選ぶ'))
        # limit_prefフィールドのchoicesとして設定
        self.fields['limit_pref'].choices = exists_prefs
        # とはえ、初期値は別のもの
        self.fields['limit_pref'].initial = '長野県'

 
結果です。

<p>
  <label for="id_limit_pref">Modelに存在する都道府県:</label>
  <select name="limit_pref" required id="id_limit_pref">
    <option value="">都道府県を選ぶ</option>
    <option value="青森県">青森県</option>
    <option value="山形県">山形県</option>
    <option value="長野県" selected>長野県</option>
  </select>
</p>

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/django-localflavor-sample

「SQLアンチパターン」読書会スペシャルに参加しました #nseg #glnagano #sqlap

1/27に、ギークラボ長野にて開催された「SQLアンチパターン」読書会スペシャルに参加しました。
「SQLアンチパターン」読書会スペシャル - connpass

 
監訳者の和田卓人さんによる講演「SQLアンチパターンのお焚き上げ(2018年版)」の他、データベースに関するトークがありました。

 
以下簡単なメモです。誤っていたらご指摘ください。

目次

 

SQLアンチパターンのお焚き上げ(2018年版)

SQLアンチパターン本の概要から入り、SQL関係の本の紹介、SQLアンチパターンを2018年ではどうなったかなど、盛り沢山な内容でした。

おおと思ったことはTwitterでつぶやいていたので、ここにリスト化しておきます*1

  • いわゆる危険色として、狙ってパターン名をカタカナにした
  • ツリーは伝統的にRDBでは難しい
  • CTEのサポートをしているDBエンジンが増えてる、CTEはSQL標準
  • 組織階層だとNestedSetがマッチした、得意不得意がある
  • 分析系のSQLの本の書き方、チューニングの本が少なかったが、でてきた 「ビッグデータ分析・活用のためのSQLレシピ」
  • SQLiteのCTEサポート
  • 何かと何かが関係する時は、日付などの情報が発生するので、idが必要ではとのこと
  • RDBよりもS3の方が信頼性が高いのが2018年、ただし整合性・トランザクションとかをきちんと考える
  • Listagg関数はSQL2016〜
  • 2018年では、プリペアドステートメントホワイトリストプログラミングが唯一の解(ソリューションを減らす)
  • 削除フラグは思考停止。せめて削除日や状態など
  • 社内に対し昔の失敗を共有できるので、SQLアンチパターンは社内読書会に向く
  • 寿命が長いものを大切にしよう
  • 構造よりもデータをキレイにする
  • URL設計とDB設計は挽回がきかないので大事にする

 
また、和田さんにサインをいただきました。ありがとうございました。

書籍「テスト駆動開発」にも関わらず、快くサインしていただきました(手元のSQLアンチパターン本は電子書籍)。

 

データベースに関するトーク

こちらもメモ書きです。

 

DjangoのORMことはじめ (kotyさん)

資料:DjangoのORMことはじめ

DjangoのORMについて、SQLをどう表現するかがまとまっていました。

また、赤字に黒背景のスライドは見づらいという知見も得られました。

 

もうひとつのアンチパターン OTLT、あるいは如何にして私はオレオレフレームワークを忌み嫌うようになったか (suno88さん)

資料:もうひとつのアンチパターン OTLT、あるいは如何にして私はオレオレフレームワークを忌み嫌うようになったか

One True Lookup Table (OTLT)に関するトークでした。

  • コードタイプ
  • コード値
  • 内容

という構造のテーブルについて、どこがつらいのかが分かりやすくまとめられていて参考になりました。

もし今後このようなテーブルに出会った時、どこが悪いのか説明しやすくなりました。

 

DBFluteの紹介 (chichi1091さん)

資料:DBFluteの紹介

DBFluteについて分かりやすい解説のトークでした。

Javaを使っていないのですが、

  • テーブル一覧HTML
  • テーブル変更一覧HTML

などを生成してくれるDB管理支援ツールが気になりました。

また、ドキュメントも充実しているとのことで、これもありがたいと感じました。

 

お前の罪を数えろ (tmtmsさん)

資料:お前の罪を数えろ

SQLアンチパターンで挙げられていた内容に対し、過去どれくらい踏んできたのかのトークでした。

踏んだ時の理由など、どうしてそれをやったのかが共有していただけたのはありがたかったです。

アンチパターンを踏んだ時に考えたことは、なかなか世の中には出てこないためです。

 

とあるCMSSQLアンチパターン (stealthinuさん)

つらいトークでした。

一度アンチパターンを使ってしまうと、それ以降もアンチパターンになってしまうというのが印象に残りました。

 

その他

今回も受付係をしました。すばらしいことに当日欠席される方もおらず、バタバタすることなく終えられたと思います。

また、お菓子係も初めて担当しました。過去に用意していただいた内容を思い出しながら、

  • 個包装のお菓子は、甘いもの2種類と、せんべい1種類
  • 飲み物は、お茶とジュースを1本ずつ

としました。せんべい系やお茶が人気でした。お茶はもう1本用意しておけばよかったかもしれません。

あと、サインに向く紙用のペンの存在を知りました。後日、紙用マッキーを入手しました。
ZEBRA | ゼブラ株式会社 | 紙用マッキー / 極細

 
最後になりましたが、和田さんをはじめとする関係者のみなさま、ありがとうございました。

*1:Tweet埋め込みだと量が多くなってしまったので、抜き出しました

Python2 + nfcpyで、「長野市バス共通ICカード KURURU(くるる)」の履歴を読んでみた

以前、Android + RubotoにてFeliCaのKURURUの履歴を読んでみました。
Rubotoを使い、Androidで「長野市バス共通ICカード KURURU(くるる)」の履歴を読んでみた - メモ的な思考的な

そこで今回は Python2 + nfcpy でKURURUを読んでみます。

 
とはいえ、nfcpyでFeliCaを読む方法がよくわかりませんでした。

調べてみたところ、以下がとても参考になりました。ありがとうございます。

 
そこで、前者のリポジトリに含まれる suica_read.py (Suicaを読むnfcpyコード) を理解しながら、KURURU用のコードを書いてみました。

なお、このレイヤを扱ったことがないため、考えたことを中心に書きます*1。もし誤りなどがありましたら、ご指摘ください。

 
目次

 

環境

 

suica_read.pyを理解する

suica_read.pyソースコードを見たところ、いくつか分からないところがあったため、順に書いていきます。

なお、理解するために使った履歴データは、以前の履歴No.1のものを使いました。

f:id:thinkAmi:20131025055753p:plain

 

struct.unpack('>2B2H4BH4B', data)

まずは

# ビッグエンディアンでバイト列を解釈する
row_be = struct.unpack('>2B2H4BH4B', data)
# リトルエンディアンでバイト列を解釈する
row_le = struct.unpack('<2B2H4BH4B', data)

についてです。

 
参考にしたリポジトリでは、標準モジュールの struct.unpack() モジュールを使って、Suicaデータを解析しています。
7.3. struct — 文字列データをパックされたバイナリデータとして解釈する — Python 2.7.14 ドキュメント

今回分からなかったのは、 >2B2H4BH4B の部分です。

Pythonドキュメントを読むと、

というフォーマット文字でした。
7.3. struct — 文字列データをパックされたバイナリデータとして解釈する — Python 2.7.14 ドキュメント

ただ、 2B2H4BH4B が何を表しているのかよく分かりませんでした。

さらに調べてみたところ、以下の記事がありました。
python - struct.error: unpack requires a string argument of length 4 - Stack Overflow

struct.calcsize()を使えば何か分かるかなと思い、Suicaを置いて試してみたところ、

f = struct.calcsize('=2B2H4BH4B')
print f
# => 16

と表示されました。

 
16byteを表していることがわかったため、PythonのドキュメントとSuicaのフォーマットと比べてみました。

バイト Pythonフォーマット Suicaフォーマット
0 B 端末種
1 B 処理
2-3 H ??
4-5 H 日付 (先頭から7ビットが年、4ビットが月、残り5ビットが日)
6 B 入線区
7 B 入駅順
8 B 出線区
9 B 出駅順
10-11 H 残高 (little endian)
12-14 3B 連番
15 B リージョン

2B2H4BH4BSuicaフォーマットに一致していました。

 
では、KURURUの場合を調べてみます。

KURURUの履歴フォーマットは、Rubotoの時と同じく以下を参考にしました。ありがとうございます。
KURURU 履歴フォーマット | あたがわの日記

そのため、KURURUフォーマットをPythonフォーマットに当てはめてみます。

バイト Pythonフォーマット KURURUフォーマット
0-1 H 年月日
2 B 降車時刻
3-4 H 機番
5 B 降車時刻
6-7 H 乗車停留所
8-9 H 降車停留所
10 B 場所、種別
11 B 会社、割引
12-15 I 残高

となりました。

残高が4byte使っていたため、

  • BやHと同じ、unsignedなCの型
  • 標準の長さが4

Pythonフォーマットは unsigned int だったため、 12-15byteのところは I としました。

残高フォーマットは unsigned int なのかを試してみたところ、

print self.row_be[8]
#=> 1500

と表示されました。残高が取れているようです。

 

(date >> 9) & 0x7f

次は年月日の「年」を求めているところです。

date = row_be[3]
(date >> 9) & 0x7f

まず、 >> はシフト演算です。
6.8. シフト演算 (shifting operation) | 6. 式 (expression) — Python 3.6.3 ドキュメント

dateの値、および2進数に直した値、および右に9bitシフトした値を見たところ、以下でした。

print date
# => 6988
print bin(date)
# => 0b1101101001100
print date >> 9
# => 13
print bin(date >> 9)
#=> 0b1101

 
次に、 0x7fPythonの16進数表記なので、2進数に直すと 1111111 です。

& はANDなので、シフト演算の結果と 0x7f とのANDを取ると、

0001101
1111111
----------------------
0001101

2進数の 0001101 は13なので、これに2000を加えれば年になりました。

 
同じように月を求めると、

print date >> 5
#=> 218
print bin(date >> 5)
#=> 0b11011010

から 0x0f とのANDにて

11011010
00001111
----------------------
00001010

1010 となりました。10進数に直すと10です。

 
日についても同様ですが、 (date >> 0) はシフト演算をしないため実質 date & 0x1f となります。

print date >> 0
#=> 6988
print bin(date >> 0)
#=> 0b1101101001100

から 0x1f とのANDにて

1101101001100
0000000011111
----------------------
0000000000001100

1010 となりました。10進数に直すと12です。

合わせると、2013年10月12日となり、正しい年月日が取得できました。

 

ServiceCode(service_code >> 6 ,service_code & 0x3f)

続いてはServiceCodeオブジェクトを生成しているところです。

service_code = 0x090f
nfc.tag.tt3.ServiceCode(service_code >> 6 ,service_code & 0x3f)

 
ServiceCodeのコンストラクタでは、

  • 第一引数は サービスナンバー
  • 第二引数は サービス属性

です。

Suicaのサービスコード 0x090f (2byte) なため、それぞれ当てはめてみると

  • サービスナンバーは、上位10byte
    • 6byte右へシフト
  • サービス属性は、下位6byte
    • 0x3f111111 なので、下位6byteを捨てる

となりました。

 
KURURUのサービスコードは 0x000f なので、同じようにしてServiceCodeオブジェクトを生成すれば良さそうです。

 

KURURUを読む時に考えたこと

続いて、KURURUを読む時に考えたことをメモします。

 

時刻の算出について

Suicaとは異なり、KURURUには乗車時刻・降車時刻があります。

時刻については、

時刻は 0 時 0 分からの経過分を 10 で割った値が入っています.つまり,そこを 10 倍した値が 0 時 0 分からの経過分です.

KURURU 履歴フォーマット | あたがわの日記

とのことなので、編集が必要です。

 
self.row_be[3] で取得する値は16進数表記のint型です。

そのため、これを16進数表記intから、10進数表記intに変換します。

今回はstr文字列に一度戻してからもう一度intにします。

# 16進のintを16進表現の文字列にする
hex_time = hex(self.row_be[3])
print hex_time
#=> 0x71

# 16進表現の文字列を10進数値にする
int_time = int(hex_time, 16)
print int_time
#=> 113

得られた値は経過分の1/10なので10倍します。また、時間表現にするため、60で割ります。

これで商が時間、余りが分になります。

なお、Pythonでは商と余りを一度に求めるのに、divmod()関数が使えます。
divmod(a, b) | 2. 組み込み関数 — Python 2.7.14 ドキュメント

# 10倍
origin_time = int_time * 10

# 商と余りを求める
hm = divmod(origin_time, 60)

 
divmod()関数は、 (商, 余り) のタプルで値を戻すので、それを以下のフォーマットで時間表現にします。

'{hm[0]:02d}:{hm[1]:02d}:00'.format(hm=hm)

format()では、タプルは要素でのアクセス hm[0] できます。
7.1.3.2. 書式指定例 | 7.1. string — 一般的な文字列操作 — Python 2.7.14 ドキュメント

また、02d は、

  • 0 : マイナス符号なし
  • 2 : 幅として、2桁
  • d : 10進の数値

となります。
7.1.3.1. 書式指定ミニ言語仕様 | 7.1. string — 一般的な文字列操作 — Python 2.7.14 ドキュメント

 

ヒアドキュメント時のインデント削除

今回、ヒアドキュメントを使って履歴を表示します。

ただ、ヒアドキュメントを使うとインデントも表示されてしまうのが問題です。

インデントを削除する方法を調べたところ、標準モジュールの textwrap を使えば良さそうでした。

 

ソースコード全体

以上を踏まえたソースコードです。

コメントは省略しましたが、後述のGitHubのコードには記載してあります。

kururu_reader.py

# -*- coding: utf-8 -*-
# 以下を参考にKURURUを読みました。m2wasabiさん、ありがとうございます。
# https://github.com/m2wasabi/nfcpy-suica-sample/blob/master/suica_read.py
import struct
import textwrap

import nfc
import nfc.tag.tt3

KURURU_SERVICE_CODE = 0x000f


class HistoryRecord(object):
    def __init__(self, data):
        self.row_be = struct.unpack('>HBHBHHBBI', data)

    def is_empty(self):
        # 年月日がオールゼロの場合、履歴が無い空のレコードとみなす
        return not all([
            self.fetch_year(),
            self.fetch_month(),
            self.fetch_day(),
        ])

    def fetch_year(self):
        return (self.row_be[0] >> 9) & 0b1111111

    def fetch_month(self):
        return (self.row_be[0] >> 5) & 0b1111

    def fetch_day(self):
        return self.row_be[0] & 0b11111

    def fetch_alighting_time(self):
        return self.format_time(self.row_be[1])

    def fetch_machine_no(self):
        return self.row_be[2]

    def fetch_boarding_time(self):
        return self.format_time(self.row_be[3])

    def fetch_boarding_stop(self):
        return self.row_be[4]

    def fetch_alighting_stop(self):
        return self.row_be[5]

    def fetch_place(self):
        result = {
            0x05: '車内 ({})',
            0x07: '営業所 ({})',
            0x0E: '券売機 ({})',
        }.get(place, '不明 ({})')
        return result.format(hex(place))

    def fetch_category(self):
        category = self.row_be[6] & 0b1111
        result = {
            0x00: '入金 ({})',
            0x02: '支払 ({})',
        }.get(category, '不明 ({})')
        return result.format(hex(category))

    def fetch_company(self):
        company = (self.row_be[7] >> 4) & 0b1111
        result = {
            0x00: '長電バス ({})',
            0x03: 'アルピコバス ({})',
        }.get(company, '不明 ({})')
        return result.format(hex(company))

    def fetch_discount(self):
        discount = self.row_be[7] & 0b1111
        result = {
            0x00: '入金 ({})',
            0x01: 'なし ({})',
        }.get(discount, '不明 ({})')
        return result.format(hex(discount))

    def fetch_balance(self):
        return self.row_be[8]

    def format_time(self, usage_time):
        hex_time = hex(usage_time)
        int_time = int(hex_time, 16)
        origin_time = int_time * 10
        hm = divmod(origin_time, 60)
        return '{hm[0]:02d}:{hm[1]:02d}:00'.format(hm=hm)


def connected(tag):
    sc = nfc.tag.tt3.ServiceCode(KURURU_SERVICE_CODE >> 6, KURURU_SERVICE_CODE & 0x3f)
    for i in range(0, 10):
        bc = nfc.tag.tt3.BlockCode(i, service=0)
        data = tag.read_without_encryption([sc], [bc, ])

        history = HistoryRecord(bytes(data))
        if history.is_empty():
            continue

        result = """
        Block: {history_no}
        日付: {yyyy}/{mm}/{dd}
        機番: {machine}
        乗車時刻: {boarding_time}
        乗車停留所: {boarding_stop}
        降車時刻: {alighting_time}
        降車停留所: {alighting_stop}
        場所: {place}
        種別: {category}
        会社: {company}
        割引: {discount}
        残高: {balance:,}円
        """.format(
            history_no=i + 1,
            yyyy=history.fetch_year() + 2000,
            mm='{:02d}'.format(history.fetch_month()),
            dd='{:02d}'.format(history.fetch_day()),
            machine=history.fetch_machine_no(),
            boarding_time=history.fetch_boarding_time(),
            boarding_stop=history.fetch_boarding_stop(),
            alighting_time=history.fetch_alighting_time(),
            alighting_stop=history.fetch_alighting_stop(),
            place=history.fetch_place(),
            category=history.fetch_category(),
            company=history.fetch_company(),
            discount=history.fetch_discount(),
            balance=history.fetch_balance(),
        )
        print '-' * 30
        print textwrap.dedent(result)


def main():
    with nfc.ContactlessFrontend('usb') as clf:
        clf.connect(rdwr={'on-connect': connected})


if __name__ == '__main__':
    main()

 

実行結果

実行してみたところ、Robutoの時と同じ内容になりました*2

$ python kururu_reader.py 
------------------------------

Block: 1
日付: 2015/01/30
機番: 3072
乗車時刻: 11:20:00
乗車停留所: 3136
降車時刻: 11:50:00
降車停留所: 1
場所: 車内 (0x5)
種別: 支払 (0x2)
会社: アルピコバス (0x3)
割引: なし (0x1)
残高: 1,500円

------------------------------

Block: 2
日付: 2015/01/30
機番: 3039
乗車時刻: 08:20:00
乗車停留所: 1
降車時刻: 08:30:00
降車停留所: 3506
場所: 車内 (0x5)
種別: 支払 (0x2)
会社: アルピコバス (0x3)
割引: なし (0x1)
残高: 1,860円

------------------------------

Block: 3
日付: 2013/10/12
機番: 3
乗車時刻: 19:20:00
乗車停留所: 0
降車時刻: 19:20:00
降車停留所: 0
場所: 券売機 (0xe)
種別: 入金 (0x0)
会社: 長電バス (0x0)
割引: 入金 (0x0)
残高: 2,160円

------------------------------

Block: 4
日付: 2013/10/12
機番: 3058
乗車時刻: 18:50:00
乗車停留所: 3271
降車時刻: 19:00:00
降車停留所: 3379
場所: 車内 (0x5)
種別: 支払 (0x2)
会社: アルピコバス (0x3)
割引: なし (0x1)
残高: 1,160円

------------------------------

Block: 5
日付: 2013/10/12
機番: 1002
乗車時刻: 13:00:00
乗車停留所: 1632
降車時刻: 13:00:00
降車停留所: 1422
場所: 車内 (0x5)
種別: 支払 (0x2)
会社: 不明 (0x1)
割引: なし (0x1)
残高: 1,330円

------------------------------

Block: 6
日付: 2013/10/12
機番: 0
乗車時刻: 09:40:00
乗車停留所: 0
降車時刻: 09:40:00
降車停留所: 0
場所: 営業所 (0x7)
種別: 入金 (0x0)
会社: アルピコバス (0x3)
割引: 入金 (0x0)
残高: 1,500円

 

ソースコード

GitHubに上げました。 felica/kururu_reader.py ファイルが今回のものです。
https://github.com/thinkAmi-sandbox/nfcpy-sample

*1:知っている人から見れば基本的なことかもしれませんが...

*2:あの時以降、2回KURURUを使っているため、履歴が増えています

Python2 + nfcpyで、W525DZのFeliCa Plugを読んでみた

毎年冬になるとつらいのが冷え性です。

今までは身体が冷えてるような気がするから冷え性だろうと思っていました。しかし、「推測するな計測せよ」という言葉を思い出しました。

そこで記録が手軽に残せる体温計を探してみました。すると、NFCでデータを転送し、専用アプリでデータを管理できる体温計がありました。
WOMAN℃ テルモ女性体温計W525DZ|女性体温計 |テルモ 一般のお客様向け情報

目的が違うような気もしますが、データを残しておきたい気持ちが勝ったため、入手しました。

 
残念なのは専用アプリがWindows向けであり、Macでは使えないことです。

もしかしたら nfcpy を使えばデータを読めるのではないかと思い、試してみることにしました。
nfcpy/nfcpy: A Python module to read/write NFC tags or communicate with another NFC device.

 
結論からすると、Macでもデータは読み込めたものの、データのフォーマットが公開されていない、もしくは、暗号化された領域にデータを保存しているため、うまくいきませんでした。

とはいえ、せっかくなので、読み込んだデータなどをメモしておきます。

 
目次

 

環境

なお、nfcpyはPython3対応を進めているようです。進捗状況は以下のissueにありました。
Support Python 3 · Issue #47 · nfcpy/nfcpy

 

実装内容

NFCタグとの対話をハンドリングする

NFCタグとの対話をハンドリングする方法は、以下にありました。
Read and write tags | Getting started — nfcpy 0.13.4 documentation

ContactlessFrontend は使い終わったら close() する必要がありますが、Pythonのwith文に対応しているため、以下のように書けます。

with nfc.ContactlessFrontend('usb') as clf:
    clf.connect(rdwr={'on-connect': connected})

 
あとは、 connected() コールバック関数を定義し、その中にNFCタグとの通信を記載すれば良さそうです。

コールバック関数には引数tagが渡されてくるため、その中身を見てみました。IDやPMMはボカしていますが、こんな感じでした。  

def connected(tag):
    print tag
    # => Type3Tag 'FeliCa Plug (RC-S926)' ID=03xxxxxxxxxxxxxx PMM=01xxxxxxxxxxxxxx SYS=FEE1

    print type(tag)
    # => <class 'nfc.tag.tt3_sony.FelicaPlug'>

    print dir(tag)
    # =>
    # ['IC_CODE_MAP', 'NDEF', 'TYPE', '__class__', '__delattr__', '__dict__', '__doc__',
    #  '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
    #  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
    #  '__subclasshook__', '__weakref__', '_authenticated', '_clf', '_format', '_is_present',
    #  '_ndef', '_nfcid', '_product', '_target', 'authenticate', 'clf', 'dump', 'dump_service',
    #  'format', 'identifier', 'idm', 'is_authenticated', 'is_present', 'ndef', 'pmm', 'polling',
    #  'product', 'protect', 'read_from_ndef_service', 'read_without_encryption',
    #  'send_cmd_recv_rsp', 'sys', 'target', 'type', 'write_to_ndef_service',
    #  'write_without_encryption']

 
この結果より、

  • システムコードが FEE1
  • 引数tagが FelicaPlug

であることから、普通のFeliCaではなく、W525DZではFeliCa Plug を使っているようでした。

 

Polling

続いて、pollingを試してみます。

nfcpyの関数 polling() の引数 system_code にはシステムコードが必要そうです。

システムコードは tag.sys に設定されていることから、それを使います。

def connected(tag):
    print tag.polling(tag.sys)
    # => (bytearray(b'\x03\xxx\xxx\xxx\xxx\xxx\xxx\xxx'),
    #     bytearray(b'\x01\xxx\xxx\xxx\xxx\xxx\xxx\xxx'))

先ほど tagをprintしたときに出てきた値を、bytearrayのタプルとして取得できました。

 

Read Without Encryption

さらに、暗号化されていない部分からデータを読み込んでみます。

今回は、tagオブジェクトの read_without_encryption() を使えば良さそうでした。

引数は2つあり、サービスコードのリストとブロックコードのリストでした。

サービスコードは nfc.tag.tt3.ServiceCode クラスのインスタンスを渡せば良さそうでした。

必要な値は、FeliCa Plug ユーザーズマニュアル v1.14の

  • p47 より、サービスコードリスト順番は 0
  • p56 より、サービスコードは 000Bh
  • p55 より、サービス数は 1

と推定したため、

sc = nfc.tag.tt3.ServiceCode(0, 0x0b)

の1要素を持つリストとしました(とはいえ、第一引数 number がサービスコードリスト順番なのかは自信がないですが...)。

 
ブロックコードについては nfc.tag.tt3.BlockCode クラスのインスタンスを使います。

コンストラクタの引数については、

  • number : ブロック番号
  • service : サービスコードリストのindex (今回は1つしかないので 0 )

を指定したのを2つ用意しました。

bc1 = nfc.tag.tt3.BlockCode(0, service=0)
bc2 = nfc.tag.tt3.BlockCode(1, service=0)

 
あとはそれらを read_without_encryption() メソッドに渡し、取得したデータを binascii.hexlify() で16進数表記したのをprintしてみました。

data = tag.read_without_encryption([sc], [bc1, bc2])
print '{}'.format(binascii.hexlify(data))
# => 02fd8c73947acef9742874c2b8429cc1d7dab10accf72bd5318863f862dc0371

 
何らかの値は取れているようですが、どの位置の数字が何を意味しているのか分からないため、解読できませんでした。

 

dump()

他にメソッドがないかを探したところ、 dump() があったため試してみました。

print tag.dump()
# => ['This is not an NFC Forum Tag.']

残念ながら、FeliCa Plugでは使えないようです。

 

dump_service()

他にもダンプできそうなメソッドとして dump_service() があったため、試してみました。

引数にはサービスコードが必要そうでしたが、先ほど作成したサービスコードを流用しました。

sc = nfc.tag.tt3.ServiceCode(0, 0x0b)

print tag.dump_service(sc)
# =>
# ['0000: 02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|',
#  '*     02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|',
#  '6962: 02 fd 8c 73 94 7a ce f9 74 28 74 c2 b8 42 9c c1 |...s.z..t(t..B..|']

dump_service() はサービスに対応する全データブロックをダンプするとのことで、実行が終わるまで少々待ちました。

データは出力されたものの、こちらもフォーマットが不明なため、解読できませんでした。

 
ここまでで、フォーマットが分からないことには解読できなさそうと考え、これ以上の追求はやめました。

 

参考

 
また、ユーザーズマニュアル中の数値については、ユーザーズマニュアル(v1.14)のp3に記載がありました。

ソースコード

GitHubに上げました。 felica_plug/read_w525.py が今回のソースコードです。
https://github.com/thinkAmi-sandbox/nfcpy-sample

Python2 + Scapyでマジックパケットを作成し、Wake on LANをしてみた

今まで、遠隔からPCの電源を入れる場合、Wake on LAN 用のツールを使っていました。
Wake-on-LAN - Wikipedia

 
そこで今回、ScapyでWake on LANマジックパケットを作ってみて、ツールの代替となるかを試してみました。

 

目次

 

環境

ネットワーク構成
-----------------------------------------
Raspberry Pi 2 Model B
(`eth0` : オンボードLANアダプタ)
-----------------------------------------
|
|
-----------------------------------------
スイッチングハブ
-----------------------------------------
|
|
-----------------------------------------
(外付けUSB有線LANアダプタ)
Windows10
(オンボードLANアダプタ)
-----------------------------------------

 

ネットワークアダプタ設定

明示的に設定したネットワークアダプタ設定です。

なお、以降の表記は

とします。

機器 アダプタ IPアドレス DGW DNS
ラズパイ eth0 192.168.10.50/24 192.168.10.1 192.168.10.1
X201s USB有線 192.168.10.201/24 192.168.10.1 192.168.10.1
X201s オンボード DHCP DHCP DHCP

 

各機器の構成

以前、ブリッジやルータを自作した時と同じ構成です。

 

Wake on LANができる環境を調査

Wake on LANができる環境を調査するため、いろいろと試してみましたので、それらのメモを残します。

 

試してみたこと
[NG] Macからマジックパケットを送信できるか

当初MacからWiFi経由でマジックパケットを送信しようとしましたが、手元の環境が良くないのか、MacからX201sへのWake on LANは成功しませんでした。

そのため、今回はMacからマジックパケットを送信するのはあきらめました。

 

[NG] Wake on LANでラズパイを起動できるか

Wake on LANでラズパイを起動してみようと思いました。

しかし、以下にある通り、Wake on LANではラズパイは起動しないことが分かりました。

 
そのため、Wake on LANでラズパイを起動することもあきらめました。

 

[NG] Wake on LAN + オンボードLANアダプタで、Windows10のX201sを起動できるか

元々、X201sではWake on LANができそうでした。ただ、Windows10のX201sで試してみたところ、起動しませんでした。

そのため、Windows10版向けのLANアダプタドライバを探してみました。

すると、LenovoではX201sがEOL扱いとなっており、Windows10版のドライバ提供はありませんでした。
Device Drivers File Matrices - ThinkPad X201s

 
そのため、Windows10のX201sのオンボードLANアダプタを使ってWake on LANすることもあきらめました。

 

[OK] X201s + 外付けUSB有線LANアダプタで起動するか

X201sに外付けUSB有線LANアダプタのLUA4-U3-AGTをつないでみたところ、アダプタのプロパティに Wake on LAN関係の設定がありました。

そこで、それらの設定を有効化後、X201sのLUA4-U3-AGT宛にマジックパケットを送信したところ、Wake on LANができました。

そのため、

の構成で試すことにしました。

 

Wake on LANするための設定

今回、LUA4-U3-AGTでWake on LANするため、LUA4-U3-AGTの アダプターのプロパテイ > ネットワークタブの構成ボタン > 電源の管理タブ より

  • このデバイスで、コンピューターのスタンバイ状態を解除できるようにする
  • Magic Packet でのみ、コンピューターのスタンバイ状態を解除できるようにする

の2つにチェックを入れました。

 
一方、オンボードのLANアダプタではなかったせいか、

  • BIOSWake on LAN設定を無効
    • Config > Network > Wake On LAN[Disabled] にする
  • 高速スタートアップを有効化
    • コントロールパネル > 電源オプション > システム設定(電源ボタンの動作を選択する) > 現在利用可能ではない設定を変更します > 高速スタートアップを有効にする のチェックを入れたまま

としても、Wake on LANができました。

 
ただ、スリープ状態の時はWake on LANできましたが、電源OFFの時はできませんでした。

とはいえ、今回はWake on LANができればよかったので、電源OFFの時の対応は気にしないことにしました。

 

Scapyでマジックパケットを送信する

マジックパケットのフォーマットについて

以下を参考にしました。

 

ScapyでUDPデータを設定する方法について

マジックパケットを送信するにはUDPでデータを送信する必要があります。

そのため、方法を調べてみたところ、Rawクラスを使い、データ部分は decode('hex') すれば良さそうでした。
scapy command for defining the data part of udp packet - Stack Overflow

 
なお、 decode('hex') はPython2系のみ利用可能ですが、今回使うScapyはPython2なため、気にしないことにしました。
How to create python bytes object from long hex string? - Stack Overflow

 

ソースコード全体

wake_on_lan.py

# -*- coding: utf-8 -*-
# from scapy.allでも良い
from scapy.sendrecv import sendp
from scapy.layers.l2 import Ether
from scapy.layers.inet import IP, UDP, Raw


# 必要に応じて、宛先のMACアドレスに変更する
TARGET_MAC_ADDRESS = '88:xx:xx:xx:xx:xx'

PREFIX_PAYLOAD = 'ff' * 6
BROADCAST_MAC_ADDRESS = 'FF:FF:FF:FF:FF:FF'
LIMITED_BROADCAST_IP_ADDRESS = '255.255.255.255'


def send_magic_packet():
    # マジックパケットの仕様に従い、文字列でペイロードを準備
    str_payload = PREFIX_PAYLOAD + (TARGET_MAC_ADDRESS.replace(':', '') * 16)
    hex_payload = str_payload.decode('hex')

    ether_layer = Ether(dst=BROADCAST_MAC_ADDRESS)
    ip_layer = IP(dst=LIMITED_BROADCAST_IP_ADDRESS)
    udp_layer = UDP()
    raw_layer = Raw(load=hex_payload)
    magic_packet = ether_layer / ip_layer / udp_layer / raw_layer

    # ブリッジやルータと異なり、戻りパケットの受け取りは不要
    # そのため、sendp()関数でL2パケットを送信するだけで良い
    sendp(magic_packet)


if __name__ == '__main__':
    send_magic_packet()

 

実行結果

X201sをスリープにした後、上記スクリプトを実行してみたところ、

pi@raspberrypi:~/scapy_sample/wol $ sudo python wake_on_lan.py
.
Sent 1 packets.

X201sが起動しました。

これにより、Scapyでマジックパケットが正しく作成・送信できたと分かりました。

 

ソースコード

GitHubに上げました。wol/wake_on_lan.py が今回のファイルです。
https://github.com/thinkAmi-sandbox/scapy-sample