やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirからpythonを呼び出してJanomeで形態素解析してみた

ErlPortなるものを発見

色々とネットサーフィンしてたらErlPortというErlangのライブラリを発見
なんとErlang環境からpythonRubyを実行できる模様...凄すぎる
ErlangのモジュールはElixirから呼び出し可能なので勝ちました

前回の記事で作成したparser関数を(janomeを使った形態素解析)Elixirから呼び出すことを目標にErlPortを触ってみます

www.okb-shelf.work

新規プロジェクトの立ち上げ

mix使って新規プロジェクトを立ち上げます
mixについてざっくり以下の記事で触れているのでよろしければご覧ください

さっそくプロジェクトを立ち上げます

> mix new call_python
> cd call_python

ErlPortをインストールするようにmix.exsに記述します
./call_python/mix.exs

 defp deps do
    [
      {:erlport, "~> 0.9"} , # <-- ここに追加
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end

以下のコマンドで外部ライブラリをダウンロードします
コンパイルに関しては自動でやってくれるのでスルーします

> mix deps.get

これでプロジェクトの準備は完了

.pyファイルの用意

今回はプロジェクトディレクトリの直下にpython_filesというディレクトリを置き
そこに.pyファイルを配置します
このパスがErlPortを使用する際に必要となります

で、ここに前回記述したparser関数を書いときます
ついでにチュートリアル的な引数を持たない最もシンプルなhello関数も作っときます
./call_python/python_files/janome_parser.py

from janome.tokenizer import Tokenizer

def parser(value_str, tag=u"名詞"):
    #elixirから文字列を受け取る際にはutf-8へのdecodeが必要
    decode_from_elixir = value_str.decode("utf-8")
    t = Tokenizer()
    res = t.tokenize(decode_from_elixir)
    if isinstance(tag, list):
        return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
        return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

def hello():
  print("hello world")

pythonファイルでの準備はこれでOKです

Elixir側から呼び出す

ではついにpythonで作成した関数を呼び出しましょう
公式ドキュメントを見るとこんな記述があります

1> {ok, PythonInstance1} = python:start().
{ok,<0.34.0>}
2> {ok, PythonInstance2} = python:start().
{ok,<0.36.0>}
3> python:stop(PythonInstance1)
ok
4> python:stop(PythonInstance2)
ok

python:start()というメゾットを呼び出すことでpython実行環境を持つプロセスを作成しているようです
これはErlangの記述なのでElixirの場合は

:python.start()

とします。またstart()にディレクトリのパスをリストで渡すことが可能です
バイナリで渡す必要があるので注意
他言語の癖でうっかり「"」を使うとエラーになります

:python.start([python_path: 'python_files'])

この作成したプロセスをpython.call()メゾットに渡すことで
.pyファイルに記述された関数をcallできる模様(公式より

2> python:call(P, operator, add, [2, 2]).
4

引数について

  • 1st: 生成したpython実行環境のプロセス
  • 2nd: 対象のファイル名
  • 3rd: ファイル内部の関数名
  • 4th: 与えたい引数(リストに格納)

先ほど作成したhello関数を対象に呼び出してみます

## プロセスを生成
iex(1)> {:ok, py_process} = :python.start([python_path: 'python_files'])
{:ok, #PID<0.171.0>}

## 生成したプロセスを使用してpython実装のhello関数を呼び出す
iex(2)> :python.call(py_process, :janome_parser, :hello, [])
hello world #無事に出力された!
:undefined #returnがないのでundefined

公式の通り、python.stop()でプロセスを終了させます

iex(3)> python.stop(py_process)
ok

.pyの編集が反映されない時

.pyファイルに編集を加えた際にstartでパスを渡している場合にはプロセスを最生成する必要があるようです
編集分が反映されない時はもう一度、python.start()を呼び出します

.pyファイルを編集

def hello():
  print("hello world")
  return 777

呼び出すも編集分が反映されていない

iex(4)> :python.call(py_process, :janome_parser, :hello, [])
hello world
:undefined

再度、プロセスを生成してhello()を呼び出すと反映されている

iex(6)> {:ok, py_process} = :python.start([python_path: 'python_files'])
{:ok, #PID<0.180.0>}
iex(7)> :python.call(py_process, :janome_parser, :hello, [])
hello world
777

コマンドで叩いていたものを関数化しておきましょう

def call_hello_func_in_python do
    {:ok, py_process} = :python.start([python_path: 'python_files'])
    res = :python.call(py_process, :janome_parser, :hello, [])
    IO.puts("result: #{res}")
    :python.stop(py_process)
    :ok
end

recompileをして上記の関数を呼び出してみます

iex(8)> recompile
Compiling 1 file (.ex)

iex(9)> CallPython.call_hello_func_in_python()
hello world
result: 777
:ok

良い感じですね
ここまで来たら呼び出す関数を別の物に変えるだけなので余裕
面倒なので最初から関数にしておきます
tagを渡す際に「u"名詞"」と記述しますがElixirからpythonにうまく渡せなかったので
python側でゴリ押ししました

def call_janome(value_str, tag) do
    {:ok, py_process} = :python.start([python_path: 'python_files'])
    result = :python.call(py_process, :janome_parser, :parser, [value_str, tag])
    :python.stop(py_process)
    result
end

./python_files/janome_parser.py

def parser(value_str, tag):
  decode_value_str_from_elixir = value_str.decode("utf-8")
  decode_tag_from_elixir = tag.decode("utf-8")
  #文字列をu-stringに変換する処理を追加
  if encode_tag_from_elixir == "名詞":
    tag = u"名詞"

  t = Tokenizer()
  res = t.tokenize(encode_value_str_from_elixir)
  if isinstance(tag, list):
      return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
  else:
      return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

こいつを実行すると

iex(23)> CallPython.call_janome("何もない午後の入江を往く船をただ見つめていた", "名詞")
[[20309], [21320, 24460], [20837, 27743], [33337]]

おや、戻り値が全てCharlistになってしまっている...
確かErlangにはStringがなくて全てbinaryになったはずなのでElixirでCharlistをStringに変換します

#resultをパイプ使ってStringに変換
result = :python.call(py_process, :janome_parser, :parser, [value_str, tag])
      |> Enum.map(&(to_string(&1)))

さぁ実行してみる

iex(25)> recompile
Compiling 1 file (.ex)
:ok
iex(26)> CallPython.call_janome("何もない午後の入江を往く船をただ見つめていた", "名詞")
["", "午後", "入江", ""]

うまくいきました!!
思ったように動作してくれるようになりました
Charlistの変換のところで割と迷った

まとめ

Elixirからpythonの関数呼ぶのすげー楽なんで流行りそう(流行れ
要はこれをElixirお得意の並列処理でゴリゴリすると...と考えると夢が広がりますね

長くなったので

  • Classの呼び出し
  • python側からAtomを返す

という処理はまた次の記事に書きます(書くとはは言っていない

おまけのコーナー

pythonのやつの記述がゴリ押しすぎたので若干修正
ましになったかも

from janome.tokenizer import Tokenizer

#ここに品詞をかいとけばu-stringに変換してくれる
PART_OF_SPPECH = {
  "名詞": u"名詞",
  "形容詞": u"形容詞",
}

def parser(value_str, tag):
  def decoder(value_str, str_code="utf-8"):
    return value_str.decode(str_code)

  tag = PART_OF_SPPECH.get(decoder(tag))
  t = Tokenizer()
  res = t.tokenize(decoder(value_str))
  if isinstance(tag, list):
      return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
  else:
      return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

【サンプルコード有り】形態素解析をササッと試すならMecabよりもJanomeが良い感じ

Janomeとは

mocobeta.github.io

Pure Python(通常のpythonのみ)で書かれている日本語の形態素解析のためのパッケージです。自然言語処理で主に行われる形態素解析を気軽に行うことが出来ます。そもそも「形態素解析って何?」という方のために形態素解析結果をお見せしますと以下のようになります。

(B'z 今宵月の見える丘により)

手をつないだら行ってみよう

形態素解析の結果

手 名詞,一般,*,*,*,*,手,テ,テ  
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ  
つない 動詞,自立,*,*,五段・ガ行,連用タ接続,つなぐ,ツナイ,ツナイ  
だら  助動詞,*,*,*,特殊・タ,仮定形,だ,ダラ,ダラ  
行っ  動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ  
て 助詞,接続助詞,*,*,*,*,て,テ,テ  
みよ  動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ  
う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ  

Janomeではこのような結果が返ってきます。形態素解析の対象とした文章(今回は「手をつないだら行ってみよう 」)を品詞ごとに区切り、それぞれの品詞の情報を教えてくれると言えば良いでしょうか。自分は古典の活用形を覚えるのが苦手だったので、形態素解析結果のそれぞれの品詞がどのような特性を持つのかは全く分かりませんが、重要なのは形態素解析パッケージによって文章を品詞に分解出来る」という点です。

僕自身は元々、Mecabという形態素解析エンジンものを使っていましたが、必要な外部パッケージのインストール、ユーザー辞書の準備がそこそこ面倒で毎度、環境構築するたびに結構苦しめられました。
そしたら何とpipのみでササッとインストールできるJanomeを教えてもらいまして、開発環境の準備がめちゃ楽になりました。

とはいいつつも、Janomeの内部ではMecabipadicを使っているようなので、僕の苦労をJanomeが負担してくれていることですね。
圧倒的感謝...

Janomeをインストールする

Pythonはインストール済みであり、ver3.4以上を想定しています

(すでにJanomeがインストール済みの方はこの章をすっとばして下さい)

仮想環境を作成するためのpythonの標準パッケージである、venvを使用してプロジェクト仮想環境を作りJanomeをインストールする場合は以下を参照下さい。ローカル環境にグローバルにJanomeをインストールはこの項目を飛ばしてください。特に理由がなければローカル環境を可能な限りクリーンに保つために仮想環境を作成したインストールをオススメします。

venvを使って仮想環境を作成してJanomeをインストール

まずは適当な名前でディレクトリを作成します。

$ mkdir janome_sample
$ ls

janome_sample

作成したディレクトリの内部に移動します。

$ cd janome_sample

venvを使って仮想環境を作成しましょう。.venvの部分は任意の名前で問題ありません。

$ python3 -m venv .venv

無事に仮想環境が作成されたかを確認します。

$ ls -a | grep ".venv"

.venv

仮想環境を立ち上げて、pipを使用して、Janomeをインストールします。

$ source .venv/bin/activate (.venv) $ pip install janome

Collecting janome
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/79/f0/bd7f90806132d7d9d642d418bdc3e870cfdff5947254ea3cab27480983a7/Janome-0.3.10-py2.py3-none-any.whl (21.5MB)
    100% |████████████████████████████████| 21.5MB 53kB/s
Installing collected packages: janome
Successfully installed janome-0.3.10

無事にインストールされたかを確認します。

$ pip freeze

Janome==0.3.10

venvで作成した仮想環境にpip経由でJanomeがインストールされていることを確認出来ました。

ローカル環境にグローバルにインストール

$ pip install janome

or

$ pip3 install janome

たったこれだけ、もう終わり。Mecabが使えるようになるまでのステップと比べると圧倒的に楽です。
qiita.com

形態素解析をするまで

全体のコード(jupyter notebook)をgitに上げていますのでご参照下さい。
github.com

最も基本的な部分のみを試してみます。

# janomeのTokenizerをインポート
from janome.tokenizer import Tokenizer

t = Tokenizer()
res = t.tokenize("手をつないだら行ってみよう")
print(res)

#result: この時点で結果が思った様に表示されない
# [<janome.tokenizer.Token at 0x112151710>,
# :
# :
#  <janome.tokenizer.Token at 0x112151908>]

forを使えば先ほどの様な解析結果を覗くことが出来ます。

for r in res:
    print(r)

#result:
# 手  名詞,一般,*,*,*,*,手,テ,テ
# を  助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
# つない    動詞,自立,*,*,五段・ガ行,連用タ接続,つなぐ,ツナイ,ツナイ
# だら   助動詞,*,*,*,特殊・タ,仮定形,だ,ダラ,ダラ
# 行っ   動詞,自立,*,*,五段・カ行促音便,連用タ接続,行く,イッ,イッ
# て  助詞,接続助詞,*,*,*,*,て,テ,テ
# みよ   動詞,非自立,*,*,一段,未然ウ接続,みる,ミヨ,ミヨ
# う  助動詞,*,*,*,不変化型,基本形,う,ウ,ウ

特定の品詞に該当する単語だけを抽出する

解析結果の配列(先ほどの例でいうres)に対して.part_of_speechメゾットを呼び出すことで、それぞれの単語の品詞を確認する事が出来ます。

res[0].part_of_speech
# '名詞,一般,*,*'

この.part_of_speechの戻り値('名詞,一般,*,*'という情報)はただの文字列なので,でsplitして、内包表記で上手く処理してあげることで各単語の品詞情報だけを取得する処理を記述する事が出来ます。

[f.part_of_speech.split(",")[0] for f in res]
# ['名詞', '助詞', '動詞', '助動詞', '動詞', '助詞', '動詞', '助動詞']

自然言語処理の前処理で頻出するのが名詞だけ取得したい、名詞形容詞だけ取得したいという作業です。特定の品詞だけを取り出しつつ .surfaceメゾットを使って解析結果から該当する品詞の単語の一覧を取得します。

#1つの品詞だけ
[token.surface for token in res if token.part_of_speech.split(",")[0] == u"名詞"]
# ['手']

#複数の品詞
target = [u"名詞", u"動詞"]
[token.surface for token in res if token.part_of_speech.split(",")[0] in target]
# ['手', 'つない', '行っ', 'みよ']

可能か限り、内包表記を使用して記述しているのは少しでも速度を落とさないようにするためです。この手の自然言語処理での前処理は何万というデータに対して行うことが多いため、極力速度が落ちるような記述(通常のfor文など)は避けています。

良い感じですね。使い勝手がいいように関数化しておきましょう。

from janome.tokenizer import Tokenizer

def parser(value_str, tag=u"名詞"):
    t = Tokenizer()
    res = t.tokenize(value_str)
    if isinstance(tag, list):
        return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
        return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
    
parser("手をつないだら行ってみよう")
#result: ['手']

この関数はこのままでは、呼び出されるたびにTokenizerインスタンスを作ってしまい、パフォーマンスが悪そうなので一度だけTokenizerインスタンスを作れば良いように、クラス内の関数にしておきます。

class JanomeParser:
    def __init__(self):
        self.t = Tokenizer()
        
    def parser(self, value_str, tag=u"名詞"):
        res = self.t.tokenize(value_str)
        if isinstance(tag, list):
            return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
        else:
            return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
        
j = JanomeParser()
j.parser("手をつないだら行ってみよう")
#result: ['手']

これでクラス化も完了しました。

ユーザー辞書の作成と読み込み

Mecab.csv形式で作成したファイルを.dic形式にcompileする必要がないため、pandasを使って簡単にユーザー辞書を作成することが出来ます。以下のコードは単語のリスト([]string)からJanome指定の.csv形式のファイルを作成する関数になります。

コピペで使えます~

# pandasがインストールされていない場合はインストールしてください
# > pip install pandas
import pandas pd

def create_user_dic(words, dic_name):
    """
      TASK: 指定された単語群からJanome指定のユーザー辞書.csvファイルを作成する
      words: []string -> 辞書に登録したい単語群
      dic_name: string -> __.csvに該当するファイル名
      return void
    """
    df = words_to_df(words)
    df = to_janome_csv_style(df)
    save_df_to_csv(df, dic_name)

def words_to_df(words):
    """
      TASK: 単語リストをpandas.DataFrame形式に変換する
      wrods: []string -> 対象の単語群
      return pandas.DataFrame
    """
    to_df_list = [[w] for w in words]
    return pd.DataFrame(words, columns=["単語"])

def to_janome_csv_style(df):
    """
      TASK: 読み込まれたdfをjanomeが指定するユーザー辞書.csv形式に変換する
      df: pandas.DataFrame -> 単語リストから生成されたdf
      return pandas.DataFrame
    """
    return df.assign(
    a=df.pipe(lambda df: -1),
    b=df.pipe(lambda df: -1),
    c=df.pipe(lambda df: 1000),
    d=df.pipe(lambda df: "名詞"),
    e=df.pipe(lambda df: "一般"),
    f=df.pipe(lambda df: "*"),
    g=df.pipe(lambda df: "*"),
    h=df.pipe(lambda df: "*"),
    i=df.pipe(lambda df: "*"),
    j=df.pipe(lambda df: df["単語"]),
    k=df.pipe(lambda df: "*"),
    l=df.pipe(lambda df: "*"),
)

def save_df_to_csv(df, file_name):
    """
      TASK: dfを.csv形式で保存する
      df: pandas.DataFrame -> to_janome_csv_style()の戻り値のdf
      file_name: ファイルの保存名(拡張子は含まない)
      return void
    """
    df.to_csv(f"{file_name}.csv", header=False, index=False, encoding="cp932")

使い方は簡単でcreate_user_dicに辞書として登録したいワードとファイル保存名(.csvを除いた)を渡すだけになります。

words = ["果てない思い", "ウルトラソウル"]
create_user_dic(words, "B'z_dic")

関数を実行したカレントディレクトリに.csv形式のファイルが生成されているかと思います。

$ ls

"B'z_dic.csv"

これでユーザー辞書の作成は完了です。

ユーザー辞書の読み込み

ユーザー辞書が読み込まれていない状態だと以下のような結果になります。

# 名詞だけを取得
parser("果てない思い")
# ['思い']

parser("ウルトラソウル")
# ['ウルトラ', 'ソウル']

せっかくユーザー辞書を作成したので読み込むようにしましょう。めちゃくちゃ簡単です。Tokenizerインスタンスの生成時に、ユーザー辞書.csvのパスを引数に指定するだけで読み込まれます。

# 作成したユーザー辞書へのパス
USER_DIC_PATH = "B'z_dic.csv"

# 簡略化のため定数値からユーザー辞書を指定
def parser(value_str, tag=u"名詞"):
    t = Tokenizer(USER_DIC_PATH, udic_enc="cp932")
    res = t.tokenize(value_str)
    if isinstance(tag, list):
        return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
        return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

実行結果

parser("果てない思い")
# ['果てない思い']
parser("ウルトラソウル")
# ['ウルトラソウル']

ユーザー辞書を読み込むように更新して、単語が分割されないようになりました。無事にユーザー辞書の読み込みに成功しているということになります。コード量が多くなってしまうため、ブログの方には貼りませんが、jupyter notebook内にてインスタンス生成時に、ユーザー辞書のパスを指定するものも作成しましたので、よければ覗いて見てください。

github.com

まとめ

Janomeはいかがでしたか。インストールも簡単で使い方もシンプル。特に使用するまでに外部のパッケージをあれこれいれる必要がなく、「形態素解析」の実行にまで非常にスムーズに辿り着くことが出来ます。速度に関してはMecabには劣ってしまうのですが、形態素解析を少し試した時にはJanomeが非常に良いのではないでしょうか。

こちらの記事では紹介していないJanomeの様々な便利機能がまだまだあり、こちらで非常に丁寧に紹介されていますので、ぜひご覧ください。私も非常に勉強させて頂きました。

note.nkmk.me

おまけ: 関数とクラス内関数で実行速度を比較

1万回の実行結果の平均値を比べてみました。コードはjupyter notebookの一番下のところにあります。割と量が多くなってしまったのでリンクを再度、貼っておきます。

実行形式 試行回数 平均値
関数 10,000 0.08182s
クラス内関数 10,000 0.08891s

速度に関しては若干、関数形式の方が勝る結果もありましたが、ほとんど誤差といっても良いかと思います。実際は速度以外にもメモリ使用率の問題などもあるので、一概にこの結果だけからどちらが良いかとは言えませんが、速度上では関数形式の方が速いということが分かりました。

参考文献

cloud-functionにFirebaseの値を取得して返すAPIをデプロイしてみた

前回

www.okb-shelf.work

前回の記事でcloud-functionを使って、元から記述されていたサンプルコードをデプロイするところまで触りました
今回はもう少し面白みのありそうなことをやってみたので記事にしました
こちらの動画を元にやってきます

やること

cloud-functionのセットアップに関しては前の記事をご覧ください
今回はFirebaseのクラウドのDatabaseに保存されている値をGETメゾット使ってAPI経由で取得します
実際の実務でもよくやることなのでcloud-functionだとどれぐらいの手間で作れるのか楽しみですね

APIの作成経験ですが

ぐらいしか触った事がないので最終的にデプロイまでの楽さを比較しようかと思ってます

FirebaseのDatabaseにデータを追加する

Firebaseのプロジェクトページの左メニューの

開発-> Databaseを選択

からDatabaseの編集が可能です

今回はデータを作成(追加)するだけなので

  • コレクションを追加(分かる範囲で適当にネーミング)
  • ドキュメントを追加(同上。適当にフィールドも設定)

こんな感じになってればOKです

f:id:takamizawa46:20190416073608p:plain
Dtabaseにデータを作成完了

cloudのDatabaseからデータをfetchする

特に難しい部分もないので、先にコード全部載せておきます

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'; //adminをimport

//この1行ないとエラーになるので注意
admin.initializeApp();

//Firebaseから特定パスの情報を取得する
export const fetchCityInfomation = functions.https.onRequest((_request, response) => {
  //doc()に取得先のパスを記述
  admin.firestore().doc('cities-weather/japan-data').get()
    //promiseをthen, catchで処理
    .then(snapshot => {
      const data = snapshot.data();
      response.send(data);
    })
    .catch(error => {
      console.log("error: ", error);
      response.status(500).send(error);
    })
})

はい、たったこれだけです。楽すぎやろ〜
クラウドにデプロイする前にローカルで動作を試してみます
ただコマンド3つ叩くのがだるいので.shファイルにしておきます

./functions/local-test.sh

npm run-script lint #tslintを実行(構文チェック)
npm run-script build #tsをjsにbuild
firebase serve --only functions --project cloud-function-114514 #ローカルでrunserverして動作チェック

全てが問題なくsucsessになるとこんな感じでlocalにapiが立ちます

 ❯  bash local-test.sh

> functions@ lint ./Desktop/firecast/functions
> tslint --project tsconfig.json


> functions@ build ./Desktop/firecast/functions
> tsc


=== Serving from './Desktop/firecast'...

i  functions: Preparing to emulate functions.
Warning: You're using Node.js v10.9.0 but Google Cloud Functions only supports v6.11.5.

##ここに注目!!
✔  functions: getWeatherInfo: http://localhost:5000/cloud-function-114514/us-central1/getWeatherInfo
info: User function triggered, starting execution
info: Execution took 1781 ms, user function completed successfully

記述されたエンドポイントを見に行ってみると...

{"city":"tokyo","temp":23}

うまく取得出来てますね

値を抽出する場合に

response.send(data.city);

とするとundifinedの可能性があるでって怒られるので色々試してみる
三項演算子だとこれで通る

.then(snapshot => {
      const data = snapshot.data();
      const returnStr = data !== undefined ? data.city: null;
      response.send(returnStr);
    })

ダセーけど普通にifでもいける

.then(snapshot => {
      const data = snapshot.data();
      if(data !== undefined){
        response.send(data.city);
      }
      response.send(data);
    })

これでbashからshell叩いてエンドポイントを改めて見に行くと

tokyo

となっています。良いですね

localでの動作が確認できたのでデプロイします
どっちかのコマンドでいけます
僕は下じゃないとエラーになりますので...

firebase deploy
firebase deploy --project cloud-function-114514

まとめ: 他のAPI作成と比べて

  • 新規プロジェクトの作成のしやすさ
  • シンプルなモデル(Database)の作成
  • パスの設定
  • デプロイの手軽さ

で圧倒的に楽ですね
ただ大規模アプリには向いていない気もする
まぁ小出しに作ればええやろ?って思想なんだと思います

問題なのは料金と把握しきれるか(共通問題)ぐらいでしょうか
この機能だけcloud-functionで爆速実装なんてのは方針としては良さげですね

【デプロイまでわずか10分】JaveScriptでcloud-functionのデプロイまでを体験した

何かサーバーレスたるものが流行ると聞く

サーバーレスって何やねん
APIなどがcallされた時に動き出して、処理が終了したらまたさよなら
常時サーバーが起動してない。呼ばれた時にパッと現れパッと消える インフラを作る必要なし。コード(関数)のみをデプロイするだけで良いと

僕はインフラの構築が一番苦手なので助かります
コード(関数)の記述に集中できるのでありがたい

CloudFunctionを使うまでの準備

今回はこちらの動画を参考に
Node.js(TypeScript)でサーバーサイドを記述しますが、正直TypeScriptの知識いらないです
Node.jsをちょろっとしか書いた事ないですが問題なく行けました

インストール

Node.jsのインストール
僕はmacを使っているのでbrewコマンドで一発です

> brew install node
> node -v
v10.9.0

その他環境でのインストールについてNode.jsのダウンロードページから
インストーラーをダウンロードして手順に沿ってインストールしていけば良いかと思います

Firebase CLIのインストール
Node.jsをインストールした際にnpmも共にインストールされています

 >  npm -v
6.4.1

npmがインストールされていることを確認した後に「firebase-tools」をインストールします
グローバルインストールしたいので -g というオプションをつけます

> npm install -g firebase-tools

この時点でローカルでの準備は終了です

firebaseにプロジェクトを追加
プランはspark(無料プラン)を選択してFirebaseのコンソールから
新規のプロジェクトを追加をクリックしてこんな感じに設定していきます

f:id:takamizawa46:20190415080809p:plain:w450
Firebaseの新規プロジェクト設定

ロケーションは適当に選んでますが無料プランなのでまぁいいでしょう
これでクラウドでの準備も整いました

Firebaseプロジェクト(ローカル)での作成

ローカルにプロジェクトを作成します Firebaseの認証が通るように、というかマナー的な感じで下記コマンドを叩きログインします

> firebase login

現時点でログインしていない場合はブラウザの新規タブが開きログインしましたで
ってな文言が表示されます

すでにログインされていた場合には

> firebase login
Already logged in as sample@gmail.com

と表示されます
ではログインもしたところで作業ディレクトリしてFirebaseのプロジェクトを開始します

> mkdir sample-functions
> cd sample-functions
> firebase init

と進めているとエラーが...

Error: HTTP Error: 401, Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.

401なので認証が通ってないっぽい。ログインしたはずなのに...
このコマンドで解消されました。再認証することを明示的に伝える必要があるのか

> firebase login --reauth

改めてfirebase initを叩く
矢印(移動)とspace(選択)とenter(決定)で目的を選択する

流れに沿って順に選択していく

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices.
(どのプロジェクトをセットアップします?)
Functions: Configure and deploy Cloud Functions

? Select a default Firebase project for this directory:
(標準の設定を選択しますか?)
[don't setup a default project]

? What language would you like to use to write Cloud Functions?
(言語は何を使いますか?)
TypeScript

? Do you want to use TSLint to catch probable bugs and enforce style? (Tslint(構文チェックライブラリ)を使用しますか?)
Yes

? Do you want to install dependencies with npm now?
(npmの依存関係をインストールしますか?)
Yes

これでプロジェクトの準備は完了です
標準でpackage.jsonにfirebase-adminのfirebase-functionsバージョンが記載されていますが
最新でない場合があるそうなので

> cd functions
> npm install firebase-admin firebase-functions

と叩き、確実に最新バージョンにしておきましょう

いよいよデプロイする(もうデプロイする)

index.ts(もしくはindex.js)にすでにサンプルのコードが用意されているのでこちらを使用します
リクエストを受け取ったら"Hello from Firebase!"を送信する超シンプルな関数です

import * as functions from 'firebase-functions';

export const helloWorld = functions.https.onRequest((request, response) => {
 response.send("Hello from Firebase!");
});

これコードの準備は終了したので後はクラウドのプロジェクトにデプロイするのみです このコマンドを叩くだけです。あっさり

> firebase deploy
> firebase deploy --project projectID(Firebaseの設定に記載されている)

僕の場合、「--project projectID」がないとエラーになったので--projectを付与していますが
firebase deployでもデプロイは通るみたいです

 ❯  firebase deploy --project cloud-function-114514

=== Deploying to 'cloud-function-114514'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint
:
:
✔  Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/cloud-function-114514/overview

わちゃわちゃとよしなにやって頂いてデプロイが無事に完了したようです
一番最後の行にURLが記載されており、今デプロイしたコードがAPI化されこのパスから確認することが可能です

対象パスに飛んでみると

Hello from Firebase!

と表示されているでしょう
エンドポイントまでを自動生成。マジで便利すぎる...
たった10分ばかしでAPIの作成からデプロイまでが終了しました
技術の進歩は恐ろしいですね

次の記事ではFirebaseのDatabaseを使ってもう少しレベルアップしたAPIの作成をやってみます

新卒がconpassで新規コミュニティ作って勉強会開いてみて得られた知見

なんで新規コミュニティを作ったのか

元々は社内で勉強会をちょろっとやってたんですが、中々上手くいかず...
一番しんどかったのは毎回リソースを用意して体験してもらうっていう

「主催者」 -> 「参加者」

一方的な関係が正直なところ苦手です
僕の理想では

「主催者」 <-> 「参加者」

お互いが情報を共有できるような勉強会なんですよね
何か学校の授業みたいでしんどい...
興味のないことを一方的に話されたり、説明されるのしんどい...
ということもあって続かず仕舞いになりました

ただ、前から自分のコミュニティを持ちたいなってことはずっと思ってました
そんな中、たまたまelixirという言語と出会いぼっちでずっと勉強してたんですが

  • 1人でやるのつらい
  • 同じものに興味がある人と繋がりたい
  • 共にインプット、アウトプットしたい

と思いました

色々、方法を考えたんですけど極力お金を削減したかったので
無難に無料で使えるconnpassを選択しました

一番時間がかかったのはコンセプトを決めること

難しかったのは「コンセプト」を明確にすることだったかなと思います
上でちょこっと触れてますが僕が作りたい勉強会ってのはこういう関係

「主催者」 <-> 「参加者」

これを文字に落とし込んで納得してもらうにはどうすればと結構悩みました
プラスでどうすればこのコンセプトが実現できるだろうかと
ざっくりプラン(要件定義)を考えないとおそらく数回で詰みます

清流elixirのコンセプト(20190414現在)

僕自身もelixirについて現状(2019/03/10)全く知識がないので(プログラミグelixirを半分読んだぐらい)
こちら(開催側)がリソースを用意して勉強してもらうという一般的な勉強会コミュニティやもくもく会ではありません
「勉強会って敷居が高いし、前提知識が必要じゃん...」と懸念する心配はなく、共にゼロから学ぶことを目標にしています
すでにelixirにお詳しい方は僕のような雑魚にアウトプットしてさらに強くなって頂ければと思います
開催者と参加者が講師と生徒のような関係ではなく互いにジャンキーに知識を共有しましょう!!

強調したかった点

  • 自分自身もほとんど初心者であること
  • 一方通行の勉強ではないこと(参加者にもアウトプットの可能性がある)
  • 初心者さよならフィルターを作らない
  • 強い人が来ても問題ない

勉強会自体はまだ2回しか開催できてませんが
自分では出来としては悪くなかったと思ってます
このコンセプトを元に実際に勉強会をやって反省した点は
「最初の動き出し部分はある程度決めておく必要があること」です

毎回、集まって勉強会をやるにしても何をテーマとしてその日のゴールを何にするのかは
最低限決めておく必要があると思いました
そうしないといつまでたっても勉強時間がスタートしないです

ダメな例: パイプ演算子を触る(どう触るのかを決める必要がある->何がゴール??)
goodな例: パイプ演算子を触って配列データをいじくる(何がゴールかがある程度決まっている)

2回目からは

  • 前回こういうの分からなかった
  • ここの部分をもっと知りたい

というフィードバックがもらえるので気持ちは楽です
そういった意味で初回の勉強会のテーマの重要性を身に染みて感じます

コンセプト & 初回テーマ が動きやすさを決める要因でしょう

勉強会を開くまでのステップ

こんな感じでコンセプトさえ決めてしまえばあとは簡単です
connpassでグループを作成してイベントをグループ主催で開催するのみです
公式ページのこちらに詳しく記述されているのでこの手順に沿って文字を打ってくだけ

一番困るのは開催場所だと思います
自前のスペースや施設があれば問題ありませんが、僕のようなものにはそんなものありません
いつも僕が使っているのは
instabaseというサービスです

全国各地のレンタルスペースをクレカ1枚で予約可能です
地域にもよりますが愛知だと1時間500円行くか行かないぐらいで割とリーズナブルで
朝昼の時間帯はもっと安いです

おめでとうございます。この時点で

  • 自分のコミュニティの作成
  • 勉強会の開催

については完了しています
あとは...

人はどうやって集めるのか

ここの知見は現状ほとんどないので偉そうな事は言えませんが1つ実感したことがあります
SNSの効果は絶大だということです

以前のブログを運営していた時は恥ずかしいからtwitterに記事をupしたぜって通知ツイートはやってなかったんですけど
試しにconnpassで勉強会開きましたってツイートすると
めっちゃ見られてる... 僕のこの時点でのフォロワーは250人ぐらいです

f:id:takamizawa46:20190414111345p:plain:w400
実際のツイートアクティビティ

このツイートがあるかないかだけで300人近くの人に見てもらえるかどうかが変わると考えると凄い

知り合いにも声をかけられました
「何か勉強会開くらしいじゃん?」って感じで
本当にみんな意外と見てるんだなと痛感

SNSの力は本当に偉大です
もっと集客できるようなノウハウは身につけていきたいです

まとめ

「割とノリで何とかなる」この一言につきる
年齢とかキャリアとか関係なしにやるかやらないかですね

やってれば自然と見てもらえるし人も来るということに気づきました
迷ってるならやった方がいいです