20191008のdockerに関する記事は9件です。

Flaskでツイート自動生成プログラム作ってみた

動機

LSTMで文書の自動生成を行う簡易なWebアプリを作成しました。
業務で自然言語処理を行うっていますが、趣味で何か目に見える楽しいものを作りたいなと考え作成に至りました。

1. 環境

  • Docker
  • Python3.6
  • Flask
  • PyTorch

githubレポジトリはこちら

2. Dockerfile

Dockerfile
FROM ubuntu:16.04

RUN apt-get update \
    && apt-get install -y mecab \
    && apt-get install -y libmecab-dev \
    && apt-get install -y mecab-ipadic-utf8\
    && apt-get install -y git\
    && apt-get install -y make\
    && apt-get install -y curl\
    && apt-get install -y xz-utils\
    && apt-get install -y file\
    && apt-get install -y sudo\
    && apt-get install -y wget

RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git\
    && cd mecab-ipadic-neologd\
    && bin/install-mecab-ipadic-neologd -n -y

RUN apt-get install -y software-properties-common vim
RUN add-apt-repository ppa:jonathonf/python-3.6
RUN apt-get update

RUN apt-get install -y build-essential python3.6 python3.6-dev python3-pip python3.6-venv
RUN python3.6 -m pip install pip --upgrade

RUN pip install flask
RUN pip install numpy
RUN pip install pandas
RUN pip install sklearn
RUN pip install gensim
RUN pip install mecab-python3
RUN pip install cloudpickle
RUN pip install torch

RUN export LC_ALL=C.UTF-8
RUN export LANG=C.UTF-8

EXPOSE 5000

MeCab Neologd, その他必要なライブラリのインストールやFlaskに必要な環境変数の設定、ポート開放など行っています。

$cd path/to/directory
$docker build -t flask-app:1.0 .

Dockerコンテナを起動する際の注意はポートを5000で開放することです。Flaskのデフォルト開放ポートが5000のためホストとコンテナで5000ポートを接続しました。

$docker run -it -p 5000:5000 -v path/to/directory:/flask-app flask-app:1.0

Dockerコンテナの起動を完了しました。

3. FlaskでWebアプリ作成

app.py
import cloudpickle
import torch
import LSTM_model
from flask import Flask, render_template, request

app=Flask(__name__)
app.debug = True

@app.route("/", methods=["GET"])
def index():
    return render_template("index.html", message="最初のメッセージを入力してね!")

@app.route("/", methods=["POST"])
def form():
    with open('data/model.pkl', 'rb') as f:
        model = cloudpickle.load(f)
    field=request.form["field"]
    maked_words = LSTM_model.generate_seq(model, start_phase=field, length=20)
    return render_template("index.html", message=maked_words)

if __name__ == '__main__':
    app.run(host='0.0.0.0')

GET, POSTで入力した文章を推論、出力を行なっています。
私がハマったポイントとしてはVMでのポート開放です。

app.run(host='0.0.0.0')

ここでhost="0.0.0.0"としておかないとアクセスできずにハマるためご注意を(あまりFlaskの仕様に慣れていませんでした)

index.html
<!doctype html>
<html lang="ja">
  <head>
    <title>tweet_maker</title>
    <meta charset='utf-8'>
  </head>
  <body>
    <h1>tweet_maker</h1>
    <p>{{message}}</p>
    <div>
      <form method="post" action="/">
        <input type="text" name="field">
        <input type="submit">
      </form>
    </div>
  </body>
</html>
LSTM_model.py
import re
import pickle
import torch
import torch.nn as nn
import MeCab

class SequenceGenerationNet(nn.Module):
  def __init__(self, num_embeddings, embedding_dim=50, hidden_size=50, num_layers=1, dropout=0.2):
    super().__init__()
    self.emb=nn.Embedding(num_embeddings, embedding_dim)
    self.lstm=nn.LSTM(embedding_dim, hidden_size, num_layers, batch_first=True, dropout=dropout)

    self.linear=nn.Linear(hidden_size, num_embeddings)

  def forward(self, x, h0=None):
    x=self.emb(x)
    x, h =self.lstm(x, h0)
    x=self.linear(x)
    return x, h

