20200802のNode.jsに関する記事は5件です。

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

概要

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

お勉強した結果ですが以下のサービスとして公開しておりますので、以下の 作ったモノ からご確認いただければ幸いです。

image.png

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

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

Glitch

https://glitch.com/

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

フレームワーク

ライブラリ

作ったモノ

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

公開先(作ったモノの動作を確認する)

https://morphological-analysis.glitch.me/

コード

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

作ったモノの説明

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

バックエンド

image.png

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/形態素解析

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

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

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

オマケ:ネガポジ判定

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

使用させていただいた negaposi-analyzer-ja では、形態素解析された結果を 単語感情極性対応表を使用してネガポジ判定をしているようです。こちらも単語感情極性対応表に依存することになります。

ネガポジ判定自体はPythonなどの機械学習などの方が向いていると思います。BertWord2Vec などを使えば、自分だけのネガポジなどを作れそうですね。

フロントエンド

image.png

<!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.jsmethodsで処理しています。返ってくるデータを整理するために、いろいろと試行錯誤していますが、もう少しイイカンジに書けそうな気がします。

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

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

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

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

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

Raspberry Pi4 地デジサーバ Chinachu+mirakurun の Node.jsをアップデートしてみた

Raspberry Pi4で地デジサーバを Chinachu +mirakurun で稼働していますが、以下のようにmirakurunをv3.2.0にアップデートした後、chinachuもアップデートしたところ、動かなくなりました。

$ sudo npm install mirakurun -g --unsafe-perm --production  //mirakurunのアップデート

$ cd chinachu
$ ./chinachu updater  //chinachuのアップデート

アップデート後、ステータス($ sudo pm2 status)を確認したところ、erroredと表示されました。

なぜ動かなくなったか

原因は、Node.js をアップデートする前に、mirakurun をアップデートしたためでした。
それで、Node.jsをv8.9.4からv10.16.3にアップデートしました。

正しいアップデートの方法

1.Chinachuのユーザー追加

まず、Chunachu は Raspberry Pi のデフォルトユーザーでは動かない(録画が乱れる)ようなのでユーザーを追加します。(Chinachuを使うためにユーザーをvideoグループに追加する。録画先のディレクトリ権限も変更。)

$ sudo adduser <追加ユーザ>
$ sudo gpasswd -a <追加ユーザ> sudo
$ sudo adduser <追加ユーザ> video
$ sudo chown <追加ユーザ>:video /mnt/hdd1

Sambaの設定も修正します。

$ sudo nano /etc/samba/smb.conf

[recorded]
   comment = Chinachu Recorded Folder
   path = /mnt/hdd1/recorded
   browsable = yes
   read only = no
   guest ok = yes
   force user = <追加ユーザ>

# sambaを再起動
$ sudo service smbd restart

# ディレクトリのオーナーを変更
$ sudo chown <追加ユーザ>:video /mnt/hdd1

2.Node.jsとMirakurunのアップデート

Node.jsをv10.16.3にアップデートします。
また、Mirakurunも更新をします。

$ sudo apt-get install npm
$ sudo npm install -g n
$ sudo n 10.16.3
$ sudo npm install -g npm

$ sudo npm install pm2 -g
$ sudo npm install mirakurun -g --unsafe --production

3.Chinachuのアップデート

その前に・・・Chinachu録画設定ファイルに追加ユーザなどを設定します。

$ cd chinachu
$ vi config.json
  "uid": "<追加ユーザ>"
  "recordedDir" : "/mnt/hdd1/recorded/",
  "temporaryDir" : "/mnt/hdd1/recordtmp/",

そして、Chinachuのアップデートをします。

~/chinachu $ ./chinachu updater

$ sudo pm2 status //全部onlineになっているか確認!
$ sudo reboot

奮闘記(参考までに・・・)

1.Node.jsの調整

$ sudo apt remove nodejs
$ sudo apt autoremove
$ wget https://nodejs.org/dist/v10.22.0/node-v10.22.0-linux-armv7l.tar.xz
$ tar -xvf node-v10.22.0-linux-armv7l.tar.xz
$ cd node-v10.22.0-linux-armv7l
$ sudo rm /usr/local/bin/node  
$ sudo cp -R * /usr/local/
$ cd

