20200315のNode.jsに関する記事は8件です。

Node.js、VSCode、WSLときどきPowerShell

WindowsでNode.js(Angular)を使ったフロントエンドの開発環境を整えるにあたってハマったところを残しておきます

開発環境

  • OS: Windows 10 Home
  • エディター: VSCode
  • コンソール: WSL1(Ubuntu)

VSCodeのGUIで使用するGitを変更する

同じレポジトリをWSLとVSCode両方から弄っている人は今すぐにVSCodeの設定を変更してください

でないとそのうちどちらからも書き込みができなくなるデッドロックが発生します

発生するタイミングは把握できませんでしたが、
VSCodeのGUIでファイルの変更を取り消す(discard changes)ときに発生する気がします

Node.js、VSCode、WSLときどきPowerShell_1.png

この症状が起きるたびにレポジトリをクローン→npm installを繰り返す羽目になるでしょう
push前だと絶望ものです

(あとから気付いたことですが、windowsを再起動すれば直るかもしれません)

Remote WSLを使ってWSLで完結させろよ!?」

気持ちはわかりますが、
どうしてもPowerShellを使わなければならない事態に直面したため、ぼくの環境ではWSLで完結させることができなくなってしまいました(後述します)

VSCodeのターミナルで使用されるShellのデフォルトを変えれば、
VSCodeが使用するGitも変わると思っていたのですが

Node.js、VSCode、WSLときどきPowerShell_2.png
Node.js、VSCode、WSLときどきPowerShell_3.png

VSCodeが使用するGitは、VSCodeのSettingを変更しなければ変更されず、Windows版のGitのままです

ではどうやって変更すればよいかというと、WSLGitというexeファイルをsetting.jsonで指定してやります

手順についてはこちらの記事WSLGitのReadmeに載っているので参考にしてください

これをすることでVSCodeのGUIで使用されるGitがGit for WindowsからwSL上のGitに切り替わります

これを行ったことで以後デッドロックには陥らなくなりました

VSCodeのデフォルトで使用される改行コードをLFにする

チームはみんなMac環境なのでレポジトリの改行コードも全部LFです

WindowsのデフォルトはCRLFなので
これを行わないと新しいファイルを作成するたびに手動で改行コードをCRLFからLFに変更しないといけないので、非常にめんどくさいです
忘れたままコミットしたときにはこれもまた絶望ものです

簡単なのでサクッとやっておきましょう

VSCodeのsetting.jsonに以下を記載するだけです

setting.json
  ..
  "files.eol": "\n",
  ..

WSL1は中~大規模のプロジェクトには耐えられない

無垢で何も知らなかった頃の僕は勿論WSL上で完結させようとレポジトリもWSL上にクローンし、VSCodeもWSLから起動し、AngularやfirebaseのCLIもWSLにインストール。
UNIXライクで快適な開発環境が整いました

開発が一段落し、ビルドしようとコマンドを叩き、

終わるまでコーヒーでも入れてくるかと席を立ち数分、ビルドは未だ終わらず。

それから暫く一服しながら見守っていた僕は思いました

「これ止まってるわ」

92% chunk asset optimization TerserPlugin

92% chunk asset optimization TerserPlugin

で処理が止まっているので調べてみると

Angularの公式でissueがたてられてました

Nodeでビルドする際、--max-old-space-size=8192で使用するメモリを増やしているのですが

これをWSL1上で行うと十分なメモリーが確保されないようで、海外の歴戦の猛者たちも諦めてPowerShellでビルドするしかないようでした

Node.js、VSCode、WSLときどきPowerShell_4.png

せっかくWSLでnode、gcloud、gitなどなど使えるようになったのに結局WindowsにNode入れるしかないのか…
やだ…汚したくない…

と思いながらも渋々僕もこのムーブに従いPowerShellでビルドしましたとさ。

(ちなみにNode.jsはUnix系でお世話になっているnodenv(anyenv)と同じ.node-versionで管理ができるnodistを使用してインストールしました)

これがあるのでWSLで触ったりVSCodeから触ったりとおかしなことを始めるようになったのですが

この記事を書きながら、Remote WSLやめる必要なかったじゃんと思い始めているのは内緒です

Settings Sync

補足で、VSCodeの設定を複数のマシンで共有できる拡張機能Settings Syncを紹介します

GitHubのアカウントがあれば簡単に設定できるので使ってない方はぜひ。

settings.json
  ..
  // @sync os=windows
  "terminal.integrated.shell.windows": "C:\\Windows\\System32\\wsl.exe",
  ..

のように、特定のOS(Mac,Linux,Windows)のみ有効にできる書き方ができるので

OSに依存した設定も安全に同期することができます

まとめ

2020年春に予定されているWindows 10のアップデートではやくWSL2とDockerが使えるようになるのを待つばかりです

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

Node.js (TypeScript) におけるキャッシュの実装方法とその戦略

現代の Web アプリケーションにおいて、キャッシュはもはや不可欠と言っていいくらい需要な技術でしょう。アプリケーションの負荷を軽減し、ユーザーへのレスポンスを高めます。
本記事では Node.js (Typescript) を使用したバックエンド API を実装する際に、どのようなキャッシュのテクニックが使えるか解説します。

※なお、説明の簡略化のためエラーハンドリングなどは省略しています。

Node.js を使用した非同期バッチパターンとキャッシュ機構

本章では以下の3つの実装パターンを比較し、Node.js を使用したキャッシュを実装していきます。

  1. キャッシュのないサンプルアプリケーション
  2. 非同期バッチ処理パターン
  3. キャッシュパターン

1. キャッシュのないサンプルアプリケーション

キャッシュの実装をする前に、簡単な Web API で提供されるアプリケーションを考えます。
例えば、チーム参加型の競技において個人の点数をチーム毎に集計するような機能を実装するとしましょう。
データベースには以下のように、名前チーム名, 点数が含まれています。
このデータから点数を集計して返却しましょう。

data.ts
export const data = [
  { name: "bob", team: "A", point: 30 },
  { name: "sam", team: "A", point: 83 },
  { name: "john", team: "B", point: 22 },
  { name: "mark", team: "B", point: 30 },
  { name: "tanaka", team: "C", point: 10 },
  { name: "steven", team: "C", point: 52 }
];

このアプリケーションはクエリパラメータにチーム名を指定すると、そのチームの合計点数を返却します。

app.ts
import * as http from "http";
import * as url from "url";
import totalScore from "./totalScore";

http
  .createServer(async (req, res) => {
    const query = url.parse(req.url, true).query;
    const sum = await totalScore(query.team);
    res.writeHead(200);
    res.end(`チーム${query.team}の合計点数は${sum}です。\n`);
  })
  .listen(8080, () => {
    console.log("server is now listening htttp://localhost:8080");
  });

キャッシュの効果を体感するために、わざと合計する処理に時間がかかるようにしておきます。今回は簡単な機能を実装していますが、実際の世界では複雑な計算をすることが多いでしょう。サーバサイドの処理で 5 秒かかってしまうアプリケーションは正直使い物になりませんね。キャッシュの仕組みを理解するには十分な題材です。

totalScore.ts
import { data } from "./data";

const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

const total = (team: string) => {
  let sum = 0;
  for (const item of data) {
    if (item.team === team) sum += item.point;
  }
  return sum;
};

export default (team: string): Promise<number> => {
  return new Promise(async (resolve, reject) => {
    console.log(`チーム: ${team} の集計処理を開始します。`);
    const sum = total(team);
    // 無理やり時間がかかる処理に偽装する
    await sleep(5000);
    console.log(`チーム: ${team} の集計処理が完了しました。`);
    resolve(sum);
  });
};

それでは実際に動作を確認してみましょう。以下では、3つのクライアントがサーバに対してリクエストを送っています。それぞれ独立して処理が実行されていることが確認できます。

nocache.gif

さて、ここまでの処理の流れを整理しておきましょう。複数のクライアントからの処理はそれぞれ独立して実行されています。つまりクライアント A からのリクエストもクライアント B からのリクエストも同様に 5 秒ずつかかっているのです。

nocache.png

2. 非同期バッチ処理パターン

それではキャッシュを導入する前に、まずは Node.js 特有の非同期処理に目をつけて非同期バッチパターンを実装してみましょう。

同じ API に対して複数の非同期処理の呼び出しがある場合、呼び出される処理をバッチ処理としてしまおうという発想です。非同期処理が終わらないうちにもう一度同じ非同期処理を呼び出すなら、新しいリクエストを作成するのではなく、すでに実行中のバッチの処理結果を返すような仕組みです。

処理の流れは以下のようになります。

queue.png

この方法は極めてシンプルでありながら、アプリケーションの負荷を抑えつつキャッシュ機構を使う必要がありません。さて、実際に実装の流れを確認していきましょう。まずは Batch を呼び出す Handler の実装方法を考えます。

API が呼び出された時に、すでに実行中の処理があれば、コールバック関数をキューに追加します。このコールバック関数はチームの点数の集計結果を返します。非同期処理が完了した時点で、キューに保存された全てのコールバック関数を呼び出します。この結果、同じリクエストを送ってきた全てのクライアントに対して一斉にレスポンスを返却できます。

totalScoreBatchHandler.ts
import totalScore from "./totalScore";

const queues = {};

export default async (team: string, callback) => {
  // 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけ
  if (queues[team]) return queues[team].push(callback);

  queues[team] = [callback];
  const score = await totalScore(team);

  // キューに入っている全ての callback 関数に計算結果を渡す
  queues[team].forEach(cb => cb(null, score));

  // キューのクリア
  queues[team] = null;
};

Batch の Handler を実装したので、リクエストを受けつける箇所からの呼び出し方も少し変えなければいけません。大した変更ではありませんね。

app.ts
import * as http from "http";
import * as url from "url";
import totalScoreBatchHandler from "./totalScoreBatchHandler";

http
  .createServer(async (req, res) => {
    const query = url.parse(req.url, true).query;
    totalScoreBatchHandler(query.team, (err, sum) => {
      res.writeHead(200);
      res.end(`チーム${query.team}の合計点数は${sum}です。\n`);
    });
  })
  .listen(8080, () => {
    console.log("server is now listening htttp://localhost:8080");
  });

アプリケーションの振る舞いを確認してみましょう。ここで、2つのクライアントはチーム A をクエリパラメータに指定し、1つのクライアントはチーム B をクエリパラメータに指定していることに注目して下さい。

チーム A を指定したリクエストが送られたあとで、2番目のクライアントが同じくチーム A を指定してリクエストを送っています。サーバのログには集計バッチ処理の開始と終了を出力するようにしていますが、チーム A の集計処理開始のログは1つしか出ていません。これは2番目のリクエストによる新たなバッチは起動されず、キューにコールバック関数が保存されるだけとなっているためです。

