『構文解析をする』『色も解析する』 「両方」やらなくちゃあならないってのが「このアプリ」のつらいところだな　覚悟はいいか？　オレはできてる

## 🔫 TL;DR （3行でまとめると）

~~[【Qiita x COTOHA APIプレゼント企画】](https://zine.qiita.com/event/collaboration-cotoha-api/) の豪華なプレゼントほしい！！~~

（自分を筆頭に）はじめて自然言語処理にふれる人がわかりやすいように Web アプリ化した [^1] 。

（ついでに得意分野である）色と組み合わせることで個性を出した。

![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317946/0f838529-a1aa-4359-7a77-ef4de1b1a66e.png)



[^1]: 公開したかったのだけど、実際には現時点で公開できていません……

## 🍔 はじめに

[【Qiita x COTOHA APIプレゼント企画】](https://zine.qiita.com/event/collaboration-cotoha-api/) というのを見つけて応募記事を眺めていたんですが、やはり多くの記事は相性のいい `Python` で書かれていました。

僕自身が [COTOHA API](https://api.ce-cotoha.com/contents/index.html) を利用するのも、自然言語処理を利用するのもはじめてだったので、記事に書かれていることを読んでも実感がわかないというか、上の空という感じでした（もちろん読んでみると面白いんですけどね！）。

ということで、はじめて触る人でも解析結果がわかりやすいように、初学者目線 [^2] で『構文解析』の API を利用したアプリを ~~公開してみました！~~ 公開できませんでした……ごめんなさい！



[^2]: 公式リファレンスで解析デモが用意されているのですが、ノード上に表示されている文字の羅列は、初めて使う僕の目には少し難しく映りました

### ▶ ~~できあがった~~ できあがらなかったもの

本当なら Web アプリを公開して利用してもらって記事を読んでもらいたかったのですが、 `nuxt genetate` でビルドされた静的コンテンツでは API 部分がうまく動作しないようでエラーになってしまいました…… :innocent:

Netlify Functions と組み合わせて動作するものを公開するつもりですが、間に合いませんでした、ごめんなさい 🙇

（エラーになるけど、一応…… [Color palettes feat. COTOHA API](https://color-palettes-feat-cotoha-api.netlify.com/) ）

<br>

ソースコードは公開していますので、ローカルに落としてもらってアプリを試してもらうことは可能です。

> 1. [Color palettes feat. COTOHA API （公開ソースコード） | GitHub](https://github.com/azukisiromochi/color-palettes-feat-cotoha-api-open) よりクローン
> 2. `~api/env.js` の `<COTOHA API Client ID>` や `<COTOHA API Client Secret>` を COTOHA API for Developers 登録時に表示されるものに置き換え
> 3. `yarn install`
> 4. `yarn dev` （エラーになる場合は `yarn run lint -- --fix` してリトライ）
> 5. http://localhost:3000 にアクセス



## 🚀 仕組みについて

さきほど紹介したアプリでは、テキストエリアに入力した文章を『構文解析』し、解析結果によりその品詞が `名詞` と判断された単語を『色の解析（ Google 検索して16進色コードを取得）』しています。

そのそれぞれに軽く触れたいと思います。



### ▶ 構文解析

[公式の説明](https://api.ce-cotoha.com/contents/api-all.html) では、

> **日本語テキストの構造と意味を解析します。**
>
> - 文の構造と意味を解析するRESTful API です。
> - 入力された文を文節・形態素に分解し、文節間の係り受け関係や形態素間の係り受け関係、品詞情報等の意味情報等を付与します。
> - 「誰(何)が」「どうした」という情報を形態素と呼ばれる単語単位で切り出すことができるため、多数のテキストデータに対して情報を抽出・解析するデータマイニングなどに応用することが可能です。

とあります。

簡単に説明すると、文章を品詞ごとに分解して、単語ごとに品詞の情報や意味情報を付与して返してくれる API です。

<br>

作成したアプリで例を見てみましょう。

まずは、テキストエリアに解析したい文字を入力してみます。

![構文解析1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317946/9d992a76-f47a-9e87-6b87-8286b1da0c29.png)


非常に難解な文章ですが、ちゃんと解析されるのでしょうか？

『解析解析解析解析ィーー！』ボタンをクリックします。

![構文解析2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317946/cdcbd3f0-099d-8f24-24e5-66a2c1ee7239.gif)


解析されました！

アプリでは品詞がわかりやすいように、色分けと、ポップオーバーをつけています。



### ▶ 色の解析

これは僕のアプリオリジナルですが、まずはサンプル動画を見ていただくのが手っ取り早いです。

![色解析1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317946/72807705-1d89-dd08-bbf7-24eecdddd233.gif)


このように、構文解析された単語から 16 進数色コードを取得して背景色に設定しています。

具体的には次のような手順で『色の解析』を行っています。

- 品詞が `名詞` のもののみにフィルタリング
- 名詞（例えば「赤」など）に ` 色 コード` というワードを付与して Google 検索
- 検索結果のタイトルから 16 進数色コードにあたる文字（正規表現で）が含まれるものを返却



![色解析2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317946/80f7fe6b-5360-a181-a8b4-33286f9cdc96.png)


アプリにも『検索キーワード」『検索結果（色コードを含むもののみ）』『 16 進数色コード』を表示して、どのような検索が行われたかを分かるようにしています。



## 🐛 実装について

ここは興味ある人だけ見れるように開閉式にしています。



### ▶ 構文解析

<details><summary>クリックで展開</summary><div>

主に COTOHA API を Nuxt.js で呼び出す方法をまとめています。

（外部 API 呼び出すコード書いたことなかったので、結構苦戦しました……。というか未だに公開できていない……）



#### ✍ Nuxt プロジェクトを作成するときに Express と Axios を選択

これは `Express` である必要が必ずしもないですし、サーバーサイドフレームワークを利用しなくても出来るのですが、より簡単にできそうだったのでチョイスしました。

```shell
? Choose custom server framework (Use arrow keys)
  None (Recommended)
  AdonisJs
> Express
  Fastify
  Feathers
  hapi
  Koa
  Micro
```

また、 API 通信を行いますので Promise ベースの HTTP クライアントである `Axios` も利用するよう選択します。

```shell
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
> ( ) Axios
  ( ) Progressive Web App (PWA) Support
```



#### ✍ API の実装

クライアントと分けるため `api` ディレクトリを用意して、そこに実装しています。

```javascript:~api/index.js
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const fetch = require('node-fetch')

app.use(bodyParser.json())

app.post('/cotoha_parse', function(req, res) {

  // ✨ トークン取得部分

  const tokenUrl = 'https://api.ce-cotoha.com/v1/oauth/accesstokens'

  const tokenHeaders = {
    'Content-Type': 'application/json',
    charset: 'UTF-8'
  }

  const tokenData = {
    grantType: 'client_credentials',
    clientId: '<COTOHA API Client ID>',
    clientSecret: '<COTOHA API Client Secret>'
  }

  fetch(tokenUrl, {
    method: 'POST',
    headers: tokenHeaders,
    body: JSON.stringify(tokenData)
  })
    .then((tokenRes) => tokenRes.json())
    .then((tokenJson) => {
    
      // ✨ 構文解析 API コール部分
      
      const accessToken = tokenJson.access_token

      const url = 'https://api.ce-cotoha.com/api/dev/nlp/v1/parse'

      const headers = {
        'Content-Type': 'application/json',
        charset: 'UTF-8',
        Authorization: `Bearer ${accessToken}`
      }

      const data = {
        sentence: req.body.sentence,
        type: 'default'
      }

      fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify(data)
      })
        .then((r) => r.json())
        .then((json) => {
          res.send(JSON.stringify(json, null, '\t'))
        })
    })
})

module.exports = {
  path: '/api/',
  handler: app
}
```

`✨ トークン取得部分` に始まるトークン取得処理では、必要な情報を付与してアクセストークン取得用の API をコールしています。

`<COTOHA API Client ID>` や `<COTOHA API Client Secret>` などは COTOHA API for Developers 登録時に表示されるものを入力してください。



`✨ 構文解析 API コール部分` に始まる構文解析の API コール処理では、取得したアクセストークンを利用して API をコールしています。

```javascript
const data = {
  sentence: req.body.sentence,
  type: 'default'
}
```

この部分で API のリクエストボディ `sentence` に渡している `req.body.sentence` が、画面でテキストエリアに入力した文章になります。



#### ✍ API を serverMiddleware に設定

作成した API を呼び出すことができるように `serverMiddleware` に設定します。

`nuxt.config.js` に次のようにコーディングすれば OK です。

```javascript:nuxt.config.js
module.exports = {
  serverMiddleware: ['~~/api/'],
}
```



#### ✍ API の呼び出し

```html
<script>
export default {
  methods: {
    cotohaParse() {
      this.$axios
        .$post(`${process.env.BASE_URL}/api/cotoha_parse`, {
          sentence: this.sentence
        })
        .then((r) => {
          // 正常終了時の処理
        }
        .catch((e) => {
          // 異常終了時の処理
        })
    },
}
</script>
```

画面のコンポーネント側では、 `$axios.$post` を利用して作成した API をコールし、その結果に応じた処理を実装します。

</div>



### ▶ 色の解析

<details><summary>クリックで展開</summary><div>

主に Google 検索と、タイトルから色コードを検索する部分（スクレイピング）をまとめています。



#### ✍ API の実装

『▶ 構文解析』側で実装した `~api/index.js` に次のコードを追加します。

```javascript:~api/index.js
// ✨ ブラウザ操作用
const puppeteer = require('puppeteer')

// ✨ 色解析用の API を追加
app.get('/search_color_code', async function(req, res) {
  // ブラウザ起動と設定
  const browser = await puppeteer.launch({
    headless: true,
    ignoreHTTPSErrors: true
  })

  const page = await browser.newPage()

  let title = null
  const keyword = req.query.keyword
  try {
    // キーワードに ` 色 コード` を付与してググる
    await page.goto('https://www.google.co.jp', {
      waitUntil: 'networkidle2',
      timeout: 4000
    })
    await page.waitFor('input[name=q]')
    await page.type('input[name=q]', keyword + ' 色 コード')
    await page.keyboard.press('Enter')

    // 検索結果からキーワードと16進カラーコードを含むタイトルを決定する.
    await page.waitForSelector('h3', {
      waitUntil: 'networkidle2',
      timeout: 2000
    })
    const elems = await page.$$('h3')
    for (const elem of elems) {
      const jsHandle = await elem.getProperty('textContent')
      const text = await jsHandle.jsonValue()
      if (text.match(new RegExp(keyword)) && text.match(/#[0-9A-Fa-f]{6}/)) {
        title = text
        break
      }
    }
  } catch (e) {
    await browser.close()
    // process.exit(200)
    res.send(null)
  }

  await browser.close()
  res.send(title)
})
```



まず、 Google 検索を行うためにはブラウザが必要ですので、ブラウザを扱うためのパッケージをインポートして利用しています。

```javascript
const puppeteer = require('puppeteer') 
```

がインポート部分です。



```javascript
// ブラウザ起動と設定
const browser = await puppeteer.launch({
  headless: true,
  ignoreHTTPSErrors: true
})
```

この部分で `headless` を `true` で指定していますが、 `false` にすれば普通に別ウインドウでブラウザが立ち上がるようになるので、開発中は `false` にしておくといいです。



```javascript
await page.waitFor('input[name=q]')
await page.type('input[name=q]', keyword + ' 色 コード')
await page.keyboard.press('Enter')
```

生成したブラウザから Google 検索画面を開き、

- `q` という名前の `<input>` 要素生成まで待って
- 構文解析で分解された `名詞` に ` 色 コード` という文字列を付与してインプットに入力して
- キーボードの `Enter` を押す

という手順で検索をしています。



```javascript
// 検索結果からキーワードと16進カラーコードを含むタイトルを決定する.
await page.waitForSelector('h3', {
  waitUntil: 'networkidle2',
  timeout: 2000
})
const elems = await page.$$('h3')
for (const elem of elems) {
  const jsHandle = await elem.getProperty('textContent')
  const text = await jsHandle.jsonValue()
  if (text.match(new RegExp(keyword)) && text.match(/#[0-9A-Fa-f]{6}/)) {
    title = text
    break
  }
}
```

コメントにあるとおりですが、検索後、

- `<h3>` 要素が生成されるまで待ち
- `<h3>` 要素をループし
- 各要素（タイトル）に 16 進数色コードが含まれる場合にタイトルを返す

という手順で色の解析を行っています。



#### ✍ API の呼び出し

```html
<script>
export default {
  methods: {
    searchColorCode(keyword) {
      this.$axios.$get(`${process.env.BASE_URL}/api/search_color_code`, {
        params: { keyword }
      })
      .then((title) => {
        // 正常終了時の処理
      })
      .catch((e) => {
        // 異常終了時の処理
     })
    },
}
</script>
```

画面のコンポーネント側では、 `$axios.$get` を利用して作成した API をコールし、その結果に応じた処理を実装します。

</div>

## 🍩 おわりに

自然言語処理と聞いて、正直僕は全然ピンと来ていませんでした。

参考記事で紹介されている 『 [原始人になれる記事](https://qiita.com/Harusugi/items/f499e8707b36d0f570c4) 』とか『 [偽中国語の記事](https://qiita.com/shoichiro-k/items/5d28be844f664100387b) 』などはリアルタイムで読んでいましたし、『 [Twitter民の年収は低い記事](https://qiita.com/Harusugi/items/caa1922d27ebea451aad) 』については診断までしていました（なぜか女性認定でしたが……）。

それでも当時 COTOHA API を利用した実装には至っていなかったのですが、今回のイベントでは実装に至りました。

やはり ~~賞品の力は偉大で~~ **『はじめてだ』というハードルを下げてくれるようなイベントや催しは大切** で、自分が主催している勉強会などにも取り入れたいなと思いました。

願わくば、~~僕に MacBook Pro をください~~ 僕が作ったアプリで COTOHA API を使った Web アプリを作ってみたいなーと、ぼんやりでも思ってくれた人がいてくれたら幸いです（おしまい）。



## 🚅 参考にしたもの

- [COTOHA API API リファレンス](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)
- [Twitter民の年収は低い 【Nuxt.js】](https://qiita.com/Harusugi/items/caa1922d27ebea451aad)
- [PuppeteerでChromeを自動操作！](https://access-jp.co.jp/blogs/development/39)