Recap of TensorFlow Dev Summit 2018 in 信州 に参加しました & LTしました #tfug #GDG信州

4/13(金)に塩尻インキュベーションプラザ(SIP)で開催された、Recap of TensorFlow Dev Summit 2018 in 信州 に参加しました。
Recap of TensorFlow Dev Summit 2018 in 信州 - connpass

資料はconnpassの他、当日の様子がTogetterにまとめられています。
Recap of TensorFlow Devsummit 2018 in 信州のまとめ - Togetter

最近、機械学習にふれており、TensorFlowまわりの近況を知りたくて参加しました。

以下メモを残しますが、誤りがあればご指摘ください。

 
目次

 

全体俯瞰及びTensorflow.js (adamrockerさん)

まずTensorFlow Dev Summit 2018の全体的な話がありました。

現地では1セッション15分、しかもいずれも延長が無いという濃密なものだったようです。

全体俯瞰で気になった内容は以下でした。

  • TensorFlow HUBで学習モデルが提供される
  • tf.kerasに統合されていく
  • Estimator推し
  • tf.data
  • 前処理まわり
  • AutoMLでのモデル構築
  • CSVも1.8から対応

 
引き続き、Tensorflow.jsのメモです。

  • Tensorflow.js + ブラウザで、機械学習・ファインチューニングと訓練済モデルの移植ができるようになる
    • ユーザに特化した体験を提供する時に、ブラウザの学習を使う
  • Tensorflow.jsで画像を認識させるには、imgタグを使えば良い
  • Separable Convolution
  • 今年のGoogleのエイプリルフールの舞台裏では、Tensorflow.jsが使われていた
    • 方向分解特徴や時間的特徴の利用

 
その後、ブラウザ(TensorFlow.js)と、モバイル(TensorFlow Lite)の画像認識デモがありました。

  • 訓練画像80件でそれなりの精度が出ていること
  • リアルタイムで画像認識し、リアルタイムに類似度を更新

を実体験できて良かったです。

 
今後も、理論を学びつつ、ツールの使い方も学んでいかないとなーと思いました。

 

TensorFlowを使ってみる (kobayutaponさん)

TensorFlowのチュートリアル後の簡単な例を、体験を交えながらの発表でした。

正弦波・矩形波を使ったTensorFlowチュートリアルのような内容で、さくさくとモデルができていったのが印象に残りました。

電子工作系ではこんな感じでデータを取るのかと参考になりました。

 

自分のための機械学習をしてみた話 (thinkAmi)

自分のLTです。

 
枠が空いていたこともあり、せっかくなので最近やっていたことをLTしました。

内容は「Google Drive APIによるOCR & Doc2Vec を使って、IPA過去問の類似度を出す」です。

会場に来ていた方に質問したところ、

と、なかなか緊張する中でのLTでした。

 

GDG信州分科会 (LED信州)

GDG信州分科会として、電子工作勉強会グループ「LED信州」を立ち上げるというLTでした。

一人で電子工作していても詰まってしまうところがあるので、身近にこのような勉強会があるのはありがたいです。

 

パネルディスカッション

kehiさんや会場からの質問に対して、adamrockerさん・kobayutaponさんが回答するという形でのパネルディスカッションでした。

会場からもマニアックな質問などが出ていて、いろいろと盛り上がりました。

GDG信州のイベントではパネルディスカッションがあることが多く、いろいろな方面からの質問や回答を聞けて良いです。

 

次回予告

GDG信州のイベントの予告がありました。

 
最後になりましたが、開催してくださった関係者のみなさま、ありがとうございました。

Python3 + google-api-python-clientで、Google Drive APIを使ってpdfファイルをアップロードし、OCR処理をする

手元の画像pdfファイルをOCR処理したいことがありました。

手軽にOCR処理する方法がないかを調べたところ、Google DriveOCR機能があることを知りました。
GoogleDriveのOCR機能を使って、大量の画像ファイルをテキストに変換する

そこで、Google DriveOCRを試してみることにしました。

 
なお、上の記事ではgdirveというCLIツールを使っていました。
prasmussen/gdrive: Google Drive CLI Client

PythonGoogle Drive APIを使ってみたかったので探してみたところ、 PyDrive がありました。
gsuitedevs/PyDrive: Google Drive API Python wrapper library

ただ、今回はファイルをアップロードするだけで複雑なことはしないため、Google APIPythonライブラリ google-api-python-client だけでGoogle Drive APIを使ってみます。
google/google-api-python-client

 
目次

 

環境

 
なお、画像としてpdf化されたファイル例として、今回は情報処理試験の過去問を使ってみます。
IPA 独立行政法人 情報処理推進機構:過去問題

今回は、基本情報処理試験の午前問題である 2017h29a_fe_am_qs.pdf を使います。

最終的なディレクトリ構造は以下となります。

