読者です 読者をやめる 読者になる 読者になる

uyamazakのブログ

仕事中の問題と解決メモ。PythonとGoogle Cloudがメイン。bizoceanで新規事業の企画と開発担当。 BigQueryを使ったビッグデータ収集・解析・リアルタイム処理プロジェクト進行中 https://github.com/uyamazak/oceanus

Docker + Pythonで設定ファイルを環境変数によって切り替える

oceanusではPythonで書いたアプリケーションをDockerで動かしています。

Dockerを使ったアプリケーションでは、コンテナごとに変わる可能性がある変数は、DockerfileでENVを使って、環境変数にセットして、プログラム側でそれを読み込んで使えます。

しかし、Pythonで書かれた設定ファイルで、dict型だったり入れ子になったリストやタプル、CPUのコア数な動的に変更されるような変数は、環境変数での管理は難しくなります。


そこで、どの設定ファイル読み込むか、ENVで切り替えられるように作る必要があります。

もう少し詳しく言うと設定ファイルを、取得した環境変数を使って動的に読み込んで、さらに内容を展開する必要があります。

Djangoのsettings.pyを起動時に指定できるのも同じでしょうか(未確認)。

共通に読み込む設定ファイルを作る

設定ファイルは、複数のアプリケーション、ファイルから呼び出される必要があります。もし、そこを動的に読み込むようにしてしまうと、その処理をいろんな所に書く必要がありDRYではありません。

そこで、元となるファイルを作り、他のファイルは常にそれを読み込む形とします。

例として、下記のように、settings.pyを読み込む共通のファイルとし、読み込むものはmy_settngs.py、other_settngs.pyとします。

- app
  - app.py
  - common
   - settings.py
   - my_settngs.py
   - other_settngs.py

app.pyから読み込む場合は下記のようにモジュールとして必要な変数を読み込みます

from common.settings import DATABASE_NAME

もちろん、DATABASE_NAMEのような設定はdefault_settings.py、other_settings.pyに宣言し、settings.pyには不要です。

例として下記のような設定です。

my_settngs.py

DATABASE_NAME = "default-oceanus"
other_settngs.py
DATABASE_NAME = "other-oceanus"

settings.pyから動的に別ファイルを読み込む

まず読み込むモジュール名を環境変数からstr型として取得するようにします。

モジュール名なので、ディレクトリは.で区切り、ファイル名の.pyは不要です。

importが慣れない人は、まずコマンドラインなどで正しく読めるかを試したほうがいいかもしれません。

環境変数名をOCEANUS_SETTINGSとします。

Dockerfileでは下記のように書いておき

ENV OCEANUS_SETTINGS common.default_settings

runする時に、-eをつかって

docker run -e OCEANUS_SETTINGS=common.other_settings

pythonで同名の変数に代入。

デフォルトが必要な場合はgetの第2引数でセットしておきます。

次にその文字列を使って動的にimportします。
動的にimportを行うには、__import__()でも可能ですが、importlib.import_moduleが推奨されています。


from os import environ

OCEANUS_SETTINGS = environ.get("OCEANUS_SETTINGS", "common.default_settings")

import_module(OCEANUS_SETTINGS)

これはまだdefault_settingsを読み込んだだけで、中身の展開はできていないので

from common.settings import DATABASE_NAME

という形では使えません。

読み込んだ設定ファイルの中身を展開する

そして、その読み込んだファイルにある変数などをすべて展開します。
ここはどうやるかしばらく、ハマりましたが、下記のページを参考にglobals()を使って、現在のモジュール(common.settings)のグローバル変数に追加することで、うまくいきました。

31.5. importlib — import の実装 — Python 3.6.0 ドキュメント

最終的には下記のような形になります。
settings.py

from os import environ
from importlib import import_module

OCEANUS_SETTINGS = environ.get("OCEANUS_SETTINGS", "common.default_settings")

globals().update(import_module(OCEANUS_SETTINGS).__dict__)

これで、環境変数OCEANUS_SETTINGSを変更して、

