メタデータ管理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://cdn-ak.f.st-hatena.com/images/fotolife/u/usadamasa/20200531/20200531232005.png

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!

参考文献

自作クラスのコンテナをstd::copyでバイナリとしてファイル出力する

#include <iostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <numeric>

class MyClass {
private:
    class BitField {
    public:
        unsigned int a_: 10;
        unsigned int b_:  8;
        unsigned int c_: 12;
        unsigned int d_:  2;
        BitField() : a_(0), b_(0), c_(0), d_(0){}
        BitField(int a,int b, int c, int d) : a_(a), b_(b), c_(c), d_(d){};
        std::string to_string() const {
            std::stringstream ss;
            ss << a_ << "," << b_ << "," << c_ << "," << d_;
            return ss.str();
        }
    } bf_;

    std::string str_;
public:
    MyClass() : str_(""){};
    MyClass(int a,int b, int c, int d, const std::string &str) : bf_(a,b,c,d), str_(str){};
    std::string to_string() const {
        std::stringstream ss;
        ss << bf_.to_string() << "," << str_;
        return ss.str();
    }
    uint32_t get_dumpsize() const {
        uint32_t sum = 0;
        sum += sizeof(bf_);
        sum += str_.length();
        return sum;
    }
    friend std::ostream &operator<<(std::ostream &os, const MyClass &my);
};

// std::copyを使用するにはoperator<<をオーバロードする
std::ostream &operator<<(std::ostream &os, const MyClass &my){

    os.put(my.bf_.a_);
    os.put(my.bf_.b_);
    os.put(my.bf_.c_);
    os.put(my.bf_.d_);
    os.write(my.str_.c_str(), my.str_.length());
    return os;
}

int main(int argc, const char * argv[]) {
    std::vector<MyClass> my_class_vector;
    for(int i = 0; i < 10; i++){
        my_class_vector.emplace_back(i + 1, i * 100, i * 1000, i + 3, "end!");
    }
    std::ofstream ofs("/Users/usadamasa/Desktop/binaryfile.bin", std::ios::binary);
    if(!ofs.is_open()){
        std::cerr << "File Open Error" << std::endl;
        return 1;
    }
    // stringがメンバ変数にあるため書き込みサイズとクラスの確保しているサイズは異なる
    std::cout << "sizeof MyClass :" << sizeof(MyClass) << std::endl;
    std::cout << "write  size    :" << my_class_vector.at(0).get_dumpsize() << std::endl;
    for(const auto &v : my_class_vector){
        std::cout << v.to_string() << std::endl;
    }
    std::copy(my_class_vector.begin(), my_class_vector.end(), std::ostream_iterator<MyClass>(ofs));
}

ファイル出力結果(2進数)

|  a     |   b    |      c        | d|           str                     |
+--------+--------+---------------+--+-----------------------------------+
|00000001|00000000|00000000 000000|11|01100101 01101110 01100100 00100001|
|00000010|01100100|11101000 000000|00|01100101 01101110 01100100 00100001|
|00000011|11001000|11010000 000000|01|01100101 01101110 01100100 00100001| 
|00000100|00101100|10111000 000000|10|01100101 01101110 01100100 00100001|
|00000101|10010000|10100000 000000|11|01100101 01101110 01100100 00100001|
|00000110|11110100|10001000 000000|00|01100101 01101110 01100100 00100001| 
|00000111|01011000|01110000 000000|01|01100101 01101110 01100100 00100001|
|00001000|10111100|01011000 000000|10|01100101 01101110 01100100 00100001|
|00001001|00100000|01000000 000000|11|01100101 01101110 01100100 00100001| 
|00001010|10000100|00101000 000000|00|01100101 01101110 01100100 00100001|

Boost::Geometryでmultilinestringをclipping

図形の上に引かれた線を、3*3などで矩形分割する必要があったため調査メモ。

環境
  • OSX 10.11.3
  • xcode 7.2.1
  • boost 1.60
  • clang 3.5.0
サンプルコード
#include <iostream>
#include <vector>

#include <boost/geometry/geometry.hpp>
#include <boost/geometry/geometries/linestring.hpp>
#include <boost/geometry/geometries/multi_linestring.hpp>
#include <boost/geometry/geometries/point_xy.hpp>