そして、1、2 番目のリクエストは(ほぼ)同時に 2 つのクライアントにレスポンスが返却されています。

queue.gif

3. キャッシュパターン

さあ、キャッシュを導入していきましょう。非同期バッチ処理パターンだけでも強力なテクニックでしたが、キャッシュを導入することでよりアプリケーションの負荷を減らし、スループットを向上させます。
非同期バッチ処理パターンよりも考え方は簡単かもしれません。処理が終わったものをキャッシュに有効期限つきで保存するだけです。

cache.png

先ほどの Handler にキャッシュの機構を足していきます。集計処理が終わったら結果を一意なキー付きで Cache に格納します。一意となるキーは今回の場合、チーム名とします。キャッシュの保持期間は 10 秒とし、保持期間のうちに再度同じパラメータのリクエストがあった場合は Cache から値を取得してクライアントに返却します。

実際のユースケースではアプリケーションサーバはスケールアウトし、複数のプロセスに分散していることが一般的です。その場合は永続化する共有領域を Redismemcached などに持たせることが好まれます。今回は説明を簡単にするため、グローバル変数にキャッシュを持つことにします。

totalScoreBatchHandler.ts
import totalScore from "./totalScore";

const queues = {};
const cache = {};

export default async (team: string, callback) => {
  if (cache[team]) {
    console.log(`キャッシュ ${team}: ${cache[team]} にヒットしました。`);
    return process.nextTick(callback.bind(null, null, cache[team]));
  }

  // 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけ
  if (queues[team]) return queues[team].push(callback);

  queues[team] = [callback];
  const score = await totalScore(team);

  // キューに入っている全ての callback 関数に計算結果を渡す
  queues[team].forEach(cb => cb(null, score));

  // キューのクリア
  queues[team] = null;

  // キャッシュの保存
  cache[team] = score;

  // キャッシュの削除予約
  scheduleRemoveCache(team);
};

function scheduleRemoveCache(team: string) {
  function delteCache(team) {
    console.log(`キャッシュ ${team}: ${cache[team]} を削除します`);
    delete cache[team];
  }
  // 10 秒したらキャッシュを削除
  setTimeout(() => delteCache(team), 10 * 1000);
}

実行してみると、その効果を体感できます。非同期バッチ処理パターンはそのまま保っています。さらに処理結果をキャッシュに保存することで、キャッシュの保持期間(10 秒間)は即座にレスポンスを返却できていることがわかります。また、実際に合計値計算を行わないためアプリケーションの負荷も下がることが期待されます。

cache

それぞれの手法を評価する

最後に3つの実装方法でどの程度パフォーマンスに差が出るのか確認してみましょう。
検証には artillery を使用します。

秒間 100 リクエストが 10 秒間、合計 1000 リクエスト発生するように負荷をかけていきます。

$ artillery quick -d 10 -r 100 -o cache.json  http://localhost:8080/?team=A

結果は以下のようになりました。

No バッチ処理 キャッシュ RPS 最小(ms) 最大(ms) 平均(ms)
1 なし なし 66.8 5003.6 5029.1 5006
2 あり なし 90.5 345.2 5340.8 2955.1
3 あり あり 95.6 3.1 5021.9 325.8



テスト結果の詳細結果(クリックして開く)

1. キャッシュのないサンプルアプリケーション

All virtual users finished
Summary report @ 22:49:31(+0900) 2020-03-14
  Scenarios launched:  1000
  Scenarios completed: 974
  Requests completed:  974
  RPS sent: 66.8
  Request latency:
    min: 5003.6
    max: 5029.1
    median: 5006
    p95: 5009.5
    p99: 5017.5
  Scenario counts:
    0: 1000 (100%)
  Codes:
    200: 974
  Errors:
    ENOTFOUND: 26

2. 非同期バッチ処理パターン

Summary report @ 22:51:21(+0900) 2020-03-14
  Scenarios launched:  1000
  Scenarios completed: 975
  Requests completed:  975
  RPS sent: 90.5
  Request latency:
    min: 345.2
    max: 5340.8
    median: 2955.1
    p95: 4904.8
    p99: 5027.1
  Scenario counts:
    0: 1000 (100%)
  Codes:
    200: 975
  Errors:
    EMFILE: 15
    ENOTFOUND: 10

3. キャッシュパターン

Summary report @ 22:53:23(+0900) 2020-03-14
  Scenarios launched:  1000
  Scenarios completed: 974
  Requests completed:  974
  RPS sent: 95.6
  Request latency:
    min: 3.1
    max: 5021.9
    median: 325.8
    p95: 4610.3
    p99: 4988.9
  Scenario counts:
    0: 1000 (100%)
  Codes:
    200: 974
  Errors:
    ENOTFOUND: 26

想定通り、キャッシュがあるの場合は最小数 ms でレスポンスを返却できています。あたりまえの話ですが、どの手法を使っても最大(ms)は 5 秒から変わりません。いくらキャッシュを使用しても、本来時間がかかる処理時間は減らないのです。キャッシュがない状態で受けたリクエストに対してはどうしても計算時間がかかってしまいます。ではこの課題に対する解決策はどのように考えたらよいでしょうか?

答えはいくつか考えられます。

  1. 本来時間がかかっている処理を見直す

    DB からの取得がボトルネックであれば、DB のインデックスや検索条件をチューニングする。
    アプリケーションの集計処理が雑なロジックの場合、高速化が見込めないか検討する。

  2. 別プロセスで実行するバッチ処理に任せる

    リクエストを受けてから計算するのではなく、事前に計算しておいた結果をキャッシュ用データストアに保存しておく。
    この方式を採用する場合、ほぼ全てのクエリパラメータに対してバッチによる計算処理を実行するため、よほどサーバリソースが豊富に使用できる場合に限られる。また、リクエストの多いクエリパラメータを判定し、優先度をつけてバッチ処理をするなどの複雑な機構が要求される。

今回は別プロセスで実行するバッチ処理に任せる方式を実装してみましょう。実行するマシン(あるいはプロセス)が異なるため、グローバル変数にキャッシュを持たせている今の仕組みは使えません。今こそ Redis を使用する時がきました。

Redis を使用して分散システムに対してキャッシュの機構を作る

スケーラブルなバッチ処理を行うために必要な永続化ストレージとして Redis を採用します。今回は Docker 上でオーケストレーションされるインフラを想定して、Redis は Docker コンテナで起動することとします。

$ docker run --name some-redis -d redis -p 6379:6379

DistributedSystem.png

起動された Redis に対して、JavaScript からアクセスしましょう。まずはクライアントライブラリをインストールします。

$ npm install redis

いままでグローバル変数でキャッシュさせていた部分を Redis に接続するように変更するだけです。コールバック関数を Promise に変換する便利なライブラリ util/promisify を使用しています。コールバック関数で実装されている非同期処理を自分でラップして実装する手間が省けて便利です。

totalScoreBatchHandlerRedis.ts
import totalScore from "./totalScore";
import * as redis from "redis";
import { promisify } from "util";
const client = redis.createClient();
const getAsync = promisify(client.get).bind(client);
const setAsync = promisify(client.set).bind(client);
const delAsync = promisify(client.del).bind(client);

const queues = {};

export default async (team: string, callback) => {
  const cache = await getAsync(team);
  if (cache) {
    console.log(`キャッシュ ${team}: ${cache} にヒットしました。`);
    return process.nextTick(callback.bind(null, null, cache));
  }

  // 他のリクエストによってすでにキューに入っている場合は、自身のリクエストも同じキューに入れるだけ
  if (queues[team]) return queues[team].push(callback);

  queues[team] = [callback];
  const score = await totalScore(team);

  // キューに入っている全ての callback 関数に計算結果を渡す
  queues[team].forEach(cb => cb(null, score));

  // キューのクリア
  queues[team] = null;

  // キャッシュの保存;
  setAsync(team, score);

  // キャッシュの削除予約;
  scheduleRemoveCache(team);
};

async function scheduleRemoveCache(team: string) {
  function delteCache(team) {
    console.log(`キャッシュ ${team} を削除します`);
    delAsync(team);
  }
  setTimeout(async () => delteCache(team), 30 * 1000);
}

バックエンドで完全に独立したバッチを実行する

さて、これで分散システムにおけるキャッシュ機構の準備が整いました。バックエンドで完全に独立して実行されるバッチを記述しましょう。
ここでは簡単のために node-cron ライブラリを使用して cron 実行することにしています。
サーバの cron によって実現したり、AWS であれば CloudWatch Events 、GCP であれば Cloud Scheduler などを使用すると良いでしょう。スケジューラとバッチ処理を分離することで、バッチ処理するサーバを常に起動することなく必要なときだけ立ち上げる構成を取ることができます。コンピューティング環境には Lambda や CloudFunction などの FaaS を使用しても良いでしょう。

totalScoreAllTeam.ts
import totalScoreBatchHandler from "./totalScoreBatchHandlerRedis";

const main = () => {
  ["A", "B", "C"].forEach(team => {
    totalScoreBatchHandler(team, (err, sum) => {
      console.log(`バッチ処理が完了しました。`);
      console.log(`チーム${team}の合計点数は${sum}です。`);
    });
  });
};

const cron = require("node-cron");
cron.schedule("*/10 * * * * *", () => main());

結果は以下のようになりました。完全にバックグラウンドでバッチを独立して実行させることにより、常にキャッシュがある状態でユーザリクエストを受け付けることができるようになりました。実際のユースケースでは今回の例のようにチームが 3 つしかないような理想的な条件ではないでしょう。その場合はリクエストが多く集中するデータを優先的にキャッシュするような機構を考える必要がある場合もあるでしょう。

実装方式 RPS 最小(ms) 最大(ms) 平均(ms)
従来のキャッシュ方式 95.6 3.1 5021.9 325.8
完全にバッチを独立させる 95.5 5.7 139.3 9

最終的な構成はこのようになりました。複数のサーバが共有できるキャッシュ用の永続化ストレージを Redis を使用することで実現しました。あとは API サーバへのリクエストを LoadBalarncer によって分散させることでスケーラブルな Web API にできます。

scalable.png

以上が Node.js を使用したキャッシュの基本的な考え方と戦略です。最後に説明したバッチをバックグラウンドで処理する方法は、場合によっては求められる要件に対してオーバーエンジニアリングとなることもあるでしょう。ユーザリクエストが秒間 200~300 程度であれば特に気にする必要はないかもしれませんが、秒間 1000 リクエストを超えたあたりからキャッシュとは真剣に向き合わなければいけません。適切な構成を採用し、サイトのパフォーマンスを上げていきたいですね。

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

伝家のトレジャーソード(COTOHA APIでつくるルー語 LINE Bot)

