Ansibleでインターネットに繋がらない環境へPythonライブラリをインストールする

インターネットに繋がらないHadoopクラスタの各slaveに対してAnsibleでPythonのライブラリを管理することを想定して試してみましたので、その時の内容を書いておきます。
hadoopクラスタへのpythonライブラリ追加として、とりあえず以下のユースケースを追加しています。 - 一つのノードに全てのライブラリをインストールする - 全てのノードに一つのライブラリをインストールする

それから要件として既にライブラリがインストール済であればインストールを実行しないようにもしたいと思います。
まず、作成したansibleプロジェクトのディレクトリ構成は以下のようになりました。

ansible
├── devserver.yml
├── install_one_library.yml
├── roles
│   └── python
│       ├── files
│       │   ├── install_shell
│       │   │   ├── build_install.sh
│       │   │   ├── install.sh
│       │   │   └── install_pip.sh
│       │   └── library
│       │       ├── cython-0.28.2.tar.gz
│       │       ├── numpy-1.11.0.tar.gz
│       │       ├── numpy-1.14.2.tar.gz
│       │       └── pip-10.0.1.tar.gz
│       ├── vars
│       │   └── library_version.yml
│       └── tasks
│           ├── init.yml
│           ├── install_cython.yml
│           ├── install_numpy.yml
│           ├── install_pip.yml
│           └── main.yml
├── staging.info
└── group_vars
    └── default.yml

それでは各ファイル毎の役割をまとめて行きたい思います。

group_vars/default.yml

---

ansible_home: "/var/ansible"
  • ライブラリのアップロード先などで使うディレクトリを定義します
  • 各roleで共通ということでプロジェクト直下の変数用にディレクトリ内にファイルを配置しています。

staging.info

[batch-servers]
ip[15:16].localdomain ansible_ssh_user=hoge
  • 通常のインベントリファイルと同じです
  • hadoopのslaveなど大量にインストールを行う場合は連番で指定するのが良さそうです

devserver.yml

---
- hosts: batch-servers
  become: yes
  vars_files:
    - vars/default.yml
    - roles/python/vars/library_version.yml
  roles:
    - python

# ansible-playbook devserver.yml -i staging.info --private-key=~/.ssh/id_rsa
# ansible-playbook devserver.yml -i staging.info --private-key=~/.ssh/id_rsa --limit ip15.localdomain
  • 全てのpythonライブラリをインストールするためのplaybookファイル
  • 特定のノードに入れたい場合は --limit でホストを直接指定します。

install_one_library.yml

---
- hosts: batch-servers
  become: yes
  vars_files:
    - vars/default.yml
    - roles/python/vars/library_version.yml
  tasks:
    - include: roles/python/tasks/init.yml
    - include: roles/python/tasks/install_{{ lib }}.yml
# ansible-playbook install_one_library.yml --extra-vars="lib=pip" -i staging.info --private-key=~/.ssh/id_rsa_nopass
  • 全てのノードに一つのライブラリをインストールするためのplaybookファイルです
  • 実行時に変数libを渡してインストール対象のたtaskを指定します。
  • 例えば --extra-vars="lib=pip" を指定したら roles/python/tasks/install_pip.yml のタスクを実行

roles/python/vars/library_version.yml

---
pip_version: 10.0.1
numpy_version: 1.14.2
cython_version: 0.28.2
  • インストールするライブラリのバージョンを管理します。

roles/python/files/libraryディレクト

  • インストール対象のライブラリを直接保存しておきます。
  • githubのreleaseからダウンロードしたものを置いておきます
  • git cloneしたものにtag指定でソースを取り出した場合たまに python setup.py --version があった

roles/python/files/install_shellディレクト

  • ライブラリインストール用のshellを配置します
  • shell内では library_version.yml で指定したバージョンがインストール済みかどうかを確認し、指定バージョンが入っていなければインストールします。

roles/python/tasksディレクト

  • ライブラリインストールのためのtaskファイルを管理します。 - main.yml 内では初期化用のファイル読み込みと全てのライブラリインストールのためのファイルをincludeします
  • init.yml は初期用のファイル

roles/python/tasks/main.yml

---

- include: roles/python/tasks/init.yml

- include: roles/python/tasks/install_cython.yml
- include: roles/python/tasks/install_numpy.yml
  • 初期化用と各ライブラリインストールのtaskを読み込んでいます。

roles/python/tasks/init.yml

---

- name: upload python library
  copy:
    src: ../files/library
    dest: "{{ ansible_home }}/python"
    mode: 0755

- name: upload python install shell
  copy:
    src: ../files/install_shell
    dest: "{{ ansible_home }}/python"
    mode: 0755

- name: be sure python-setuptools is installed
  yum: name=python-setuptools state=installed

- name: be sure python-devel is installed
  yum: name=python-devel state=installed


- include: roles/python/tasks/install_pip.yml
  • pythonライブラリインストールのために必要なモジュールのインストールを行います。
  • roles/python/files/library, roles/python/files/install_shellをアップロードします。
  • python-setuptoolsとpython-develは事前にインストールされている必要がある
  • pipも初期化処理でインストールします。

