個人情報保護の手段としてのリネージ

TL;DR

  • データガバナンスにおいてリネージが大事だよ。
  • Apache AtlasというOSSでリネージができるよ。
  • このへん情報が少ないのでもっと知りたいよ。

ここから本編

データ分析をするにあたり、そのデータがどこから来たものなのか、監査をするにあたり、不適切なデータの利用がされていないのか、知りたい人は多いと思います。 とりわけ、上記のような動機をもってる人が所属する組織は、多数の部門に分かれ、それぞれの部門で採用している技術スタック、ミドルウェアが異なったりします。

  • データの由来を知るにも担当者はそもそも誰なのか、どこの誰に聞けばわかるかがわからない。
  • データ基盤を運用してるがあるデータセットの変更がどこにどう影響を与えるのかわからない。
  • 個人情報開示請求をユーザから受けたけど、いったいどこにどうデータが伝わっているのかわからない。

そういった辛みを解決するための手法として、データリネージというものがあります。

メタデータとリネージ

しんゆうさんゆずたそさんの活動により、メタデータ管理、データマネジメントの重要性( メタデータ整備 )については徐々に認知されていると思います。 しかしながら、単独のデータセットについてではなく、複数のデータセットを束ね、関連付けて整理する活動についてはいまだ言及が少ないように思えます。

データリネージは、メタデータのうちの一領域で、情報の履歴・ライフサイクルをトラッキングすることです。 トラッキングする要素は、データの入出力、処理を行うプロセス、データセット間でなにが伝わったかなどです。

個人情報保護のためのリネージ

データ分析基盤を持ち運用している組織であれば、複数のサービスそれぞれのデータセットに管理者が存在し、個人情報の漏洩や目的外利用を監督していると思います。 *1
分析基盤への流れが単純であれば、それぞれのサービス内容やデータセットの内容、また分析施策の内容を把握し、現実的なコストでリスクを抑えることができます。

f:id:usadamasa:20200522215835p:plain
データ分析基盤への流れ_1

しかしながら、複数のデータ分析基盤があり、かつ連携がある場合、後続の分析基盤がそれぞれのデータセットの来歴を把握するコミュニケーションコストは指数関数的に増大します。

f:id:usadamasa:20200522220719p:plain
データ分析基盤への流れ_2

稼働当初は分析基盤βが適切に管理していたとしましょう。
しかしある日から分析基盤αへ、サービスCと一緒に扱ってはいけない、サービスXのデータが含まれてしまったとします。
分析基盤βの管理者はそれを把握することができるでしょうか?
分析基盤βと分析基盤αの間で綿密な情報共有が行われていればαのほうからβへ連絡することで気付くことができるかもしれませんが、間に挟まるステークホルダーが増えるにつれて難しくなるでしょう。

またデータ分析者があるデータセットを施策に使いたいと思ったとき、プライバシーポリシーとして問題ないかチェックするプロセスを取る組織もあると思いますが、この5営業日かかります、となるとデータ分析のプロセスは滞り、機会費用が発生します。

このリードタイムを忌避しチェックを怠れば、よくて施策が世に出る直前でストップ・すべて台無しになるか、大体がサービスそのものが炎上し事業の継続性が損なわれます。

データガバナンスツールApache Atlas

前置きが長くなりましたが、上記のような課題を避け、データの流れを可視化するツールとして、Apache AtlasというOSSがあります。

atlas.apache.org

Apache Atlasは2015年にApache Incubator入りし、2017年に昇格しました。現在のリリースバージョンは2.0で、現在3.0の開発が進められています。*2 主なコミッターはHadoopなどを開発するHortonworksのようです。
また、clouderaがソリューションとして提供しているようです。*3

IBMの研究者は、メタデータ管理のOSSとして "Apache Atlas is the lead contender as the choice of such an open source project." と言及しています。*4

概要

Apache AtlasはREST APIとWebUI、そして主にHadoop関連のプロダクトをサポートするメタデータのコレクタで構成されます。
しかし、柔軟なスキーマ定義が可能なため、Hadoopに限らず、Oracle、Bigqueryなどの表形式構造化データはもちろん、s3、gcsなどの非構造化データを含めあらゆるデータセットを扱うことができます。 *5

リネージとClassification Propagation