やりたいこと・経緯

  • 入力した日本語文の一部をカタカナ表記の英語に変換(=ルー語を生成)し返すLINE Botをつくってみる
  • ルー語とは:日本語文の一部を単語単位でカタカナ表記の英語に置き換えたルー大柴さんの(流行ったのは十年以上前の)ネタ
  • たとえば「言わぬがフラワー」「塵も積もればマウンテン」など
  • 実はすでにルー語変換というウェブサイトがあり、そのサイトの開発経緯には「文章を形態素解析 → 単語を英訳 → 英語をカタカナ読みに翻訳 」していると書いてある
  • 日本語文の形態素解析にはMeCabを利用しているそう。COTOHA APIを使って作ったらどうなるだろうか、という単純な思いつき

COTOHA APIについて

  • https://api.ce-cotoha.com/
  • 文の形態素解析や固有表現抽出などの自然言語処理や音声処理をRESTなAPIでやってくれるAPI
  • ひととおりに触ってみて、非常に精度がよく、アイディア次第で色々な面白いものがつくれそう
  • 今回は「キーワード抽出」のAPIを利用しています

つくったもの

Image from Gyazo
最後のほうに友だち登録リンクものせました

仕様

  • ユーザーからの日本語文入力を受け取る
    • COTOHA APIのキーワード抽出にかけ、キーワードを抽出
    • 最上位のキーワード(APIからは複数の返り値もありえる)をGoogle 翻訳で英語に変換
    • 英語に変換したキーワードを、カタカナに変換
  • ユーザーからの入力のうちキーワードを変換されたカタカナに置き換え、返信

技術

const BASE_URL = 'https://api.ce-cotoha.com/api/dev/'

async function extractKeywords(documentArray) {
    const urlEndPoint = 'nlp/v1/keyword'
    return axios({
        url: BASE_URL + urlEndPoint,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json;charset=UTF-8',
            'Authorization': `Bearer ${await getAccessToken()}`
        },
        data: {
            'document': documentArray
        }
    }).then(res => {
        const keywords = res.data.result
        if (keywords.length === 0) return false
        return keywords
    }).catch(err => {
        console.log(err.response.status)
        console.log(err.response.data)
        return false
    })
}
  • ソースコード(環境変数除く)https://github.com/embokoir/cotoha-lou-linebot
  • サーバーはNode、全面的にasync-await / axios
  • デプロイはGithubからGlitchへクローン
  • Google翻訳部分はCloud Translation APIを使うのが精度も速度もいいとは思うものの面倒だったのでGASでそれっぽいAPIを自作
  • 英語→カタカナ変換の部分はEnglish to Katakana ConverterのAPI(非公式)を使わせていただきました *1

考察など

  • ルー語は、文のうちどこをどう英語化するかが妙で、ルー大柴さんはその点が絶妙。片っ端から英訳しているわけでもないし、またいわゆる日本語英語みたいなものも織り交ぜている
  • 当初はCOTOHA APIの構文解析APIを使用していたが、文からすべての名詞とかを抽出したところでどれを英訳すると面白くなるのかという部分で実装が難しくなってくることに気づき、キーワード抽出APIに乗り換えたところ、いい感じにワードを選んでくれた。ちなみにCOTOHA API的に「キーワード」とは「名詞」「主語」「目的語」「反復される語」などの属性を持ちやすい気がした
  • ルー語化する文はことわざや早口言葉やお堅い文が向いている

結果比較

本家のものは名言・格言『ルー大柴さんの気になる言葉』一覧リストを参照しました

元々の日本語 本家ルー語 このBot ルー語変換サイト
言わぬが花 言わぬがフラワー 言わぬがフラワー 言わぬがフラワー
一寸先は闇 一寸先はダーク 一寸先はダークネス 一寸アフターはダーク
寝耳に水 寝耳にウォーター 寝耳にウォーター 寝耳に水
阿吽の呼吸 阿吽のブレス 阿吽のブリージング 阿吽のブレス
三日坊主 スリーデイズ坊主 スリーデーズシェーブド 三デイクルーカット
身を粉にする 身をパウダーにする 身をパウダーにする ボディーをミールにする
二兎を追う者は一兎をも得ず 二兎をチェイスする者は一兎をもゲットせず 二兎を追う者はワンラビットをも得ず 二兎をランアフターするパースンは一ラビットをも得ず
雨降って地固まる レイン降ってグランド固まる 雨降ってソリディファイ レイン降ってアース固まる

結論

  • ルー語ってコンピュータ的に生成しようとすると実は結構奥深い
  • COTOHA APIが抽出するキーワードは、ルー語の面白さに割と近づけているのでは…?
  • 嵐の前のセレニティー、伝家のトレジャーソードあたりが個人的にツボ
  • 気になる方はこちらかQRコードから友だち登録して遊んでみてください 675gmuga.png


*1 このBotをGlitchで動かしていたところ、運営者の方よりAmazon関係のサーバからのアクセスは基本的に禁止ということで一時ブロックされましたが、事情を説明し解除していただきました、ありがとうございます。(GlitchのうしろではAWSが動いているみたいですね)

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

COTOHA APIだけでおじさんになろう

はじめに

  • 読みやすさ重視のため、本文におじさん構文は登場しません。期待された方には深くお詫び申し上げます。
  • また、デモサイトやバックエンドに関することは後日別記事にしようと思います。
  • 前提知識 => JavaScriptのみ(!)
  • 記事中に登場するコードは、axiosとfsが動くnode環境ならコピペで実行可能です。COTOHAの気軽さをお試しください。
  • 筆者は趣味でコードを書く大学生です。動けばいいやの精神が強いのですが、ITの世界に進むならこれじゃあかんやろと思っているので、コードにとどまらずいろいろご指摘くだされば幸いです。
  • ソースコード

おじさん:wink:と一緒:kissing_heart:に考えよう:thinking:

普通の文章を入れたらその内容がおじさん構文になったらおもしろくね?
ということでCOTOHA APIのみを使ってデモページを作ってみました。

注意:thinking:したこと・開発動機:triumph:

COTOHA APIの記事はキャンペーンの影響で数多作成されており、この記事もその例外ではありません。しかし、「COTOHA API だけで言語を処理する」記事はあまり見当たりませんでした。僕自身が自然言語処理に全く知識がなく、また、pythonを使うことができない(jsで書いています)ため、他のライブラリを使うことができませんでした。なにより僕の頭の中では

そもそもCOTOHA APIって、僕みたいになにも知らなくても簡単気楽に実装できるところがいいんじゃないの?

という気持ちが強くありました。そういうわけで、この記事ではJavaScriptさえ読めれば誰でもわかることを目指し、自然言語処理に関わることはCOTOHA API以外のライブラリ、APIなどは使わずに実装しました。読者対象は「なんとなく興味を持っているけれど」の層です。難しい話はなしです。他のライブラリと合わせることでより真価を引き出し素晴らしいプロダクトを開発されている方は他にたくさんいらっしゃいますので、この記事で興味を持たれた方はぜひそちらもご覧ください。
開発動機は純粋にプレゼントほしいぃという気持ちもありつつ、最近僕の中で話題だった、おじさん構文を作れるかもしれない!とリファレンスを読みながら思ったからです。プロダクトは未完成ですが、雰囲気を楽しんでいただけたらなと思います。
3/15 22時追記 ほぼ完成しました。

デモサイト

https://storage.googleapis.com/vue-oji-cotoha/index.html

使い方

  1. COTOHA APIの利用登録を済ませ、Client ID とClient secretを取得する
  2. デモページにてIDとsecretを入力し、「Access Tokenを取得」を押下
  3. 「アクセストークンの取得に成功しました!」が表示されたら、おじさん構文にしたい文書を入力して「おじさん構文化」を押下
  4. :heart_eyes:おじさん:heart_eyes:

構成

最初はウェブページ上にすべての機能を実装してやろうと考えていたのですが、Cross-Origin Resource Sharing (CORS)に引っかかるじゃん!ということで、いったんGoogle Cloud Platformのfunctionに情報を渡して結果をフロントに表示する構成にしています。 このせいでClient IDやsecretをHTTP通信してますやべー

おじさん構文化の仕組み

本記事のメインです。

そもそもおじさん構文って?

実際に存在しているおじさん構文を見たことがないため、Twitterなどで見かける「私たちがおじさん構文と認識するもの」をおじさん構文として定義し、これを再現することを目標とします。
高校の友達に、いま某外国語学科に所属している変態言語オタクがいたので、「おじさん構文を自動作成しようと思うんだけど、定式化できないかな」と相談したところ、以下のレポートを頂戴しました。言語系に進んでいらっしゃる方は、筆者デアショコがフィードバックが欲しいということでしたので、コメント欄にてよろしくお願いします。

「おじさん構文解析」全文展開

題「おじさん構文解析」
著者 デアショコ

1,序論

何が「おじさん構文」を「おじさん構文」たらしめているかについて、実例を見ながら分析していき、一定のルールを見つけ出す。その上で、「適当な文をおじさん構文に変換する」という今回の目的のため、日常に使う適当な文を1、2文例に取り、違和感の無いおじさん構文化することを本稿の目的とする。

2,具体例とそこに共通して認められるルールについて

具体例1

Aチャン❗️オハヨ?✨✨
天本のお寿司?、おいしかったネ??

そうそう❗️❗️昨日も話したけど、Aちゃんと今度ゴルフ行きたいな❗️❗️⛳?クルマはおじさんのアウディ?✨で行こうね??

あとあと❗️❗️今月のハワイ旅行✈✨だけど、25日(月)に出て、30日(土)に戻ってくるで大丈夫かな❓❓?

海にも入るから水着も忘れないでね??オジサン、ウルフギャングも予約しちゃいまーす❗️?Aちゃんは来てくれるだけでOK牧場だよ❗️❗️??
(笑)よろしくね❗️❗️?✨

具体例2

ベイビーちゃんおはよう☀
昨日マック?食べたいけど、ダイエット中だから我慢?して、おにぎり?にしたよ~❗️❗️褒めて~?✨

でも、その後おにぎり?食べたこと忘れて、ビッグマック?のLセット食べちゃった?
アンビリーバブルだよ~~?ベイビーちゃんに会えないから。ボケちゃったのかも❗️❓
来週一緒にマック?行こうね?クーポン券?あるから、ポテトL奢ってあげるよ✌
聖母マリアのような優しい笑み?が恋しくて、毎日恋ダンス踊ってるよ?
世界で一番愛してる。
ギュッ?

その1:最初の挨拶

呼びかけから入るが、その際、相手を「ちゃん付け」する傾向が認められる。名前にちゃん付けするか、そのままあだ名的に呼ぶなどの揺れは認められる。テンプレ化を目指すのならば、「ベイビーちゃん」に統一するのも一つの手と思われる。