from common.settings import DATABASE_NAME

のように読み込めば、common.default_settingsや、common.other_settingsの設定ファイルを読みこむことができます。

この動的読み込み&展開は設定ファイルに限らずいろんな所で応用がききそうです。

実践力を身につける Pythonの教科書

実践力を身につける Pythonの教科書

Google Cloud Datalabに任意のパッケージをインストールする

自社では基本的にBigQueryにデータをぶちこんでいるので、データ解析や、機械学習にはJupyterのGoogleカスタマイズ版であるDatalabを使っています。

cloud.google.com

基本的な使い方はJupyter(IPython)と一緒です。

BigQueryをはじめ、Storage等、Google環境に必要なライブラリは一通り入っていますが、やはり色々やろうとすると、pip installやら、apt-getやらが必要になってきます。


そこで2つ、カスタマイズ方法を紹介します。

使うパッケージは一回だけと思われる場合

コマンドでインストールします。再起動時などには消えるので、それでもかまわない場合。


まずdocker psで、datalabのコンテナIDを取得しておき、下記のコマンドを行う

docker exec {コンテナID} pip install {欲しいパッケージ名}

もしくは、bashなどで入ってからインストールする

docker exec -it {コンテナID} bash

bashが立ち上がるのでpipとかapt-getする。

apt-getの場合は、updateしないと駄目な場合が多いので、bashで入ったほうが楽かも。

永続的に使いたい場合

自分でDockerfileを使って、拡張します。

好きな名前でディレクトリを作ってDockerfile作成

mkdir mydatalab
cd mydatalab
vim Dockerfile

私の場合は下記のような感じ。

FROM gcr.io/cloud-datalab/datalab:local

# install from pypi
RUN pip install --upgrade pip && pip install -q \
bigquery-python \
cython \
oauth2client \
sendgrid \
gensim \
scipy \
networkx \
suds

RUN apt-get update && apt-get install -y \
    gcc

githubからもがんばれば出来る。


ビルド

sudo docker build -t mydatalab:latest

起動

sudo docker run -d -p "8080:8080" \
    -v "/path/to/content:/content" \
    mydatalab:latest

ビルドとか起動とかはシェルスクリプト化しておくと便利

ただCloud Datalabは、Google社内と同じくPython2.7なのでPython3にしかないものを使うときはいろいろと大変そう。まだ困ったことはない。

Dockerエキスパート養成読本[活用の基礎と実践ノウハウ満載!] (Software Design plus)

Dockerエキスパート養成読本[活用の基礎と実践ノウハウ満載!] (Software Design plus)

大規模分散型リレーショナルデータベースGoogle Cloud Spanner来た

以前からGoogleのセミナーなどでは話題に上がっていた、分散RDBがついに公開されました。

これまでトランザクションが必要なDBは分散するのが難しく、注文が集中するECや、ゲームなどでは、RDBボトルネックになっていましたが、もしCloud Spannerが期待通りのものであれば、これまで高負荷DBサーバーの苦労は簡単に解決できそう。

Cloud Spanner | Automatic Sharding with Transactional Consistency at Scale  |  Google Cloud Platform


itpro.nikkeibp.co.jp


jp.techcrunch.com

Word2Vecを使って行動履歴から関連商品を出す

bizoceanで集めたデータを使って何かできないかを考えていたら、少し前に話題になったWord2Vecを使って書式のダウンロード履歴を文章として入力すれば、近いベクトルの書式、つまり関連性の高い書式が出せるのでは?と思い試してみた。

Word2Vecについては下記がわかりやすい。
deepage.net

ただの単語の羅列をn次元のベクトル化してくれるのは、データの用意も簡単な上、いろいろ加工もしやすくて便利。


データのもととなるbizoceanは、請求書、履歴書、契約書などの書式のダウンロードサイト。
www.bizocean.jp

ほとんどが無料だけど、2016年から有料販売もできるようになった。

他のECサイトなどでは、ダウンロードではなく、購入になるだろう。