roles/python/tasks/install_pip.yml

---
- name: Extract pip
  command: tar zxvf "{{ ansible_home }}/python/library/pip-{{ pip_version }}.tar.gz" --directory "{{ ansible_home }}/python/library/"

- name: Install pip
  command: "{{ ansible_home }}/python/install_shell/install_pip.sh {{ ansible_home }}/python/library/pip-{{ pip_version }} pip"

 - pipのインストールを行います。

roles/python/files/install_shell/install_pip.sh

#!/bin/bash
LIBRARY_DIR=$1
LIBRARY=pip
IS_INSTALLED=0
cd $LIBRARY_DIR

set -e
trap 'setup_install' ERR


function setup_install {
  python setup.py install
}

SOURCE_VERSION=`python setup.py --version`

if test `python -m ${LIBRARY} -V | grep -oE "${SOURCE_VERSION}[^ ]*" | head -n1` == `echo $SOURCE_VERSION` ; then
  echo "already installed"
else
  echo "not installed"
  setup_install
fi
  • pipのインストール用shellです
  • インストール済みかどうかをチェックします

roles/python/tasks/install_numpy.yml

- include: roles/python/tasks/install_cython.yml

- name: Extract numpy
  command: tar zxvf "{{ ansible_home }}/python/library/numpy-{{ numpy_version }}.tar.gz" --directory "{{ ansible_home }}/python/library/"

- name: Install numpy
  command: "{{ ansible_home }}/python/install_shell/build_install.sh {{ ansible_home }}/python/library/numpy-{{ numpy_version }} numpy"
  • numpyのインストールを行います。
  • 依存するライブラリは includeでタスクを読み込むようにしています。

roles/python/files/install_shell/build_install.sh

#!/bin/bash
LIBRARY_DIR=$1
LIBRARY=$2
IS_INSTALLED=0
cd $LIBRARY_DIR

function find_installed_version {
    _LIBRARY=$1
    _VERSION_WITH_EQ=`pip freeze | grep $_LIBRARY | grep -oE "==.+"`
    echo ${_VERSION_WITH_EQ:2}
}

function setup_build_install {
  python setup.py build
  python setup.py install
}


INSTALLED_VERSION=`find_installed_version $LIBRARY`
SOURCE_VERSION=`python setup.py --version`

if test `echo $INSTALLED_VERSION` == `echo $SOURCE_VERSION` ; then
  echo "already installed"
else
  setup_build_install
fi
  • buildが必要な場合のinstall用shellです

Typescriptで作成したパーサコンビネータを使ってJSONをパースしてみる

以前作成したTypescriptでのパーサコンビネータを利用してJSONをパースしてみたいと思います。

まずJSONの構文定義を確認してみます。確認したところarrayはelementsを含み,elementsはvalueを含みvalueはarrayを含むといった再帰的な構造を扱える必要があることがわかりますので以前作成したor-parserを修正してパーザ初期化後に候補のパーザを足せるようにする事により、valueパーザを使用するarrayパーザを初期化→arrayパーザ自体をvalueパーザの候補に追加とする事で再帰的に処理できるようにします。以前のor-parserは以下のように修正します。

import {Parser} from './parser'
import {ParseResult, ParseSuccess, ParseFailer} from './parser-result'

export class OrParser implements Parser<[Array<any>]> {
    psArray: Array<Parser<any>> = new Array()
    constructor(psArray: Array<Parser<any>>) {
        this.psArray = psArray
    }
    addParser(parser: Parser<any>): void {
        this.psArray.push(parser)
    }

    parse(input: string) :ParseResult<any> {
        const results: Array<ParseResult<any>> = new Array()
        for(const i in this.psArray) {
            const psresult = this.psArray[i].parse(input)
            if(psresult instanceof ParseSuccess) {
                return new ParseSuccess<any>(this, psresult, psresult.value, psresult.next);    
            } else {
                results.push(psresult)
            }
        }
        return new ParseFailer<any>(this, results, null,input);
    }
}

addParserにより候補パーザを後から追加できるようにしています。

valueをパースできるようにする

JSONの構文定義からvalueの部分のパーザを作成します。

const dqParer = P.string('"')
const dqStop = P.stop(new Array('"'))
const wordParse = P.triplet(dqParer, dqStop, dqParer)

const intParse =  P.plus(P.or(arryToParsers(Array("1","2","3","4","5","6","7","8","9","0"))))
const floatingParse = P.seq(intParse, P.option(P.seq(P.string("."),intParse)))
const numParse = P.seq(P.option(P.string("-")), floatingParse)

const nullParse = P.string("null")
const trueParse = P.string("true")
const falseParse = P.string("false")

const valueParser = P.or([wordParse, numParse, nullParse, trueParse, falseParse])

objectとarrayは後から追加するようにします。

elementsをパースできるようにする

const spSeq = P.continues(P.string(" "))
const cmParse = P.string(",")
const elementsParser = P.seq(valueParser, P.continues(P.seq(P.triplet(spSeq, cmParse, spSeq), valueParser)))

elementsの定義は value , elements となっており自分自身を不空ようにしていますが, elementsの部分は, valueの連続と同じなのでこのように実装します。