Apache AtlasはメタデータDataSetProcessの2つに大きく区別します。 DataSetはあらゆるデータを含む抽象的な概念。Processは、DetaSetとDataSetをつなぐ概念と捉えられます。

下の画像は、Apache Atlasのオフィシャルサイトから拝借したもので、緑の丸、赤の丸がDataSet、青の歯車の丸がProcessを示しています。 左のデータを出発とし、一度テーブルにロードした後、さらにViewによって複数のデータセットに分割しています。

f:id:usadamasa:20200522223219p:plain
リネージ

これだけでは特に面白くもないと思いますが、Apache Atlasの特徴的な機能としてClassification Propagation(分類の伝播)があります。
下図のように、一番最初のデータに、PII(個人を識別できる情報)というClassificationを付与すると、Processを介して後続のテーブルにまでそのClassificationが伝播し、最後のDataSetに対しても、そのDataSetにはPIIが含まれているという情報を付与することができます。

f:id:usadamasa:20200522224115p:plain
classification

このClassificationは様々な値を定義することが可能で、複数のDataSetを入力とするDataSetについて、入力のDataSetそれぞれに別々のClassificationを付与した場合、出力のDataSetには両者のClassificationが付与されることとなります。

この機能によりデータ分析基盤においての個人情報の目的外利用が可視化できると期待しています。

データの収集について

Apache AtlasはREST APIを提供しており、任意の方法でデータを収集することができます。 atlas.apache.org しかしながら、データソースにアクセスしメタデータを採取する具体的な実装は提供されていません。

この部分に関しては、開発が必要です。 たとえば@k1Lowさんの開発する tblsの出力データを利用することや、他のメタデータ管理ツールで収集したデータをAPIで連携するなどが考えられます。

github.com

www.yasuhisay.info

Apache Atlasの活用事例

Apache Atlasは、誤解を恐れずに言えば、メタデータ管理について低レイヤな基盤となる機能を提供しており、実際に組織で運用するには様々な工夫が必要と考えています。 Apache Atlasをバックエンドとして、WebUIや周辺機能を実装したOSSを提供している事例として、 lyftが開発するamundsenや、ODPiが開発するegeriaがあります。

まとめ

煩雑な文になってしまいましたが、データ整備、データガバナンスについては昨今議論が活発になってきたところです。
具体的な実装方法についても情報共有できていけたらと思います。

Appendix

メタデータ管理OSS個人的まとめ

いろいろ触ったのでまとめる。(今後追記予定あり)

TL;DR

データガバナンスツールのOSSにおいて、世間的にデファクトスタンダード的なものも、個人的にこれは!というものも見た限りなかった。 テクニカルメタデータの収集はだいたいどこも同じな一方、ビジネスメタデータ、リネージへの取り組みには顕著な差がある。 お金があるなら有償製品を導入したほうがいいかもしれない。 1 データガバナンスツールは、JIRAみたいなビジネスツールとして捉えるべきという所感。

変更履歴

  • 2020-05-18
    • Egeriaを追加

前提と関心のある領域

  • ベンチャーではなく様々な領域の事業を扱う大きめの企業。
  • マルチクラウド、マルチベンダー、マルチプラットフォーム。データストアは数百以上。
  • ETL基盤、データ分析基盤はすでに存在し、内製のメタデータ管理ツールもある。
  • データ利活用よりもガバナンスを強化したい。

調べたOSS一覧と諸元

プロダクト名 開発元 ライセンス リポジトリ
Amundsen lyft Apache Licence 2.0 https://github.com/lyft/amundsen
datahub linkedin Apache Licence 2.0 https://github.com/linkedin/datahub
metacat Netflix Apache Licence 2.0 https://github.com/Netflix/metacat
Apache Atlas Apache Foundation Apache Licence 2.0 https://github.com/apache/atlas
Egeria ODPi Apache Licence 2.0 https://github.com/odpi/egeria

Amundsen

  • フロント、バックエンドともにPython
  • 検索にElasticsearch、メタデータのストアにNeo4jを利用。
  • Pull型2

pros

  • マイクロサービス的に画面のサービス、検索のサービス、メタデータのサービスに別れている。
  • データ収集処理が抽象化されており再利用しやすそう。

cons

  • ビジネスメタデータを入れる余地は少ない。
  • メタデータのストアをNeo4jからApache Atlasに入れ替える改修が行われておりいまからの導入は不確実性が高いように思えた。