def make_wakati(sentence):
  tagger = MeCab.Tagger("-Owakati -d /usr/lib/mecab/dic/mecab-ipadic-neologd")
  sentence = sentence.replace(",\n", " ")
  # MeCabで分かち書き
  sentence = tagger.parse(sentence)
  # 半角全角英数字除去
  sentence = re.sub(r'[0-90-9]+', "0", sentence)
  sentence.translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))
  # 記号もろもろ除去
  sentence = re.sub(r'[\._-―─!@#$%^&\-‐|\\*\“()_■×+α※÷⇒—●★☆〇◎◆▼◇△□(:〜~+=)/*&^%$#@!~`){}[]…\[\]\"\'\”\’:;<>?<>〔〕〈〉?、。・,\./『』【】「」→←○《》≪≫\n\u3000]+', "", sentence)
  # スペースで区切って形態素の配列へ
  wakati = sentence.split(" ")
  # 空の要素は削除
  wakati = list(filter(("").__ne__, wakati))
  return wakati

def sentence2index(sentences):
  wakati = make_wakati(sentences)
  with open("data/w2i.pkl", "rb")as data:
    word2index = pickle.load(data)
  id_stc = [word2index[i] for i in make_wakati(sentences)]
  return id_stc

def generate_seq(net, start_phase="私は", length=200, temperature=0.8, device="cpu"):
  net.eval()
  result=[]

  start_tensor=torch.tensor(
      sentence2index(start_phase), dtype=torch.int64
  ).to(device)

  x0=start_tensor.unsqueeze(0)
  o, h=net(x0)
  out_dist=o[:, -1].view(-1).exp()
  top_i=torch.multinomial(out_dist, 1)[0]
  result.append(top_i)

  for i in range(length):
    inp=torch.tensor([[top_i]], dtype=torch.int64)
    inp=inp.to(device)
    o, h=net(inp, h)
    out_dist=o.view(-1).exp()
    top_i=torch.multinomial(out_dist, 1)[0]
    result.append(top_i)
  with open("data/i2w.pkl", "rb")as data:
    index2word = pickle.load(data)
  res = "".join([index2word[int(i.to("cpu").numpy())] for i in result])
  return start_phase+ res

LSTMの推論はこちらで行なっています。学習データはgithubにアップしているためそちらを参照ください。コード中のi2wは単語とインデックスの関係を表すpickleファイルです。

4. 動かしてみる

flask run --host 0.0.0.0

上記コードを入力後のURLにアクセスすると動作します。

スクリーンショット 2019-10-08 16.45.25.png
実際の画面はこんな感じです。興味ある方は是非git cloneして遊んで見てください。

参考にさせていただいた記事

こちらの記事を参考にさせていただきました。
https://qiita.com/oreyutarover/items/909d614ca3b48d2c9e16

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flaskでツイート自動生成Webアプリ作ってみた

動機

LSTMで文書の自動生成を行う簡易なWebアプリを作成しました。
業務で自然言語処理を行うっていますが、趣味で何か目に見える楽しいものを作りたいなと考え作成に至りました。

1. 環境

  • Docker
  • Python3.6
  • Flask
  • PyTorch

githubレポジトリはこちら

2. Dockerfile

Dockerfile
FROM ubuntu:16.04

RUN apt-get update \
    && apt-get install -y mecab \
    && apt-get install -y libmecab-dev \
    && apt-get install -y mecab-ipadic-utf8\
    && apt-get install -y git\
    && apt-get install -y make\
    && apt-get install -y curl\
    && apt-get install -y xz-utils\
    && apt-get install -y file\
    && apt-get install -y sudo\
    && apt-get install -y wget

RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git\
    && cd mecab-ipadic-neologd\
    && bin/install-mecab-ipadic-neologd -n -y

