tobb422のブログ

スタートアップエンジニアの奔走

<子育てIoT> 時間を遡って、動画を保存してくれるカメラを作ってみた

はじめに

先日、待望の第一子が無事生まれました!
既にメロメロで、親バカを存分に発揮しているのですが...
せっかくプログラミングを趣味にしているので、子育て × プログラミングでなにか作れないか?と思い、
表題の通り、動画撮影をするカメラをラズパイ4を利用して作ってみました。

なぜ、時間を遡って動画保存がしたいかというと、
子供はいつ何をしてくれるかわからないもので、私も常にカメラを構えているわけではないので、
「いまのは、後世に残しておきたい!!」と思ったときには、時既に遅し...
という状態になりかねないからです。
(はじめから親バカ全開で行きます)

このカメラでできること

※ () 内は、私ですw

  • (あ、今の瞬間非常に可愛い!!!と思う)
  • buletooth ボタンを押下する
  • ボタンを押したタイミングから約30秒遡って、動画を保存する
  • 作成した動画を Slack に投稿する
  • (いつでも可愛い動画が見放題) & (この瞬間を逃さない)

用意するもの

※ プログラミングの知識は、書籍やらネット上で手に入るとして、入手困難なものは特になく、amazon で揃えられました!

f:id:tobb422:20200718015330p:plain

開発工程

(ラズパイのセットアップは、趣旨から外れるので省きます)

  • bluetooth ボタンと ラズパイをつなぎます → こちらの記事を参考にさせていただきました
  • カメラモジュールやマイクを取り付ける
    • カメラモジュールつけるの初めてだったので、つけるのちょっとドキドキしましたw
    • こちらの記事ですごく丁寧に説明があり、今回、カメラモジュールやマイクを扱うのは、初めてでしたが、割とすんなりとできました!
  • プログラミング頑張る

です。
開発は、ラズパイにsshでログインした状態で行いました!

プログラムの大まかな処理

実際にどんな処理の流れは、以下です。

  • カメラから取得できるコマ送り画像データを蓄積する
    • 今回は、30秒分ためて、あとは古いデータから捨てていくという感じです
  • マイクから取得できる音声データを蓄積する
  • bluetooth からの信号を検知して、30秒分のコマ送り画像から、動画を作成
  • 作成した動画と音声を組み合わせる(データ形式は、.mp4)
  • (今回は、iPhone で動画を主に見る予定だったので) 完成したデータ形式を .mov に変更する
  • slack に、完成した動画をアップロードする

以下で、各処理をもう少し詳細に書いていきます!

プログラムの詳細

※ 説明の都合上、前述したプログラムの処理の流れとは、順番が異なります。

bluetooth ボタンからの信号を、検知するプログラム

evdev というパッケージを利用しました。

参考資料:

from evdev import InputDevice

while True:
    device = InputDevice('/dev/input/event1')
    event = device.read_one()
    if event != None:
        # event があれば、ここにくる

カメラ, マイクから各データの取得 & 動画に加工する

OpenCV を利用して、カメラモジュールから画像データを読み取っています
動画データの形式変換や音声との合成には、ffmpeg を利用しています。

参考資料:

import cv2
import pyaudio
from pydub import AudioSegment
import wave
import ffmpeg
from evdev import InputDevice

device = InputDevice('/dev/input/event1')

# まずは、カメラモジュールからデータを読み取る
camera = cv2.VideoCapture(0)