datahub

  • もともとWhereHowsというモノリシックなガバナンスツールのリプレイスで生まれた。
  • 検索にElasticsearch、メタデータのストアにNeo4jを利用。3
  • Push型で、kafkaを通じてCDCを処理できる。
  • Java

pros

  • テーブルレベルについてオーナーシップを細かく設定できる。
    • DB単位にはできない
  • avroでスキーマ管理されているので厳格な運用ができる。
  • データの上流下流を定義できる。(リネージ)
  • データセットが非推奨であることを明示的に設定可能。-> ライフサイクル管理の概念がある

cons

  • 画面からデータを投入することができない。すべてAPI経由。人間の手を入れないほうが管理が楽という割り切りかもしれない。
  • elasticsearchのバージョンが5系。ESの最新バージョンは8なので結構な遅れがある。(ES自体に破壊的変更が入ってる)

metacat

  • 画面のないAPIサーバ。UIは非公開か。
  • Java
  • Auditログ機能がある。

(とっつきが悪くあまり調べられなかった…)

Apache Atlas

pros

  • 強力な型システムにより、どんなミドルウェアにも対応できる。
  • ビジネスメタデータも型システムで表現すれば格納できる。
  • ClassificationとLineageを組み合わせて、Classificationの伝播を表現できる。
    • あるテーブルにPII(個人を識別できる情報)が入っるとフラグづけすると、 それをETLした先のテーブルにも自動的にPIIのフラグが設定される。
  • Lineageが強そう。

cons

  • 型システムが強力だが自分で設計する必要がある。概念の学習コストも高い。
  • その汎用性ゆえか、画面の動線がいまいちで、すべての情報がフラットに見える。
    • APIが公開されているので自分たちで実装したほうがよいと思った。

Egeria

https://github.com/odpi/egeria/blob/master/open-metadata-publication/website/open-metadata-types/Figure-1-Open-Metadata-Areas.png?raw=true

pros

cons

  • コンポーネントがやたら多い。
  • webuiが見当たらない? どうやって動かすんだこれ。

まとめ

いずれのOSSも成熟してるとは言い難い。 metacatの記事でコメントされていたが、基本的に各社のビジネスにフォーカスした必要な分だけの機能となっており、再利用は難しい。 とりわけ、対応データソースにはムラがあり、実際の収集をする実装が公開されていないため、結局自分たちで実装する必要がある。また、ガバナンスの観点ではほぼカバーされておらず、いわゆる分析用途がフォーカスされている感がある。

メタデータストア部分と、データグラフ構築は再利用できるかもしれない。 APIを利用したWebUI、ワークフローなどはスクラッチするべきか。 いずれも各プロダクトの独自スキーマ定義を理解する必要があり、学習コストは高い。

未調査または有償製品

プロダクト名 開発元 有償製品 紹介記事
Dataportal AirBnb Democratizing Data at Airbnb - Airbnb Engineering & Data Science - Medium
Alation Alation o https://www.alation.com/
Databook Uber https://eng.uber.com/databook/
AWS Glue DataCatalog Amazon o https://docs.aws.amazon.com/ja_jp/glue/latest/dg/populate-data-catalog.html
GCP Data Catalog google o https://cloud.google.com/data-catalog
Collibra Collibra o https://www.collibra.com/
Ground UC Berkeley’s RISE Lab http://www.ground-context.org/

Appendix

[^4] 2020-05-18 追記


  1. まだ基礎調査段階だが、有償製品はCollibraがよさそうに見える]

  2. To Push or Pull? That is the question. | SAP Blogs

  3. Neo4j人気だな。

Oracleに接続可能ななるべく小さいpython実行環境のDocker imageをビルドする

今回のお題

接続先がOracleなシステムでPythonが実行可能なコンテナイメージを作りたい。
小さいイメージは正義。

tl;dr

  • Oracle公式のイメージを利用する。
  • Docker multi stage buildを活用する。
  • alpineは苦行。

完成形は最後のほうにあります。

要件

MUST

  • 接続先は既存システムのOracle。変更不可。
  • 実行環境はパブリッククラウド内。
  • コードはgit管理。
  • 開発言語は任意。(今回はData Classなどの機能を使ってみたかったのでpython3.7とした)

WANT

  • 環境構築は手軽にやりたい。
  • デプロイは高速にしたい。

構築までの道のり

まずは素朴に

ローカル確認