RUN apt-get install -y software-properties-common vim
RUN add-apt-repository ppa:jonathonf/python-3.6
RUN apt-get update

RUN apt-get install -y build-essential python3.6 python3.6-dev python3-pip python3.6-venv
RUN python3.6 -m pip install pip --upgrade

RUN pip install flask
RUN pip install numpy
RUN pip install pandas
RUN pip install sklearn
RUN pip install gensim
RUN pip install mecab-python3
RUN pip install cloudpickle
RUN pip install torch

RUN export LC_ALL=C.UTF-8
RUN export LANG=C.UTF-8

EXPOSE 5000

MeCab Neologd, その他必要なライブラリのインストールやFlaskに必要な環境変数の設定、ポート開放など行っています。

$cd path/to/directory
$docker build -t flask-app:1.0 .

Dockerコンテナを起動する際の注意はポートを5000で開放することです。Flaskのデフォルト開放ポートが5000のためホストとコンテナで5000ポートを接続しました。

$docker run -it -p 5000:5000 -v path/to/directory:/flask-app flask-app:1.0

Dockerコンテナの起動を完了しました。

3. FlaskでWebアプリ作成

app.py
import cloudpickle
import torch
import LSTM_model
from flask import Flask, render_template, request

app=Flask(__name__)
app.debug = True

@app.route("/", methods=["GET"])
def index():
    return render_template("index.html", message="最初のメッセージを入力してね!")

@app.route("/", methods=["POST"])
def form():
    with open('data/model.pkl', 'rb') as f:
        model = cloudpickle.load(f)
    field=request.form["field"]
    maked_words = LSTM_model.generate_seq(model, start_phase=field, length=20)
    return render_template("index.html", message=maked_words)

if __name__ == '__main__':
    app.run(host='0.0.0.0')

GET, POSTで入力した文章を推論、出力を行なっています。
私がハマったポイントとしてはVMでのポート開放です。

app.run(host='0.0.0.0')

ここでhost="0.0.0.0"としておかないとアクセスできずにハマるためご注意を(あまりFlaskの仕様に慣れていませんでした)

index.html
<!doctype html>
<html lang="ja">
  <head>
    <title>tweet_maker</title>
    <meta charset='utf-8'>
  </head>
  <body>
    <h1>tweet_maker</h1>
    <p>{{message}}</p>
    <div>
      <form method="post" action="/">
        <input type="text" name="field">
        <input type="submit">
      </form>
    </div>
  </body>
</html>
LSTM_model.py
import re
import pickle
import torch
import torch.nn as nn
import MeCab

class SequenceGenerationNet(nn.Module):
  def __init__(self, num_embeddings, embedding_dim=50, hidden_size=50, num_layers=1, dropout=0.2):
    super().__init__()
    self.emb=nn.Embedding(num_embeddings, embedding_dim)
    self.lstm=nn.LSTM(embedding_dim, hidden_size, num_layers, batch_first=True, dropout=dropout)

    self.linear=nn.Linear(hidden_size, num_embeddings)

  def forward(self, x, h0=None):
    x=self.emb(x)
    x, h =self.lstm(x, h0)
    x=self.linear(x)
    return x, h

def make_wakati(sentence):
  tagger = MeCab.Tagger("-Owakati -d /usr/lib/mecab/dic/mecab-ipadic-neologd")
  sentence = sentence.replace(",\n", " ")
  # MeCabで分かち書き
  sentence = tagger.parse(sentence)
  # 半角全角英数字除去
  sentence = re.sub(r'[0-90-9]+', "0", sentence)
  sentence.translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))
  # 記号もろもろ除去
  sentence = re.sub(r'[\._-―─!@#$%^&\-‐|\\*\“()_■×+α※÷⇒—●★☆〇◎◆▼◇△□(:〜~+=)/*&^%$#@!~`){}[]…\[\]\"\'\”\’:;<>?<>〔〕〈〉?、。・,\./『』【】「」→←○《》≪≫\n\u3000]+', "", sentence)
  # スペースで区切って形態素の配列へ
  wakati = sentence.split(" ")
  # 空の要素は削除
  wakati = list(filter(("").__ne__, wakati))
  return wakati