# 出力する動画ファイルの設定を行う
fps = 9 # フレームレート, 1秒間でどれだけフレームを使用するか, 値は調整が必要 → 後述します
w = int(camera.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
fourcc = cv2.VideoWriter_fourcc(*'XVID')
video = cv2.VideoWriter('sample.avi', fourcc, fps, (w, h))

# 出力する音声ファイルの設定を行う
P = pyaudio.PyAudio()
CHUNK = 1024*5
CHANNELS = 1
FORMAT = pyaudio.paInt16
RATE = 44100
stream = P.open(format=FORMAT, channels=CHANNELS, rate=RATE, frames_per_buffer=CHUNK, input=True, output=True)

frames = []
voices = []
while True:
    # カメラからフレームを取得し続け、動画へ書き込む
    _, frame = camera.read()
    frames.append(frame)
    if len(frames) > 30:
        # 30秒くらいの動画撮影にしたいので、過去のものはどんどんしてる
        frames.pop(0)

    # 音声データを読み込み続けて、list に流す
    input = stream.read(CHUNK, exception_on_overflow=False)
    voices.append(input)
    if len(voices) > 30:
        # フレーム同様
        voices.pop(0)

    event = device.read_one()
    if event != None:
        # ここなかでためた framesやvoicesから動画を作っていきます

        # frames から元となる動画データを作成
        video = cv2.VideoWriter('sample.avi', fourcc, fps, (w, h))
        for f in frames:
            video.write(f)

        # voices から音声データを作成
        wf = wave.open('sample.wav', 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(P.get_sample_size(FORMAT))
        wf.setframerate(RATE)
        wf.writeframes(b''.join(voices))
        wf.close()

        # .wav → .mp3
        base_sound = AudioSegment.from_file('sample.wav', format='wav')
        base_sound.export(now+'-tmp.mp3', format="mp3")
        
        # 動画データと音声データを統合する
        audio_stream = ffmpeg.input('sample.mp3')
        video_stream = ffmpeg.input('sample.avi')
        ffmpeg.run(ffmpeg.output(video_stream, audio_stream, 'sample.mp4', vcodec="copy", acodec="aac"))

        # .mp4 → .mov
        ffmpeg.run(ffmpeg.output(ffmpeg.input('sample.mp4'), 'sample.mov', pix_fmt='yuv420p'))

動画ファイルをSlackへアップロード

参考資料

import requests

params = {
    'token': TOKEN, # SLACK API より取得
    'channels': CHANNEL, # 投稿したいチャンネルのID
    'initial_comment': '動画だよ'
}

files = { 'file': open('sample.mov', 'rb') }
requests.post('https://slack.com/api/files.upload', params=params, files=files)

f:id:tobb422:20200718015438p:plain

個人的にハマったポイント

映像と音声がずれる

前述もしたこちらの部分で、while 文の中でカメラ, マイクからデータを取得し続けているのですが、
このデータの粒度が違うため、合成したときに、音声が短いとか、映像が長すぎるみたいな問題が起きました。

# カメラからフレームを取得し続け、動画へ書き込む
_, frame = camera.read()
frames.append(frame)

# 音声データを読み込み続けて、list に流す
input = stream.read(CHUNK, exception_on_overflow=False)
voices.append(input)

fps(フレームレート)の設定を変えることで解決しました。
まず取得した音声データの長さを取得します。

base_sound = AudioSegment.from_file('sample.wav', format='wav')
base_sound.export(now+'-tmp.mp3', format="mp3")
# mp3ファイルへ変換した直後、以下で音声データの長さを取得できます
print(base.sound.duration_seconds) 

同じく読み取りを行っている frames に溜まったスタックしたフレーム数がわかるはずなので(list に追加しているため)
ここで、
音声データの長さ / フレーム数 → 最適なfps(フレームレート = 1秒間で使用するフレーム数) が取得できます。
ここで取得した fps を設定しておくことで、映像と音声を統合したときのズレが解消されます。

動画の加工処理を行っていると、次の読み取りにいかず、現実の時間とズレが生じる

サンプルとしてあげたコードでは、動画加工を行っている間、
while文が止まっているため、次のデータの読み取りにいかないです。

動画加工 → slack へのアップロードが終わってから、溜まっているデータの読み取りにいくので、
次回、動画加工に入るタイミングでは、現実よりも動画加工の時間分、遅れてデータが frames や voices に溜まっていることとなります。

解決策としては、動画加工 ~ slack へのアップロードを threading モジュールを利用して、別スレッドで実行するように修正しました。

def worker(frames, voices):
    # 動画加工やslackのアップロード処理を書いておく

while True:
    # カメラからフレームを取得し続ける
    _, frame = camera.read()
    frames.append(frame)
    if len(frames) > 30:
        frames.pop(0)

    # 音声データを読み込み続ける
    input = stream.read(CHUNK, exception_on_overflow=False)
    if len(voices) > 30:
        voices.pop(0)

    device = InputDevice('/dev/input/event1')
    event = device.read_one()
    if event != None:
        fs = copy.copy(frames)
        vs = copy.copy(voices)
        # 別 thread にて動画加工を実行することで、遅延をなくし、処理を回し続けられるようにする
        t = threading.Thread(target=worker, args=(fs, vs))
        t.start()

まとめ

非常に長い記事になってしまいましたが、
以上が「時間を遡って動画保存をするカメラ」の開発ログでした!

ラズパイがむき出しの状態は、子供向けにはちょっと可愛らしさが足りないので、
猿のぬいぐるみをケース代わりに作成してもらった(嫁作)のですが、
出産に間に合わず、現在この状態で止まっております。首を狩られた姿...w
(いつか完成させてくれるはず...)
f:id:tobb422:20200726144906p:plain

せっかく作ったので、
子育ての中で、しっかり活用していきたいと思います!

fp-ts, io-ts を使ってみた所感

はじめに

ざっと、10ヶ月ぶりの更新....

最近、業務でAPIサーバーを TypeScript で開発するという案件がありました。
プロジェクトの主要な開発は、Scala で開発しており Cats という関数型ライブラリを使用しています。
そこで、 TypeScript でも同じように書けないかな?と探していたところ、 fp-ts, io-ts というライブラリを発見したので、早速試してみました!

fp-ts, io-ts キャッチアップ!

これらのライブラリでは、
関数型言語で使用されるような型や型クラス, それらを扱うための関数が、内包されています。

fp-ts

Either を例にして、どうやってキャッチアップしたかの紹介です。

// 型定義と関数を一部抜粋しています。
export declare type Either<E, A> = Left<E> | Right<A>

export declare function fold<E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B): (ma: Either<E, A>) => B

今回は、シンプルなので Either を例にしましたが、
プロジェクトでは、Task, EitherT, Traversable 辺りを取り入れました!

io-ts

fp-ts で、一通り揃っている感じはするのですが、

Runtime type system for IO decoding/encoding

上記の部分を io-ts がカバーしてくれています!
私が利用したのは、サーバーが受け取ったリクエストボディの内容が型と一致しているかバリデーションを行う箇所で利用しました。
少々クセがあるように感じましたが、js のランタイムで型の恩恵が得られるのは、良いなという印象です。
fp-ts 同様で、ドキュメントテストコードを見ながらキャッチアップする方法が、理解しやすいかなと感じました。

import * as t from 'io-ts';

const types = t.type({
  name: t.string,
  email: t.string,
  age: t.number,
});

types.decode(request.body);
// 与えられた body が types で定義した型と違うとバリデーションエラーを返す
// decode は、 Validation<A> ( = Either<Errors, A>) を返す ※ A は、定義した型

fp-ts-contrib (fp-ts をパワーアップする)

fp-ts, io-ts を導入するだけでも、関数型の恩恵を受けられるところは大きいのですが、
上記のライブラリをさらに使いやすくするライブラリもあったので、そちらも合わせて試してみました。
fp-ts-contrib

A community driven utility package for fp-ts

GitHub の説明としては、上記のように書いてあります。
私の場合、今回は Do という概念を利用しました。
Haskell の Do や Scala の for...yield と同じようなことを実現できるので、
普段、関数型で書かれている方には取っ付き易いと思いますし、
TypeScript を書ける方で、これから関数型も触ってみたいと思われている方には、導入としてぴったりだと感じています。

こちらもキャッチアップに関しては、ドキュメントテストコードが非常に充実しているので、そちらから読み解いていくのが良さそうに思います。

// テストコードを参照し、一部コードを付け加えている
const user = Do(option)
  .bindL('name', () => some('bob'))
  .bindL('email', () => some('bsmith@example.com'))
  .let('constant', 10)
  .bindL('nameLen', ({ name }) => some(name.length))
  .letL('emailLen', ({ email }) => email.length)
  .doL(() => Either.right('nothing'))
  .return(({ name, email, nameLen, emailLen, constant }) => ({ name, email, nameLen, emailLen, constant }))

// let: 引き渡す変数の定義
// bind: 関数の結果を引き渡すことができる
// do: 関数の実行
// return: 最終的な返り値 
// サフィックスのL → 値ではなく、関数を引数に取る

まとめ

fp-ts, io-ts を利用することで、TypeScript で関数型拡張ができると思います。
絶対使ったほうがいいということはないんですが....
要所要所で利用すれば、スッキリ書くこともできそうだなと考えています。
また、関数型に普段から慣れている方は、 fp-ts-contrib を使うことで、よりリッチに書けると思います。
(あまり TypeScript 書くことはないかもしれませんが...)

関数型に入門する際に、言語も概念もまとめてキャッチアップするのは、なかなか骨が折れると思うのですが、
JavaScript, TypeScript に書き慣れている方にとっては、
書き慣れた言語で新しい概念に触れることができるので、入門するには最適なのかな?と思いました!

良いチームの正体

はじめに

完全に迷走して、自分でも調子乗ったタイトルつけたなって思うので、許してください。

スタートアップで開発をしているエンジニアです。
「良いチームってなんだ?」という議題について考えることが多く、
実務を通して得た学びや、書籍を通じて得た視点を
言語化する練習も兼ねて、つらつらと書いていきます。

目的 → 結果へのシフト

1人で開発をしていたところから、2, 3人 と人数がふえてい増えていく中で
チームに対して考え方が変わってきました。

チームビルディングを始めたとき

  • 良いチームってどうやったらつくれるか?
  • カッコいい開発チームを作りたい

上記のようなことを思い、
「理想のチーム」について考え、取り組む
というサイクルを回していました。
これ自体見当違いだ!と思っているわけではないのですが、私自身の考えとして
「良いチームを作ることが目的」ということに非常に違和感を持つようになりました。

いまでも「カッコいい開発チームを作りたい」という漠然とした理想は持っている一方で、
こうした良いチームというものは、あくまで自分達が取り組んだ結果かもしれないと考えています。
私の中でこの変化は非常に大きいものでした。

いまチームビルディングに対して、わたしが考えているのは

  • 事業をスケールさせる方法
  • 開発速度を上げるために必要なこと
  • メンバーが自律的に動けるきっかけ

といった具合に、いわゆる「仕組みづくり」です。

実際これは程度の問題で
「良いチームとは?」を考えて、仕組みづくりにたどり着ける方も多くいると思います。
わたしは、
「チーム・事業にとって、良い仕組みを作ること」を目的にするほうがしっくり来たという話です。

よく「良いチームには、良い文化がある」という話を目にしますが、
わたしは、「良い文化が、良いチームを作る」という発想のほうが好きで、
実際にチームビルディングをしていく中で、後者のほうがしっくりきています。

ここからは、「良い文化が良いチームを作る」という前提で話を進めます。

良いチームは、成長で作られる

私の好きな言葉に
「偉大な(高機能な)チームは成長によって作られる。雇うことで作られるものではない」
という言葉があるのですが、最近この言葉がより一層好きになってきました。

事業が上手くいっていない、またはチームとして上手く成果を出せていないときに
チームとしての真価が問われているなと思います。
具体的には、あえて極端な言い回しをしますが、
今いる人材では決して上手く回らないと決めつけ
「スーパースターの獲得にかける」チームは、個人的にあまり好きではありません。
スーパースター級な人材が多いことは、良いチームの必要条件ではないはずです。
もし必要条件であれば、私は良いチームに一生属せませんww つらすぎます!

というのも、
優秀な人材を確保することはもちろん必要なことで、それに越したことはないですが、
その場その場のメンバーで成果を出せる(結果、良いチームを作れる)ことこそ、
再現性も高く、非常に価値のあることだと考えています。

どんなにスーパースター級な人材が集まったとしても、チームとして課題は生まれます。
「良い文化が良いチームを作る」という前提に立ち返ると
チームで課題が出た際に必要なのは、文化(仕組み)を見直すです。
こうした思考が持てると、よく会社言っていることなのですが
「個人 vs 個人」の関係ではなく「チーム vs 課題」の関係を作りやすいです。

課題が出てきた際に、

  • ○○ が、パフォーマンスを出せてない
  • ○○ は、いつもサボっているの

みたいな「個人 vs 個人」の構図に持っていくと(例で出したのは少し極端すぎましたが…)
一向に話が進みません、、そして心理的安全性も担保されないです。

事業を前に進めるためには、課題を一つ一つ解決していく必要があり
その課題を解決していくには、「チーム vs 課題」の構図をいつも保てるような仕組みを考える必要があると思っています。

「チーム vs 課題」として、課題を一つ一つ解決していくことは、
良い文化が生まれやすく、結果良いチームにつながると思うので、非常にいいなと考えています。

良い文化は、実験から生まれる

(そろそろ、着地見失ってきました。。。)

良い仕組みの話をすると、
「じゃあ、良い仕組みってたとえば何?」的な議論になるのですが、
私は、チーム・事業の状況や最終的なゴール・ビジョンによって変わるものだと思ってます。

基準として、

  • 事業をより前に進めることができる
  • 仕事がしやすい(働きやすい)
  • 個々人が 〜 がしたいという想いが持つことができる

みたいなものはあるかなと考えています。

結局、みんなで考えて、本当にこの仕組みはやりやすいのか(事業が回るのか)を試していくことでしか
良い文化(仕組み)は作れないのだと思っています。
ここを一種の実験だと楽しめる人は、素敵だな〜と感じる今日このごろです。

課題に対して、仕組みや環境を整えることで解決しようと取り組み
カイゼンを回すことで、良い文化は生まれ、結果、良いチームが作られる(はず)

まとめ

(完全に着地点見失って辛くなってきたので、ここで無理やり終わらせます。)

最近チームビルディングで考えていることは

  • 良い文化が、良いチームを作る
  • 良いチームは、成長から生まれる
  • 良い文化は、実験から生まれる

でした。

<読書メモ> React Design Patterns and Best Practices 2nd Edition

はじめに

先日、React + TypeScript を使った新規プロジェクトを担当することになりました。
TypeScript は普段から使用していることもあり、
まあ大丈夫かなと思っていたのですが、React が約2年ぶり?くらいだったため、
思い出すことを目的に React Design Patterns and Best Practices という本を読みました。
本投稿は、その読書メモです。
※ 読書メモなので、私が読むことを想定して書き殴ります。
ターゲットとしている層は、「既にReactでアプリケーションを書いている人がステップアップするという位置づけ」らしいです。

↑ あとで気がついたが、なぜかこの書籍だけはてなブログの「Amazon商品紹介」で選択しても表示できない...
https://www.amazon.co.jp/gp/product/1789530172

雑感としては、知らなかった概念や機能を知るきっかけになったが、
プロダクトに導入するには、この書籍だけでは理解を深めるのが難しかったです。
(英語力の問題では....?w)

(私が)初めて知った & 使えそうと思った概念?機能? をまとめてみる

React Hooks

今回の新規プロジェクトで既に使ってみています。
後述する React.memo とそうなんですが、関数で定義していくことに優しい世の中になっているように感じます。
結果、気がついたらほぼ全てのコンポーネントfunction で定義してました。
正直、自分の英語力と技術力ではよくわからなかったので、
以下の記事とドキュメントを利用して、キャッチアップしました。
(まだ、 useStateuseEffect くらいしか使えていないのですが....)
qiita.com ja.reactjs.org

React Context

コンポーネント間で渡すデータを props でやり取りするのではなく
コンポーネントツリー内で共通で利用できるデータといったイメージですかね...?
手元で動かしてみて、こんな感じかと理解した程度で、プロダクト内に取り入れられていないです...
ja.reactjs.org

Function As Child

HoC と比較して語られているケースを良く見ます。
要は、childrenコンポーネントを渡すのではなくて、関数を渡すということです。
最初この章を読んだときは、そんなことができるんだ〜と思いましたw
コンポーネントで切り替わっていく state 等で動的に子コンポーネントを変化させたいときに有効なのでしょうか?
テストで試した程度で、まだプロダクト内では使えていないのですが...
qiita.com

react-motion で実際に使用されているソースコードを見つけたので、
そちらを見るとなんとなく理解が深まるかもしれません。
該当のコンポーネント

Pure Component → React.memo

qiita.com PureComponent は、知っていたのですが
その関数版というか、 React.memo が公開関数になっており、
関数で定義したコンポーネントをラップして、返してくれるようです。
recomposepure みたいなものですね
ただ、なんでも React.memo を使えば正解というわけではなさそうです。用法用量お守りください。
qiita.com

React.Fragment

ja.reactjs.org ドキュメントに書いてあると通りなのですが、無駄なノードを追加しなくて済む書き方と認識しておくので問題なさそうです。

まとめ

非常に雑ですが、読書メモなのでこれで良しとします。
序盤でも触れましたが、普段開発してて知らない概念や機能に触れることができたのはよかったかな〜
と思っています。 ( → ドキュメントに書いてるけど...)

Go + Headless Chrome で HTML から PDF を生成する基盤を作成した

久々にブログを更新!!
(サボりがちなので、心機一転ここからまた継続して更新できるように頑張ります....)

はじめに

one visa というプロダクトの開発を行っており、
その中で、コア機能の一つとなる PDF 生成を Go + Headless Chrome で作成するという取り組みをしたので
その取り組みについて紹介いたします!

どうやって実現するか

  • 生成したい書類を Webページとして作成する(HTML + CSS
  • 作成した Webページをレンダリングする Webサーバーを立てる
    • → (今回は、Go縛りで Echo を使って Webサーバーを立てました!)
  • 該当ページに headless chrome でアクセスする
  • 表示したページを PDF として保存する

この一連の流れを作成します! 図で表すとこんな感じです。

f:id:tobb422:20190716010611p:plain
流れのイメージ

また、今回は異なるページを PDF として生成し、最後に 一枚の PDF として合体させる必要があったので、
ゴルーチンを利用して、上記で説明したプロセスを並列で行います!

利用するツール

今回は、Chrome DevTools Protocol を利用して、Chrome を操作します。
この Chrome DevTools Protocol を go で簡単に操作できるライブラリである chromedp を利用します!
また PDF の合成には、 pdftk という PDF ファイルを編集するコマンドラインツールを利用しました!

実装

上記を実現するためのサンプルです。
echo を使った Webページのレンダリングは省略しています。
また、私は Docker コンテナに必要なツールをインストールして稼働させました!
もちろん、必要なツールが揃っていれば、ローカルで直接稼働させることもできるはずです。
(必要なツール chromedriver chrome pdftk

Docker上で、Headless Chrome を動かすということ自体はよく記事になっているので、省略します。
qiita.com

また、 pdftkapt-get install で Docker に組み込めるのでこちらも省略します。

package main

import ...

func main() {
  // 生成したいPDFに該当するWebページのパスを持った構造体をマップでひとまとめにする
  // → 後述します
  builders := []builder.PDFBuilder{
    &Sample{ Path: "sample" }
  }

  // 各WebページをPDFとして保存して
  fileNames, err := createPDF(); if err != nil {
    return
  }

  // 生成されたPDFを1つのPDFへ合体します
  mergePDF(fileNames)

  // 残ってしまった不要なファイルを削除する
  deleteTmpFile(fileNames)
}

func createPDF(builders []builder.PDFBuilder) (fileNames []string, err error){

  var wg sync.WaitGroup
  wg.Add(len(builders))

  // ゴルーチンを利用して、各PDF生成を並列で処理します
  for _, builder := range builders {
    go func(builder builder.PDFFactoryServicer) {
      defer wg.Done()
      builder.Exec()  // PDF生成
    }(v)
  }

  wg.Wait()

  // 全てのPDFが生成されたことを確認しています
  for _, builder := range builders {
    _, err := os.Stat(builder.Name()); if err != nil {
      return nil, err
    }
    fileNames = append(fileNames, builder.Name())
  }

  return fileNames, nil
}

func mergePDF(fileNames []string) (mergePDFName string, err error) {
  mergePDFName = "merge.pdf"

  mergeCommand := append(fileNames, "cat", "output", mergePDFName)
  // exec.Command を利用して pdftk のコマンドを実行しています
  err = exec.Command("pdftk", mergeCommand...).Run(); if err != nil {
    return "", err
  }

  return mergeFileName, nil
}

// 各ページごとに生成したPDFを削除
func deleteTmpFile(fileNames []string) error {
  for _, v := range fileNames {
    if err = os.Remove(v); err != nil {
      return err
    }
  }
  return nil
}
// WEBページへアクセスし、PDFとして保存するロジックをまとめる
package print

func Screenshot(path string, output string) error {
  // chromedb を実行する
  ctx, cancel := chromedp.NewContext(context.Background())
  defer cancel()

  // chrome で実行したいタスクを走らせる
  var buf []byte
  err := chromedp.Run(ctx, createTask("URL"+ path, `div`, &buf)); if err != nil {
    return err
  }

  // 指定した名前でファイルを作成する
  file, err := os.Create(output); if err != nil {
    return err
  }
  defer file.Close()

  writer := bufio.NewWriter(file)
  writer.Write(buf)
  writer.Flush()
}

func createTask(url string, sel string, res *[]byte) chromedp.Tasks {
  return chromedp.Tasks{
    chromedp.Navigate(url), // URL へアクセスして
    chromedp.WaitVisible(sel, chromedp.ByQuery), // 表示されるのを待って
    printToPDF(res), // pdf として保存する
  }
}

// chromeでPDFとして保存させる処理
func printToPDF(pdfbuf *[]byte) chromedp.Action {
  return chromedp.ActionFunc(func(ctx context.Context) error {
    buf, err := page.PrintToPDF().WithPrintBackground(true).Do(ctx)
    if err != nil {
      return err
    }
    *pdfbuf = buf
    return nil
  })
}
// PDF 生成を担当する構造体
package builder

// WebページからPDFを生成機能を持ったインターフェース
type PDFBuilder interface {
  Exec()
  Name() string
}

// 以下のように、生成したい Path を持った構造体を作る
type Sample struct {
  Path string
}

func (service *Sample) Exec() {
  print.Screenshot(service.Path, service.Name())
}

func (service *Sample) Name() string {
  return service.Path + ".pdf"
}

完成イメージ

完成形は、以下のような処理の流れになります!

f:id:tobb422:20190716021116p:plain
完成イメージ

まとめ

PDF 生成を Webページから行う方法についてまとめてみました
今回は、go で実装しましたが、 chrome 操作を簡単に行うことができれば言語は問わないと思います。
最後、PDF に変換されるため、レイアウトをうまく調整することが難しかったりもしたのですが、
PaperCSS といったツールを使えば、その辺りもうまくできそうです。
(※ 私はゴリッと生のCSSでコーディングしたので、テキトーなことを言っています)
HTML + CSS の構成なので、デザインの変更は簡単に行なえますし、
動的なデータもWebサーバーを利用して簡単に生成可能です!
(今回、例では出していませんが実際はクエリを利用して、動的なデータの表示や生成するページを切り替えるといったことも可能です!)
ゴルーチンを利用すれば、一定の速度を保って簡単にスケールできる環境も作れると思うので
大量のPDF生成にお困りの方にはおすすめです!(← そもそもここの需要が少なそうw)

チームの開発速度をあげる

はじめに

one visa というスタートアップ企業にてエンジニアをしています。
開発チームを発足するということになり、最初はわたし1人の開発チーム? だったところから
現在は、5人のエンジニアで開発を進めるまでにチームが大きくなってきました。
β版プロダクトから正式版リリースに向け、チーム一丸となって開発を進めており、
より一層スピード感を持って開発できるよう日々試行錯誤しています。
今回は、「スピードをあげるために何が必要かと」いままで考えたことをまとめておこうと思います。

チームの開発速度を上げる

チームの開発速度を上げるためには、以下2点を実現する必要があると考えました。

  • 同時に実装できる機能を増やす
  • それぞれのリードタイム(企画 ~ 実装, リリース)を短くする

以降、上記2つを実現する方法を考えます。

同時に実装できる機能を増やす

現在、開発チームでは、ストーリー毎に開発を進めており
(たとえば、ユーザーがサインアップできるといったイメージ)
要は、同時にこなすこのストーリーの数を増やせば良いと考えています。

  • 新たに開発者を増やす
  • 既存の開発者が分身する

これまでの経験から、同一人物が同時に複数のストーリーをこなすのはあまり現実的ではないと考えており
並行するストーリーを増やすためには、新たに開発者を増やすことが必要だと考えています。
(絶賛、採用中ですwよろしくおねがいします!)
ただ開発者を増やせば良いというわけではなく、
プロジェクトを任せて要件定義〜実装, リリースと一貫して担当していただける方を採用しなければなりません。
逆に言うと、ストーリー数を増やすために採用するので、
人物像(スキルセット)は明確になり、ターゲットは絞りやすいです。
採用して、同時進行できるストーリー数が増やせなければ意味がないので、採用をゴールにしないよう心がけています。

また、新たにエンジニアがジョインしてから、自走できるまでに1年を費やすようでは、
結局開発速度をあげることにはつながっていないので、
自走できるようになるまでのコストを低くしていくことも必要だと考えています。
たとえば、

  • チーム思想が明確にあり、意思決定の指針となっている
  • ドキュメントが充実している

といったことが満たされていれば、自走までのキャッチアップもしやすいと思います。

それぞれのリードタイム(企画 ~ 実装, リリース)を短くする

1ストーリー辺りにかかる時間が短くなれば、プロジェクトは全体的に前倒しになるため
リードタイムを短くできるよう心がけています。
もちろん、担当者のレベルに依存してしまう場合もありますが、
仕組み化を進めて、あらゆる面でチューニング可能だと考えています。
たとえば、以下はチューニング可能な項目です。

  • 変更・拡張しやすいアプリケーション設計になっている
  • ドキュメントが書かれており、属人化していない
  • タスクのゴールが明文化されている
  • CI/CDが自動化されている
  • チームの思想やバリューが明文化されていて、意思決定時の指針がある
  • MTGを集約して、開発にまとまった時間を確保できるようにする

上記のようなことが実現できていると、要件定義, 実装, もちろん実際のデプロイ時間など
あらゆる工程を少しずつ短くでき、トータルでかかる時間は大幅に短縮されると考えています。
漠然と開発速度あげろといわれると施策が思いつかなかったですが、
各工程を割り出して、それぞれのリードタイムを短くするためには?
と考えることで、より具体的な施策へ繋げらることができたと思います。
(今後、具体的にどういったことに取り組んでいるかもブログで紹介できるといいなぁとおもっています。)

まとめ

チームの開発速度を上げるためには、

  • 同時に実装できる機能を増やす
  • それぞれのリードタイム(企画 ~ 実装, リリース)を短くする

ことが必要ではないかと考えています。 そこから下位分析をしていくと、

  • ドキュメントをしっかり書く
  • チームのバリューを明確にする

といった割とよく言われているプラクティスにつながりました。

開発チームとしては、クライアントに継続的に価値を提供し続けられる開発体制を作るべきであり、
その際に、開発速度は非常に重要な指標ではないかと考えています。
ここは、根性論で乗り切ることは無理で、リソースが限られているスタートアップだからこそ よりハックしていかなければいけないのではないかなぁ〜と日々思っている今日このごろでした。

(最近ブログ更新をサボっていて、久々に書くとうまく文章かけない。)

SPAで利用するAPI設計を考える...

はじめに

ここ 1, 2年は、SPA + API という構成 Web アプリケーションを開発することが多く、
その度に、毎回悩み・後悔しているのですが...
書籍や Web 上に公開されている記事を読んだり、チームで API を設計・開発していく中で
少しずつではありますが、考え方がまとまってきたので、
未来の自分のためにも一度整理しておきます。
※ こちらのメモでは、チームで API を開発することを想定しています。

先人に学ぶ

Web API: The Good Parts では、
設計の美しい WebAPI について以下のような要素を挙げていました。

  • 使いやすい
  • 変更しやすい
  • 頑強である
  • 恥ずかしくない

また、APIデザインの極意 Java/NetBeansアーキテクト探究ノート では、
APIの品質検査方法として、以下を挙げています。
[補足]
こちらの書籍は、公開APIを前提にしているケース(LSUDs)が多く、
今回議題にあげているような、クローズドな Web API (SSKDs)を想定しているわけではないのですが、
いずれにせよ、参考にできることは非常に多いと、私は考えています。

  • 理解しやすさ
  • 一貫性
  • 発見できること
  • 局所性

(※ 詳しくは、書籍をご覧ください。)

上記の項目や、他の Web上で公開されている記事, チーム開発を通して、
現在、私の中では一貫性のある API がホットワードになっています。
一貫性が存在することで、特に理解しやすさ, 使いやすさは向上するのではないか
と考えおり、チーム開発において、ハッピーになれる人が増えると思っています。

一貫性があるAPIを開発するために...

一貫性を確保することは、非常に難易度が高いです、、
そして、チームの人数(関係者)が増えれば増えるほど、運用が難しいように思えます。
チーム開発をしていく中で、一貫性がある API を開発するために心がけていることをメモしておきます。

  • なるべく Web の標準に従う
  • クライアントを意識したリソースを定義する
  • ドキュメントを書く(利用者向け & 開発者向け)

詳しく追っていきます。

なるべく Web の標準に従う

結論からいうと、Web 標準というよりも、REST に従うことを意識するのが良いかなと思っています。

  • URL で、スコープ(どんなリソースか)を定義し
  • HTTP メソッドで、アクション(何をするか)を定義し
  • 統一された リソース を返す

上記に従う形で、設計をするのが良いのではないかと考えています。

クライアントを意識したリソースを定義する

上記の RESTful な API 設計にも重なる項目なのですが、
「統一されたリソース」は、必ずしも、バックエンドが知っている「モデル(= エンティティ)」と同じではないと考えています。
ここが、SPA で利用する API 設計らしさかなと思うのですが、(よく BFF として語られているようにも思います。)
今回想定しているような API 設計では、SPA を意識したもので良くて、
SPA で扱いやすい単位に、各エンティティを集約することが重要な気がします。
「1つのリソース = 1つのモデル」ではない
ということです。
私は、この集約する層をドメイン駆動設計にちなんで、Repository 層と呼ぶことが多いです。

f:id:tobb422:20190514092034p:plain
Repository層

あらゆるエンティティをもう少し SPA で利用しやすい単位にまとめて返すことが良いと考えています。
ただし、各ページに依存してしまうのは良くないので、難しいところです。
リソースを定義するために、
フロントエンドチームと議論しながら、そのアプリケーションで扱うドメインの定義を行っていく必要があると思います。

ドキュメントを書く(利用者向け & 開発者向け)

利用者向けのドキュメントでは、
主に、公開しているエンドポイント、そして定義されているリソース
を書いていくようにしています。
最近、Nest.js という FW を使って、個人アプリを作ることが多いのですが、
その場合は、Swagger のライブラリを導入することで自動でドキュメントを出力してくれます。
色んな FW で同じような仕組みを取り入れていると思うので、そちらを利用するのが便利だと思います。

開発者向けのドキュメントでは、
命名規則や、アンチパターンなどをまとめておくようにしています。
リソースの定義も書いておくと良いかもしれません。
一貫性を確保には、メンバー間での調整がかなり必要で
調整をなくすためにも、明文化できるものは全て記載しておくくらいでも良いかなと思っています。
また、こちらはドキュメントというよりも設計全般の話ですが、
開発者は、全てを知らなくても良いコーディング方法を見つけられる(選択的無知)
を意識して置くことも必須です。

まとめ

SPA で利用する API 設計の勘所として、一貫性がある API をテーマにしました。
そして、一貫性がある API を運用する要素として、以下の要素挙げました。

  • なるべく Web 標準に従う(RESTful な API
  • クライアントを意識したリソースを定義する
  • ドキュメントを書く(利用者向け & 開発者向け)

もちろん、これが正解というわけではなく、1つの考え方として思ってもらえると幸いです。

参考にした書籍・Webページ

[書籍]
Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)
Web API: The Good Parts
APIデザインの極意 Java/NetBeansアーキテクト探究ノート
エリック・エヴァンスのドメイン駆動設計

[Webページ]
翻訳: WebAPI 設計のベストプラクティス
ぎんざRuby会議01にて、「マイクロサービス指向 Rails API 開発ガイド」という発表をしました