Dockernizeする前に、ローカルでの動作を確認します。

pythonからOracleに接続するための公式のライブラリは cx_Oracleです。 *1*2
pipenvで入れましょう。*3

$ pipenv install
$ pipenv install cx_Oracle
$ cat Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
cx-oracle = "*"

[requires]
python_version = "3.7"

Oracleインスタンスの構築

接続先のOracleインスタンスもDockerで立てておきます。*4

macOSでOracle Database使いたい - Qiita

上記に従い本体のバイナリを入手します。バージョンは 19.3.0 Standard Edition 2 としました。 *5

$ git clone git@github.com:oracle/docker-images.git
$ cd docker-images/OracleDatabase/SingleInstance/dockerfiles
$ mv /your/download/path/to/LINUX.X64_193000_db_home.zip 19.3.0/
$ ./buildDockerImage.sh -i -s -v 19.3.0 # 完了まで10分ぐらいかかる
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
oracle/database     19.3.0-se2          91da406c90ef        23 seconds ago      6.65GB
oraclelinux         7-slim              874477adb545        6 weeks ago         118MB

無事イメージが作成されました。
とくにカスタマイズはせず起動します。

$ mkdir -p $HOME/tmp/oradata
$ docker run --name docker_oracle \
    -p 1521:1521 -p 5500:5500 \
    -v $HOME/tmp/oradata:/opt/oracle/oradata \
    oracle/database:19.3.0-se2
# ..snip..
#########################
DATABASE IS READY TO USE!
#########################

起動しました。コンテナが立ち上がっていることが確認できます。

$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS                    PORTS                                            NAMES
6c7d9db8a2bb        oracle/database:19.3.0-se2   "/bin/sh -c 'exec $O"   16 minutes ago      Up 16 minutes (healthy)   0.0.0.0:1521->1521/tcp, 0.0.0.0:5500->5500/tcp   docker_oracle

せっかくなのでコンテナ内からOracleに接続してみましょう。

$ docker exec -it docker_oracle /bin/sh
$ sqlplus /nolog
$ SQL> connect sys/cO11MX9C5Bk=1@ORCLCDB as sysdba
Connected.
SQL> select SYSDATE from DUAL;

SYSDATE
---------
26-SEP-19

SQL> 

Oracleサーバの構築はここまでです。*6

実行

さて先程構築したOracleに接続してSYSDATEをSELECTする単純なpythonを書きます。

#!/usr/bin/env python3

import cx_Oracle

print(cx_Oracle.clientversion())

# パスワードは勝手に払い出される
conn = cx_Oracle.connect(user='sys', password='cO11MX9C5Bk=1',
                            dsn='localhost:1521/ORCLCDB', mode=cx_Oracle.SYSDBA)
curs = conn.cursor()
curs.execute('select SYSDATE from DUAL')
row = curs.fetchone()
print(f'Hello Oracle ! {row[0]}')

このまま実行するとクライアントライブラリがないエラーがでます。

$ pipenv run ./main.py 
Traceback (most recent call last):
  File "./main.py", line 5, in <module>
    print(cx_Oracle.clientversion())
cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "dlopen(libclntsh.dylib, 1): image not found". See https://oracle.github.io/odpi/doc/installation.html#macos for help

Oracleの公式*7からライブラリをダウンロードして任意の場所に展開します。

  • instantclient-basic-macos.x64-19.3.0.0.0dbru.zip
  • instantclient-sdk-macos.x64-19.3.0.0.0dbru.zip

今回は $HOME/opt/oracle/instantclient_19_3 としました。 LD_LIBRARY_PATH も通しておきましょう。

$ pipenv run ./main.py
(19, 3, 0, 0, 0)
Hello Oracle ! 2019-09-26 02:16:43

無事動きました。ここまで前フリでした。

Dockerfileを書く

ようやくDockerに手を付けます。

上記のpython環境をDockerfileで記述します。

まずはpython3.7およびpipenvの入ったイメージを作成します。
ベースイメージは python:3.7-slim-buster としました。*8*9

0版

##
# Building runtime image.
##
FROM python:3.7-slim-buster

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .
ADD main.py .

RUN apt-get update -y \
    && apt-get -y install \
    && pip install --no-cache-dir pipenv \
    && pipenv install --deploy