def sentence2index(sentences):
  wakati = make_wakati(sentences)
  with open("data/w2i.pkl", "rb")as data:
    word2index = pickle.load(data)
  id_stc = [word2index[i] for i in make_wakati(sentences)]
  return id_stc

def generate_seq(net, start_phase="私は", length=200, temperature=0.8, device="cpu"):
  net.eval()
  result=[]

  start_tensor=torch.tensor(
      sentence2index(start_phase), dtype=torch.int64
  ).to(device)

  x0=start_tensor.unsqueeze(0)
  o, h=net(x0)
  out_dist=o[:, -1].view(-1).exp()
  top_i=torch.multinomial(out_dist, 1)[0]
  result.append(top_i)

  for i in range(length):
    inp=torch.tensor([[top_i]], dtype=torch.int64)
    inp=inp.to(device)
    o, h=net(inp, h)
    out_dist=o.view(-1).exp()
    top_i=torch.multinomial(out_dist, 1)[0]
    result.append(top_i)
  with open("data/i2w.pkl", "rb")as data:
    index2word = pickle.load(data)
  res = "".join([index2word[int(i.to("cpu").numpy())] for i in result])
  return start_phase+ res

LSTMの推論はこちらで行なっています。学習データはgithubにアップしているためそちらを参照ください。コード中のi2wは単語とインデックスの関係を表すpickleファイルです。

4. 動かしてみる

flask run --host 0.0.0.0

上記コードを入力後のURLにアクセスすると動作します。

スクリーンショット 2019-10-08 16.45.25.png
実際の画面はこんな感じです。興味ある方は是非git cloneして遊んで見てください。

参考にさせていただいた記事

こちらの記事を参考にさせていただきました。
https://qiita.com/oreyutarover/items/909d614ca3b48d2c9e16

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerでkotlinをcliでコンパイル、javaで実行できる環境をつくった

はじめてkotlinを勉強しようと思ったのですが、macにkotlinコンパイラをいれるのも少し嫌だったので、docker composeで一式つくりました。
ビルドツールを使用することは想定しておらず、kotlinの簡単な文法を動かしながら確認できる環境を用意するというコンセプトのもと作成してます。
必要な方は参考にしてみてください。

できたもの

./docker-compose.yml

version: "3.7"

services: 
  java:
    build: ./docker/java
    tty: true
    volumes:
      - ./src:/usr/local/src

./docker/java/Dockerfile

FROM openjdk:13-slim

RUN apt-get update && apt-get install -y git \
    unzip \
    zip \
    curl \
    vim

WORKDIR /usr/local/src
RUN curl -s https://get.sdkman.io | bash 
RUN /bin/bash -l -c "chmod a+x $HOME/.sdkman/bin/sdkman-init.sh;$HOME/.sdkman/bin/sdkman-init.sh;sdk install kotlin"

CMD ["/bin/bash"]

ディレクトリ構成は以下のようになってます

.
├── docker
│   └── java
│       └── Dockerfile
├── docker-compose.yml
└── src
    └── test.kt

つかいかた

下準備

docker-compose.ymlを配置したディレクトリで、以下のコマンドを実行するのみです。

$ docker-compose build
$ docker-compose up

うごかす

実際に、test.ktをコンパイルして、javaコマンドで実行します。
コンパイルする対象は./src/test.ktです。

fun main(args: Array<String>) {
    println("Hello, World!")
}

コンテナに入って、コンパイルして、実行します。

$ docker-compose exec java bash
root@3b62768acd0b:/usr/local/src# kotlinc test.kt -include-runtime -d test.jar
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
root@3b62768acd0b:/usr/local/src# java -jar test.jar
Hello, World!
root@3b62768acd0b:/usr/local/src#

-Xverify:noneは駄目だというwarningがでてしまいますが、コンパイルができて、javaコマンドで実行することができました。