その2:文末

 文末に付く「!」は、基本的に二つ付く傾向が認められる。これは具体例1に特に顕著である。具体例2においても、語尾の「!」が二つ付いているのが散見される。また、文章の内容や発話者の感情に応じて、顔文字が使い分けられているのが確認できる。これは「おじさん構文」に限らず普通の発話においても見られる特徴だが、その顔文字がほぼ全文にあり、多用されているという点が「おじさん構文」の特徴と考えられる。具体例2に見られる面白い特徴として挙げられるのが、文末では無く、名詞の後に顔文字が使用されている点である。具体例2の「~ダイエット中だから我慢?して、~」がその例である。これまで、普通名詞の後にその名詞を表す絵文字が使用されるのは指摘されてきた。今回の例は、普通名詞というよりは、発話者の感情を表すものであると言える。我慢をすることに対する発話者のネガティブな感情が顔文字?に現れている。単なる名詞の具現化だけでなく、聞いてもいない感情を自ら表しにきているところが、「おじさん構文」らしさの1つと、ここから言える。無論、感情を顔文字の使用により表すのはごく自然なことだが、
・その多用
・名詞の直後に持ってくることによるその感情の強調
が、普通の文章と「おじさん構文」の違いだと考えることができる。

その3:✨と?の使用の区別の仕方

 相手に対して誘いや依頼をする際は、?が使用されることが多いと考えられる。これは、お願いする相手(おそらく女性)に対する好意を明確に示すものだと思われる。
・具体例1:クルマはおじさんのアウディ?✨で行こうね??(勧誘)
・具体例2:来週一緒にマック?行こうね?(勧誘)
・具体例1:海にも入るから水着も忘れないでね??(依頼)

対して、✨は、単純な感情の高まりとそれによる事柄の強調、もしくは、自分自身のことを強調する際に使用されると思われる。
 ・具体例1:今月のハワイ旅行✈✨だけど、(強調)
 ・具体例1:よろしくね❗️❗️?✨(強調的)
 ・具体例2:褒めて~?✨(自身の強調)

3. 以上のルールを踏まえた上での、「おじさん構文」化

例1

「コーディングが楽しくてやめられないけど、ひたすら眠くて仕方ないジレンマ」
→「コーディング(?)が楽しくて?やめられない?けど、ひたすら眠くて?仕方ないジレンマ??」

解説

本来はコーディングの後に何かしらコーディングを表す絵文字が欲しかったが見つけることができなかったため割愛。今回は「楽しくて」「やめられない」「眠くて」「ジレンマ」等、発話者の感情を表す単語が多い。ポイントは「やめられない」と「ジレンマ」の箇所である。今回の「やめられない」は、直前の「楽しくて」から推測されるようにネガティブな意味では無いと思われるため、泣き笑いの表情を選択した。「ジレンマ」の箇所については、ジレンマは、『眠ることができず、困っている』という意味で使用されていることが明白なため、その『困っている』発話者の感情を表現するため、汗をかいている顔文字と、汗の絵文字を使用した。

例2

「図書館でいろんな本を借りてたけど、これだけはどうしても欲しくなって買ってしまいました。これ一冊は必ず仕上げようと思います!」
→「図書館?でいろんな本?を借りてた✨けど、これだけはどうしても?欲しくなって?買ってしまいました?✨。これ一冊?は必ず仕上げようと思います❗️ ❗️」

解説

 今回は「本」というわかりやすい普通名詞の登場で、絵文字を置きやすくなった。「借りてた」の箇所に関しては、『いろんな本を借りる』という文言を『自分の行為の強調』と捉えたため、✨を置いた。「どうしても」の箇所は賛否あるとは思うが、『他は買わないがこれだけは買う』という意思を表すため含みのある?を採用した。

4. まとめ

 今回、様々な文章に対応しうる「おじさん構文」のルールをある程度見つけ出すことを目的に、「おじさん構文」の分析を行い、通常の文章を違和感のないおじさん構文に変換することを試みた。選択する顔文字や付け加えた絵文字に関してはまだまだ賛否や議論の余地があると思うので、その洗練は今後の課題としたい。

5.出典、資料

https://neetola.com/ojisan/
具体例として扱わせていただいた
https://twitter.com/gyozaisgood/status/1236355783090008064?s=21
https://twitter.com/gyozaisgood/status/1232326974636359680?s=21
「おじさん構文」化の例に使用させていただいた。

資料:今回使用した顔文字一覧
???????????

普通名詞の絵文字に関しては省略した。


展開した文章はここまで、以下本文


要点は

  • 名前をチャン呼びする
  • 全文に顔文字
  • 自身の感情を絵文字で表現

この三つになりそうです。ここから、文章への加工内容を考えると以下のようになりました。

  • 文末にその文に対応した顔文字の付与
  • 各名詞や特徴的な表現の後ろに、対応するEmojiの付与
  • 人名が出た際、「チャン」付けする

この加工を以下で行っていきます。

1,渡された文章を文にする

ユーザーは文だけでなく、長い文章などを入力することが考えられます。この場合、文の文末に付与する絵文字が名詞の後ろに付与するものを除けば一つだけになってしまい、おじさん感がありません。そのため、与えられた文章を文に区切る必要があります。
文の区切りを判定するために、今回は「終助詞」を選びました。他にも適切なものがあればコメント欄で教えていただきたいです。この開発ではCOTOHA APIに依存することをテーマにしているので、終助詞判定も当然COTOHA APIにしてもらいます。この判定には「構文解析」APIを利用します。