ENTRYPOINT ["pipenv", "run"]
$ docker build . -t cx_oracle-slim # 出力は省略
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
cx_oracle-slim      latest              e7b3dc4bafad        About a minute ago   239MB

この時点でのイメージサイズは 239MB です。

課題と対応

さてこの状態ではinstantclientが入っていないため、当然実行時にエラーとなります。

docker run -it  cx_oracle-slim ./main.py
Traceback (most recent call last):
  File "./main.py", line 5, in <module>
    print(cx_Oracle.clientversion())
cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "libclntsh.so: cannot open shared object file: No such file or directory". See https://oracle.github.io/odpi/doc/installation.html#linux for help

ではDockerfileでinstantclientを取得できるようにしたいところですが、Oracleのリソース取得にはライセンスの同意が必要です。
つまり、wgetcurlでの取得が面倒です。

対処としては以下のような方策が考えられます。

  1. instantclientのzipをgitにコミットしておきイメージビルド時に展開する。一番手軽ではありますが、gitに大きなバイナリ*10をコミットするのは望ましくありません。

  2. instantclientのzipをコードとは異なるgitリポジトリにコミットしておく。コードとは異なるリポジトリにzipのみをコミットしておき、ビルド時にcloneしておく方策です。 コードのリポジトリはキレイになりますが、どこにリポジトリを作るのか、バージョンアップはどうするのか、という将来の課題を残します。*11

  3. Oracle公式イメージをベースにしてpython環境を構築する。Oracle公式がgithubで公開しているイメージ oraclelinux:x-slim *12を利用すると、ライセンスの同意などを回避できる模様。素のイメージは368MBです。

$ docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
oracle/instantclient   19                  bdf41080d895        4 seconds ago       368MB

1版

運用保守を鑑みて、1,2は捨て、3の方針としました。
このときのDockerfileは以下です。

FROM oraclelinux:7-slim

ARG release=19
ARG update=3

RUN yum -y install oracle-release-el7 \
    && yum-config-manager --enable ol7_oracle_instantclient \
    && yum -y install \
        oracle-instantclient${release}.${update}-basic \
        oracle-instantclient${release}.${update}-devel \
    && rm -rf /var/cache/yum

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .

ENV LC_ALL=en_US.UTF-8