arrayをパースできるようにする

const aoParser = P.triplet(spSeq, P.string("["), spSeq)
const acParser = P.triplet(spSeq, P.string("]"), spSeq)
const arrayParser = P.triplet(aoParser, P.option(elementsParser), acParser)
valueParser.addParser(arrayParser)

arrayのパースを作成した後,valueParserに追加するようにしています。

membersをパースできるようにする

const coParer = P.string(":")
const pairParser = P.seq(P.seq(P.triplet(spSeq, wordParse, spSeq), coParer), P.triplet(spSeq, valueParser, spSeq))
const membersParser = P.seq(pairParser, P.continues(P.seq(P.triplet(spSeq, cmParse, spSeq), pairParser)))

objectをパースできるようにする

const objParser = P.triplet(P.triplet(spSeq, P.string("{"), spSeq), P.option(membersParser), P.triplet(spSeq, P.string("}"), spSeq))
valueParser.addParser(objParser)

objectのパースを作成した後,valueParserに追加するようにしています。

jsonをパースできるようにする

const jsonParser = P.seq(valueParser, P.seq(spSeq, P.end()))

valueParserでパース後に残りの文字が無くなっていたらパース完了としています。

それでは動きを見てみます。

const str ='[1,2,{"1":[1,2,"a"]},"3", [ -5, [{"a": "b"}] ] ]'
const result  = jsonParser.parse(str.replace("\r","").replace("\n",""))
console.info(result)

ParseSuccess {parser: SeqParser, childResult: Array(2), value: Array(2), next: ""}

成功しているようです。あとは結果の値を扱いやすい形に変換したら良さそう。

Typescriptでパーサコンビネータを書いてみる

Typescriptは静的に型付があるので安全ではあるのですが、JavascriptではJSON.parseなどの結果を動的にたどることができたのに対してTypescriptでは静的型にして返す必要があり、Javascriptを書くときと比べて煩わしさがあったりします。Typescriptで自由な形のJsonを扱えるようになりたいと思っていましたので、いっその事自分でパーサコンビネータを書いてそれからJsonのパーサを生成できるようにしたいと思います。 パーサコンビネータの実装について調べたところ@kmizu氏のJavaによるパーサコンビネータ実装の記事が参考になったので、まずはこれをTypescriptで書き直したいと思います。

パーサコンビネータとは

パーサコンビネータは、単純な部品(パーサ)の組み合わせ(コンビネーション)で構文の解析を行うもののようです。例えば特定の文字が含まれている、連続しているなどの判定を行う単純な部品を作成しておいてそれを組み合わせればJsonxmlの解析が行えるようになるといったものです。似たものにパーサジェネレータがありますが、パーザジェネレータはホスト側のプログラミング言語の別の構文で書かれた定義ファイルを読み取り、そこからホスト側のプログラミング言語で書かれたパーザを自動生成するものでSQLが馴染み深いかと思います。

文字判定のパーザを実装する

まず、パーサのインターフェースを実装します。

export interface Parser<T> {
    parse(input: string ):ParseResult<T>;
}

このインターフェースを実装した文字判定のパーザを実装するのですが、次はパーザの結果が成功か失敗かを判定するクラスを作成します。

export interface ParseResult<T> {}

export class ParseSuccess<T> implements ParseResult<T> {
    value: T;
    next: string;

    constructor(value: T, next: string){
        this.value = value
        this.next = next
    }

    public toString(): string{
        return "Success(" + this.value + ", " + this.next + ")";
    }
}

export class ParseFailer<T> implements ParseResult<T> {
    message: string;
    next: string;

    constructor(message: string, next: string){
        this.message = message
        this.next = next
    }

    public toString(): string{
        return "Failer(" + this.message + ", " + this.next + ")";
    }
}

それから文字列判定のパーザを実装します。

export class StringParser implements Parser<string> {
    literal: string;
    public constructor(literal: string) {
        this.literal = literal;
    }
  
    getLiteral():String{
      return this.literal;
    }

    parse(input: string):ParseResult<String> {
        if(input.startsWith(this.literal)){
            return new ParseSuccess(this.literal, input.substring(this.literal.length))
        }
        return new ParseFailer("expect: " + this.literal, input);
    }
}

パーザ生成用関数は一つのファイルでまとめておきます。

export function string(literal:string) {
    return new StringParser(literal)
}

こうする事で ParserGen.string("Hello, ") のようにする事でパーザの生成が行えます。ParserGenの部分は import * as ParserGen from '../parser/parser-genrator' のようにimportするときに指定します。 実際に動かしてみると以下のように文字列の判定が行えます。

    const str1 = "Hello, world!"
    
    const helloParser = ParserGen.string("Hello, ")
    const worldParser = ParserGen.string("world")
    console.info(helloParser.parse(str1))
    console.info(worldParser.parse(str1))

ParseSuccess {value: "Hello, ", next: "world!"} ParseFailer {message: "expect: world", next: "Hello, world!"} 正常に動いています。

0回以上の文字の連続を判定する

文字の連続は以下のように判定できます。

export class ContinueParser implements Parser<any> {
    parser: Parser<any>

