次ログ

次ログ

ゆるりと働いているSREの技術ブログのような何か。趣味の話も書く

Go言語でクロスコンパイルした実行ファイルをGitHubにリリースしてみた

前書き

せっかく作ったツールなので、いろんな環境用に実行ファイルを生成して、 GitHubからダウンロードできるようにしたいと考える人は多いと思います。

幸いにもGo言語は標準でクロスコンパイルをサポートしてくれています。 ちょろっとシェルスクリプトを書くだけで、一発で異なる環境用の実行ファイルを作成で きます。 問題は、それを公開する方法です。

僕は今までDropboxで配布していましたが、 なんというか、すごく古臭いやりかたなイメージ。 それとGitHubのリリース機能を今まで使ったことがなかったので、 これを機会に少しだけリリース機能を使ってみよう、という趣旨です。

今回はGo言語で書かれたソースからリリースするためのツールの GoReleaseというサードパーティ製のツールを利用して、リ リースしてみます。

今回の成果物

作成したリポジトリ GitHub - jiro4989/md4pt: マークダウンのテキストをプレーンテキストで表示したときに読みやすく整形

リリースした実行ファイル Releases · jiro4989/md4pt · GitHub

環境

項目 内容
OS Ubuntu Gnome 17.04
Go言語 1.9
プロセッサ Intel(R) Core(TM) i7-3667U CPU @ 2.00GHz
メモリ 8GB RAM

僕の今までのやり方

今回Go言語で書いたツールを配布するまでは、 Javaでツールを書いてDropboxにアップしていました。

今回はビルドまでの手順はやりましたが、 実際にはGo言語の手動リリースはやっていません。

今回リリースを初めて行ったのですが、 JARのときと同じ手順でやっていたこうなっていただろう、という想定で書いています。

お手製ビルド

まずベタな方法としては、下記のようなシェルスクリプトを書いて複数の実行ファイルを 生成することだと思います。 最初、僕は下記の方法でやろうとしていました。

#!/bin/bash
# -*- coding: utf-8 -*-

set -eux

mainsrc=./md4pt.go
dirname=./dist

if [ -e $dirname ]; then
  : $dirname is exist
else
  mkdir $dirname
fi

GOOS=linux GOARCH=amd64 go build -o $dirname/md4pt_linux $mainsrc
GOOS=darwin GOARCH=amd64 go build -o $dirname/md4pt_mac $mainsrc
GOOS=windows GOARCH=amd64 go build -o $dirname/md4pt_win.exe $mainsrc

ls $dirname
: build completed

Dropboxにリリース

手動ビルドした後は、実行ファイルをzipかtar.gzで圧縮して公開します。 今回のようなクロスコンパイルの場合でしたら、下記のようなシェルスクリプトになると 思います。

前提として、環境にDropboxディレクトリを同期して置く必要が有ります。 すると、Dropboxディレクトリに配置したものが、Web上のDropboxにも適用されるように なります。

#!/bin/bash
# -*- coding: utf-8 -*-

set -eux

bins=`ls -d dist/*`
for b in $bins; do
  gzfile="${b}.tar.gz"
  tar czf $gzfile $b README.md
  mv $gzfile ~/Dropbox/tools/
done

全体の自動化

手動ビルドの節で使用したスクリプトをbuild.sh 手動リリースの節で使用したスクリプトをrelease.sh とすると、ソースの編集がおわって一気にビルドしてリリースするなら下記のようなスク リプトを実行します。

#!/bin/bash
# -*- coding: utf-8 -*-

set -eux

./build.sh
./release.sh

一応、リリース作業自体は自動化できました。 でもバージョン管理などの観点からあまり良くないと思います。

よって、GitHubにリリースする方法に移行します。

GoReleaseを使ってGitHubにリリース

Gitにリポジトリを作成

中身はすっからかんでいいのでリポジトリを作成。

今回はmd4pt というリポジトリを作成して、実際にリリースしました。

GoReleaseのインストール

go get github.com/goreleaser/goreleaser

GoReleaseの設定ファイルの作成(.goreleaser.yml)

Goのリポジトリのルートディレクトリに.goreleaser.ymlを作成します。 内容は下記。

builds:
  - binary: md4pt
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - 386
archive:
  format: tar.gz
  replacements:
    amd64: 64-bit
    darwin: mac
    linux: linux
  name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
  files:
    - README.md

リリース用のスクリプトの作成

.gitignoreに下記の内容を記述

token.txt
/dist/

名前は何でも良いですが、release.shという名前で 下記の内容のシェルスクリプトを作成。

#!/bin/bash
# -*- coding: utf-8 -*-

set -eux
: $1

git tag $1
GITHUB_TOKEN=`cat token.txt` goreleaser --rm-dist

token.txtというのは、 GitHubのSettings -> Personal access tokensの項目で作成するキーのみを書いたテキ ストファイルです。

https://github.com/settings/tokens

tokenは他の人に知られてはいけないので、シェルスクリプト本体に含めないようにする 必要がありました。