RUN yum update -y \
    && yum -y install \
        libaio \
        python36 \
    && rm -rf /var/cache/yum/* \
    && yum clean all \
    && pip3 install --no-cache-dir pipenv \
    && pipenv install --deploy

ADD main.py .

ENTRYPOINT ["pipenv", "run"]

Dockerで立てたOracleサーバと通信するためにdocker run時にlinkしてやる必要があります。
main.pyの接続先名を localhost から ora_server に置き換えたのち実行します。

$ docker run -it --link docker_oracle:ora_server cx_oracle-slim ./main.py
(19, 3, 0, 0, 0)
Hello Oracle ! 2019-09-26 04:51:55

無事当初の目的である、Docker内からOracleと通信が可能になりました。

課題

しかしながら、oraclelinux自体がCentOSをベースにしているようで、yumでは2019-09-26現在python37は入りません。
python36を妥協して使うにしても出来上がったイメージのサイズは 685MB となかなかなサイズとなりました。
pythonのライブラリが増えてきた時を考えるともう少しダイエットさせたい気持ちがあります。

軽量化の取り組み

さてようやく本題です。

満たしたい事項を箇条書きにします。

  • python37を使いたい
  • instantclientのバイナリを自前で管理したくない
  • イメージサイズは小さくしたい

上記を解決するために、Dockerのmulti-stage buildsを利用します。

疲れてきたのでいきなり最終的なDockerを示します。

最終版

##
# Buidling intermediate image.
#  instantclientのみほしいが、wgetなどではユーザ認証が必要になるため、公式のイメージから拝借する
##
FROM oraclelinux:7-slim AS oracle-client

ARG release=19
ARG update=3

RUN yum -y install \
        oracle-release-el7 \
    && yum-config-manager --enable ol7_oracle_instantclient \
    && yum -y install \
        oracle-instantclient${release}.${update}-basic \
        oracle-instantclient${release}.${update}-devel \
    && rm -rf /var/cache/yum

##
# Building runtime image.
##
FROM python:3.7-slim-buster
COPY --from=oracle-client /usr/lib/oracle /usr/lib/oracle

## XXX: バージョン指定がバラけてる
ARG release=19
ARG update=3

ENV LD_LIBRARY_PATH=/usr/lib/oracle/${release}.${update}/client64/lib

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .

RUN apt-get update -y \
    && apt-get -y install \
        --no-install-recommends \
        libaio1 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && pip install --no-cache-dir pipenv\
    && pipenv install --system --deploy \
    && pip uninstall -y \
        pipenv \
        virtualenv-clone \
        virtualenv

ADD main.py .

ENTRYPOINT ["./main.py"]

最終版のイメージサイズは426MBと、第1版の40%減となりました。

 docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
cx_oracle-slim         latest              bd5a907578db        6 seconds ago       426MB

トピック

以下最終版に至るまでのトピックです。

まず oraclelinux:7-slim から実行に必要な必要な資材を特定しました。
oracle-instantclient${release}.${update}-xxx/usr/lib/oracle 配下にyumでインストールされるようでした。
oraclelinux:7-slimAS oracle-client としておき、こちらを COPY --from=/usr/lib/oracle でpython37のベースイメージに持ってきます。

この時点でイメージサイズは459MBと、第1版よりも200MB以上小さくなりました。また、python3.7が使える環境です。

docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
cx_oracle-slim         latest              9377e5551843        6 seconds ago       459MB

/usr/lib/oracle 配下のファイルは全体で225MB、cx_Oracleにはほぼ必須のもので、削れるにしてもojdbc8.jarなどの4MBでしたのでDockerfileの可読性を踏まえてこれ以上は追求しないこととしました。

次にapt, pipenv周りを削ります。

apt-getでは --no-install-recommends を付与することで余計なパッケージのインストールを抑止できますが、今回は効果がありませんでした。*13

pipenvは仮想環境を構築しますが、今回はDocker内のためシステムにインストールされるPythonは単一のバージョンです。したがって仮想環境用のバイナリをコピーする必要はありません。
--system を付与することで、pipenvに書かれたパッケージをsystemにインストールしてくれます。 さらに、pipenvはinstall実行後に不要となりますので、pipからも削除可能です。これで更に30MB削減です。*14

以上です。

*1:https://oracle.github.io/python-cx_Oracle/

*2:プロダクションではSQLAlchemyを経由するのが一般的だと思うが今回は使わない。

*3:pipenvは各自入れておいてください。

*4:Oracle Cloudで楽に済ませようとしたらユーザ登録でカード決済がなぜか通らなくて泣いた。

*5:https://www.oracle.com/database/technologies/oracle-database-software-downloads.html

*6:小一時間かかった…。

*7:Instant Client for macOS (Intel x86)

*8:The best Docker base image for your Python application (July 2019)

*9:当初 `python:3.7-alpine` で試みましたがglibc周りで断念。

*10:zipは75MB程度ある

*11:経験として誰も管理せず腐る傾向にある。

*12:https://github.com/oracle/docker-images/blob/master/OracleInstantClient/dockerfiles/19/Dockerfile

*13:今後必要なパッケージが増える際に効いてくるはずです

*14:Pipenv と Docker を使った開発環境のベストプラクティス - kawasin73のブログ

夫が転職して1年が経ちました

新卒で入社したSIerから今の会社に転職して2016-04-01付けでちょうど1年になる。
そもそもなんで転職したくなったのか、転職していいことあったのか、 期待した結果が得られたのか、雑にまとめる。*1

転職理由

  • 受託開発における、作ったら作りっぱなしの商習慣に不満があった
  • 常駐先から強制される旧態依然とした開発環境でフラストレーションがあった
  • ユーザに喜ばれているという実感が得たかった

転職して得たかったもの

  • 自分の作ったもの/価値を提供しているものを継続的に良くしていっているという実感
  • 自分の学びを業務にフィードバックする知的欲求の充足
  • 世の中の誰かの課題やニーズを解決できているという満足感

実際どうだったか

だいたいOK

詳しく
  • すでに構築されているそこそこ複雑なシステムの中で日々良くしていくというサイクルに組み込まれるのは楽。
  • 既存システムの制約を超えない"プログラミング"の範囲では色々試せるし。
  • ただし、一旦運用に乗っているものを変えることはやはり、調整コスト、そこまでしていいことあるの?、といった説明責任が生じる。
  • 部署がたくさんあって、フロントエンドとバックエンド間の意識の断絶を感じることが最近多い。
  • これは自分が信頼や既存システムの知識を獲得していくことでうまく立ち回るしかないのかと思う。
  • 自分自身の粗忽さの影響で迷惑かけることがまだまだあるのでなんとかしたい
  • 尊敬できる人は多いので、勝手を言わせてもらえば辞めないで欲しい

もっとちゃんと書きたかったけど酔っ払ってるので雑に終わる。

*1:退職エントリはカッコ悪いという風潮はどこから来るのだろうね?

TemplateインターフェースクラスでStrategyパターンを実現する

実装メモ

複数の条件をパスしたレコードだけ抽出するみたいな用途を想定。

#include <iostream>
#include <vector>
#include <string>

/**
 * Template Interface class
 */