    constructor(parser: Parser<any>) {
        this.parser = parser
    }

  
    parse(input: string) :ParseResult<Array<any>> {
        var next: string  = input
        const values: Array<any> = new Array()
        while(true) {
            const previous = next;
            const result = this.parser.parse(next);
            if(result instanceof ParseSuccess) {
                values.push(result.value);
                next = result.next
            } else {
                return new ParseSuccess(values, previous);
            }
        }
    }
}

1回以上の文字連続を判定する場合はparse関数の実装を少し修正すれば行えるかと思います。 それから、パーザ生成の関数を書きます。

export function many( parser: Parser<any>) {
    return new ContinueParser(parser)
}

動作確認は以下のようになります。

    const str2 = "Hello, Hello, world!"
    const helloParser = ParserGen.string("Hello, ")
    const helloContinuesParser = ParserGen.continues(helloParser)

    console.info(helloContinuesParser.parse(str2))

ParseSuccess {value: Array(2), next: "world!"} パースに成功しています。

2つのパーザの連続を扱えるようにする

2つのパーザの連続を扱える場合はいかにようになります。

export class SeqParser implements Parser<[any, any]> {
    lhs: Parser<any>
    rhs: Parser<any>

    constructor(lhs: Parser<any>, rhs: Parser<any>) {
        this.lhs = lhs
        this.rhs = rhs
    }

  
    parse(input: string) :ParseResult<any> {
        const lresult:ParseResult<any>  = this.lhs.parse(input);
        if(lresult instanceof ParseSuccess) {
            const value1:ParseResult<any> = lresult.value
            var next = lresult.next
            const rresult:ParseResult<any>  = this.rhs.parse(next);
            if(rresult instanceof ParseSuccess) {
                const value2:ParseResult<any> = rresult.value
                return new ParseSuccess<[any, any]>([value1, value2],
                    rresult.next
                );
            } else if(rresult instanceof ParseFailer) {
                return new ParseFailer<any>(next + " is not match", rresult.message);
            } else {
                return new ParseFailer<any>(next + " is not match", input);
            }
        } else {
            return new ParseFailer<any>(next + " is not match", input);
        }
    }
}

やっていることは単純で左側のパーザが成功したら右側のパーザも実行するというものになります。 パーザ生成の関数も用意します。

export function seq( lps: Parser<any>, rps: Parser<any>) {
    return new SeqParser(lps, rps)
}

動きを確認してみます。

    const str2 = "Hello, Hello, world!"
    const helloParser = ParserGen.string("Hello, ")
    const worldParser = ParserGen.string("world")
    const manyHelloWorld = ParserGen.seq(ParserGen.continues(helloParser), worldParser)
    
    console.info(manyHelloWorld.parse(str2))

ParseSuccess {value: Array(2), next: "!"}

2つのパーザの連続をさらに組み合わせることはできますが、パーザの生成が大変になるかもしれないのでその場合は3つのパーザの連続を実装する方が良いかもしれないです。

    const str = "Hello(world)"
    const manySeqParser = ParserGen.seq(ParserGen.seq(ParserGen.seq(ParserGen.string("Hello"), ParserGen.string("(")), ParserGen.string("world")), ParserGen.string(")"))
    console.info(manySeqParser.parse(str))

Success(Hello,(,world,), )

3つのパーザの連続を扱えるようにする

2つのパーザの連続を組み合わせだけだと配列などの判定が面倒になると思いますので3つのパーザを組み合わせれるようにしてみます。

import {Parser} from './parser'
import {ParseResult, ParseSuccess, ParseFailer} from './parser-result'

export class TripleParser implements Parser<[any, any, any]> {
    fhs: Parser<any>
    shs: Parser<any>
    ths: Parser<any>

    constructor(fhs: Parser<any>, shs: Parser<any>, ths: Parser<any>) {
        this.fhs = fhs
        this.shs = shs
        this.ths = ths
    }
  
    parse(input: string) :ParseResult<any> {
        const fresult:ParseResult<any>  = this.fhs.parse(input);
        if(fresult instanceof ParseSuccess) {
            const value1:ParseResult<any> = fresult.value
            var next = fresult.next
            const sresult:ParseResult<any>  = this.shs.parse(next);
            if(sresult instanceof ParseSuccess) {
                const value2:ParseResult<any> = sresult.value
                next = sresult.next
                const tresult:ParseResult<any>  = this.ths.parse(next);
                if(tresult instanceof ParseSuccess) {
                    const value3:ParseResult<any> = tresult.value
                    next = tresult.next
                    return new ParseSuccess<[any, any, any]>([value1, value2, value3],tresult.next);
                } else {
                    return new ParseFailer<any>(next + " is not match", input);
                }
            } else {
                return new ParseFailer<any>(next + " is not match", input);
            }
        } else {
            return new ParseFailer<any>(next + " is not match", input);
        }
    }
}

それからパーザ生成の関数を実装します。

export function bracket( fps: Parser<any>, sps: Parser<any>, tps: Parser<any>) {
    return new TripleParser(fps, sps, tps)
}