しかし、毎回コマンドラインから入力するのも面倒だし、履歴からたどるのも面倒だったので、 .gitignoreにtoken.txtを書いておいて、catして埋め込むようにしました。

git push

gitに最新のデータをpushします。

git remote add origin $URL
git add .
git commit -m "init commit"
git push origin master

リリースの実行

コマンドラインから以下のコマンドを実行します。
./release.sh v0.0.1

これで、GitHubの指定のリポジトリのリリースにv0.0.1タグでリリースされています。

まとめ

Go言語のクロスコンパイルとリリース作業の自動化を行いました。

Go言語は、Pythonでのpip, Rubyでのgemと同様の、サードパーティ製のライブラリやツー ルを容易に導入できる機能を標準でサポートしています。

このことにより、容易に成果物をWebに公開できました。 今後はこの方法でリリース作業を行っていこうと思います。

Go言語 学習メモ03 - 総当り組み合わせ二次元配列の作成

前書き

どうもです、次郎です。

有給とってGo言語の学習してました。

以前Pythonの記事で紹介したツールの移植がほぼ完了して、今はドキュメントの整備と設 定ファイル編集ツールの作成をしています。

実際ツールを作成するとなったら、いろんな仕様を駆使しないといけないので、 ツール作成を学習に利用するという試みは結構成功だったなぁと。 ある程度使えるようになったらEffective GOとかも買わないとか考えてます。

ということで、今回はそんなツールの中で実装した、総当り組み合わせの実装につ いてメモします。

注意
二次元配列、と書いてますけれど実際はスライスです。
二次元スライスという読みが奇妙なので、ここでは二次元配列と呼びます。

目次

目的

ゲーム用のキャライラストを作成するときに、イラストの差分を用意することがあると思います。

差分の用意は人によってまちまちで、いろんなタイプがあると思います。 表情のみ変える方もいれば、ポーズごと変える方もいると思います。

主な差分は目、眉、口とかでしょうか。
あるいは、それらを組み合わせた顔をまるごと置き換える感じでしょうか。

たとえば以下のような感じの二次元配列です。

[
   ["eye1", "mosue1", "eyebrows1"]
  ,["eye1", "mosue1", "eyebrows2"]
  ,["eye1", "mosue1", "eyebrows3"]
  ,["eye1", "mosue2", "eyebrows1"]
  ,["eye1", "mosue2", "eyebrows2"]
  ,["eye1", "mosue2", "eyebrows3"]
  ,["eye1", "mosue3", "eyebrows1"]
  ,["eye1", "mosue3", "eyebrows2"]
  ,["eye1", "mosue3", "eyebrows3"]
]

これらのうち、あとから口の差分が増えたり、オプションとしてメガネとか猫耳とか尻尾とか
組み合わせの要素が増えたときに、差分画像が一気に増えます。

また、差分の修正が発生したときに組み合わせ直す作業が面倒です。
配布するためにZip圧縮し直す必要もあります。

僕の場合は表情のみ変えるタイプの人なのですが、その表情の差分用意が非常にめんどくさかったので、 それの自動化を行う上で、総当り組み合わせを適用した二次元配列を作成する必要がありました。

今回のコードではそれを実装したものになります。

コード

// 画像組み合わせを保持するための二次元配列patternListに対して、
// 総当り組み合わせの配列を追加する。
//     usage:
//     SetImagePatten(&patternList, nil, images, 0)
func SetImagePatten(patternList *[][]image.Image, pattern []image.Image, imgArr [][]image.Image, index int) {
    if len(imgArr) <= index {
        p := make([]image.Image, len(pattern))
        // リターン先で値が更新されてしまうので、
        // コピーをスライスに追加する
        copy(p, pattern)
        *patternList = append(*patternList, p)
        return
    }

    if index == 0 {
        pattern = make([]image.Image, 0)
    }

    for _, v := range imgArr[index] {
        pattern = append(pattern, v)
        SetImagePatten(patternList, pattern, imgArr, index+1)
        pattern = pattern[:len(pattern)-1]
    }
}

この再起関数を利用しているCLIツールのコード全体は下記。
https://github.com/jiro4989/igen

実装面での工夫点

今回の総当り組み合わせで使ったテクニックは下記のとおりです。

  • 再起関数
  • スライス
  • ポインタ
  • rangeループ

再起関数

今回、画像の総当り組み合わせを実装する上で、forループによる愚直な実装は絶対に嫌 でした。

というのも、柔軟性が失われる上に要素が増えたときに対応できないからです。

下記はPythonで実装したときに、画像の総当り組み合わせを実装したコードです。

others = pattern['others']
if others != None:
    i = 8
    for eyes in others['eye']:
        eye = eyes['name']
        for eyebrows in eyes['eyebrows']:
            for mouse in others['mouse']:
                i += 1
                baseimg  = Image.open(f'{imgdir}/{base}.{ext}').convert(colormodel)
                processing_image(i, out_formatter, outdir, imgdir, ext, opt, baseimg, eyebrows, eye, mouse)