まず、文章のように、ユーザーごとのDL履歴を1行ずつ、スペース区切りで書き出す。

環境はoceanusを使って、BigQueyにぶち込んだアクセスログを、Google Cloud Datalab(Jupyter)で。

cloud.google.com


SQLは下記のような感じ。

SELECT doc_ids FROM
(
  SELECT
    sid,
    GROUP_CONCAT_UNQUOTED(doc_id, " ") as doc_ids,
    count(doc_id) as count
  FROM
  (
  SELECT
    sid,
    doc_id
  FROM
   ( SELECT 
      sid,
      dt,
      REGEXP_EXTRACT(url, r'/doc/download/complete/([0-9]+)/') as doc_id,
    FROM TABLE_DATE_RANGE(
      {TABLE_PREFIX},
      TIMESTAMP("{FROM}"), 
      TIMESTAMP("{TO}")
    )
    WHERE
      evt = "pageview" AND 
      url CONTAINS "/doc/download/complete/"
      ORDER BY dt ASC
    )
  GROUP BY
    sid,
    doc_id
  )
  GROUP BY
  sid
)
WHERE count > 1


これの実行結果をファイルに書き出す。to_fileで一発。

import datalab.bigquery as bq
query = bq.Query("""上記のSQL""")

query.to_file("donwload_relative_.csv")

csvは下記のようなファイルになる。1つだけでは意味が無いので、2DL以上した人のみを対象にした。3ヶ月程度で20万行弱、7MBのデータになった。

doc_ids
100454 103502 103002
104552 102249 102230 104526
108875 517686 516829 517682 108177 517680 517677 517679
520271 518137 104615 100956 516761 519367 519366 102746 519880 103574
520693 108875 521525 519565 521536 516128 109213 521490 521528 521445 521537 100550 521522 521677 520694 109042 517770 521696 521790 521208 521904 519006 521807 521686 109209 521275 101698 102384 520599 103661 103189 107848 521252 521768 518169
104443 109407 107643 521478 103113 519969 103428 103104 103177 515111 104487
104710 516744 516750 104712 104714
102728 109481 108971 105489 104446
103438 516191
516761 102763 516791
103101 106929 106932 100549 103141 102068 100556
500108 104449 519125 517878 519706 102724 500110
519467 517568 103601 518222 103114 517998 109407 519365 519737 515198 518201 103183 521047 100570 520320 515257 519250 102880
516741 520351
520351 104713
521206 521258 521270 521213 521217 521259 521481 521246
521256 521234 521534 521496 521538 521230 521267 521536 521229 521552 521411 521258 521246 521247 521461 521212 521532 521265 521537 521266 521522 521495 521255 521225 521447 521549 521525 521528 521213 521487 521444 521254 521541 521453 521553 521523 521242 521249 521251 521257 521542 521494 521530 521478
519250 520268 102901 518138
500418 104393 518306
518205 106898 101682 516772 103714 104761 100971
104570 102349
521537 521213 521536 521229 521252 521246 521443 521542 521218 521322 521517 521532 521247 521250 521214 521534 521237
104475 109362
516797 515042 516780 516486 516768
516744 104710 107846 519511 515633 520353 515644 104715
516268 515041 103127 104478 515040 109390 103126
521227 521252 521521 521487 521213 521524 521105 521537 521646 521231 521530 519982
519509 520358
104714 104712
518362 518354
104714 104710 104711

Word2Vecの実装には、gensimを使う。別途pipでインストール。今回はDatalabなので、Dockerfileを使ってインストールしておいた。
詳しいカスタマイズ方法は↓
uyamazak.hatenablog.com



gensim: models.word2vec – Deep learning with word2vec

from gensim.models import word2vec
#data = word2vec.Text8Corpus('donwload_relative_20170213-01.csv')
data = word2vec.LineSentence("donwload_relative_" + filename + ".csv")

ググって出て来る記事だとText8Corpusが使われてるけど、行ごとに処理してくれる?LineSentenceを使う。