index.js
/*コピペ前にすること
npm install fs axios
*/
const axios = require('axios')
const fs = require('fs')
class Cotoha{
  constructor(sentence,cotoha_token){
    this.sentence = sentence
    this.cotoha_token = cotoha_token
  }
  client(){
    const axiosConfig = axios.create({
      headers:{
        "Authorization": `Bearer ${this.cotoha_token}`,
        "Content-Type": "application/json"
      },
      baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",
    });
    return axiosConfig;
  }
  async parse(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/parse",{"sentence":this.sentence})//parseが構文解析のリクエスト
      const result = res.data.result;
      return result
    }catch(e){
      console.log(e)
    }
  }
}
//cotoha_token(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して
//ボタン押下してから、サイト下部のLogをご覧ください
const main = async () => {
  const inputMsg = "今日のランチはハンバーガーだった、優美ちゃんはなんだった?"
  const cotoha_token = "hogehogehogehoge"
  const cotoha = new Cotoha(inputMsg, cotoha_token)
  const outputMsg = await cotoha.parse()
  fs.writeFile("./parse.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})
}
main()
/*実行コマンド
node .
*/

レスポンスは以下のようになっています。

parse.json※長い!!
parse.json
[
    {
        "chunk_info": {
            "id": 0,
            "head": 1,
            "dep": "D",
            "chunk_head": 0,
            "chunk_func": 1,
            "links": []
        },
        "tokens": [
            {
                "id": 0,
                "form": "今日",
                "kana": "キョウ",
                "lemma": "今日",
                "pos": "名詞",
                "features": [
                    "日時"
                ],
                "dependency_labels": [
                    {
                        "token_id": 1,
                        "label": "case"
                    }
                ],
                "attributes": {}
            },
            {
                "id": 1,
                "form": "の",
                "kana": "ノ",
                "lemma": "の",
                "pos": "格助詞",
                "features": [
                    "連体"
                ],
                "attributes": {}
            }
        ]
    },
    {
        "chunk_info": {
            "id": 1,
            "head": 2,
            "dep": "D",
            "chunk_head": 0,
            "chunk_func": 1,
            "links": [
                {
                    "link": 0,
                    "label": "adjectivals"
                }
            ]
        },
        "tokens": [
            {
                "id": 2,
                "form": "ランチ",
                "kana": "ランチ",
                "lemma": "ランチ",
                "pos": "名詞",
                "features": [],
                "dependency_labels": [
                    {
                        "token_id": 0,
                        "label": "nmod"
                    },
                    {
                        "token_id": 3,
                        "label": "case"
                    }
                ],
                "attributes": {}
            },
            {
                "id": 3,
                "form": "は",
                "kana": "ハ",
                "lemma": "は",
                "pos": "連用助詞",
                "features": [],
                "attributes": {}
            }
        ]
    },
    {
        "chunk_info": {
            "id": 2,
            "head": 3,
            "dep": "D",
            "chunk_head": 0,
            "chunk_func": 1,
            "links": [
                {
                    "link": 1,
                    "label": "agent"
                }
            ],
            "predicate": [
                "past"
            ]
        },
        "tokens": [
            {
                "id": 4,
                "form": "ハンバーガー",
                "kana": "ハンバーガー",
                "lemma": "ハンバーガー",
                "pos": "名詞",
                "features": [],
                "dependency_labels": [
                    {
                        "token_id": 2,
                        "label": "nsubj"
                    },
                    {
                        "token_id": 5,
                        "label": "cop"
                    },
                    {
                        "token_id": 6,
                        "label": "punct"
                    }
                ],
                "attributes": {}
            },
            {
                "id": 5,
                "form": "だった",
                "kana": "ダッタ",
                "lemma": "だった",
                "pos": "判定詞",
                "features": [
                    "連体"
                ],
                "attributes": {}
            },
            {
                "id": 6,
                "form": "、",
                "kana": "",
                "lemma": "、",
                "pos": "読点",
                "features": [],
                "attributes": {}
            }
        ]
    },
    {
        "chunk_info": {
            "id": 3,
            "head": 4,
            "dep": "D",
            "chunk_head": 1,
            "chunk_func": 2,
            "links": [
                {
                    "link": 2,
                    "label": "adjectivals"
                }
            ]
        },
        "tokens": [
            {
                "id": 7,
                "form": "優美",
                "kana": "ユミ",
                "lemma": "優美",
                "pos": "名詞",
                "features": [
                    "名",
                    "固有"
                ],
                "attributes": {}
            },
            {
                "id": 8,
                "form": "ちゃん",
                "kana": "チャン",
                "lemma": "ちゃん",
                "pos": "名詞接尾辞",
                "features": [
                    "名詞"
                ],
                "dependency_labels": [
                    {
                        "token_id": 4,
                        "label": "acl"
                    },
                    {
                        "token_id": 7,
                        "label": "name"
                    },
                    {
                        "token_id": 9,
                        "label": "case"
                    }
                ],
                "attributes": {}
            },
            {
                "id": 9,
                "form": "は",
                "kana": "ハ",
                "lemma": "は",
                "pos": "連用助詞",
                "features": [],
                "attributes": {}
            }
        ]
    },
    {
        "chunk_info": {
            "id": 4,
            "head": -1,
            "dep": "O",
            "chunk_head": 0,
            "chunk_func": 1,
            "links": [
                {
                    "link": 3,
                    "label": "aobject"
                }
            ],
            "predicate": [
                "past"
            ]
        },
        "tokens": [
            {
                "id": 10,
                "form": "なん",
                "kana": "ナン",
                "lemma": "何",
                "pos": "名詞",
                "features": [],
                "dependency_labels": [
                    {
                        "token_id": 8,
                        "label": "nmod"
                    },
                    {
                        "token_id": 11,
                        "label": "cop"
                    },
                    {
                        "token_id": 12,
                        "label": "punct"
                    }
                ],
                "attributes": {}
            },
            {
                "id": 11,
                "form": "だった",
                "kana": "ダッタ",
                "lemma": "だった",
                "pos": "判定詞",
                "features": [
                    "終止"
                ],
                "attributes": {}
            },
            {
                "id": 12,
                "form": "?",
                "kana": "",
                "lemma": "?",
                "pos": "句点",
                "features": [
                    "疑問符"
                ],
                "attributes": {}
            }
        ]
    }
]


APIをたたくときに、解析してほしい文章を入れるだけでいいのはとても魅力的です。
これから終助詞の場所を判定し、文に切り分けています。

2,切り分けた各文がどんな文か調べる

次は切り分けられた文の末尾に絵文字を付けていきます。このとき、各文がどんな文かによって付与する顔文字も変えていきます。
COTOHA APIには文タイプ判定という、

挨拶や同意、約束などの発話行為のタイプを判定します。
同時に、叙述文、命令文、質問文などの文タイプを出力します。

( API一覧より)、その文がどんな文なのか調べるAPIがあるので、この結果をもとに顔文字を付け替えていきます。

index.js
/*コピペ前にすること
npm install fs axios
*/
const axios = require('axios')
const fs = require('fs')
class Cotoha{
  constructor(sentence,cotoha_token){
    this.sentence = sentence
    this.cotoha_token = cotoha_token
  }
  client(){
    const axiosConfig = axios.create({
      headers:{
        "Authorization": `Bearer ${this.cotoha_token}`,
        "Content-Type": "application/json"
      },
      baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",
    });
    return axiosConfig;
  }
  async sentenceType(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/sentence_type",{
        "sentence":this.sentence,
      });
      const result = res.data.result
      return result
    }catch(e){
      console.log(e)
    }
  }
}
//cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して
//ボタン押下してから、サイト下部のLogをご覧ください
const main = async () => {
  const inputMsg = "今日のランチはハンバーガーだった、優美ちゃんはなんだった?"
  const cotoha_token = "0oTUaaBrA5zALXOGyxxnkcgxAhVH"
  const cotoha = new Cotoha(inputMsg, cotoha_token)
  const outputMsg = await cotoha.sentenceType()
  fs.writeFile("./sentenceType.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})
}
main()
/*実行コマンド
node .
*/

sentenceType.json
{
    "modality": "interrogative",
    "dialog_act": [
        "information-seeking"
    ]
}

modalityは投げた文が「叙述」「質問」「命令」の3つのどれかかを判別した結果を返してくれます。dialog_actは投げた文のタイプをより詳細に判別した結果を返してくれます。今回はdialog_actの結果を利用します。

返り値 日本語説明 Emoji
greeting 挨拶 ?
information-providing 情報提供 ❗❗
feedback フィードバック/相槌 ?
information-seeking 情報獲得 ?
agreement 同意 ?
feedbackElicitation 理解確認 ?
commissive 約束 ?
acceptOffer 受領 ?
selfCorrection 言い直し ?
thanking 感謝 ? ?
apology 謝罪 ?
stalling 時間埋め ?
directive 指示 ?
goodbye 挨拶(別れ) ?
declineOffer 否認 ?
turnAssign ターン譲渡 ?
pausing 中断
acceptApology 謝罪受領 ?
acceptThanking 感謝受領 ?

このどれかが返ってくるので、対応するEmoji(上記Emoji列)を文末に付与します。

3,名詞の後ろにいい感じのEmojiを付与

文末に絵文字を付与しただけでは、おじさん感がありません。文中いたるところに絵文字を付与して、よりおじさんに近づきましょう。構文解析の結果は品詞分解されているため、適当な名詞の後ろに適当な絵文字を付与すればいいおじさんになれそうですが、この「適当な絵文字」をそれぞれの名詞につけるのは自然言語処理の力がないとできそうにありません。
COTOHA APIには「固有表現抽出」というAPIがあり、これを使って文中の名詞の判定とどんな名詞なのかを調べようと思います。

index.js
/*コピペ前にすること
npm install fs axios
*/
const axios = require('axios')
const fs = require('fs')
class Cotoha{
  constructor(sentence,cotoha_token){
    this.sentence = sentence
    this.cotoha_token = cotoha_token
  }
  client(){
    const axiosConfig = axios.create({
      headers:{
        "Authorization": `Bearer ${this.cotoha_token}`,
        "Content-Type": "application/json"
      },
      baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",
    });
    return axiosConfig;
  }
  async unique(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/ne",{
        "sentence":this.sentence
      })
      await fs.writeFile("./output/unique.json",JSON.stringify(res.data,null,"\t"));
      const result = res.data.result;
      return result
    }catch(e){
      return e
    }
  }
}
//cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して
//ボタン押下してから、サイト下部のLogをご覧ください
const main = async () => {
  const inputMsg = "今日のランチはハンバーガーだった、優美ちゃんはなんだった?"
  const cotoha_token = "hogehoge"
  const cotoha = new Cotoha(inputMsg, cotoha_token)
  const outputMsg = await cotoha.unique()
  fs.writeFile("./unique.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})
}
main()
/*実行コマンド
node .
*/

unique.json
[
    {
        "begin_pos": 0,
        "end_pos": 2,
        "form": "今日",
        "std_form": "今日",
        "class": "DAT",
        "extended_class": "",
        "source": "basic"
    },
    {
        "begin_pos": 17,
        "end_pos": 19,
        "form": "優美",
        "std_form": "優美",
        "class": "PSN",
        "extended_class": "",
        "source": "basic"
    },
    {
        "begin_pos": 3,
        "end_pos": 6,
        "form": "ランチ",
        "std_form": "ランチ",
        "class": "ART",
        "extended_class": "Dish",
        "source": "basic"
    },
    {
        "begin_pos": 7,
        "end_pos": 13,
        "form": "ハンバーガー",
        "std_form": "ハンバーガー",
        "class": "ART",
        "extended_class": "Dish",
        "source": "basic"
    },
    {
        "begin_pos": 19,
        "end_pos": 22,
        "form": "ちゃん",
        "std_form": "ちゃん",
        "class": "ART",
        "extended_class": "Title_Other",
        "source": "basic"
    }
]

返り値の"extended_class"は、対象の語が「人」「食べ物」「神」「島名」などを数多くの選択肢(100以上)からどれに類するかを返してくれます。(割り当てがないものもあります。上記例だと「今日」は"extended_class"がありません)
あとは返ってきた値に対してあらかじめ決めておいたEmojiを付与します。



100以上の項目に対応する絵文字を決めるのが途方もなさすぎるので、一旦リリースすることにいたしました...絶賛絵文字付与中です。

4,実行結果

入力文
今日のランチはハンバーガーだったよ、優美ちゃんはなんだった?

出力文(2020年3月15日、名詞後置修飾未実装)
今日のランチはハンバーガーだったよ?優美ちゃんはなんだった??

理想の出力文(名詞後置修飾完全実装後)
今日のランチ:fork_and_knife:はハンバーガー:fork_and_knife:だったよ?優美チャンはなんだった??
(ランチもハンバーガーもdishのため、同じ絵文字の予定

終わりに

感想

名詞に対して自分でEmojiを選ぶのが非常にめんどうでした。これは人工知能やAIだとかいうのを使えばいいのか、はたまた今回僕が作ったものも人工知能の一角なのか...難しい世界です。
難しい世界ですが、今回のプロダクトを作るのは非常に簡単でした。リファレンスも日本語で簡潔だし、リクエスト文も簡単、レスポンスも明確、いいことずくめでした。 むしろasync/awaitの非同期に手こずりました IDやsecretをユーザーに依存しないサービスにしようとすると月10万円のCOTOHAの利用料が飛ぶのでできそうにありませんが、エンジニア専用だったり利用数の少ないサービスならいろいろ面白いことができそうなので、挑戦してみようと思いました。
実装方法については後日記事にする予定ですので、もしよろしければご覧ください。

Special Thanks

高校の同級生、デアショコ君には定式化、もとい、僕のやりたいことをコードに落とすプロセスを手伝ってもらっただけでなく、絵文字選定のお手伝いまでしてくれて...本当に助かりました。ありがとう。賞がもらえて退院したら焼き肉行こう。

コード

index.js全文
index.js
/*コピペ前にすること
npm install fs axios
*/
const axios = require('axios')
const fs = require('fs')
class Cotoha{
  constructor(sentence,cotoha_token){
    this.sentence = sentence
    this.cotoha_token = cotoha_token
  }
  client(){
    const axiosConfig = axios.create({
      headers:{
        "Authorization": `Bearer ${this.cotoha_token}`,
        "Content-Type": "application/json"
      },
      baseURL:"https://api.ce-cotoha.com/api/dev/nlp/v1",
    });
    return axiosConfig;
  }
  async parse(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/parse",{"sentence":this.sentence})//parseが構文解析のリクエスト
      const result = res.data.result;
      return result
    }catch(e){
      console.log(e)
    }
  }
  async sentenceType(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/sentence_type",{
        "sentence":this.sentence,
      });
      const result = res.data.result
      return result
    }catch(e){
      console.log(e)
    }
  }
  async unique(){
    const axiosBase = await this.client();
    try{
      const res = await axiosBase.post("/ne",{
        "sentence":this.sentence
      })
      const result = res.data.result;
      //console.log(result)
      return result
    }catch(e){
      return e
    }
  }
}
/*
cotoha_tokne(Access token)はデモサイトにて取得できます。Client IDとClient Secretを入力して
ボタン押下してから、サイト下部のLogをご覧ください
*/
const main = async () => {
  const inputMsg = "今日のランチはハンバーガーだった、優美ちゃんはなんだった?"
  const cotoha_token = "hogehoge"
  const cotoha = new Cotoha(inputMsg, cotoha_token)
  const outputMsg = await cotoha.unique()//すきなメソッドを選らんで実行してください。
  fs.writeFile("./output.json",JSON.stringify(outputMsg,null,"\t"),()=>{console.log("fs end")})
}
main()
/*実行コマンド
node .
*/

リンク集