上記のコードでは、組み合わせのリストを生成するのではなくて、もう直接画像ファイル を開いて操作しているので、見てくれは全然違いますが、やっていることは同じです。

こういうのの実装には多分再起関数が有用なのだろと考えたので、苦手な再起関数に挑戦 しました。
ちなみに、最初にPythonで再起関数を作成してからGoで書き直すって感じで実装しました 。
コードのテストをするのにもやっぱりPython楽ですからねー。

Pythonで実装したコードを貼り付けようと思ったらどうも間違えて消してしまったみたい です。もったいない... orz

スライス

JavaPythonでいうところのリストです。 下記のような感じで宣言します。

sl := make([]int, 0)
// または
var sl = []int

配列の宣言の要素数を省略するとスライスとして扱われるようです。
というか、スライス自体は配列のポインタなので、見ているものは配列と同じだそうです 。
http://golang.jp/go_spec#Slice_types

ハマったところ

makeで要素数を最初に指定すると、ゼロ初期化された値が格納された状態で初期化される 。

コード

   sl := make([]int, 5)
    fmt.Println(sl)
 
    sl = append(sl, 1)
    fmt.Println(sl)

結果

[0 0 0 0 0]
[0 0 0 0 0 1]

JavaだとListを宣言するときに、事前に要素数がわかっているときは

List<Integer> list = new ArrayList<>(5);

って宣言してからaddするほうが高速に動作するので、その癖をGoでやったらはまった...
どうようのことをGoでやる方法がわからないので、とりあえず空スライスとして宣言する ように実装しています。

それはさておいて、
Javaだとlist.add(elem)
Pythonだとlist.append(elem)
Goだとsl = append(sl, elem)で、
とごちゃごちゃになってきますね...

ポインタ