template <typename T>
class Filter{
public:
    virtual bool filter(const T &v) const = 0;
    virtual ~Filter(){}
};

/**
 * Templateクラスの継承は親クラスに<>をつける
 */
class LengthFilter : public Filter<std::string> {
public:
    virtual bool filter(const std::string &v) const {
        std::cout << "LengthFilter:4" << std::endl;
        return v.length() > 4;
    }
};

/**
 * コンストラクタ、メンバ変数をもてるのは通常のclassと変わらない
 */
class StringFilter : public Filter<std::string>{
    const std::string matcher;
public:
    StringFilter(const std::string &s) : matcher(s){}

    virtual bool filter(const std::string &v) const {
        std::cout << "StringFilter:" << matcher << ", target:" << v << std::endl;
        return v == matcher;
    }
};

/**
 * 異なる型を具体化した継承
 */
class IntegerFilter : public Filter<int> {
public:
    virtual bool filter(const int &i) const {
        std::cout << "IntegerFilter:4" << std::endl;
        return i > 4;
    }
};

int main() {
    // std::stringで具体化した子クラスを格納するコンテナ
    std::vector<std::shared_ptr<Filter<std::string> > > filters;

    // 型パラメータが同一なら同じコンテナに入れられる
    filters.emplace_back(new LengthFilter());
    filters.emplace_back(new StringFilter("Hoge"));
    filters.emplace_back(new StringFilter("Hello"));

    // 型パラメータが異なればコンパイルエラー
    //filters.emplace_back(new IntegerFilter());

    std::vector<std::string> v;
    v.push_back("Hello");
    v.push_back("Hoge");
    for(const auto &s : v){
        std::cout << "target :"<< s << std::endl;
        for(const auto &f : filters) {
            // 異なるFilterクラスをそれぞれ適用する処理
            if (f->filter(s)) {
                std::cout << "*** Match   ***" << std::endl;
            } else {
                std::cout << "*** Unmatch ***" << std::endl;
            }
        }
        std::cout << std::endl;
    }
    return 0;
}

ICUでShift-JIS, EUC-JP, UTF-8の相互変換

コード

ヘッダ

#ifndef string_encoder_hpp
#define string_encoder_hpp

#include <string>

namespace encoding {
    class Encoder{
    public:
        // From EUC-JP
        static std::string EucToSjis(const std::string &value);
        static std::string EucToUtf8(const std::string &value);

        // From Shift-JIS
        static std::string SjisToEuc(const std::string &value);
        static std::string SjisToUtf8(const std::string &value);

        // From UTF-8
        static std::string Utf8ToEuc(const std::string &value);
        static std::string Utf8ToSjis(const std::string &value);
    };
}

#endif /* string_encoder_hpp */

実装

#include <vector>

#include <unicode/unistr.h>

#include "string_encoder.hpp"

namespace encoding {
    namespace internal {
        namespace encode_name {
            //! EUC-JP
            static const std::string kEucJp     = "euc-jp";
            //! Shift-JIS
            static const std::string kShiftJis  = "shift-jis";
            //! UTF8
            static const std::string kUtf8      = "utf8";
        }

        std::string encode(const std::string &value, const std::string &from, const std::string &to){
            icu::UnicodeString src(value.c_str(), from.c_str());

            // 出力バッファにnullptrを渡し変換後のバイト数が取得する
            const int length = src.extract(0, src.length(), nullptr, to.c_str());

            // 変換
            std::vector<char> result(length + 1);
            src.extract(0, src.length(), &result[0], to.c_str());

            return std::move(std::string(result.begin(), result.end() - 1));
        }
    }