int main(int argc, const char * argv[]) {
    namespace bg = boost::geometry;

    using point_d2 = bg::model::d2::point_xy<double>;
    using linestring_d2 = bg::model::linestring<point_d2>;

    linestring_d2 ls;
    ls.push_back(bg::make<point_d2>(1.1,1.1));

    point_d2 lp;
    bg::assign_values(lp, 2.5, 2.1);
    bg::append(ls, lp);

    // print 2 points "((1.1, 1.1), (2.5, 2.1))"
    std::cout << bg::dsv(ls) << std::endl;
    // sqrt{(2.5-1.1)^2 + (2.1-1.1)^2} = 1.72…
    std::cout << "distance " << bg::distance(ls.at(0), ls.at(1)) << std::endl;

    // boxは境界点を持ちlinestringとの計算に使える
    using box_d2 = bg::model::box<point_d2>;
    box_d2 b;
    bg::envelope(ls, b);
    std::cout << bg::dsv(b) << std::endl;

    // distance equal to length
    std::cout << "length " << bg::length(ls) << std::endl;

    std::cout << "number of points 1: " << ls.size() << std::endl;
    std::cout << "number of points 2: " << boost::size(ls) << std::endl;
    std::cout << "number of points 3: " << bg::num_points(ls) << std::endl;

    // 点から線への垂線の長さ
    point_d2 p(1.9, 1.2);
    // 0.38
    std::cout << "distance of " << bg::dsv(p)
    << " to line: " << bg::distance(p, ls) << std::endl;

    // linestringはsegmentと見做して計算することもできる
    double d = bg::distance(p, bg::model::segment<point_d2>(ls.front(), ls.back()));
    std::cout << "distance " << d << std::endl;

    // linestringは平滑化できる
    linestring_d2 ls_redundancied = {
        {3,3},
        {3.8, 4},
        {6,6},
        {4,9},
        {5,8},
        {7,7}
    };

    linestring_d2 ls_simplified;
    bg::simplify(ls_redundancied, ls_simplified, 0.5);
    std::cout << "not simplified :" << bg::dsv(ls_redundancied) << std::endl;
    // removed (3.8, 4) and (5,8)
    std::cout << "simplified :" << bg::dsv(ls_simplified) << std::endl;


    //
    // linestringのclipping
    //
    box_d2 cb(point_d2(1.2, 1.2), point_d2(4.5, 2.5));
    std::vector<linestring_d2> clipped;

    ls.push_back(point_d2(1.3,1.3));
    // 矩形, input, output
    bg::intersection(cb, ls, clipped);

    std::cout << "before clipped : " << bg::wkt(ls) << std::endl;
    for(const auto &v : clipped) {
        // 複数の線ができるが、multilinestringとしては解釈してもらえないのでfor文で確認
        std::cout << bg::wkt(v) << std::endl;
    }

    //
    // multilinestringのclipping
    //
    using multilinestring = boost::geometry::model::multi_linestring<linestring_d2>;
    multilinestring mls;

    linestring_d2 ls_1 = {
        {1,9},
        {2.8, 7},
        {3,6},
        {4,5},
        {5,4},
        {6,2}
    };
    mls.push_back(ls_1);
    linestring_d2 ls_2 = {
        {1,2},
        {5.8, 3},
        {5.9,4},
        {6.0,5},
        {6.1,6},
        {9,11}
    };
    mls.push_back(ls_2);

    box_d2 cb_2(point_d2(2,3), point_d2(6, 9));
    multilinestring mls_clipped;
    bg::intersection(cb_2, mls, mls_clipped);
    std::cout << "before mls(A)   : " << bg::wkt(mls) << std::endl;
    std::cout << "clipping box(B) : " << bg::wkt(cb_2) << std::endl;
    std::cout << "after mls(C)    : " << bg::wkt(mls_clipped) << std::endl;

    //
    // multi_pointのclipping
    //
    using bg_multipoint = boost::geometry::model::multi_point<point_d2>;
    bg_multipoint mp;
    mp.push_back(point_d2(1.4, 1.4));
    mp.push_back(point_d2(2.0, 2.0));
    mp.push_back(point_d2(2.9, 2.9));
    mp.push_back(point_d2(3.0, 3.0));

    bg_multipoint clipped_mp;
    box_d2 b2(point_d2(1.5,1.5), point_d2(3.0, 3.0));
    for(const auto &p : mp){
        // intersectionではなくintersectsで1点ずつ確認する必要があるぽい
        if(bg::intersects(b2, p)){
            clipped_mp.push_back(p);
        }
    }
    // MULTIPOINT((1.4 1.4),(2 2),(2.9 2.9),(3 3))
    std::cout << bg::wkt(mp) << std::endl;
    // MULTIPOINT((2 2),(2.9 2.9),(3 3))
    std::cout << bg::wkt(clipped_mp) << std::endl;
}
目で見て確認*1

before mls(A)
MULTILINESTRING((1 9,2.8 7,3 6,4 5,5 4,6 2),(1 2,5.8 3,5.9 4,6 5,6.1 6,9 11 ))
f:id:usadamasa:20160301121537p:plain

clipping box(B)
POLYGON((2 3,2 9,6 9,6 3,2 3))
f:id:usadamasa:20160301121740p:plain

after mls(C)
MULTILINESTRING((2 7.88889,2.8 7,3 6,4 5,5 4,5.5 3),(5.8 3,5.9 4,6 5))
f:id:usadamasa:20160301121834p:plain

線と矩形の接点にちゃんと点がある。
なお、矩形から完全に外れたlinestringは無視されるらしい。

その他

今回はC++かつ既存コードがboostを使用していてためBoost::Geometryを使ったが、他にもいろいろあるぽい。

参考文献