さらに、/usr/local/srcをボリュームマウントしてますので、コンパイル後にできたjarファイルは、ホストマシンのファイルシステムでも確認可能です。

.
├── docker
│   └── java
│       └── Dockerfile
├── docker-compose.yml
└── src
    ├── test.jar
    └── test.kt

さいごに

これから勉強を進めていく中で、ビルドツール使いたくなったり、いろいろ不都合がでてくるかもしれませんが、一旦環境が整ったということで、どんどん勉強していきたいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker CentOSイメージを使用してPHP-FPMを実行する

CentOSイメージをベースにしてphp-fpmが動作するコンテナを作成します。サンプルです。

リポジトリ
https://github.com/kakizaki/sample_docker_phpfpm_cent

動機

DockerHubにはphp-fpmのための公式のイメージがありますが、ベースはdebianまたはalpineだったと思います。
実際のサーバー環境はCentOSを使用していて、yumによりパッケージをインストールをしています。
コンテナでもyumが使えて、同じように書けると楽です。

使用手順

docker-compose upを実行すると、nginxコンテナとphp_fpmコンテナが起動します。
ブラウザでhttp://localhost/index.phpを表示すると、phpinfoの実行結果が表示されると思います。

はまった点

php-fpmの設定を誤っていたことで、nginxコンテナからphp_fpmコンテナへアクセスできずに、はまってしまいました。
(unixソケットを使用する場合には不要ですが)

php/www.conf
; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
; must be separated by a comma. If this value is left blank, connections will be
; accepted from any ip address.
; Default Value: any
; HACK 外部からのアクセスを許す場合はコメント(any)にするか、IPを指定する
;listen.allowed_clients = 127.0.0.1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-compose up -d を実行したらport is already allocatedのエラーが表示されたときの対処法 

状況

docker-compose up -dを実行すると以下のエラーが発生。
どうやら3306は既に割り当てているものがあるらしい。

ERROR: for qiita_clone_database_1  Cannot start service database: driver failed programming external connectivity on endpoint qiita_clone_database_1 (36532cbe7d470eac9dce4a34198da5ebc15cd86aae371e98c9fb2d0272b6eeb9): Bind for 0.0.0.0:3306 failed: port is already allocated

対処法

sudo lsof -i:ポート番号で指定したポートを使用しているプロセスを確認することができる。
sudo lsof -i:3306で以下のログが表示される。

COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
com.docke 1372 khiro   15u  IPv6 0x68b93b7c3d446421      0t0  TCP *:mysql (LISTEN)

COMMANDの下にあるcom.dockeが実行しているプロセス名。
PIDの1372がプロセス番号を指す。

sudo kill プロセス番号で指定したプロセス番号を削除できるので,今回はsudo kill 1372を実行。

その後、再度docker-compose up -dを実行して以下のような表示がでれば完了。

Creating qiita_clone_database_1 ... done
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】Growi構築でコケたところ

今回は、仕様でwikiが必要になり、Growiという和製wikiに目をつけた。

この参考サイトを参考にして、AWS上にEC2をたて、その中にドッカーを構築。ドッカー内にGrowiを構築する。

参考サイトにあるコマンドに1つだけ、コマンドを追加する。
※EC2の初期設定で、パッケージの最新化を行うyum updateを実行する。

コマンドリストは以下

sudo su -
yum update
yum install -y docker
docker -v
curl -L "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
docker-compose -v
systemctl enable docker
groupadd docker
gpasswd -a $USER docker
systemctl restart docker
exit
sudo su -
yum install -y git
cd /home/ec2-user
git clone https://github.com/weseek/growi-docker-compose.git growi
cd /home/ec2-user/growi
vi docker-compose.yml
docker-compose up -d

ここでこけました。

vi docker-compose.yml

参考サイトでは、ポートの変更に言及していますが、
ポートの変更とパスワードシードを変更しないと、ドッカーの起動時に失敗しました。