今回は、Node.jsはv10.22.0にしましたが、v12やv14でも可能なようです。

ここで、以下の実行でエラーとなりました。

$ sudo apt-get install npm

パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています                
状態情報を読み取っています... 完了
インストールすることができないパッケージがありました。おそらく、あり得
ない状況を要求したか、(不安定版ディストリビューションを使用しているの
であれば) 必要なパッケージがまだ作成されていなかったり Incoming から移
動されていないことが考えられます。
以下の情報がこの問題を解決するために役立つかもしれません:

以下のパッケージには満たせない依存関係があります:
 ・・・

依存関係が壊れているようなので aptitude をインストールし、以下のコマンド実行することにしました。

$ sudo apt-get install aptitude
・・・
$ sudo aptitude install npm
・・・
以下のアクションでこれらの依存関係の問題は解決されます:

     以下のパッケージを現在のバージョンに一時固定する:
1)     libnode-dev [インストールされていません]       
2)     libnode64 [インストールされていません]         
3)     libssl-dev [インストールされていません]        
4)     node-gyp [インストールされていません]          
5)     npm [インストールされていません]               

この解決方法を受け入れますか? [Y/n/q/?]

Y を選択して先に進めます。
そして、次のコマンドを実行しましたがエラーになってしまいました。

$ sudo apt-get update

それで一旦最初からやり直すことにしました。

$ sudo apt remove --purge nodejs npm
$ sudo apt clean
$ sudo apt autoclean
$ sudo apt install -f
$ sudo apt autoremove
$ sudo apt install curl
$ cd ~
$ curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
$ sudo apt-get install -y nodejs

$ curl -L https://www.npmjs.com/install.sh | sh

結局、以下の方法が良かったようです。

$ sudo npm install -g npm 
$ sudo npm install pm2 -g
$ sudo npm install arib-b25-stream-test -g --unsafe
$ sudo npm install mirakurun -g --unsafe --production

そして、ステータスの確認をします。

$ sudo pm2 status
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 2  │ chinachu-operator  │ fork     │ 3    │ stopped   │ 0%       │ 0b       │
│ 1  │ chinachu-wui       │ fork     │ 1    │ stopped   │ 0%       │ 0b       │
│ 0  │ mirakurun-server   │ fork     │ 0    │ online    │ 0.3%     │ 76.9mb   │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

pm2 に登録して保存します。

$ sudo pm2 start processes.json
$ sudo pm2 save

また、gitの修正を元に戻しておきます。

$ git fetch origin
($ git reset --hard origin/masterでエラー)

試しで録画をし、確認してみたところ、ひどく映像が乱れているのでチューニングを行いました。

2.Mirakurunのチューニング

バッファを増やします。

$ sudo mirakurun config server
#highWaterMark: 25165824 # integer (bytes)
#highWaterMark: 67108864 # 64MB
#highWaterMark: 134217728 # 128MB
highWaterMark: 268435456 # 256MB

次にヒープサイズを増やします。

$ sudo pm2 start mirakurun-server --node-args --max_old_space_size=1024

mirakurun config server で下記設定を追加。

disableIPv6: true

rocesses.jsonを修正してプロセスサイズの最大値も引き上げておく。

$ sudo vi /usr/lib/node_modules/mirakurun/processes.json
    :
      "node_args" : "--max_old_space_size=2048",

再度、mirakurunインストールをします。

$ sudo npm install mirakurun -g --unsafe --production

そして、以下を実行します。

$ export PATH=/home/pi/chinachu/.nave:$PATH 
$ cd /usr/lib/node_modules/npm
$ sudo npm install

ここまで試して、chinachuとmirakurunのステータスは online であるものの、録画が乱れたままでした。結局、前述の「1.Chinachuのユーザー追加」で解決しました。

主な参考サイト