製作物
https://storage.googleapis.com/vue-oji-cotoha/index.html
製作物のレポジトリ
https://github.com/flat35hdapp/vue-oji-cotoha/
COTOHA API 利用者登録(↑サービス利用前に利用者登録をお済ませください)
https://api.ce-cotoha.com/contents/developers/index.html
本記事を書くきっかけになったプレゼント企画
https://zine.qiita.com/event/collaboration-cotoha-api/
COTOHA APIについてより知りたい方はこちらから
https://api.ce-cotoha.com/
COTOHA APIの機能一覧(オススメ!!)
https://api.ce-cotoha.com/contents/api-all.html
作者Twitter(最近は開発に関わることもつぶやいてます)
https://twitter.com/gyozaIsGood
定式化やEmoji選定協力のデアショコ
https://twitter.com/der_schoco

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

事故を出さない虎の巻

「機能は追加(修正)する」「事故も起こさない」。 「両方」やらなくっちゃあならないってのが「エンジニア」のつらいところだな。 覚悟はいいか?オレはできてる。

事故の定義

ここで述べる事故とは、ユーザのサービス利用を妨げる事象、サービスの信頼が失われる事象を指す。
事故足りうる事象に次のものが挙げられる。

  • インフラ障害(ネットワーク障害、サーバ障害)
  • DB障害(データ不整合、デッドロック)
  • 脆弱性(情報漏えい、インジェクション、改ざん、不正アクセス)
  • メール・SNSの誤送信
  • 実装バグ(二重課金、サーバエラー、操作不能、意図しない動作)

事故足り得る前提として1つ目に規模の問題がある。10人が使ってるサービスの障害と100万人が使ってるサービスの障害を比較するのであれば、明らかに後者のほうが重大な事故である。次にサービス全体へ影響を与えた障害なのか、ごく一部のユーザにのみ影響を与えた障害なのか事故の規模を考慮する必要がある。
(あまりにも規模が小さいものであれば、事故が起こった原因は置いておいて対応としては示談で済むかもしれない)
2つ目にサービスが取り扱っている情報の重要度に依存する。例えば、個人情報を扱っていないサービスであれば盗まれるものが無いのでセキュリティに関してあまり考えなくてもよいし(今どきそんなサービスほぼ無いけど)、銀行のシステムであればネットワークアクセスログはもちろん全て取る必要があるし、改ざんも不正アクセスも防がないといけない。

人間はミスをする生き物なので、手動でやる限りいつか同じミスをしたり、別の人だったら同じミスを犯す。
いずれもチェックが手動だけでなく自動化できればそれに越したことがない。
事故が起こる要因として実施者の想定の範囲外で起こるケースが多々にしてあるので規模が大きいシステムを運用している場合はメンバー間で情報を共有することや修正によってどこに影響を与えるかキーワード検索したり想像力を働かせることが大事。
(例えば、修正変更がメインシステムだけでなく担当範囲外を超えたサブシステムに影響を与える場合など)
監視や通知の仕組みを作り、エラーメッセージを読むことが大事。
AWSやGCPなどのクラウドサービスでシステム構築してる際は、Datadogを使うのがおすすめ。

なお、特に言語や実行プラットフォームは何でも良いのだがPaaSクラウドを用いて構築された一般的なウェブサービスを想定して書いている。

インフラ障害

主にネットワークや認証・認可周りの設定間違いサーバリソース不足に関して事故が起こりやすい。
一度設定すればほぼいじらないものと、機能改修によって都度チェックが必要なものと、常に監視が必要なものと分類される。

プライベートネットワークの設定間違い

AWSなどでVPC(Virtual Private Cloud)を使ってサーバ間のアクセスに仮想プライベートネットワークを構築する際、
以前まで疎通できていたサーバ間の通信が繋がらなくなるような事故。
対策としては設定したネットワークの各サーバ間、サーバからインターネットへの疎通確認をすること。
一度確認したらネットワークを再構築する際以外は起こりえない。

DNSレコードの設定間違い

AWSのRoute53のようなサービスでDNSレコードを設定している場合にCNAMEレコードやAレコード、TXTレコードなどの設定を間違えてしまい起こる事故。
ドメインを変更した際などに設定忘れしたり、疎通確認を忘れた場合に事故が起きる。
一度確認したらドメインを変更する際やレコードを変更する以外は起こりえない。

SSL証明書の期限切れ

SSL証明書の期限が切れて数年後にいきなりhttps通信できなくなり、ブラウザ警告が出る事故。
SSL証明書の自動更新設定をするか、期限が切れる前に通知するなどの対策が必要。

認証・認可の設定間違い

クラウド関連の機能追加・変更する際にクラウドサービスへのアクセス権限を間違えて変更した場合などに起こる事故。
例えば、AWSのIAMを使っている場合などにS3アクセス権限をReadの権限のみでWriteの権限を忘れたりなどに起こる。
対策は実際にプログラムや設定したユーザ権限で実行してアクセスできているか書き込みできているか確認する。

キャッシュの設定間違い(CDNの設定)

CloudFrontなどのCDNの設定で特定のURLパスに対してキャッシュを持つことができる。
キャッシュを持つことで2回目以降は高速にレスポンスを返すことができるが、
whitelistパラメータの設定漏れがある場合、パラメータがALBやサーバに渡されていない事故が発生する。
APIキャッシュを作成する際は実装パラメータを追加した際、whitelistにも追加する必要があるので注意したい。

サーバ障害

ここで指すサーバはAPIサーバ・DBサーバなどを含む。いずれも常時監視が必要。
原則、単一障害点を作らない
(例えば、サーバが一台しかなくてサーバを落としたらシステム全体が止まるなど)
以下のサーバのリソースが問題となる

  • サーバのCPU性能不足による遅延
  • サーバのメモリ不足
  • サーバのディスク不足

具体的には次のような対策がある

CPUスペックを上げる

無限ループなど負荷が高い処理でCPUが100%に張り付いた状態が続くと他の処理ができなくなり、パフォーマンスが落ちる。CPU稼働率とAPIレスポンス時間などを監視し、CPUリソースが足りない場合は負荷がかかっている処理を修正したり、CPUスペックを引き上げる必要がある。
また、AWSのt2インスタンスやHerokuのdynoなどはCPUリソースの上限があり、上限を超える稼働はサーバが止まるので注意が必要。CPUブーストを使ったり、課金したりする必要がある。

スワップ領域を作成する

スワップ領域を作成しておくことでメモリ不足になったときにディスク領域を一時的に使うことができる。
スワップ領域はディスク領域のため、IO処理が発生しパフォーマンスは落ちるがメモリ領域がパンクして最悪サーバが止まることはない。
(スワップ領域を使いだす前にはメモリを増やしたいが・・・)
メモリがパンクしてるとvimなどのエディタでファイルを開くこともできなくなったりするので、サーバを再起動したり、lessでファイルを開いて編集したりする。(過去の経験上lessが一番軽かった)

ログローテーションを設定する、アップロードファイルはAPIサーバ内に保存しない

ログファイルを何もしないとサーバのディスクを専有していき、いずれディスクをパンクさせる。
logrotateコマンドを使うことで古いログファイルを定期的に消すことができる。(これによりログファイルでのパンクを防げる)
ファイルのアップロードなどをサービスで扱う場合はサーバに直接アップロードするのではなく、S3などのファイルストレージサービスにアップロードする。

バックアップを残す

AWS EC2を使っているのであれば、AWSの設定画面にてEC2インスタンスまるごとバックアップを取ることができる。主にすぐに復旧できない不具合があった場合にDBのバックアップと合わせて、ロールバックする。

監視設定をする

CPU、メモリ、ディスクやヘルスチェックに対して一定時間レスポンスがないなどに関して、
Datadogで閾値を超えたら通知(メール、Slack)を飛ばすことが可能。

冗長化、オートスケールする

サーバを複数台構成にして、ロードバランサー経由でのアクセスにすれば、1台落ちても別のサーバでカバーできるため、システムを止めなくてすむ。(ロードバランサーのヘルスチェックでサーバを死活監視している)
負荷が増えてきた場合はサーバの台数を増やしてオートスケールする。
AWS LambdaやFirebase Functionsなどのサーバレスなマネージドサービスで稼働するのであれば、オートスケーリングも自動でやってくれてメモリの増設などは設定のみで済む。
また、Dockerコンテナでサーバを作成している場合はAWS Fargateを使ってDockerコンテナをオートスケーリング稼働する方法もある。

クラウド自体の障害を確認する

まれにだがクラウドサービス自体が障害を起こしている場合がある。
AWS:AWS Service Health Dashboard
GitHub:GitHub Status
長期的に復旧されない場合はビジネスインパクトが非常に大きい、対策としては一時的にリージョンを分散させて冗長化させるなどがある。

実装バグ対策

事故の原因としてはこれが一番多い。
実装者がシステム仕様や言語仕様やライブラリに関する理解が乏しくて発生する場合が多い。
また技術的負債が溜まっており、設計ミスやコードの可読性や統一性、検索容易性が失われている場合、より事故が起きやすい。
コードレビューで見るべきものに関してはコードレビュー虎の巻にまとめた。

セキュリティの事故

ユーザのシステムの信頼を失墜させる事故。個人情報の流失や最悪金銭的な損害にも発展する。
当然だが、httpだと盗聴される(というかもはやブラウザで警告出る)のでhttps通信する。
ユーザが比較的自由に入力できるフォーム入力周りはセキュリティホールになりがちなので特に注意する。

  • XSS:悪意のあるユーザがブラウザで実行できる悪意のあるJSスクリプトなどをフォーム送信してDB保存する。DB保存されたデータを別のユーザがブラウザで参照した場合に悪意のあるJSスクリプトが実行され、ブラウザに保存されているログイントークンなどの情報を悪意のあるユーザに送信してしまう。対策としては入力時にエンコードしてしまう、フロントエンド側で実行コードとして表示しないなどがある。Reactなどのフレームワークを使っている場合は自動的に無害化してくれる。(dangerouslysetinnerhtmlは除く)
  • SQLインジェクション:悪意のあるユーザがSQL文をフォーム送信し、DBの情報を盗み取ったり改ざんしたりする行為。直接ユーザ入力内容をクエリに埋め込むのでなく、プレイスホルダ機能を使うなどでクエリに問題がある際は実行させない方法などがある。
  • セッションハイジャック:他のユーザのログイントークンを盗み出し、あるいは推測し、他のユーザとしてログインできてしまうこと。これができると他のユーザになりすましや情報取得できてしまう。ログイントークンを改ざんや流出させない、ログイントークンをユーザ別に発行せずにid=1など推測しやすい情報でセッション切り替えできてしまうなどの実装をしない。ログイントークンには改ざん不能なJsonWebTokenなどを使う。
  • CSRF:APIサーバがCORFを許可している場合、外部のサイトからフォーム送信できてしまう。このためフィッシングサイトで本物サーバ側へリクエストさせ、ユーザの情報を盗む手段に使える。特にログイン情報など重要な情報を送信するフォームにはフォームを表示するたびにワンタイムトークンを埋め込み正規のフォーム送信か確認する。
  • DoS攻撃:無意味な大量アクセスでサーバをダウンさせようとする攻撃。ファイアウォール機能でIPを一時的にbanするなどの対策がある。AWSだとWAFを使うなど