これでHello(world)のようなカッコで囲むものは以下のように判定できます。

    const str = "Hello(world)"
    const manySeqParser = ParserGen.seq(ParserGen.string("Hello"), ParserGen.bracket(ParserGen.string("("), ParserGen.string("world"), ParserGen.string(")")))
    console.info(manySeqParser.parse(str))

ParseSuccess {value: Array(2), next: ""} 実際に触ってみると、カッコなどで囲む前提の構文の場合は3つのパーざを組み合わせれるようにしたほうが扱いやすくなることが分かります。

Optionのパーザを実装する

Optionは以下のように実装します。

import {Parser} from './parser'
import {ParseResult, ParseSuccess, ParseFailer} from './parser-result'

export class OptionParser implements Parser<[any]> {
    ps: Parser<any>

    constructor(ps: Parser<any>) {
        this.ps = ps
    }

    parse(input: string) :ParseResult<any> {
        const psresult:ParseResult<any>  = this.ps.parse(input);
        if(psresult instanceof ParseSuccess) {
            const value1:ParseResult<any> = psresult.value
            return new ParseSuccess<any>(value1,psresult.next);    
        } else {
            return new ParseSuccess<any>(null,input);
        }
    }
}

それからパーザの生成関数を追加します。

export function option(parser: Parser<any>) {
    return new OptionParser(parser)
}

これで動作が確認できます。

    const str = "Hello!  world"
    const fParser = ParserGen.seq(ParserGen.string("Hello"), ParserGen.option(ParserGen.string("!")))
    const sParser = ParserGen.continues(ParserGen.string(" "))
    const tParser = ParserGen.seq(ParserGen.string("world"), ParserGen.option(ParserGen.string("!")))
    console.info(ParserGen.seq(ParserGen.seq(fParser, sParser), tParser).parse(str))

ParseSuccess {value: Array(2), next: ""}

複数のパーザから一致するのもで判定する

Orパーザを実装します。配列の中から最初にパースに成功したものを結果として返すようにします。

import {Parser} from './parser'
import {ParseResult, ParseSuccess, ParseFailer} from './parser-result'

export class OrParser implements Parser<[Array<any>]> {
    psArray: Array<Parser<any>>
    constructor(psArray: Array<Parser<any>>) {
        this.psArray = psArray
    }

    parse(input: string) :ParseResult<any> {
        for(const i in this.psArray) {
            const psresult = this.psArray[i].parse(input)
            if(psresult instanceof ParseSuccess) {
                return new ParseSuccess<any>(psresult.value, psresult.next);    
            }
        }
        return new ParseFailer<any>(null,input);
    }
}

それからパーザの生成関数を追加します。

export function or(psArray: Array<Parser<any>>) {
    return new OrParser(psArray)
}

これで動作が確認できます。

    const str = "{hello}"
    const parser1 = ParserGen.bracket(ParserGen.string("["), ParserGen.string("hello"), ParserGen.string("]"))
    const parser2 = ParserGen.bracket(ParserGen.string("{"), ParserGen.string("hello"), ParserGen.string("}"))
    const ps = ParserGen.or(new Array(parser1, parser2))
    console.info(ps.parse(str))

ParseSuccess {value: Array(3), next: ""}

全ての文字列を解析できたか判定できるようにする

現在は前方が一致していたらパース成功と判定しているので全ての文字列を解析し切れているかどうかを判定できるようにします。

export class EndParser implements Parser<any> {
    public constructor() {}
    parse(input: string):ParseResult<String> {
        if(input == ""){
            return new ParseSuccess(null, input);
        }
        return new ParseFailer("expected: end, actual: " + input, input);
    }
}
export function end() {
    return new EndParser()
}

それで動作を確認します。

    const str = "hello, world!"
    const parser1 = ParserGen.string("hello")
    const parser2 = ParserGen.seq(parser1, ParserGen.end())
    console.info(parser1.parse(str))
    console.info(parser2.parse(str))

ParseSuccess {value: "hello", next: ", world!"}
ParseFailer {message: ", world! is not match", next: "expected: end, actual: , world!"}

特定の文字に一致しないものを抽出する

要素の値やkeyなど任意の値をとるものについてはストップワードが見つかるまで取り出せるようにしたいと思います。

// 特定の文字列以外を読み込む
export class StopWordParser implements Parser<Array<string>> {
    stopWords: Array<string>;
    public constructor(stopWords: Array<string>) {
        this.stopWords = stopWords;
    }

    words(input:string):string {
        const indexs = this.stopWords
          .map(s=>{return Number(input.indexOf(s))})
          .filter(i=>{return i >= 0})
          .sort((a:number, b:number) => {return a - b})
        
        switch (indexs.length) {
          case 0:
           return input
          default:
            return input.substring(0, indexs[0])
        }
    }


    parse(input: string):ParseResult<String> {
        const muchString = this.words(input)
        if (muchString.length > 0 ){
            return new ParseSuccess(muchString, input.substring(muchString.length))
        } else {
            return new ParseFailer<any>(" much stop word: " + this.stopWords.toString(), input);    
        }
    }
}

パーサを生成できるようにします。

export function stop(stopWords: Array<string>) {
    return new StopWordParser(stopWords)
}