DIGITAL CASSIS SODA内の記事ー https://mimimopu.com/chinachu-errored/
東京工業大学デジタル創作同好会の記事ー https://trap.jp/post/1089/

最後に

うれしいことに、Raspberry Pi4の発売により、地デジサーバを入れる人の人口が増えています。
今後、アップデートをする人も増えると思います。
少しでも、この記事が参考になれば幸いです。

また、Chinahuのこれからの改善や発展を期待しています!

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

Obnizで動体検知センサー(HC-SR505)を利用して、動体検知したらスマホに通知。

「動体検知したらスマホに通知」をやりたい

こんなことをやってみよう。

使うのはこちら。

<ハードウェア>

<ソフトウェア>

  • Node.js
  • IFTTT

HC-SR505

Obnizに、HC-SR505を直刺しします。

Node.js

Node.jsで用意したコードです。

const ifttt_event = "obniz"; //イベント名
const ifttt_secret_key = "IFTTTのキーを入れます"; //キー
const Obniz = require("obniz"); // デバイスに接続
var obniz = new Obniz("番号を入れます"); //Obnizの番号を指定
var webclient = require("request");
obniz.onconnect = async function () {

  var sensor = obniz.wired("HC-SR505", {vcc:0, signal:1, gnd:2});

  //IFTTTのwebhooksのURLを指定。
  const IFTTT_URL = 'https://maker.ifttt.com/trigger/' + ifttt_event + '/with/key/' + ifttt_secret_key;

  sensor.onchange = async function(val){


    if (val) {
      //動きを検知したとき
      obniz.display.clear();
      obniz.display.print("Moving Something!");

      //IFTTTリクエスト
      webclient.post({url: IFTTT_URL}, function (error, response ,body){console.log(body);});
    }
    else{
      //検知しないとき
      obniz.display.clear();
      obniz.display.print("Nothing moving");
    }
  }
 }

検知したとき

動体検知すると、スマホに通知が届きます。
(”誰か来ました”については、IFTTTのメッセージで設定しています)

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

SQL文で変数を使う方法 in JavaScript

はじめに

Node.jsでExpress+Mysqlを用いてRestApiを作成する際、postでの処理に詰まったため備忘録として記事を書きます。

問題

id(int) name(String)
1 test

このようなテーブルに対して、以下のソースを用いてpostでレコードを挿入する際、数値は入るが文字列はエラーが出て入れることができなかった。

sample.js
id=req.body.id; 
name=req.body.name"; 

//jsonで{id:2,name:”sample”}というデータを送信

sql="INSERT INTO api_tbl VALUES("+id+","+name+")";
connection.query( sql,function (error, results, fields) {
   if (error) throw error;
   res.send("ok");
});

原因としては、postリクエストを送った際、sql文をセットしている変数sqlに以下のような文字列が格納されていたからと考えた。

sql="INSERT INTO api_tbl VALUES(2,sample)"

ここからnameの文字列を「""」で囲うために、変数nameの値を以下のように書き換えることで実装できた。

name= '"'+req.body.name+'"'

おわりに

SQL文はあまり学習していないので変なところで詰まってしまった…反省。
基本的な構文はすらすら書けるようしっかり学習します。
今回の手法、もっと簡単にできるやり方があったら教えてください!

(2020/8/2 追記)
@nagtkk さんのコメントで、プレースホルダ(?)をつかってsql文に値をセットするやり方を教えてもらいました!
実際に試してみたらしっかりと動作し、またソースも見やすく簡単に実装できたためこちらの手法を推奨します。

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

windows で npm i に失敗する

やりたいこと

npm i

エラー

npm ERR! cb.apply is not a function

npm ERR! A complete log of this run can be found in:

解決法

nodejs のインストールに nodist を使っていたが、node を直接インストールして実行するように変更すると動くようになった。

実行したコマンド

# nodist のアンインストール
> choco uninstall nodist
# node を直接インストール
> choco install -y nodejs.install

# shell を開きなおす
> node -v
v14.7.0
> npm -v
6.14.7
> npm i -g npm
+ npm@6.14.7
added 434 packages from 880 contributors in 6.276s
>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む