他にも色々あるが、パスワードは生でDB保存せずにハッシュ化する。認証必須のAPIと認証不要のAPIを切り分けるは必須。

ルーティング周りの事故

パス追加時に他のパスを上書きしてしまったりキャッシュ起因で発生する

ルーティング追加時に他のルーティングを上書きしてしまう

例えば、APIパスを追加した際に既存のルーティングを上書きしてしまい、対象のAPIにアクセスできなくなる事故。
(SPAであればReact Routerなどの疑似ルーティングのRouteを上書きして、対象のページが表示できなくなる事故もある。)

POST /api/hoge
POST /api/hoge/:id // ←追加
POST /api/hoge/:key // ←URLパラメータ的には異なるがパス的には上のAPIが優先され、到達できなくなる 

追加する際は他のパスのアクセスを上書きしていないか確認し、順番を変えたり、別のパスにするのが大事。
具体的にはAPIの呼び出しテストを作成することでCIで再帰テストをすることが可能。
指定のパスに対しての呼び出しテストなので呼び出される想定の関数はモック化して良い。

キャッシュがあるのに古いAPIやページのパスを消してしまう

古いAPIやページのURLを削除してしまうとブラウザキャッシュが残っていて古いAPIやページのURLにアクセスが来てしまい問題となる。
特にSPAの場合、古いbundle.jsはCDNキャッシュ&ブラウザキャッシュが消えるまで&ブラウザリロードするまで残り続けるため、CDNキャッシュクリアと古いAPIは新しいAPIにリダイレクトする必要がある。
他にも、Google Botなどは古いパスのキャッシュを持っているため、古い方にアクセスが来る。
キャッシュを使ってる場合は古いAPIやページにアクセスが来るので新しいページに301リダイレクトする必要がある。

外部API起因の事故

外部サービスのAPIを呼び出している場合、外部APIの仕様を確認する必要がある

エラー時の処理

考慮漏れしがち。API仕様を見てもどんなエラーが返ってくるかわからない場合や通信エラーの場合でも制御する必要がある。
エラー時に後の処理を行うかの判断もしないといけない。

APIリクエスト上限

これもAPI仕様を見ていないと見落としがち、タチが悪いのはlocalだとリクエスト数少なくて問題ないが、本番環境に上げたら、大量にリクエストしてしまってエラーになる場合がある。課金などで上限を上げれる場合が多いが、代替手段がある場合はそもそも使わないか元が取れる場合に限る。上限を上げれない場合は、APIリクエスト数の上限を超えないようにキューイング(バッチ化)するか、リアルタイム性を求められる場合はエラーとして返す(ユーザに待ってもらう)方法がある。

API呼び出しアカウントのセッション切れ、セッション上限

ステートレスでない(Rest API)でないステートフルなAPIの場合、ユーザアカウントでのセッションを保つ場合がある。セッション切れした場合は再度ログインする必要があるので再ログイン処理の対応も必要となる。
外部サービスのセッションを持つAPIなどはセッション上限などを超えないようにする必要がある

メモリリーク

GC(ガベージコレクション)が無い言語(C、C++など)はもちろん動的メモリ確保した際に利用後、明示的にメモリ解放しないとメモリ領域を食いつぶす。
GCがある言語(JavaScriptなど)でもnewでメモリアロケーションしたインスタンスが循環参照してる場合はGCでもメモリ解放されずメモリリークとなる。
メモリリーク箇所の検出を行い、循環参照している場合は弱参照(WeakRef)やスマートポインタを使う方法がある。
そもそもnewを極力使わない、循環参照させないのも手である。

ちなみにJavaScriptの場合はプロポーサル段階だがWeakRefが存在しているのと
NodeJSのメモリリーク検出方法が参考になる。

パフォーマンス低下による事故

レコード数が多いテーブルはパフォーマンスに注意する。
バックエンドの処理はパフォーマンスが低下するとレスポンス時間が遅くなりシステムがハングする。
readの方はテーブルのよく検索に使われるフィールドにindexを貼る、マイグレーションスクリプトやバッチ処理での大量Writeはbulk処理をして短時間で書き込む対策が必要。
あとはマルチプロセス(cluster)でCPUコア数分サーバ起動することで暫定的な負荷分散はできる。
システム全体のパフォーマンスのボトルネックを見つけるにはframegraphを出力する。

例えば、NodeJSは0から始めるNode.jsパフォーマンスチューニングに調査方法がよくまとまっている。

データ不整合の事故

データ挿入などのデータマイグレーションスクリプトの実装間違えや途中エラーはデータの不整合を起こすので、トランザクションで実装し、問題がある場合はロールバックする。
さらには実行後に問題があった場合に備えて、実行前にはデータのバックアップも取っておく。
また、課金周りなど複数テーブルへの書き込みが必要な重要な処理はトランザクションして不整合を防ぐ。

APIマイグレーションの事故

サブシステムのAPIをメインのシステムから参照している場合にサブシステムのAPIをアップグレードして別のデータを返す必要がある場合、
基本的にサブシステムとメインシステムを完全同時にリリースすることはできないので、ステップ踏む必要がある。

  1. サブシステムの新APIを実装、旧APIはまだ消さない。サブシステムをリリースする。
  2. メインシステムにサブシステムの新API呼び出しの処理を実装、旧APIを消す。メインシステムをリリース。
  3. サブシステムの旧APIを消す。サブシステムをリリースする。

変更がサブシステムに影響を与えてしまう事故

変更がメインシステムだけでなくサブシステムにも影響を与えないか考慮する。
この辺はシステムの全体像がわかっていないと厳しい、有識者が実装、レビューするしかない。
普段から情報をシェアし合う体制が必要。
特に起こりやすいのはDBのテーブルフィールドを変更・削除した場合に
redashなどのBIツールのクエリやsalesforceなどの別システムへのデータ同期を自動で行っている場合などに影響がでて事故る。

排他制御の事故

DBトランザクション、マルチスレッドの排他制御などは、処理をブロッキングしてしまうため、解除し忘れるとシステムをハングさせる。(デットロック)
例えばトランザクション開始時にtry構文でwrapしてやり、finally文で必ずロック解除するようにするなどの対策がある。

ライブラリ(OSS)のバージョンアップに伴う事故

これはnpmやgemなどのパッケージマネージャーツールで3rdパーティライブラリを管理している場合に起こる
ライブラリを使って良いのは保守を上回るメリットがある場合だけで、オーバスペックなライブラリは容量を食うし(特にユーザがアプリやJSファイルをDL際に影響する)、ライブラリのバージョンアップが義務付けられるのでそもそも不要なライブラリは入れずに言語仕様や標準のAPIで実装する。
ライブラリのバージョンは動作確認が取れるまで無闇に上げずに固定しておく(メジャーバージョン、マイナーバージョン、パッチバージョン)。
また、package-json.lockやyarn.lockなど詳細な依存関係を管理しているファイルは3rdパーティライブラリが依存しているライブラリのバージョンが記載されているため、無闇に消してはいけない。
これらのファイルを消してしまうと再インストールした際に3rdパーティライブラリが依存しているライブラリのバージョンが引き上がって事故ることがあるからだ(1回あった)。

技術的負債

事故直接の要因ではないかもしれないが、怠ると事故を引き起こす要因となりえるもの。

型付きの言語で実装する

特にバックエンドは型付きの言語で実装したほうが良い(NodeJS+TypeScript、go、Java)。
理由としては、静的コンパイルによってケアレスミスが防げるからだ。

  • 型チェックで意図しない型のパラメータが引数に渡ってしまうのを防げる
  • 型チェックでパラメータの引数への渡し忘れを防げる
  • 型があることでprimitiveなデータなのかクラスやオブジェクトの型なのかすぐに判別がつく
  • 型チェックがあることでoptionalな引数かそうでないかが型定義でわかる(TypeScriptの場合)
  • 戻り値の型がわかる

TypeScriptでの実装の場合、TypeScriptの為のクリーンコードが参考になる。

設計

KISS(シンプルな設計・実装にする)を心がける。
クラスを使う場合、SOLID原則デメテルの法則も意識すると仕様変更にも強く、テストしやすい。(依存と関心の分離)

  • デフォルト引数を与えてフェールセーフにする、ただし空関数をデフォルト引数に指定するなどの場合は実装漏れなどはエラーは握りつぶさずにエラーログを送信してすぐ発見できるように通知する
  • DBテーブルフィールドの直接の変更、削除は事故になるので別フィールドを追加して処理とデータも移行してから元のフィールドを削除する
  • DRYに則って同じような処理は関数に共通化する。変更が少ないユーティリティは共通化してもよいが、過度な共通化は無駄に影響範囲を広げてしまう・・・あなたはDRY原則を誤認している?
  • 呼び出し箇所が多い関数やテーブルフィールドの修正時には影響範囲に注意する。依存グラフをツールで出力するなどで把握する
  • ビジネスロジックはインタフェース、abstractで抽象化する方が仕様変更に対応しやすいので望ましい。実装は委ねられるが、引数と戻り値の型が保証される。

可読性、検索のしやすさ

命名規則、コーディングルールを統一する。プロジェクトが小さいうちは良いのだが、プロジェクトが大きくなるとファイルの即時検索ができないと明らかに作業効率が落ちる。
キャメルケース、スネークケースなどはどれか一つに統一する(混ぜない!)。
意外に大事なのがtypoを防ぐ、別の意味で使っているのに同じ変数名や関数名にしない、表記揺れをなくす。
これは、修正漏れを防いだり、誤解を招くことに起因する。(ドメインモデルを統一する)
プロジェクト内の既存のtypoの検索にはあいまい検索でfzfなどが使える。
コード量に比例してバグの量も増えるので、YAGNI(無駄な実装をしない、残さない)を常に心がける。

  • lintを入れてコーディングルールを統一する
  • 適切なコメントを入れる(主に機能やビジネスロジックの仕様の説明)、できる限り簡潔に書く
  • ファイル名、変数名、関数名は命名規則を統一する、中身がわかりやすい名前をつける(typoしない)
  • コードが追いにくくなるので関数の呼び出し(コールスタック)を深くしない
  • ネストは深くしない(条件分岐の早期リターンする、非同期コールバック処理はawaitする)
  • 変数、DBテーブルフィールドのダブルミーニングはしない
  • 継承より合成を優先して使う(継承だと不要な変数、メソッドまで継承するリスクや可読性・メンテナンス性が落ちる)、継承自体を禁止する必要はないが、子継承までが限界だと思う