これで以下のように文字列の解析が行えるようになります。

    const str = "hello: ['world', '!', '!']"

    const sqParer = P.string("'")
    const sqStop = P.stop(new Array("'"))
    const coParer = P.string(":")
    const coStop = P.stop(new Array(":"))
    const spSeq = P.continues(P.string(" "))
    const cmParer = P.string(",")

    const wordParse = P.bracket(sqParer, sqStop, sqParer)

    const arrayParse = P.bracket(
      P.string("[")
      , P.option(P.seq(
          wordParse, 
          P.continues(P.seq(
            P.bracket(spSeq, cmParer, spSeq), 
            wordParse))))
      ,P.string("]")
    )
    const attributePaser = P.seq(P.seq(coStop, coParer), P.seq(spSeq, arrayParse))
    console.info(attributePaser.parse(str))

ParseSuccess {value: Array(2), next: ""}

どうにかパースできているようです。とりあえずいま実装した機能で頑張ればJSONのパースはできそうな気がします。

Typescriptでの@typesメンテ不備の対応

ReactでTypescript開発をした際にハマったのでめも。
react-data-gridが便利そうだったので@types/react-data-gridも合わせてnpm innstall --saveでインストールしてみたのだがreact-data-gridで定義されている関数がTypescriptで利用することができなかった。
@typesで定義をみてみたが確かに関数の定義が反映されていないのが原因の様であった。

@typesの方ではいつ対応されるかわからないので、とりあえず以下の対応で使える様にしました。 - @types/react-data-gridの定義はバックアップを取っておく - node_modulesにある@typesの定義をアンインストール

npm uninstall --save @types

  • tsconfig.jsonのcompilerOptionsにpathとbaseUrlを追加する。pathに静的型づけの定義ファイルの配置先を設定し、baseUrlはとりあえず空にして以下の様に設定しました。
{
   "compilerOptions": {
       "outDir": "./dist/",
       "sourceMap": true,
       "noImplicitAny": true,
       "module": "commonjs",
       "target": "es5",
       "jsx": "react",
       "baseUrl": ".",
       "paths": { // 自作jsライブラリ使用のためのTypescript定義ファイル置き場所
           "*": ["./ts-def/*"]
        }

   },
   "include": [
       "./src/**/*"
   ]
}
  • pathsに設定したディレクトリに@types/react-data-gridのバックアップをコピーし、利用したい関数の定義を追加する。

Javaパフォーマンス(オライリー)を読んでみた

JVMの管理についてガベージコレクションやヒープメモリの調査方法などプログラミングをするだけではあまり意識しなかったことについて知れたのはよかったと思います。

2章パフォーマンステストのアプローチ

Javaアプリケーションの正確なパフォーマンス測定方法としてのマイクロベンチマークについて載っています。テストコードを書くときに気にしなければいけないこと(評価結果を利用しなければ処理されないかもとか)それから、JavaJITコンパイラを使っており事前にコンパイルで生成した中間コードをプログラムの実行時に機械語に変換するという様になっており正確な処理時間を測定するにはウォームアップが必要になるとかを知ることができました。それから性能が改善されたがどうかの調査としてt検定を使う方法について説明がありました。

4章JITコンパイルの仕組み

JITコンパイルの仕組みについて、Javaバイトコードを吐き出すことで様々な環境で動かせる様になり、実行時はまとめて機械語に変換するのではなく評価が必要になったタイミングでコンパイル(Javaバイトコード機械語)するといった基本的なことについての説明があります。それからチューニングの観点で階層的コンパイルというものを知れたので試してみたいかと思いました。

5章ガベージコレクションの基礎

Javaのパフォーマンスに大きく影響を与えるガベージコレクションの基本的な内容、young領域やold領域(残り続ける)の扱いやガベージコレクタのアルゴリズムとしてCMSガベージコレクターとG1ガベージコレクターに触れています。基本的には標準的なガベージコレクタのアルゴリズムを使うと思うのですが、コードをいじらないでパフォーマンス改善が必要になった場合はjstatコマンドなどを活用しfull gcの発生を確認しつつjvmのオプションいじっての動作確認が必要な場合もあるかも知れないと思いました。

sshjを試してみた

javaで作られているsshクライアントであるsshjを試してみました
https://github.com/hierynomus/sshj/blob/master/examples/src/main/java/net/schmizz/sshj/examples/LocalPF.java

単純にssh経由でコマンドを実行するだけでなくsshプロトコルを利用しているscpやsftpも利用できて、シンプルなターミナルも作れるので作業の自動化に役に立ちそうな気がします。 サンプルも簡単に動かすことができました。
https://github.com/hierynomus/sshj/tree/master/examples/src/main/java/net/schmizz/sshj/examples

ssh接続設定

  private SSHClient sshConnectSetting(String host, String username, String password) throws IOException {
    SSHClient ssh = new SSHClient();
    ssh.loadKnownHosts();
    ssh.connect(host);
    // keepAliveの感覚設定
    ssh.getConnection().getKeepAlive().setKeepAliveInterval(5);
    // 接続時のsshキーの指定はauthPublickeyを認証時のパスワードにはauthPasswordを使う
    // ssh.authPublickey(keyPath);
    ssh.authPassword(username,password);
    return ssh;
  }