モデルの作成。これが重い。

model = word2vec.Word2Vec(data, size=200, window=5, min_count=20, workers=8)

実行マシンは社内のサーバーマシンで
Intel(R) Xeon(R) CPU E3-1230 v5 @ 3.40GHz、メモリ32GBを兼ね備えているけど、20分ぐらいかかる。

モデル作成が終わったらmost_similarを使って、一番近いものを出す。
これは爆速、一瞬で帰ってくる。

200次元とはいえ、単純な行列の計算になるから早いのだと思ってる。

targetに書式IDを入れる。

out=model.most_similar(positive=[target], topn=15)

IDだけだとよくわからないので、書式名が入ってるテーブルから名前を取ってpandasでくっつけた。

一番人気の「請求書003 シンプルな請求書」を入れてみた
請求書003 シンプルな請求書|テンプレートのダウンロードは【書式の王様】

102693 請求書004 0.841390490532
517568 エクセル請求書(窓付封筒、長3対応) 0.830850958824
517868 請求書01 0.827962636948
519366 スタイリッシュな請求書(イエロー) 0.82549983263
102417 請求書006 0.825079739094
520271 自動計算付きの請求書(エクセル) 0.824660122395
102418 請求書012(サービス業用) 0.815610051155
104445 請求書008(サービス業用) 0.815160274506
102748 請求書05 0.814028024673
102419 請求書007 0.808267712593
519880 請求書&送付状 0.795116245747
104615 請求書001 0.793960869312
517422 請求書(窓付封筒、長3対応) 0.793055713177
518137 請求書010 0.787589609623
520984 シンプル請求書_スマホ入力用 0.785955011845


スケジュール表
スケジュール表|テンプレートのダウンロードは【書式の王様】

520286 スケジュール表:ガントチャート式【改】 0.960333704948
521092 工程管理表 スケジュール管理表 2016年3月~ 0.960129082203
521176 工程管理表 スケジュール管理表 2016年9月~ 0.946121633053
516756 Excelで作るA41枚の月間スケジュール管理 0.942580997944
104454 建築業用作業工程表(年間) 0.912206470966
500428 工程管理_04 0.912127077579
521111 2016年度カレンダー 一覧(2016年4月~) 0.886768221855
521186 課題管理表_2016年 0.879617512226
516786 Wordで作るA41枚の月間スケジュール管理 0.879371285439
517912 年間スケジュール2014 0.877403259277
500422 工程管理_02 0.875747382641
521100 2016年度カレンダー 0.862578749657
500426 工程管理_03_製造業 0.861161231995
517999 2014年 年間カレンダー 0.856253266335
104453 建築業用作業工程表(月間) 0.849661827087


ニワトリとカラフルな花飾りの年賀状
ニワトリとカラフルな花飾りの年賀状|テンプレートのダウンロードは【書式の王様】
当たり前だけど、年賀状でもポップなものを入れれば、ポップなものが返ってくる。

521915 富士山と門松の年賀状2017 酉・鳥 0.953142642975
521445 ニワトリと花の年賀状 0.952863097191
521416 ニワトリと富士山の年賀状 0.946253061295
521446 ひよこの年賀状 0.944137215614
521530 楽しい音楽隊 0.94397175312
521225 鶏の家族がかわいらしい年賀状 0.937339186668
521523 虹を渡って行こう! 0.934369027615
521533 お散歩 0.926785349846
521251 木にとまる鶏の年賀状 0.925209403038
521497 ニワトリとヒヨコの年賀状 0.92281216383
521542 (word)酉年をお花で彩る年賀状 0.917050004005
521913 (word)コマ回し名人(酉年) 0.913929462433
521520 ハートの仲良し一家 0.912433743477
521517 水引飾り・酉:jpg 0.910482287407
521524 パンダと体操 0.910432219505

作成したモデルはメモリ上にあるけど、ファイルに書き出しておくこともできる

model.save("donwload_relative.model")
model.load("donwload_relative.model")