    //! From EUC-JP To sjis
    std::string Encoder::EucToSjis(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kEucJp,
                    internal::encode_name::kShiftJis));
    }
    //! From EUC-JP To UTF-8
    std::string Encoder::EucToUtf8(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kEucJp,
                    internal::encode_name::kUtf8));
    }

    //! From sjis To EUC-JP
    std::string Encoder::SjisToEuc(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kShiftJis,
                    internal::encode_name::kEucJp));
    }
    //! From sjis To UTF-8
    std::string Encoder::SjisToUtf8(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kShiftJis,
                    internal::encode_name::kUtf8));
    }

    //! From UTF-8 To EUC-JP
    std::string Encoder::Utf8ToEuc(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kUtf8,
                    internal::encode_name::kEucJp));
    }
    //! From UTF-8 To sjis
    std::string Encoder::Utf8ToSjis(const std::string &value){
        return std::move(internal::encode(value,
                    internal::encode_name::kUtf8,
                    internal::encode_name::kShiftJis));
    }
}

動作確認

#include <iostream>
#include <string>
#include <vector>

#include "string_encoder.hpp"

template <typename T>
std::string test(const T &expected, const T &actual){
    return (expected == actual ? "Match" : "Unmatch!");
}

int main(int argc, const char * argv[]) {
    const std::string utf8_string = "aこれはウにこーど";
    std::cout << utf8_string << " is utf8 string, length:" << utf8_string.length() << std::endl;
    std::cout << std::endl;

    // utf8     -> sjis
    const std::string sjis_string = encoding::Encoder::Utf8ToSjis(utf8_string);

    // binary of "aこれはウにこーど" by sjis
    const std::string sjis_dump = {
        'a',
        '\x82', '\xb1', // "こ"
        '\x82', '\xea', // "れ"
        '\x82', '\xcd', // "は"
        '\xb3',         // "ウ"
        '\x82', '\xc9', // "に"
        '\x82', '\xb1', // "こ"
        '\x81', '\x5b', // "ー"
        '\x82', '\xc7'  // "ど"
    };
    std::cout << "test sjis is " << test(sjis_dump, sjis_string) << std::endl;

    // sjis     -> eucjp
    const std::string euc_string = encoding::Encoder::SjisToEuc(sjis_string);
    // binary of "aこれはウにこーど" by eucjp
    const std::string euc_dump = {
        'a',
        '\xa4', '\xb3', // "こ"
        '\xa4', '\xec', // "れ"
        '\xa4', '\xcf', // "は"
        '\x8e', '\xb3', // "ウ"
        '\xa4', '\xcb', // "に"
        '\xa4', '\xb3', // "こ"
        '\xa1', '\xbc', // "ー"
        '\xa4', '\xc9'  // "ど"
    };
    std::cout << "test euc  is " << test(euc_dump, euc_string) << std::endl;

    // eucjp    -> utf8
    const std::string return_utf8 = encoding::Encoder::EucToUtf8(euc_string);

    // check
    std::cout << "test utf8 is " << test(utf8_string, return_utf8) << std::endl;

    // 機種依存文字
    const std::string euc_contains_machine_dependent_char_string = {
        'a',
        '\xa4', '\xb3',         // "こ"
        '\xa4', '\xec',         // "れ"
        '\xa4', '\xcf',         // "は"
        '\xad', '\xc0', '\x0a', // "㍉"
        '\xa4', '\xcb',         // "に"
        '\xa4', '\xb3',         // "こ"
        '\xa1', '\xbc',         // "ー"
        '\xa4', '\xc9'          // "ど"
    };
    const std::string ㍉ = encoding::Encoder::EucToUtf8(euc_contains_machine_dependent_char_string);
    std::cout << "test ㍉    is " << test(euc_contains_machine_dependent_char_string, encoding::Encoder::Utf8ToEuc(㍉)) << std::endl;

    // 絵文字はsjisに含まれていないので変換できない
    const std::string 🍣 = {
        '\x3f', '\x0a'
    };
    const std::string sushi_string = "🍣";
    std::cout << "test 🍣   is " << test(sushi_string, encoding::Encoder::SjisToUtf8(🍣)) << std::endl;
}

結果

aこれはウにこーど is utf8 string, length:25

test sjis is Match
test euc  is Match
test utf8 is Match
test ㍉    is Match
test 🍣   is Unmatch!

参考文献