文章から単語を取り出して数えるサービスを作成してみた

# 概要

「単語を数えるプログラミング」をテーマにお勉強してみました。
単語を数えるということで形態素解析を用いて文章から単語を取り出して単語数を数えてみるサービスを作ることにしました。

お勉強した結果ですが以下のサービスとして公開しておりますので、以下の [作ったモノ] (https://qiita.com/tamoco/items/79746d0fd7b9d149f432#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%83%A2%E3%83%8E) からご確認いただければ幸いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/b9a754b8-e366-3434-835d-7093b66e63c0.png)

# 使用しているフレームワークとライブラリ

今回は`Glitch`という`Node.js`のアプリを公開するためのサービスを使用して公開しています。

## Glitch
https://glitch.com/

フレームワークとライブラリについては以下を使用しております。

## フレームワーク

- バックエンド
  - [Express] (https://expressjs.com/ja/)
- フロントエンド（HTML直書き）
  - [Vue.js] (https://jp.vuejs.org/index.html)
- CSS
  - [Bulma] (https://bulma.io/)

## ライブラリ

- 形態素解析
  - [kuromoji.js] (https://github.com/takuyaa/kuromoji.js)
- ネガポジ判定（前向き・後ろ向き判断）
  - [negaposi-analyzer-ja] (https://github.com/azu/negaposi-analyzer-ja)
- グラフ
  - [Chart.js] (https://www.chartjs.org/)


# 作ったモノ

公開しているサイトとコードは以下のアドレスでご確認いただけます。

## 公開先（作ったモノの動作を確認する）
https://morphological-analysis.glitch.me/

## コード
https://glitch.com/edit/#!/morphological-analysis

# 作ったモノの説明

コードについては上記のコードをご確認いただければ幸いです。
以下に特記したい内容を記載します。

## バックエンド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/86600923-531e-1343-0726-610520981a41.png)

```javascript
const express = require("express");
var compression = require('compression')
const app = express();
const kuromoji = require("kuromoji");
const analyze = require("negaposi-analyzer-ja");
app.use(compression())
app.use(express.static("public"));
app.set("json spaces", 2);

const builder = kuromoji.builder({
  dicPath: "node_modules/kuromoji/dict"
});

app.get("/", (request, response) => {
  response.sendFile(__dirname + "/views/index.html");
});

app.get("/morphological", (request, response) => {
  builder.build(function(err, tokenizer) {
    if (err) {
      throw err;
    }
    let reqMsg =
      request.query.msg ||
      "形態素解析とは文章を意味を持つ最小の単位に分けるイメージです";
    let tokenizedResult = tokenizer.tokenize(reqMsg)
    let result = {
      send_msg: reqMsg,
      morphological_result: tokenizedResult,
      negaposi_score: analyze(tokenizedResult)
    };

    console.log("tokened:" + result);
    response.json({ result });
  });
});

app.get("/negaposi", (request, response) => {
  builder.build(function(err, tokenizer) {
    if (err) {
      throw err;
    }
    let reqMsg =
      request.query.msg ||
      "形態素解析とは文章を意味を持つ最小の単位に分けるイメージです";

    const score = analyze(tokenizer.tokenize(reqMsg));
    builder = null;
    response.json({
      message: reqMsg,
      negaposi_score: score
    });
  });
});

// listen for requests :)
const listener = app.listen(process.env.PORT, () => {
  console.log("Your app is listening on port " + listener.address().port);
});

```

`Express`による`API`サーバーです。文章を単語ずつに区切る形態素解析するための`API`として実装されています。ウェブサーバーとしてURLのトップにアクセスされた場合は、`views/index.html`を表示するようにしています。

### 形態素解析

[https://ja.wikipedia.org/wiki/形態素解析] (https://ja.wikipedia.org/wiki/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90)

上記のWikipediaから抜粋させていただきます。
辞書と呼ばれる単語の品詞等の情報にもとづき、形態素（Morpheme, おおまかにいえば、言語で意味を持つ最小単位）の列に分割し、それぞれの形態素の品詞等を判別する処理です。

今回の場合は`kuromoji.js`という形態素解析のライブラリを使用しています。
`kuromoji.builder`で辞書を読み込むことで形態素解析の処理を行っています。

なお、形態素解析の品質は辞書に依存することがほとんどで、今回はデフォルトの辞書を使用しているため、流行語などで単語を区切ったりすることはできません。

### オマケ：ネガポジ判定

`kuromoji.js`で形態素解析された結果を使ってネガポジ判定もできるようなので実装してみました。ネガポジ判定とは、主に人の発言や発想などが、前向き（ポジティブ）か後ろ向き（ネガティブ）かを判定するモノになります。

使用させていただいた [negaposi-analyzer-ja] (https://github.com/azu/negaposi-analyzer-ja) では、形態素解析された結果を [単語感情極性対応表] (http://www.lr.pi.titech.ac.jp/~takamura/pndic_ja.html)を使用してネガポジ判定をしているようです。こちらも単語感情極性対応表に依存することになります。

ネガポジ判定自体はPythonなどの機械学習などの方が向いていると思います。[Bert] (http://nlp.ist.i.kyoto-u.ac.jp/index.php?BERT%E6%97%A5%E6%9C%AC%E8%AA%9EPretrained%E3%83%A2%E3%83%87%E3%83%AB)や[Word2Vec] (https://ja.wikipedia.org/wiki/Word2vec) などを使えば、自分だけのネガポジなどを作れそうですね。

## フロントエンド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/3e71bd93-20c0-9d96-fb09-e65fdff55193.png)

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>morphological-analysis</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
    />
    <link
      href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/bulma@0.8.2/css/bulma.min.css"
    />
  </head>

  <body>
    <section class="hero is-info is-fullheight" id="app">
      <div class="hero-body">
        <div class="container has-text-centered">
          <div class="column">
            <div class="box">
              <div class="field is-grouped">
                <p class="control is-expanded">
                  <input
                    class="input"
                    v-model="sendMsg"
                    type="text"
                    placeholder="形態素解析する文章を入力してください"
                  />
                </p>
                <p class="control">
                  <a class="button is-info" @click="morphoExec">
                    実行する
                  </a>
                </p>
              </div>
            </div>
            <div class="subtitle" v-if="isLoading">
              <progress
                class="progress is-large is-warning"
                max="100"
              ></progress>
            </div>
            <h2 class="subtitle has-text-left" v-if="sendMsg">
              {{sendMsg}}
            </h2>

            <h2 v-if="dataset.length > 0" class="bar-graph">
              <chartjs-horizontal-bar
                :labels="labels"
                :data="dataset"
                :bind="true"
                :datalabel="dataLabel"
                :option="chartOption"
                :height="chartHeight"
              ></chartjs-horizontal-bar>
            </h2>
            <h1 class="subtitle" v-if="negaposiScore">
              ネガポジスコア： {{negaposiScore}}
            </h1>

            <h3 class="subtitle has-text-left" v-if="meishiCountList">
              <p class="subtitle has-text-left">単語一覧</p>
              <li v-for="(item, key) in meishiCountList" class="has-text-left">
                {{ key }} : {{ item }}
              </li>
            </h3>
          </div>
        </div>
      </div>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/vue-chartjs@2.6.0/dist/vue-chartjs.full.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.js"></script>
    <script src="https://unpkg.com/hchs-vue-charts@1.2.8"></script>
    <script>
      "use strict";
      Vue.use(VueCharts);
      new Vue({
        el: "#app",
        data: {
          sendMsg: null,
          meishiCountList: null,
          negaposiScore: null,
          isLoading: false,
          labels: [],
          dataset: [],
          dataLabel: "名詞 カウント",
          chartOption: {
            legend: {
              display: false
            },
            responsive: true,
            maintainAspectRatio: false,
            scales: {
              xAxes: [
                {
                  ticks: {
                    max: 5,
                    min: 0,
                    stepSize: 1
                  }
                }
              ]
            }
          },
          chartHeight: 500
        },
        methods: {
          morphoExec() {
            this.negaposiScore = null;
            this.labels = [];
            this.dataset = [];
            this.sendMsg =
              this.sendMsg ||
              "何も文字が入力されなかったのでサンプルの文章で形態素解析を実行します。何か文字を入力することで単語をカウントします。";
            const sendMsg = this.sendMsg;
            this.isLoading = true;
            const vueThis = this;
            axios
              .get("/morphological?msg=" + sendMsg)
              .then(function(response) {
                const morphoResult = response.data.result.morphological_result;
                let meishiList = [];
                morphoResult.filter(function(item, index) {
                  if (item.pos == "名詞") {
                    meishiList.push(item.surface_form);
                    return true;
                  }
                });
                let meishiCount = {};

                for (let i = 0; i < meishiList.length; i++) {
                  let key = meishiList[i];
                  meishiCount[key] = meishiCount[key]
                    ? meishiCount[key] + 1
                    : 1;
                }

                vueThis.labels = Object.keys(meishiCount);
                vueThis.dataset = Object.values(meishiCount);
                vueThis.negaposiScore = response.data.result.negaposi_score;
                vueThis.meishiCountList = meishiCount;
                vueThis.chartOption.scales.xAxes[0].ticks.max = Math.max.apply(
                  null,
                  Object.values(meishiCount)
                );

                vueThis.chartHeight = 30 * meishiList.length;
                console.log(
                  "最大値:" + Math.max.apply(null, Object.values(meishiCount))
                );
                console.log(vueThis.meishiCountList);
              })
              .finally(() => (this.isLoading = false));
          }
        }
      });
    </script>
    <style>
      .bar-graph {
        margin-left: auto;
        margin-right: auto;
        padding-top: 10%;
        padding-bottom: 10%;
        background-color: #f5f5f5;
      }
    </style>
  </body>
</html>
```

### Vue.js

`Express`でもテンプレートエンジンとして使用できるのですが、今回のフロントエンドのフレームワークは`Vue.js`を使用しています。単純に`Vue.js`が書きやすかったので選択（いろいろとダメですが・・・）しました。

バックエンドの`API`への接続から、形態素解析された後のデータの処理を`Vue.js`の`methods`で処理しています。返ってくるデータを整理するために、いろいろと試行錯誤していますが、もう少しイイカンジに書けそうな気がします。

### Bulma

CSSや見た目のデザイン面では`Bulma`を使用しています。今回は以下のテンプレートから参考させていただきました。

https://bulmatemplates.github.io/bulma-templates/templates/landing.html

### Charts.js

以下のサイトを参考にグラフを追加しました。

https://vuejsexamples.net/vue-bar-chart/

当初はなかなか`Chart.js`のプロパティ（設定）の理解ができませんでした。
しかしながら元のライブラリのコードを読んだり、ドキュメントをちゃんと読むことで少しずつ理解することできました。具体的にやったことは`Option`を設定する（上書きする）ことでグラフの設定をカスタマイズすることができました。

# 課題

形態素解析するときに辞書を読み込むので極端にメモリを消費しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412322/1b6963ed-3581-e3ac-ee66-c36667173452.png)

今後辞書を良いものに変えると辞書の容量も増えることになり、もっとメモリを消費することになります。もし品質を向上させる場合は、`Glitch`をアップグレードするか他のサービスを使うことを検討する必要がありそうです。

ちなみに当初は [Netlify Functions] (https://www.netlify.com/products/functions/) を使おうとしていて、辞書ファイルの読み込みが上手くいきませんでした。これはファイルの読み込み（`fs.readFile`）に工夫がいるとのことで、今回は`Glitch`を選択しています。

コードも改善余地があり形態素解析した結果の処理などはもっとイイカンジにできると思います。そもそも`Express`に統一した方がよいかもしれません。ただ、今回はお勉強が目的でどんどん動かしていくモノを作っていけたので一旦は良い結果が得られたと思います。