また、今回、最初はケチって、t2.microで実行しましたが、
スペックが足りず、t3.smallで実行しないと起動できないことがわかりました。
※気づくのになかなか苦労してしまいました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Debug timezone in Docker

  • Code
package main

import (
        "fmt"
        "github.com/uniplaces/carbon"
)

func main() {
        loc := "Asia/Tokyo"
        now, _ := carbon.NowInLocation(loc)
        today := now.StartOfDay()
        tomorrow := today.AddDay()
        fmt.Println(now, today, tomorrow)
}
$ docker run --rm -it --privileged -v $(pwd):/app golang bash

$ cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

$ date +%T -s "07:00:00"

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerで環境構築しているPCでVagrantの環境構築は厳しい?

タイトルの通りなんですけど・・・・・・

実はDockerで環境構築しているPCにVagrantでVagrant upができなかったお話。

ことの発端は
Vagrantで環境構築→別件でDockerでコンテナを開始しました。
(その時もなんかエラーが出て何だかんだしててで解決しました)

後日、PCでVagrant up後、下記のエラーが発生↓
vagrant.png
調べてみると解決策は出てきます。

Hyper-Vを無効にしなければいけないみたいですね。
https://qiita.com/masoo/items/b73dadb0e99f9be528fe
https://qiita.com/koji9004/items/662eab6617de3533d190

どこを調べても、これ以外の方法が・・・・・

Docker使うのにHyper-V使うので、毎回Hyper-Vを有効・無効にする度に
再起動するのはちとめんどい・・・・・

そんな感じのどーでもいい記事でした。

※ちなみに、Hyper-V無効後に有効にしたらDockerのMySqlのデータ吹っ飛びました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Boot + Docker + Github での環境構築 クイックスタート

Spring Bootのアプリ作成

1. Spring InitilizarでHelloWorldのアプリ作成

以下のリンクをクリックして、Generateでダウンロード

https://start.spring.io/

2. HelloWorldのコントローラーを作成

HelloWorldを表示するアプリ作成。

HelloWorld.java
@RestController
public class HelloWorld {
    @RequestMapping("/")
    public String helloWorld() {
        return "hello world";
    }
}

以下のURLをクリックすると、ローカル環境にアクセスできることを確認

http://localhost:8080/

3. Githubにデータをプッシュする

上記の作成したモジュールをGitに追加する

https://github.com/harutotanabe09/SpringBootHelloWorld

4. Dockerファイルを用意する

・ できるだけ軽量コンテナを使用する(alpineを使用する)
・ コンテナ内でのGradleのインストールは不要
・ gradlew bootRunでコンパイルから実行まで実施する

Dokcerfile
FROM openjdk:8-alpine

RUN apk update && \
    apk upgrade && \
    apk add --no-cache \
    bash \
    curl-dev \
    ruby-dev \
    build-base \
    git

RUN mkdir /usr/app
WORKDIR /usr/app

# アプリケーションをダウンロード
RUN git clone https://github.com/harutotanabe09/SpringBootHelloWorld.git . 

# アプリケーションを実行
ENTRYPOINT ["sh", "./gradlew", "bootRun"]

5. Dockerコマンドでためす

・上記で作成したDockerFileのディレクトリで以下実行
・DockerFileのimageのビルド&コンテナ起動する (rmオプションで実行停止したときプロセス削除)

# ビルドコマンド
docker build -t my-java-app .
# 実行
docker run -p 3000:8080 my-java-app --rm

6. Docker-Compose作成

・上記で作成したDockerFileをコマンド1つで実行できるようにするためにDocker-Composeを作成

docker-compose.yml
version: "3.7"
services:
  javaapp:
    build: .
    ports:
      - "3000:8080"

docker-compose upを実行してみる

image.png

・ローカルで動いていることを確認

http://localhost:3000/

7. 作成したDockerFileとDockerComposeをGithubにプッシュ

・サンプルとして以下に配置

https://github.com/harutotanabe09/SpringBootHelloWorldDocker.git

ワンポイント:Gitのサブモジュールでアプリ側にGitのDockerFileを入れると見分けがしやすい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む