PRの掟(おきて)

事故を起こさないようにするためのPRのルール、PRの役割を複数持たせない(単一責任)
コード量に比例してエラー数も増えるため、修正量は少なくする。
特にソースファイルをまたいでいる数が多い修正や依存が強い箇所の修正は危険なので、極力修正を混ぜない。
リリース後作業が必要なもの、重要な修正に関してはチェックリストをつける。

テスト

資産になるテストを書き、CIで再帰テストを行う。

  • 正常系、境界値、異常系の単体テストを書く
  • 重複したテストを書かない
  • できる限り並列テストにする
  • 条件が複雑なものほど再現が困難なため単体テストで網羅する、単体テストできるような構造にする
  • 単体テストとAPIテストなど異なる種別のテストは同じファイルに書かない(フォルダ分けする)
  • E2Eテストは壊れやすいが網羅性が高いので、サービスのコア機能などにピンポイントで使う
  • 表示の差分テストはビジュアルリグレッションテストが良い(UIライブラリのバージョンアップにも追従できる)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

lamda, api gateway使って、クライアントに値を返すなど

主に自分用メモ。

client->api gateway->youtube apiとかやりたい。

参考: Amazon API Gateway とは?

まずlamda, api gaytewayを触ってみる

下記を見ればだいたいわかる。
ゼロから作りながら覚えるAPI Gateway環境構築

lamdaでのコードをローカルで作る際にはこちらを参考にした。
https://qiita.com/toshihirock/items/d28505442526e0ae7793

次にapi gateway->youtube apiをやってみる

ココやる上で、手元ではうまくresponse返ってきてるが、lamdaのところではresponseがnullになってて、なんでだろうとしていた。
結論は、event handlerが非同期的に処理される?ので、promiseとかでちゃんと作って値を返さないとだめだった。

youtube用には下記のnpmを利用した。
https://github.com/nodenica/youtube-node

外から叩いてみて、値が返ってたらOK!

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

npmパッケージ公開の手順

プログラミング歴半年の素人が書いています。

間違いのないようご自身でも良く調べた上でお願いいたします。

以下の記事を参考にさせていただきました。
https://qiita.com/TsutomuNakamura/items/f943e0490d509f128ae2

npmにサインアップ

まずはnpmの公式サイトから、会員登録しましょう。

https://www.npmjs.com/

npm ユーザの作成

ターミナルからユーザー情報を登録します。

$ npm set init.author.name "Ai Uehara"
$ npm set init.author.email "ai-uehara@example.com"
$ npm set init.author.url "http://qiita.com/aiuehara"
$ npm adduser  # 会員登録情報の入力

npmパッケージの作成

npm initを実行するとpackage.jsonファイルが作成され、npmパッケージとしてディレクトリを管理することができるようになります。

$ npm init

npmパッケージに必要なプログラムを記述する

以下のようなファイル構成になります。
index.jspackage.jsonが必須。
その他に必要なファイルがあればもちろん追加してOK。

作業ディレクトリ/
  +-- index.js
  +-- package.json
  +-- test/
    +-- test-index.js

ライブラリの追加

自分のnpmパッケージに必要なライブラリをインストールするときは、以下のコマンドを使用することで、自動的にpackage,jsonに依存関係の記述が追加される。

$ npm i --save パッケージ名

開発環境のみで使用するライブラリの追加

テストなど開発環境のみで使用するパッケージは、以下のコマンドを使用する

$ npm i --save-dev パッケージ名

ライセンスファイルの作成

公開するnpmパッケージのライセンスについて、明記しておきます。
誰でも自由に使用できるライセンス(MIT)として公開することが一般的です。

LICENSE.txtファイルを作成し、The MIT Licenseから丸々コピーします。

Copyright <YEAR> <COPYRIGHT HOLDER>の部分を、書き換えて保存します。

npmに公開

$ git add -A
$ git commit -m "first commit"
$ git tag -a v1.0.0 -m "My first version v1.0.0"
$ git push origin tags/v1.0.0
$ npm publish ./

バージョンアップするとき

一度npmに公開したパッケージをパージョンアップさせる時には、バージョン情報も合わせて更新する必要があります。

パッチ バージョンアップ

$ npm version patch              # <- v1.0.0 からv1.0.1 にアップ
v1.0.1
$ git tag                        # <- git のtag も自動的に作成される
v1.0.0
v1.0.1
$ git push origin tags/v1.0.1    # <- ただし、git push まではやってくれないので、必要に応じて自分でgit push ...
$ npm publish ./                 # <- npm で公開

マイナーバージョンアップ、メジャーバージョンアップ

$ npm version minor    # v1.0.1 からv1.1.0 にアップ
v1.1.0

$ npm version major    # v1.0.1 からv2.0.0 にアップ
v2.0.0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsの非同期処理をPromiseから理解しようとしてみた

はじめに

今回は、Node.jsの非同期処理について、自分の備忘録も兼ねて記事を作成しました。

私はJavaScriptの言語仕様なんかをあまり知らない状態でNode.jsを触ったせいで、非同期処理に関する部分ではまり、多くの時間を無駄にしてしまいました。

かなり初歩的な内容かもしれませんが、勉強した内容をまとめます。

同期処理とは?

上から順番にプログラムが実行されていくことです。

「上から順番に」という言葉が適切かどうか分かりませんが、「一つ一つの処理が、一個前の処理の終了をまって処理されていく」っていう説明よりは個人的に分かりやすい気がします。

コードにすると、下記の通りです。

console.log(1);
console.log(2);
console.log(3);
console.log(4);
console.log(5);

実行結果は、下記のようになります。

1
2
3
4
5

非同期処理とは?

同期処理ではないものが非同期処理なので、上から順番にプログラムを実行されないことと言えます。

JavaScriptでは、ユーザーの入力やAPIを叩いてデータを持ってくる時、それからファイルを操作する時などに、非同期処理になります。

これはJavaScriptがシングルスレッドなので、そういった「制約」を非同期で処理することによっってフォローしています。

コードにすると、下記の通りです。

ここでは例として、遅延処理を用いています。

const three = () => {
  setTimeout(() => {
    console.log(3);
  }, 1000);
}

console.log(1);
console.log(2);
three();
console.log(4);
console.log(5);

実行結果は、下記のようになります。

1
2
4
5
3

setTimeoutで処理の実行が1秒後に設定されたthree関数が呼び出されています。

これが同期処理であれば、実行結果としては順番に1から5までの数字が出力されますが、Node.jsではこういった処理は非同期になるので、three関数の終了を待たずに次の処理へ進み、最後にthree関数の結果が返されています。

これが非同期処理です。

Promiseとは?

なぜPromiseが必要か?

上記で述べた非同期処理ですが、何でもかんでも非同期処理にすると不都合なこともあります。

例えば、ファイルの内容を読み込んで、ファイルの中身を出力するような場合、

(hogeと書かれたhoge.txtというファイルが存在するとします)

const fs = require('fs');

const result = fs.readFile('hoge.txt');
console.log(result);

上記のコードだと、hoge.txtを読み込む処理は非同期に処理されます。

しかし、hoge.txtを読み込み終わるより先にresultが出力されてしまうので、上記のようなコードでは想定する結果を得ることができません。

そんな時に使えるのが、Promiseです!

Promiseは、その名の通り、その処理を行うことを約束することができます。

もっと簡単にいうと、Promiseを使えば本来非同期に行われる処理を、同期処理のように書くことができます。

上記のようなケースを解決するためには、Promiseが必須なのです。

Promiseの使い方

Promiseオブジェクトを返す関数を定義します。基本的にはそれだけです。

試しに先ほどのhoge.txtを読み込むコードのうち、実際にファイルを読み込む処理の部分をPromiseを使って同期的に処理できるように書き換えてみます。

const fs = require('fs');

const readAsync = return new Promise((resolve, reject) => {
  resolve(fs.readFile('hoge.txt'));
})

これでPromiseオブジェクトを返す関数を作成できました。

あとはconsole.logで出力するタイミングを、この関数の処理が実行された後になるように全体のコードを書き換えます。

このような処理をするときは、非同期関数に.then節を記述します。

const fs = require('fs');

const readAsync = return new Promise((resolve, reject) => {
  resolve(fs.readFile('hoge.txt'));
})

readAsync().then((result) => {
  console.log(result);
})

これでファイルが読み込まれるのを待ってからconsole.logでファイルの中身が出力されるようになります!

Promiseオブジェクト作成の際に引数に渡しているresolveとrejectですが、

resolveには非同期処理が成功した時に値が入り、失敗した時にはrejectに値が入ります。

例えば、存在しないfuga.txtというファイルを読み込もうとした場合、非同期関数の結果は失敗になるので、上記の例のようにresolveではなく、rejectに値が入ります。

const fs = require('fs');

const readAsync = return new Promise((resolve, reject) => {
  reject('指定されたファイルは見つかりません');
})

readAsync().then((result) => {
  console.log(result);
}).catch((err) => {
  console.log(err);
})

Promiseオブジェクトを返す非同期関数の.catch節でエラーをハンドリングしています。

上記のコードの場合、下記のような出力になります。

指定されたファイルは見つかりません

Promise.allとPromise.raceについて

Promise.allとPromise.raceはどちらも複数の非同期関数を実行するためのものです。

それぞれの違いは、Promise.allは指定した全ての関数がresolveでもrejectでも全ての関数が実行されます。

Promise.raceは、指定した全ての関数の中で一つでもresolveまたはrejectになったら、その関数の結果のみを返して処理を終了します。

また、Promise.allでは実行の順番を保証するので、例えば下記のようなコードでも出力は順番通りになります。

const a = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 10 * 1000); // 10秒待つ
})

const b = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 5 * 1000); // 5秒待つ
})

const c = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3);
  }, 1 * 1000); // 1秒待つ
})

Promise.all([a, b, c]).then((values) => {
  console.log(values[0]);
  console.log(values[1]);
  console.log(values[2]);
})

setTimeoutの値を見ると、出力される値の順番的には3, 2, 1となりそうですが、実際の出力は、

1
2
3

となります。

Promise.raceのソースは下記のようになります。

返す値は関数一つ分なので、Promise.allのように配列で受け取る必要はありません。

const a = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 10 * 1000); // 10秒待つ
})

const b = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 5 * 1000); // 5秒待つ
})

const c = return new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3);
  }, 1 * 1000); // 1秒待つ
})

Promise.race([a, b, c]).then((value) => {
  console.log(value);
})

上記のコードだと、一番早く処理が終わるのは非同期関数bなので、出力は、

2

となります。

おわりに

ジャバスクリプトムズカシイ。

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