ssh接続キーの指定にはSSHClientのauthPublickeyメソッッドを使います。認証時のパスワードが必要な場合はauthPasswordを呼び出します。

コマンド実行

public void sshjSample() throws IOException {
  final SSHClient ssh = sshConnectSetting("***", "***", "***");
  try {
    // コマンド実行
    sessionCommand(ssh.startSession(), "echo 'hello sshj' >> hello_sshj.txt", 5);
  } finally {
    ssh.disconnect();
  }
}
private void sessionCommand(Session session, String command, int timeout) throws IOException {
  try {
    final Session.Command cmd = session.exec(command);
    System.out.println(IOUtils.readFully(cmd.getInputStream()).toString());
    cmd.join(timeout, TimeUnit.SECONDS);
    System.out.println("\n** exit status: " + cmd.getExitStatus());
  } finally {
    session.close();
  }
}

scp

private void scpDownloadSample(SSHClient ssh, String downloadFilePath, String saveFileDir) throws IOException{
  ssh.newSCPFileTransfer().download(downloadFilePath, new FileSystemFile(saveFileDir));
}
private void scpUpload(SSHClient ssh, FileSystemFile uploadFile, String uploadPath) throws IOException{
  ssh.newSCPFileTransfer().upload(uploadFile, uploadPath);
}

sftp

private void sftpUploadSample(SSHClient ssh, FileSystemFile uploadFile, String uploadDir) throws IOException{
  final SFTPClient sftp = ssh.newSFTPClient();
  try {
    sftp.put(uploadFile, uploadDir);
  } finally {
    sftp.close();
  }
}

private void sftpUploadSample(SSHClient ssh, String downloadFilePath, String saveFileDir) throws IOException{
  final SFTPClient sftp = ssh.newSFTPClient();
  try {
    sftp.get(downloadFilePath, new FileSystemFile(saveFileDir));
  } finally {
    sftp.close();
  }
}

ローカルポートフォワード

private void localPortFowardSample(SSHClient ssh, String localHost, int localPort, String remoteHost, int remotePort) throws IOException{
  final LocalPortForwarder.Parameters params
          = new LocalPortForwarder.Parameters(localHost, localPort, remoteHost, remotePort);
  final ServerSocket ss = new ServerSocket();
  ss.setReuseAddress(true);
  ss.bind(new InetSocketAddress(params.getLocalHost(), params.getLocalPort()));
  try {
    ssh.newLocalPortForwarder(params, ss).listen();
  } finally {
    ss.close();
  }
}

リモートポートフォワード

private void remotePortFowardSample(SSHClient ssh, String remoteHost, int remotePort) throws IOException{
    try {
      ssh.getRemotePortForwarder().bind(
              new RemotePortForwarder.Forward(remotePort),
              new SocketForwardingConnectListener(new InetSocketAddress(remoteHost, remotePort)));

      ssh.getTransport().setHeartbeatInterval(30);

      // Something to hang on to so that the forwarding stays
      ssh.getTransport().join();
    }catch (IOException e){
      e.printStackTrace();
    }
  }

ターミナル

private void samplePTY(String host, String username, String password) throws IOException{
    final SSHClient ssh = new SSHClient();

    final File khFile = new File(OpenSSHKnownHosts.detectSSHDir(), "known_hosts");
    ssh.addHostKeyVerifier(new ConsoleKnownHostsVerifier(khFile, System.console()));

    ssh.connect(host);
    ssh.authPassword(username,password);
    try {

      // ssh.authPublickey(System.getProperty("user.name"));
      final Session session = ssh.startSession();
      try {

        session.allocateDefaultPTY();

        final Session.Shell shell = session.startShell();

        new StreamCopier(shell.getInputStream(), System.out, LoggerFactory.DEFAULT)
                .bufSize(shell.getLocalMaxPacketSize())
                .spawn("stdout");

        new StreamCopier(shell.getErrorStream(), System.err, LoggerFactory.DEFAULT)
                .bufSize(shell.getLocalMaxPacketSize())
                .spawn("stderr");

        // Now make System.in act as stdin. To exit, hit Ctrl+D (since that results in an EOF on System.in)
        // This is kinda messy because java only allows console input after you hit return
        // But this is just an example... a GUI app could implement a proper PTY
        new StreamCopier(System.in, shell.getOutputStream(), LoggerFactory.DEFAULT)
                .bufSize(shell.getRemoteMaxPacketSize())
                .copy();

      } finally {
        session.close();
      }

    } finally {
      ssh.disconnect();
    }
  }

Haskellで100マス計算を解いてみた

Haskellでの100マス計算を以下のように実装しました。

module Try.Hyakumasu where

data MathData = MathData {col :: [Int], row :: [Int]} deriving (Show, Eq)

hyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]
hyakumasu f x = [f c <$> (col x) | c <- (row x)]

printMath :: (Int -> Int -> Int) -> MathData -> IO ()
printMath f a = do
    mapM_ listPrint $ [[0] ++ col a] ++ zipWith (\a b -> [a] ++ b) (row a) (hyakumasu  f a )