$ tree
.
├── 2017h29a_fe_am_qs.pdf
├── __init__.py
├── client_id.json
└── google_drive_ocr_uploader.py

 

Google Drive APIを使うための事前準備

Python Quickstartを参考に、事前準備を行います。
Python Quickstart  |  Drive REST API  |  Google Developers

プロジェクトの作成まわりは、以前のGmail APIの時と同様で良さそうです。
Python3 + google-api-python-clientで、Gmail APIを使ってメールを送信する - メモ的な思考的な

 
認証情報が含まれる client_id.json ファイルは、実行用スクリプトと同じディレクトリに入れます。

 

調査事項

認証まわり

公式ドキュメントの get_credentials()関数まわりを使用します。
Step 3: Set up the sample - Python Quickstart  |  Drive REST API  |  Google Developers

なお、Python2.6関係のソースコードは削除しました。

 
Scopeはどうしようかと思いましたが、公式ドキュメントに

Per-file access to files created or opened by the app. File authorization is granted on a per-user basis and is revoked when the user deauthorizes the app.

https://developers.google.com/drive/v3/web/about-auth#OAuth2Authorizing

とあったため、 https://www.googleapis.com/auth/drive.file を使うことにしました。

 

アップロードする時のMIMEタイプについて

どのようなMIMEタイプにすればよいか見たところ、

The extracted text will appear in the Google Docs document alongside the embedded image.

https://developers.google.com/drive/v3/web/manage-uploads#importing_to_google_docs_types_wzxhzdk18wzxhzdk19

とありました。

ドキュメントにある表より、pdfの場合はGoogle DocsにするMIMEタイプを指定すれば良さそうでした。

Google DocsMIMEタイプは application/vnd.google-apps.document でした。
Supported MIME Types  |  Drive REST API  |  Google Developers

 

ocrLanguageの設定先について

OCRをどの言語で実行するかについては、 ocrLanguage に設定すれば良さそうでした。
Files: create  |  Drive REST API  |  Google Developers

ライブラリのドキュメントを読むと、create()メソッドの引数に ocrLanguage がありました。
https://developers.google.com/resources/api-libraries/documentation/drive/v3/python/latest/drive_v3.files.html

OCR言語コードは ISO 639-1 codeで指定すればよいので、日本語の場合は ja とします。

 

bodyパラメータで指定するファイル名について

アップロードするファイル名を指定します。

Google Docsにするからといって拡張子を外すと、以下のようにHTTP 400エラーが発生しますので、拡張子をつけたままにします。

googleapiclient.errors.HttpError: <HttpError 400 when requesting https://www.googleapis.com/upload/drive/v3/files?ocrLanguage=ja&alt=json&uploadType=resumable returned "Bad Request">

 

実装

google_drive_ocr_uploader.py

import argparse
import os
import pathlib

import httplib2
from apiclient import discovery
from googleapiclient.http import MediaFileUpload
from oauth2client import client
from oauth2client import tools
from oauth2client.file import Storage


UPLOAD_FILE_NAME = '2017h29a_fe_am_qs.pdf'
MIME_TYPE = 'application/vnd.google-apps.document'
SCOPES = 'https://www.googleapis.com/auth/drive.file'
CLIENT_SECRET_FILE = 'client_id.json'
APPLICATION_NAME = 'ipa-google-drive-api-client'

flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args()


def get_credentials():
    """Gets valid user credentials from storage.

    If nothing has been stored, or if the stored credentials are invalid,
    the OAuth2 flow is completed to obtain the new credentials.

    Returns:
        Credentials, the obtained credential.
    """
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir,
                                   'drive-python-quickstart.json')

    store = Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        credentials = tools.run_flow(flow, store, flags)
        print('Storing credentials to ' + credential_path)
    return credentials


def upload_with_ocr():
    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('drive', 'v3', http=http)

    local_file_path = pathlib.Path(__file__).resolve().parent.joinpath(UPLOAD_FILE_NAME)

    media_body = MediaFileUpload(local_file_path, mimetype=MIME_TYPE, resumable=True)

    body = {
        'name': local_file_path.name,
        'mimeType': MIME_TYPE,
    }

    service.files().create(
        body=body,
        media_body=media_body,
        ocrLanguage='ja',
    ).execute()


if __name__ == '__main__':
    upload_with_ocr()

 

実行結果

$ python google_drive_ocr_uploader.py 

と実行後、自分のGoogleドライブの直下のディレクトリを見ると 2017h29a_fe_am_qs.pdf ファイルが存在します。

ファイルを開くとOCR結果が表示されました*1

複数ページのpdfでも、全ページOCR処理がなされていました。

 

ソースコード

GitHubに上げました。 google_drive_api ディレクトリの中にあるものが今回のファイルです。
thinkAmi-sandbox/google-api-python-client-sample

*1:掲載してよいのか分からないため、結果は省略します

#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を使っているため、履歴が増えています