func SetImagePatten(patternList *[][]image.Image, pattern []image.Image, imgArr [][]image.Image, index int) {

patternList *[][]image.Imageがそれですね。
この再起関数自体は戻り値を持たずに、引数で渡されたポインタの参照先の要素を変更す ることで、値を変更しています。

ポインタを使うときは、関数の引数で型の前に*をつけて、呼び出し側で&を使うみたい です。

// 関数定義
func f(x *int) {
  *x = 1
}

// 関数呼び出し
x := 0
f(&x)
fmt.Println(x)

大学のC言語の勉強のときにやったような記憶があるくらいでまともに使うのは今回が初 めてです。
Javaの参照渡しと似てるけれど、具体的に何が違うんだろう...

rangeループ

for _, v := range imgArr[index] {
    pattern = append(pattern, v)
    SetImagePatten(patternList, pattern, imgArr, index+1)
    pattern = pattern[:len(pattern)-1]
}

Javaの拡張for文、Pythonのinループと同じですね。 基本的にループをintカウンタで回してインデックスで要素を取得する方法は嫌いなので 、forループはほとんどこちらを利用してます。 もとの要素を変更する必要があるときだけカウンタを使います。

今回はインデックスが不要なので一つ目の要素は_で握り潰しています。
地味にこのインデックスと一緒にループって便利なんですよね。
Javaだと拡張for文でインデックスが欲しくなったらわざわざループの外で変数を宣言し ないといけないので。

まとめ

ということで、今回再起関数を実装するに当たって、いろんなGo言語の機能を利用しまし た。

少し複雑な処理を実装するとなると、一気に勉強量が増えて大変ですが、多分自分にはこ の方法が一番身につきやすいです。モチベーション維持にも効果的ですし。

Go言語 学習メモ02 - vimでの開発環境構築

どうもです。次郎です。

やっと休みになったので、 VimでGo言語を開発するためにやったメモを残します。

目次

セットアップ環境

Linux Mint 18.2 Mate

セットアップ

書式設定

僕のVimの環境では、インデントの設定に下記の設定をしています。

  • タブ文字を半角スペースに置換
  • タブ文字の文字幅は2文字

Go言語のgo fmtで自動整形されるとき、インデントはタブ文字で行われます。そこはGo 言語の習わしに合わせるようにしたほうがよいので、Go言語だけ異なる設定になるように 、vimの設定を追加します。

set noexpandtab
set tabstop=4
set shiftwidth=4
inorea iferr if err != nil {<CR>fmt.Println(err)<CR>os.Exit(1)<CR>}<ESC>

上記の設定を$HOME/.vim/vimfiles/after/ftplugin/go.vimのファイルに記述します。

iferrは、Goだと頻繁に発生するerr処理のためのスニペットです。個人利用する分だと、 err処理はos.Exit(1)で十分だと思ったので。

GOPATH環境変数を追加

$HOME/.bashrcにGOPATHを追加します。 go getで追加されるパッケージはここに追加されます。

export GOPATH=$HOME/.go

gocodeのインストール

以下のコマンドをターミナルで実行します。
go get github.com/nsf/gocode

実行が終わると$HOME/.goの配下にインストールされます。

vimプラグイン設定の追加

.vimrcにプラグイン読み込みの設定を追加します。

僕の環境では dein.vim を使用しているので、deinのコマンドを記述します。

call dein#add('fatih/vim-go')
call dein#add('vim-jp/vim-go-extra')

上記をdeinの設定の箇所に追加してvimを起動しなおします。

次にvimのコマンドで :GoInstallBinariesを実行します。

処理が完了したら、下記の設定を.vimrcに追加します。

exe "set rtp+=".globpath($GOPATH, "src/github.com/nsf/gocode/vim")

結果

:Godoc packagename でドキュメントを読むことができませんでしたが、関数の補完や、 ファイル保存時に自動で整形、インポート追加が実行できることを確認しました。これで Go開発をスムーズに進められそうです。

参考リンク

Go言語 学習メモ01

とりあえずGo言語の学習をしてみることにしました。

情報を集めるのに結構苦労してるので、備忘録と作業メモを兼ねて、 何をやったのかを随時記録していこうと思います。

目次

Go環境構築

#!/bin/bash
# -*- coding: utf-8 -*-

sudo apt-get install -y golang
sudo apt-get install libgtk2.0-dev libglib2.0-dev libgtksourceview2.0-dev -y
go get github.com/mattn/go-gtk/gtk
go install github.com/mattn/go-gtk/gtk

上記のスクリプトを実行。 GUIも使えるようになりたくてgo-gtkをインストールするも、 JavaFXみたいなGUIエディタがなくてひとまず保留。

インストールしたのはバージョン1.6.2です。

$ go version
go version go1.6.2 linux/amd64

公式を見に行ったら最新バージョンは1.9でした。 早めにバージョンアップしようと思います。

Hello World

package main

import "fmt"

func main() {
  fmt.Println("Hello World")
}

まずはこれがないと始まらないですよね。

コマンドライン引数を使う

CLIツールを作るならこれができないと話にならない。

package main

import (
  "fmt"
  "os"
  "flag"
)

func main() {
  fmt.Println("os.Args : ", os.Args)
  //fmt.Println("os.Args[0] : ", os.Args[0])
  //fmt.Println("os.Args[1] : ", os.Args[1])

  i := flag.Int("intvar", 0, "help msg")
  s := flag.String("strvar", "hoge", "help msg")
  flag.Parse()

  fmt.Println("flag.Args() : ", flag.Args())
  fmt.Println("i : ", *i)
  fmt.Println("s : ", *s)
}

コマンドライン引数を扱う方法は2種類あるみたい。

参考サイト : http://qiita.com/uokada/items/f0e069a751679dcf616d

os.Args

os.Argsは普通のコマンドライン引数っぽい。 pythonでいうsys.argvと同じ感じです。

第1引数(Args[0])にはスクリプト自身が格納されているようで、 入力した値は第2引数(Args[1])から使う感じですね。

$ go run args.go -intvar 1 -strvar aaa
os.Args :  [/tmp/go-build513703329/command-line-arguments/_obj/exe/args -intvar 1 -strvar aaa]
os.Args[0] :  /tmp/go-build513703329/command-line-arguments/_obj/exe/args
os.Args[1] :  -intvar
flag.Args() :  []
i :  1
s :  aaa

flag.Args()

pythonでいうargparse.ArgumentParser()と同じ感じです。 オプション名とデフォルト値、ヘルプメッセージを設定できます。

こちらの場合はflag.Parse()を実行しないと、引数が渡されていないように振る舞うため 、引数を渡してもデフォルト値で初期化されるみたいです。

一瞬悩んだのは、引数で渡された値を使うときはポインタとして使わないといけない点で す。

$ go run args.go 1 2 3
os.Args :  [/tmp/go-build676071249/command-line-arguments/_obj/exe/args 1 2 3]
os.Args[0] :  /tmp/go-build676071249/command-line-arguments/_obj/exe/args
os.Args[1] :  1
flas.Args() :  []
i :  0xc82000e2c8 # ←ここ
s :  0xc82000e3b0 # ←ここ

普通に変数名を指定して使おうとしたらアドレスが表示されて「うん?」ってなりました 。*varnameって感じで指定しないといけないんですねー。

そもそもポインタを使わないといけない言語に触るのが随分ぶりなので、違和感バリバリ です。

ひとまず、コマンドライン引数を使うときは、flag.Args()を使っておけばよい感じです かね。

:= って何?

ここでちょっと疑問。 := っていう不思議な代入式が何なのか気になりました。

package main

import "fmt"

func main() {
  var i = 1
  j := 2
  fmt.Println(i)
  fmt.Println(j)
}

こんなコードを動かしても普通に動いたのでなんでだろう? と思ってググったらどうもローカル変数にのみ使える記法だそうです。

参考サイト : http://www.geocities.jp/m_hiroi/golang/abcgo01.html

:= をグローバルスコープにかいてみる

ソースコード

package main

import "fmt"

a := 3
func main() {
  var i = 1
  j := 2
  fmt.Println(i)
  fmt.Println(j)
  fmt.Println(a)
}

実行結果

$ go run var.go 
# command-line-arguments
./var.go:5: syntax error: non-declaration statement outside function body

つまり、varで変数を定義してあればグローバル変数で、:=で変数を定義してあればロー カル変数、って感じでコードを読み解くためにある記法...、ということなのでしょうか ...

単純に人間が読みやすいようにするために導入されてるなら、インデントなりスコープな りで判断がつく気がするので、何かしら他の役割もあるってことなのでしょうか。

おいおい調べようと思います。

その他

端末で go fmt と入力すると、実行したカレントディレクトリ内のgoファイルが整形される。 これ結構便利ですね。

試しに下記のコードをgo fmtしてみました。

package main

import "fmt"

func main() {
  tmpvar := 1 // int var
  hyperverylongvarname := "very long string" // string var
  var tmptmpvar string // var test
  if ( true ) { fmt.Println("true") }
}

結果は、下記のようになってました。

package main

import "fmt"

func main() {
    tmpvar := 1                                // int var
    hyperverylongvarname := "very long string" // string var
    var tmptmpvar string                       // var test
    if true {
        fmt.Println("true")
    }
}

変更されていたのは下記の部分です。

  1. 半角スペースインデントをタブインデントに
  2. インラインコメントの桁
  3. 不要な ( ) の削除
  4. { に改行を挿入

結構これって重要な機能だと思います。

僕は半角スペースでインデントする人ですけれど、 どっちかに統一するのって結構大変なので、 プログラムが勝手に整えてくれるのは大助かりです。

まとめ

今回はGoをLinuxMint環境にインストールして、 シンプルな標準出力とコマンドライン引数を使う方法を学習しました。

シンプルな仕様といっても覚えないといけないことはたくさんありますね。 地道に学習していこうと思います。

Go言語の勉強をする目的は、Pythonでかいた画像処理スクリプトPython実行環境のないWindows環境用に配布するためです。

なので、なるべく早いうちに画像処理系の機能の使い方を学習しようと思っています。

以上です。

差分画像を組み合わせた画像をツクールMV規格の画像ファイルにまとめるスクリプト

3連投稿です。 多分もう投稿するネタがないので、しばらくブログの更新が止まると思います。

まえがき

普段が私が絵を描く時に一番つまらない作業は、差分ファイルを作成することです。

なぜなら、差分を作り終わったあとで修正したい箇所が見つかった時、差分を作成する作 業をやり直す必要があるからです。しかも、差分が増えれば増えるほどその作業が増えて 、面倒になります。

そのくせ、やることは非常につまらない作業で、マウスでカチカチするだけというもので す。脳みそがマッハで腐っていきます。

この問題を解決しよう、ということで、自分用にスクリプトを作成しました。

目次

完成したスクリプト

上記のスクリプトを組み合わせて、実際の動作させるスクリプトは下記のようになります。

@echo off

set target_dir=actor019
set outname=%target_dir%_R_%%03d
set facename=%target_dir%_face%%03d
set img="%target_dir%\img"

if exist "%target_dir%\" (
  python layover_image.py "%img%\image_layer.json" %outname% -o "%img%\stand\right"
  python flippicts.py "%img%\stand"
  mkdir "%img%\face"
  python trim4mv.py "%img%\stand\right" %facename% -o "%img%\face"
  copy "%userprofile%\Documents\mydocs\image\rpg_mv\actors\README.html" %img%
  python pyzip.py "%img%" -n "%target_dir%" -x "%target_dir%\src" "%img%\src" "%img%\image_layer.json" "%img%\face\coordinates.csv"
  echo bat処理が終了しました。
) else (
  echo 指定したディレクトリ%target_dir%は存在しません。
)

pause

処理のフロー

今回の作業を自動化するにあたって下記の手順を想定しました。

  1. レイヤー分けした差分画像を組み合わせて、1枚の画像ファイルを生成する。
  2. 画像を任意の順番で組み合わせて、複数の画像を一気に生成する。
  3. 生成された画像から、左右反転した画像を生成する。
  4. 画像をトリミングして、RPGツクール用の8枚パネル上に配置する。
  5. 配布するようにZIPに圧縮する。
  6. Dropboxにアップする

これらの一連の手順をすべて単一のpythonスクリプトでやらせることも可能でしたが、 役割がそれぞれ全く異なるので、別スクリプトに分けておいて、batやbashでコマンドを つなぐようにしました。

また、今回は自分が配布するときのことを考えて、ZIP圧縮することまで自動化するよう にしました。Dropboxにアップする部分は、batchスクリプトの方でやるようにしています 。デスクトップでDropboxに同期するようにしていると、単純にcopyするだけで、Dropbox に配置できるからです。

設定ファイル

{
  "img":{
    "ext":"png",
    "colormodel":"RGBA",
    "base"   : "base",
    "option": [null,["red","highlight"]],
    "pattern" : {
      "necessary":[
          { "eyebrows" : "normal" , "eye" : "normal"   , "mouse" : "normal"  }
        , { "eyebrows" : "normal" , "eye" : "normal"   , "mouse" : "smile"   }
        , { "eyebrows" : "normal" , "eye" : "surprise" , "mouse" : "whisper" }
        , { "eyebrows" : "normal" , "eye" : "normal"   , "mouse" : "spin"    }
        , { "eyebrows" : "normal" , "eye" : "smile"    , "mouse" : "smile"   }
        , { "eyebrows" : "angry"  , "eye" : "normal"   , "mouse" : "shout"   }
        , { "eyebrows" : "sad"    , "eye" : "normal"   , "mouse" : "whisper" }
        , { "eyebrows" : "normal" , "eye" : "smile"    , "mouse" : "normal"  }
      ],
      "others":{
        "mouse":["normal","smile","whisper","spin","shout"],
        "eye":[
          { "name":"close"   ,"eyebrows":["normal"] },
          { "name":"scornful","eyebrows":["normal","angry"] }
        ]
      }
    }
  }
}

組み合わせる画像を配置するための、imgディレクトリのrootに配置します。

設定は単純です。 ファイル名の拡張子なしを指定しているだけです。

necessaryは必須の項目です。 この8個は一番最初に生成される画像になります。 1枚目のパネル画像の並び順を指定したい時に使います。

othersはオプションです。 最初の8枚の生成が終わったあとに処理されます。

正直、これで設定するのはやめようと思いました。

後で見た時に思い出すのに苦労するのと、配布するにはやや複雑な設定ファイルになるの で。

所感

実際に動かしてみたら、かなり作業が楽になりました。

絵のメンテとそれを配布する場所に反映する作業は、非常に手間がかかる割に見返りが少 ないです。

特に、ツクールMV用にトリミングする作業は非常に面倒で不毛な作業です。

拙作のTKoolFacetileMaker2でだいぶ楽になったとはいえ、自動化にまでは至っていませ んでした。

一度行って終了、ならまだ良いのですが、何度も修正することや、今後もやるであろう作 業に無駄が残るのは良くないので、それを自動化できたことの意義は大きいです。

今後

今回のスクリプトPythonで作成しています。 つまり、Windowsの一般ユーザの環境に標準でインストールされていません。

なので、一般向けに配布することを前提にした、スクリプトとして別の言語で書きなおそ うと思います。

現時点での候補としては、下記のものを考えています。

  • Java

    • メリット
      • 一番使い慣れているので、開発も保守も安定している
      • 実行環境も大体にPCにすでにあるため、配布も容易
      • 名前のブランドがあるので、ユーザ的にも安心?
    • デメリット
      • そろそろ新しい言語が勉強したい身としてはあまり触りたくない
      • IDEのサポートがあっても書くのが結構しんどい
  • Kotlin

    • メリット
      • JavaVM言語なので大体の環境で動作させられる。
      • Androidの正式言語の一つにあがっているので、今後の展望に明るい
      • Javaほど書くのがしんどくない。IDEのサポートもある
      • 実行可能ファイルのファイルサイズの増加はやや控えめ (800KBほど)
      • Javaのノウハウが大体使えるので、比較的学習が楽
      • JavaFXも使えるのでGUIアプリも作れる。
    • デメリット
      • 言語仕様がJavaより複雑なので、そのあたりの学習コストがかかる
      • ファイルサイズが増加する
      • 型推論といった仕様の理解がまだ甘いので、バグが見つかった時にサポートできる か怪しい
  • Go

    • メリット
      • 言語仕様がかなりシンプルで、学習コストが低めらしい
      • ロスコンパイル可能で、速度も早い
      • サーバサイド言語としての話題性もあるので、今後に期待できる。 事前に勉強しておけば仕事に繋がりうるかも。
    • デメリット
      • 初めて触るタイプの言語なので、学習コストが未知数
      • GUIアプリの作成は期待できない

途中までKotlinで作成を進めていたのですが、結局Kotlinの勉強するならJavaでいいかな ぁとか最近思うようになってきたので、これを機会にGo言語に触ってみようと思ってまし た。

GUIアプリの作成がやや厳しそうな話を聞いていますが、今回のスクリプトは自動化が目 的なので、CLIでいいかなぁと考えています。それならGoでもできそう。

なので、今回の移植作業はGo言語で行おうと考えています。
一体どんな言語なのか、すでにワクワクしています。

以上です。

静的な小説サイトからテキストを抽出して保存するスクリプト

どうもです。
ブログ更新忘れていたので、連投をば。

まえがき

もともとRPGツクール用のツールとかを配布するときに、 コミュニティサイトしか情報がなかったら良くないかも、 と思って開始したブログでしたが、 せっかくなので技術発信とかも少しでもしといたほうが、 今後の自分の糧になるのかなぁ、と思ったので、 そういう記事も書いていこうと思います。

目次

Webクローリングスクリプト

表題のとおり、今回はクローリングスクリプトです。

Bashwgetというコマンドを使えば、Webクローリングができるという情報を知って、自分がよく見ていた小説サイトの情報を抜き取って保存してみました。

参考サイト : http://girigiribauer.com/archives/925/

作成したスクリプト

#!/bin/bash
# -*- coding: utf-8 -*-

set -eux
: $1 $2 $3

# 対象サイトからHTMLを取得する
wget -r --random-wait $1 || exit 1

# ドメイン名のみ取り出す
dirname=`echo $1 | sed -E "s@https?://@@g" | sed -E "s@/.*@@g"`

# 不要なファイルを削除する
rm $dirname/*.png
rm $dirname/*.jpg
rm $dirname/*.gif
rm $dirname/*.zip

# ディレクトリ内のフォルダを取得
files=`find $dirname | xargs`
for file in $files; do
  if [ -f $file ]; then
    # 文字コードを変換して、別名ファイルで出力
    html=`cat $file | iconv -f $2 -t $3`
    title=`echo $html | sed -e "s@.*<title>\(.*\)</title>.*@\1@g"`

    cat $file | iconv -f $2 -t $3 \
      | sed -E 's@<[^>]*>@@g' \
      | sed -E 's@(^ +| +$)@@g' > "${dirname}/${title}.txt"

    # 元のファイルを削除
    rm $file
  fi
done

解説

引数チェック

set -eux
: $1 $2 $3

引数チェックの方法の一つです。 Qiitaの記事で知りました。
http://qiita.com/m-yamashita/items/889c116b92dc0bf4ea7d

今回の引数はそれぞれ下記の用途で使用しています。

HTML取得+ドメイン名の取得

# 対象サイトからHTMLを取得する
wget -r --random-wait $1 || exit 1

# ドメイン名のみ取り出す
dirname=`echo $1 | sed -E "s@https?://@@g" | sed -E "s@/.*@@g"`

wget再帰的に取得した時、ドメイン名のフォルダに保存されます。

そして、保存されるファイル名は、htmlファイルのファイル名なので、 必ずしも、その小説のタイトルではない場合があります。

なので、findしてhtml内のタイトルをファイル名にして保存するようにしたいわけです。 そこでwgetした時のURLからドメインのみを抜き出します。

sedするときは/(スラッシュ)を使うことが多いですけれど、 今回は@を使ってます。 URLが混じってると失敗してしまうので。

フォルダ内のファイルを処理

# ディレクトリ内のフォルダを取得
files=`find $dirname | xargs`
for file in $files; do
  ...
done

先ほど取得したドメイン名のフォルダをfindして、ループで処理します。

HTMLの文字コードの変換

# 文字コードを変換して、別名ファイルで出力
html=`cat $file | iconv -f $2 -t $3`

保存したファイルはUnicode文字として保存されていましたので、 それをUTF-8に変換する必要がありました。 なので、iconvで変換するようにします。

タイトルの取得

title=`echo $html | sed -e "s@.*<title>\(.*\)</title>.*@\1@g"`

次に、ファイル名がわかりやすくなるように、HTML内のタイトル要素を取得します。

ここも先ほどと同様にsedを使います。 sed正規表現と後方参照でタイトル部分のみを抜き出します。

HTMLからテキストのみを取り出して整形

cat $file | iconv -f $2 -t $3 \
  | sed -E 's@<[^>]*>@@g' \
  | sed -E 's@(^ +| +$)@@g' > "${dirname}/${title}.txt"

今回は小説サイトのテキストが必要なので、不要なタグを削除します。
また、インデントが存在していても見づらいので、それも削除します。

削除したら、先ほど取得した$titleをファイル名として保存します。

ここで、先ほど取得した$htmlを使用していないのは、 echo $htmlすると改行文字が削除されてしまっていたからです。

対処方法がわからなかったので、ここではファイルから再度取得して文字コードを変換し た結果をパイプするようにしました。

ここでも同様にsedします。 sedが便利すぎてやばいです。 でも何でも正規表現すると遅くなるとかって話を聞いたので、あまり良くはないのかも…。

所感

そんな感じで、好きな小説サイトのテキストを抜き出して閲覧できるようになりました。
閲覧するときは、less -N $filenameで読めば、ターミナルで読書ができます。

問題点としては、一昔前のような静的な小説サイトでないと機能しないということです。

Ctrl + U してソースを閲覧して、テキストがすっからかんになっているような 動的にページが構築されているようなサイトだと、ほぼ失敗します。

次に、1ページに1話である必要があります。 1話が何ページにも渡っていて、それらを1つのファイルにまとめるような処理は含まれていません。

他にも、ページ内に小説の記事以外の要素が含まれていてもダメです。

ブログサイトのように、他の記事へのリンクや、サイドメニューとか ヘッダとかの要素も取り込んでしまうので、それらを省く処理も場合によっては必要です。

この辺は課題ですね。

キャラ設定を考えるためのアプリ作りました

お久しぶりです。次郎です。

現状報告

仕事が忙しくて、ゲーム制作に着手できないので、
ゲーム制作の少しでも楽にするためのモノばかり作っています。

最近はもっぱらPythonBashを勉強していて、
ちょっとした作業の自動化とかしつつ、ゲーム制作を放置しています。

たとえば、キャラの表情の差分を組み合わせて、
一気に差分画像を生成するツールとかもPythonで作成しました。

配布とかはしていませんが、Github上にはすでに存在します。
ただし、ドキュメントとかは一切用意していませんが・・・。

せっかくつくったツールなので、誰かに公開したいと思っているのですが、
いくつか問題がありまして、Pythonは実行環境がないと動かせないのと、
exe化するとファイルサイズが巨大化する点です。

なので、配布するなら自分用の簡単なのを作って、
Javaに移植するといった感じで今まで作っていたのですが、
最近Javaを書くのが結構しんどくて…。

代替案として、Go言語というのが良さそう? と最近知ったので、Pythonと平行しつつ、Go言語も勉強しようかなぁと
思っている次第です。

目次

作成したアプリ

前置きはそこまでに、今回作成したアプリについて、一応紹介します。
下記のリンク先がそれです。

https://jiro4989.github.io/charaseat/

今回の実装ではそれなりにたくさんのデータを扱うということだったので、 Javascriptで動的にデータを追加するには、処理が重いかと考えたので、 アップ前にPythonでHTMLを生成してGithubにアップするようにしています。

htmlのテーブル部分生成スクリプト

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
from datetime import datetime
from os.path import splitext, basename

def main():
    datas = [
              "res/data/一人称.html"
            , "res/data/二人称.html"
            , "res/data/性格.html"
            , "res/data/髪型.html"

            , "res/data/トップス.html"
            , "res/data/ズボン.html"
            , "res/data/スカート.html"
            , "res/data/ドレス.html"
            , "res/data/制服.html"
            , "res/data/アンダーウェア.html"
            , "res/data/履物.html"
            , "res/data/かぶりもの.html"
            , "res/data/寝巻き着.html"
            , "res/data/水着.html"
            , "res/data/装身具.html"

            , "res/data/その他.html"
            ]

    checkboxtext = \
            '<label>' \
                '<input' \
                ' type="checkbox"' \
                ' name="{title}"' \
                ' value="{title}:{text}"' \
                ' onclick="updateRecords();"' \
                ' />' \
                '{fulltext}' \
            '</label>'

    tables = []
    for filepath in datas:
        with open("table.html") as table:
            tabletext = table.read()
            with open(filepath) as datafile:
                caption = splitext(basename(filepath))[0]

                line = datafile.readline()
                trs = []
                while line:
                    line = line.rstrip("\n")
                    if line == "":
                        continue

                    if line.startswith("<"):
                        text = re.sub(r"</?[^>]+>", "", line)
                    else:
                        text = line

                    text = checkboxtext.format(title=caption, text=text, fulltext=line)
                    text = "<tr><td>{}</td></tr>".format(text)
                    trs.append(text)
                    line = datafile.readline()
                tbody = "\n".join(trs)
                newtable = tabletext.format(caption=caption, tbody=tbody)
                tables.append(newtable)

    htmllines = []
    with open("template.html") as template:
        text = "\n".join(tables)
        line = template.readline()
        while line:
            if "{target}" in line:
                htmllines.append(text)
            elif "{time}" in line:
                now = datetime.now().strftime("%Y/%m/%d")
                htmllines.append(now)
            else:
                htmllines.append(line)
            line = template.readline()

    with open("index.html", "w") as indexhtml:
        indexhtml.write("".join(htmllines))

if __name__ == '__main__':
    main()

テーブル埋め込みのテンプレートhtml

<div class="charaTableArea">
  <input type="button" value="ランダム設定" onclick="setRandomContent('{caption}');" />
  <table border="1">
    <caption>{caption}</caption>
    <thead>
      <tr style="background-color: yellow;">
        <td>項目</td>
      </tr>
    </thead>
    <tbody>
      {tbody}
    </tbody>
  </table>
</div>

やりかたは単純。

dataフォルダ配下に、1行に1データを記述しただけのテキストファイルを配置し、 それをPythonで読み込んで、ファイル名をcheckboxのnameに、データをvalueとする 文字列を生成して、それをtable.htmlの{tbody}にformatで埋め込んでいるだけです。

これで、スクリプトを実行すると index.htmlが更新されるという仕組みです。

pushする前にスクリプトを実行するスクリプトを用意しておけば、 データを更新して、pushするだけで完成、というわけです。

お手軽です。

所感

極限までデザインは手抜き、いろいろ手抜きです。   自分用に作ったアプリですが、正直使うか怪しいです。

今回のアプリ制作を通して得た知見としては、
GithubPagesでWebアプリを作る方法がわかった、ということくらいです。

以上です。