listPrint :: [Int] -> IO ()
listPrint a = print $  concat $ map (\x ->  concat [" " | c <- [1..(4 - length (show x))]] ++ show x) a

各処理について

まず行と列の情報を持ったデータ型を以下のように定義しています。
data MathData = MathData {col :: [Int], row :: [Int]} deriving (Show, Eq)
例えば行が[1,2,3,4]で列が[5,6,7]のデータに対して計算をするのであれば以下のようにインスタンスを生成します。
MathData [1..4] [5..7]

MathDataのデータ型と(Int -> Int -> Int)型の関数を受け取ってマスごとの計算を行なっているのが以下の処理になります。

hyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]
hyakumasu f x = [f c <$> (col x) | c <- (row x)]

関数の定義はhyakumasu :: (Int -> Int -> Int) -> MathData -> [[Int]]の部分で計算内容はhyakumasu f x = [f c <$> (col x) | c <- (row x)]になります。 for内包表記を使って各列の要素を順番に取り出し、<$> を使い行の配列の各要素に対して引数で渡した(Int -> Int -> Int)型の関数を適用しています。つまり、以下のようになっています。

Prelude> (+) 1 <$> [1..10]
[2,3,4,5,6,7,8,9,10,11]

計算結果の各行を表示するのには以下の関数を使っています。

listPrint :: [Int] -> IO ()
listPrint a = print $  concat $ map (\x ->  concat [" " | c <- [1..(4 - length (show x))]] ++ show x) a

(\x -> concat [" " | c <- [1..(4 - length (show x))]] ++ show x)の部分配列内の要素を4文字詰めで表示するための文字列に変換しています。map関数により各要素に適用した後concatでmap適用後の配列を連結し一つの文字列にしています。それからprintで画面への出力を行なっています。
画面への標準出力のため関数の定義はlistPrint :: [Int] -> IO ()で出力はIO ()で画面への標準出力を返すことを示しています。

計算結果全体の出力は以下のようになっています。

printMath :: (Int -> Int -> Int) -> MathData -> IO ()
printMath f a = do
    mapM_ listPrint $ [[0] ++ col a] ++ zipWith (\a b -> [a] ++ b) (row a) (hyakumasu  f a )

関数の定義はprintMath :: (Int -> Int -> Int) -> MathData -> IO ()となっていて引数として計算結果を渡すのではなく、計算に使う関数と行と列のデータMathDataをそのまま渡していてこれはMathDataの行と列が表示に必要のためですが計算結果がわかりづらいのでhyakumasuの結果とMathDataの行と列のデータを合わせたものを返す関数を別途用意した方が見通しはよくなりそうです。 zipWith (\a b -> [a] ++ b) (row a) (hyakumasu f a )については各行の計算結果の先頭に列の要素を連結しています。動きをみてみると以下のようになります。

Prelude> zipWith (\a b -> [a] ++ b) [1,2,3] [[1,2,3],[2,3,4],[5,6,7]]
[[1,1,2,3],[2,2,3,4],[3,5,6,7]]

ちなみにzipWithをつけない場合は外側の配列にそのまま適用されますが、これは欲しい結果ではありません。

Prelude> (\a b -> [a] ++ b) [1,2,3] [[1,2,3],[2,3,4],[5,6,7]]
[[1,2,3],[1,2,3],[2,3,4],[5,6,7]]

それから[[0] ++ col a] の部分はMathDataの行のデータに対して先頭に0を追加したものを返しています。mapM_ listPrint の部分についてlistPrintはIO ()を返しており、mapMはIOモナドに対するmap関数であるmapMを使用しています。 最終的な実行結果は以下のようになります。

main :: IO ()
main = do
    printMath (+) $ MathData [1..10] [1..10]
    printMath (*) $ MathData [1..10] [1..10]

"   0   1   2   3   4   5   6   7   8   9  10"
"   1   2   3   4   5   6   7   8   9  10  11"
"   2   3   4   5   6   7   8   9  10  11  12"
"   3   4   5   6   7   8   9  10  11  12  13"
"   4   5   6   7   8   9  10  11  12  13  14"
"   5   6   7   8   9  10  11  12  13  14  15"
"   6   7   8   9  10  11  12  13  14  15  16"
"   7   8   9  10  11  12  13  14  15  16  17"
"   8   9  10  11  12  13  14  15  16  17  18"
"   9  10  11  12  13  14  15  16  17  18  19"
"  10  11  12  13  14  15  16  17  18  19  20"
"   0   1   2   3   4   5   6   7   8   9  10"
"   1   1   2   3   4   5   6   7   8   9  10"
"   2   2   4   6   8  10  12  14  16  18  20"
"   3   3   6   9  12  15  18  21  24  27  30"
"   4   4   8  12  16  20  24  28  32  36  40"
"   5   5  10  15  20  25  30  35  40  45  50"
"   6   6  12  18  24  30  36  42  48  54  60"
"   7   7  14  21  28  35  42  49  56  63  70"
"   8   8  16  24  32  40  48  56  64  72  80"
"   9   9  18  27  36  45  54  63  72  81  90"
"  10  10  20  30  40  50  60  70  80  90 100"