モデル作成さえバッチなどで処理してしまえば、2万件近くあっても、瞬時にレスポンスできるのでAPI化も簡単にできそう。


ユーザーの購入履歴、行動履歴に基づいたレコメンドなどを考えていて、難しいものは避けたい方は、こんな感じで気軽にWord2Vecを使うのもありだと思う。


おまけ

networkxを使って、近いものをネットワーク表示してみた。読めない。
f:id:uyamazak:20170214163219p:plain

Python3でipaddressモジュールを使って、IPアドレス認証を行う

本番環境のWEBアプリケーションで、デバッグ情報を表示させる際、サーバー情報なども全部さらけだすので、最低限IP認証だけ行いたいと思った。

IPアドレスの文字列比較なら簡単だけど、できればネットワークアドレス(192.168.0.0/24等)で許可できると便利。

検索した所、python3からは標準でipaddressというモジュールがあるのを知った。Googleが作ったipaddrが元になってるらしい。

21.28. ipaddress — IPv4/IPv6 操作ライブラリ — Python 3.5.2 ドキュメント


下記のような感じで書いてみた。

許可するIPのリスト名はDjangoのsettings.pyから取った。

strでカンマ区切りで指定して後で分割する。

import ipaddress

INTERNAL_IPS_V4 = "192.168.0.0/16,127.1.1.1"


def is_internal_ip(client_ip_str) -> bool:
    internal_ip_list = INTERNAL_IPS_V4.split(",")
    client_ip = ipaddress.ip_address(client_ip_str)
    for internal_ip in internal_ip_list:
        ip_network = ipaddress.ip_network(internal_ip)
        if client_ip in ip_network:
            return True

    return False

ipaddress.ip_networkは、/なしで普通のIPアドレスを指定すると/32があるとして良しなにしてくれるので、IPアドレスとネットワークアドレスが一緒でも問題なく動いた。

>>> is_internal_ip("192.168.50.1")
True
>>> is_internal_ip("192.167.50.1")
False
>>> is_internal_ip("127.1.1.1")
True
>>> is_internal_ip("127.1.1.2")
False
>>> is_internal_ip("127.1.2.1")
False
>>> is_internal_ip("255.255.255.0")
False
>>> is_internal_ip("192.168.277.777")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/oceanus/app/common/utils.py", line 77, in is_internal_ip
    client_ip = ipaddress.ip_address(client_ip_str)
  File "/usr/local/lib/python3.6/ipaddress.py", line 54, in ip_address
    address)
ValueError: '192.168.277.777' does not appear to be an IPv4 or IPv6 address


そもそも与えられたclient_ip_strがIPアドレスとして正しいかは見ていないので、変なの入れるとエラーが出るけど、自分で設定するんだから大丈夫だよね。

入門 Python 3

入門 Python 3

データ解析インターンの予定を立ててみる

以前の記事でインターン募集をしていましたが、1件応募があり、採用の方向で進んでいるので、ざっくり今後のスケジュールを立ててみる


前提条件いろいろ

対象

大学2年生

マーケティングに興味があり、データ分析、プログラミングのインターンを探していた。

プログラミング未経験

期間

週2で6ヶ月以上

就活が本格化するまで。

目的

大学生

就職活動のため

自社側

会社の知名度アップ

社員(自分)の教育能力を付けるため

採用のため

前提条件以上。




まずゴールを設定。

ゴール

ざっくりだけど

仕事ができる人にする(なる)

bizocean自体も書式で仕事を効率よくできるようにするサービスだし、インターン生を仕事ができる人にできるのであれば、採用にも、教育スキル的にも良いと判断できそう。


求めること

自社社員との世代ギャップを活かした視点、発見。


評価軸

面談はよくやるけど、ただ「データ解析のインターンしてました」だけだと就活の時に弱い気がする。

実際にその過程と成果がネット上で見られるようになっていれば、履歴書にURL書くことでかなりアピールできそう。

そのため、成果物として3つを設定。

- bizoceanの技術ブログを執筆
- データ分析を行いグラフ、レポートの作成
- 企画、提案資料を作成

この3つをメンターが質的に、アクセス数などを量的に判断して評価する。

スケジュール

当たり前だけど途中のサポートや進捗確認はメンターがする。

入門 ~2、3ヶ月

bizoceanとは

まず、データはうちのbizoceanのアクセスデータ、行動データ、会員データなどを使うので、うちのビジネスはなにか、そのデータが何を意味しているのか、どうやって記録しているのかを知ってもらう。

www.bizocean.jp

社員にインタビューして、先入観などを知るのもいいかも。

途中成果として、「bizoceanとは○○○である」みたいなのを100個書くとかもいいかな。大学生の視点で出すと既存の社員には意外かもしれない。

データ分析とは

まだ簡単なところしか手を付けられてないけど、どういった手順でデータを可視化していくのかを見てもらう

Pythonと環境について

データ解析にはJupyterのGoogleカスタマイズである、Google Cloud Datalab上でPythonを書き、BigQueryからデータを取って使ってもらう。

アプリ開発よりはPythonで覚える必要がある範囲が狭いので(Pythonで使うライブラリが限られているという意味)、実際に動かしながら基本的な文法、エラー対処に慣れてもらう。

実践 ~2、3ヶ月

テーマを考える

bizoceanと、データについて理解してもらったら、自身の興味や、会社の課題に合わせて、データを使って何をするのかのテーマを決めてもらう。

一つだけだと視野が狭くなるので、2、3個以上とする。

できれば既存の社員の思い込みを打破し、新しい発見となるようなものがいい。


- bizocean会員は実は○○だった!
- bizocean会員が辞める本当の理由
- こういうコンテンツがあれば、会員は○○万人増やせる!

さっき拾った記事だけどこういうフレームワーク使うのもありかも
medium.com

資料作り

データを使って、テーマについて資料を作成する

発表

月一の全体会議にて発表


絶対内定2018 インターンシップ

絶対内定2018 インターンシップ

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

CeleryでWokerとbeatを同時起動する

タスクキューイングができるCeleryを使っているけど、一部認証が1時間で切れる箇所があり、期限切れの前に定期的に再認証を行う処理が必要になった。

そのため、Celeryでcron的なことができるbeat機能を使った。

Periodic Tasks
Periodic Tasks — Celery 4.0.2 documentation

ドキュメントから下記部分を参考に再認証処理を追加

from celery import Celery
from celery.schedules import crontab

app = Celery()

@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    # Calls test('hello') every 10 seconds.
    sender.add_periodic_task(10.0, test.s('hello'), name='add every 10')

    # Calls test('world') every 30 seconds
    sender.add_periodic_task(30.0, test.s('world'), expires=10)

    # Executes every Monday morning at 7:30 a.m.
    sender.add_periodic_task(
        crontab(hour=7, minute=30, day_of_week=1),
        test.s('Happy Mondays!'),
    )

@app.task
def test(arg):
    print(arg)
Setting these up from w


最初はworkerの他にbeatの起動が必要なのを知らずに、全然動かんと思ってしまった。

しかし、今回の再認証処理は、同一インスタンス内で再認証するので、別プロセス、別コンテナ(Docker)になってしまうと、全く意味がない。

なんとか一プロセスでworkerとbeatを起動できないかと思ってたら簡単にできるよう。

stackoverflow.com

python manage.py celery worker --beat

 ※--beatの代わりに-Bでも同じ


が、ドキュメントを見てみると
Periodic Tasks — Celery 4.0.2 documentation

this is convenient if you’ll never run more than one worker node, but it’s not commonly used and for that reason isn’t recommended for production use:

あまり一般的ではなく、本番環境ではおすすめしないとのこと。

Worker内で行う再認証などは、素直にWorker内で完結させたほうが良さそう。



Celery、リトライ、時間制限(1秒に1回だけとか)、cronのような定期実行までできて便利すぎてやばい。