フロントエンドエンジニアが「COTOHA DECO」を作ろうとする

すでに **[多くの記事](https://qiita.com/search?utf8=%E2%9C%93&sort=&q=created%3A%3E2020-02-06+created%3A%3C2020-03-15+tag%3ACOTOHA)** があるので、**[キャンペーン](https://zine.qiita.com/event/collaboration-cotoha-api/)** については割愛。

# はじめに

**Qiita** なのに、

**[COTOHA API](https://api.ce-cotoha.com/) 使いやすい** のに、

**[COTOHA × python](https://qiita.com/search?utf8=%E2%9C%93&sort=&q=created%3A%3E2020-02-06+created%3A%3C2020-03-15+tag%3ACOTOHA+tag%3Apython)** の記事ばかり。



**立ち上がれ！フロントエンドエンジニア！**



ということで、 **[COTOHA × CSS](https://qiita.com/search?utf8=%E2%9C%93&sort=&q=created%3A%3E2020-02-06+created%3A%3C2020-03-15+tag%3ACOTOHA+tag%3Acss)** の記事を書く。

**[COTOHA API](https://api.ce-cotoha.com/)** を使って **文章をデコる** サービス

 **「COTOHA DECO」** が作成可能か検証する。

※ 本記事は、 **[COTOHA × JavaScript](https://qiita.com/search?utf8=%E2%9C%93&sort=&q=created%3A%3E2020-02-06+created%3A%3C2020-03-15+tag%3ACOTOHA+tag%3Ajavascript)** で、**データの取得から加工まで**、
**すべてJavaScriptで行っています。**
**Python 使えなくても COTOHA 使えるよ！！**

# まずは結果

このテキストのみを入力して、
(出典): https://www.nikkei.com/article/DGXMZO51758280S9A101C1000000/

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/46d9c6ea-e7eb-2082-4914-75421090e5e7.png)

↓

こうなる

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/8379fb84-fa44-6042-4d49-0135b152ead4.png)

# 仕様と設計

まずは、**[API 一覧](https://api.ce-cotoha.com/contents/api-all.html)** から**デコる仕様**を考える。

1. **[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)** を利用して、**単語などの余白を微妙に調整**する。
2. **[固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)** を利用して、**人名・組織・地名・時間などに応じて、文字色やフォントを変える**。
3. **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)** を利用して、**スコアに応じたドロップキャップ**する。
4. **[感情分析](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** を利用して、**ポジティブまたはネガティブなフレーズに下線**を引く。
5. **[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** を利用して、**要約文にマーカー**をつける。
6. **[照応解析](https://api.ce-cotoha.com/contents/reference/apireference.html#correspond)** を利用して、**指示詞や代名詞に対応する先行詞をツールチップ(title)で表示**する。

**いけそうだ。**

# 課題

**タグで囲ってクラスつければいいか**と安易に考えたが、

各APIのレスポンスを眺めていると、下記の通り**各APIのレスポンスの要素が同じではない**ことに気付く。

 - **[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)** ⇒ **文節情報(chunk)**と**形態素情報(token)**
 - **[固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)** ⇒ **固有表現情報**  
 - **[照応解析](https://api.ce-cotoha.com/contents/reference/apireference.html#correspond)** ⇒ **文(tokensの要素)**と**照応解析結果**と**形態素情報(token)**  
 - **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)** ⇒ **抽出キーワード**
 - **[感情分析](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** ⇒ **感情フレーズ(emotional_phrase)**
 - **[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** ⇒ **要約?**

なお、各レスポンスがの要素がどの位置（何番目か）についても、、
**文字位置**で持っていたり、**形態素番号**で持っていたり、持っていなかったり。。

 - **[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)**, **[固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)** ⇒ **文字位置** 
 - **[照応解析](https://api.ce-cotoha.com/contents/reference/apireference.html#correspond)** ⇒ **形態素番号**
 - **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)**, **[感情分析](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)**, **[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** ⇒ 位置情報なし

ついでに、リクエストに渡す引数のキーも、 **document** と **sentence** の2つのIFがある。。
(なお、**document** は`string / array(string)`,  **sentence** は`string`のようだが、**[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** は **document** なのに`string` ?)

 -  **[照応解析](https://api.ce-cotoha.com/contents/reference/apireference.html#correspond)**, **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)**, **[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** ⇒ **document**
 - **[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)**, **[固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)**, **[感情分析](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** ⇒ **sentence**

**どういう関連があるのか、APIのリファレンスからは読み解きにくい。。**

(**形態素情報(token)が、文字の次の最小粒度**ということは確実だが。。)

(たぶん、**単体でのAPIのIFの使いやすさを重視**して、組合せた使用を想定していないのかな。。)

...

**自然言語処理**はフロントエンドエンジニアには敷居が高いのか。。

# 方針

とにかく**タグで囲ってクラスをつければ何とかなる**と思う。~~(適当)~~、

また、今回は、デコることは、**人にわかりやすく可視化することを目的**としているので、
複数APIの情報を**正確にマージできなくても、分かりやすいモデル**のほうがいいだろう。

と、いうことで、APIのサンプルや、他の記事や、実際にAPIを叩いて推測した結果から、
以下、 **勝手な仮定** をたてて、構造化する。

 - すべての　**文章(docment)**　は　**文(sentence)**　で区切れる。  
**[照応解析](https://api.ce-cotoha.com/contents/reference/apireference.html#correspond)** もしくは、 **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)**
 のリクエストパラメータの`do_segment`に`true`を指定すれば　**文(sentence)**　が得られる。
 - すべての **文(sentence)** は **文節(chunk)** で区切れる。  
**[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)**で **文節(chunk)** が得られる。
 - **形態素情報(token)** が、「冠名詞、名詞、Number」の場合、それに続く「冠名詞, 名詞, 名詞接尾辞, Number, 助数詞, 助助数詞」をつなげて **単語(word)** とする。
 - **[構文解析](https://api.ce-cotoha.com/contents/reference/apireference.html#parsing)** で得られた、 **形態素情報(token)** の**依存先情報(dependency_labels)**のうち、後方に連続してつながっている「助動詞や、非自立の補助用言、コピュラ(auxおよびauxpassおよびcop)」を連結させて **フレーズ(phrase)** が得られる。
 - **フレーズ(phrase)** は **単語(word)** で区切れる。  
 - **[キーワード抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#keyword)** で得られる **キーワード** および　**[感情分析](https://api.ce-cotoha.com/contents/reference/apireference.html#sentiment)** で得られる **感情フレーズ(emotional_phrase)** は **フレーズ(phrase)** である。
 - **[固有表現抽出](https://api.ce-cotoha.com/contents/reference/apireference.html#entity)** で得られる **固有表現** は **フレーズ(phrase)** もしくは **フレーズ(phrase)** を連結させたものである。
 - **[要約(β)](https://api.ce-cotoha.com/contents/reference/apireference.html#summarization)** で得られる要約は、 **文(sentence)** と一致するか、文に句点等を足したものである。

**つまり、入力された文章を、以下の6階層(深さ固定)の木構造にする。
文字が入力されているのは葉となるtokenのみである。**

``` pug:pug(HTML)イメージ
.document(文章)
  .sentence(文)
    .chunk(文節)
      .phrase(フレーズ)
        .word(単語)
          .token(形態素情報)
```

**APIの結果と要素を比較し、一致する場合にクラスを付与する**

# 実装

特定フォルダ下に、**テキストを配置**し、
**node.jsで実行**すると、**HTML(with CSS)を生成する**スクリプトを書いた。
※ COTOHA APIの認証情報が必要

私の環境では、COTOHA APIの実行結果が安定しなかった
(タイミングしだいで、エラーを返却することもあった)ので、
成功したレスポンスの内容はファイルとして書き出し、
ファイルが存在する場合はAPIを実行しないような工夫(配慮)をしている。

なお、API実行部分は、 @yohwhtさんの
**[メントスと囲碁の思い出」をCOTOHAさんに要約してもらった結果。COTOHA最速チュートリアル付き - Qiita](https://qiita.com/youwht/items/16e67f4ada666e679875)** の **COTOHA最速チュートリアルサンプルコード(Python)** を
**[Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb)** にコピペして疎通してから、
JavaScript用に書き直すことで、スムーズに疎通できた。
 **[マジ感謝ね](https://qiita.com/umaniel_/items/19ff60ab09b2fc5f4810)**

``` javascript
const request = require('request');
const fs = require('fs');

// flat Polyfill
if (!Array.prototype.flat) {
  Array.prototype.flat = function(depth) {
    var flattend = [];
    (function flat(array, depth) {
      for (let el of array) {
        if (Array.isArray(el) && depth > 0) {
          flat(el, depth - 1); 
        } else {
          flattend.push(el);
        }
      }
    })(this, Math.floor(depth) || 1);
    return flattend;
  };
}

const DEVELOPER_API_BASE_URL   = "https://api.ce-cotoha.com/api/dev/";
const ACCESS_TOKEN_PUBLISH_URL = "https://api.ce-cotoha.com/v1/oauth/accesstokens";
const CLIENT_ID     = "ココニアイデイカク";
const CLIENT_SECRET = "ココニシークレットカク";

const main = async () => {
  let accessToken = await getAccessToken();
  await cotohaMultiParse(accessToken, 'sumomo');
  await cotohaMultiParse(accessToken, '外郎売');
  await cotohaMultiParse(accessToken, 'kinoko');
}

const cotohaMultiParse = async (accessToken, folderName) => {
  console.log(`■${folderName} のフォルダに対する処理を実施します。`);
  let document = fs.readFileSync(`${folderName}/00_raw.txt`, 'utf-8');
  // 前処理: tokenの区切りがparseとcoreferenceで変わらないように前処理。
  document = document.replace(/\s*\n\s*/g, '\n');
  fs.writeFileSync(`${folderName}/01_replace.txt`, document);
  let parse = await getParse(accessToken, folderName, document);
  let ne = await getNe(accessToken, folderName, document);
  let coreference = await getCoreference(accessToken, folderName, document);
  let summaries = await get3Summary(accessToken, folderName, document);
  let keyword = await getKeyword(accessToken, folderName, document);
  let sentiment = await getSentiment(accessToken, folderName, document);
  let parseChunkIndex = 0;
  let parseTokenIndex = 0;
  let parseIndex = 0;
  let coreferenceSentenceIndex = 0;
  let coreferenceTokenIndex = 0;
  let phrases = [];
  let chunk = { phrases: phrases, classes: [] };
  let sentence = { chunks: [ chunk ], classes: [] };
  let documentTree = {
    sentences: [ sentence ],
    chunks: [ chunk ],
    phrases: [],
    words: [],
    tokens: [],
  };
  let parseEndFlg = false;
  let coreferenceEndFlg = false;
  let tokenAlert = false;
  while (true) { //phrases単位でループする。
    let parseChunk = parse[parseChunkIndex];
    let parseToken = parseChunk.tokens[parseTokenIndex];
    let parseNextFlg = (parseToken == null);
    let coreferenceSentence = coreference.tokens[coreferenceSentenceIndex];
    let coreferenceToken = coreferenceSentence[coreferenceTokenIndex];
    let coreferenceNextFlg = (coreferenceToken == null);
    if (coreferenceNextFlg) {
      coreferenceSentenceIndex++;
      coreferenceTokenIndex = 0;
      coreferenceSentence = coreference.tokens[coreferenceSentenceIndex];
      if (coreferenceSentence == null) {
        coreferenceEndFlg = true;
      } else {
        coreferenceToken = coreferenceSentence[coreferenceTokenIndex];
        if (coreferenceToken == null) {
          coreferenceEndFlg = true;
        } else {
          if (!parseNextFlg) console.log(`警告: 照応解析のsentences(${coreferenceSentence.join(',')})の区切り位置を、構文解析のchunks(${parseChunk.tokens.map(v => v.form).join(',')})の区切り位置が超えています。`);
          phrases = [];
          chunk = { phrases: phrases, classes: [] };
          sentence = { chunks: [ chunk ], classes: [] };
          documentTree.sentences.push(sentence);
        }
      }
    }
    if (parseNextFlg) {
      parseChunkIndex++;
      parseTokenIndex = 0;
      parseChunk = parse[parseChunkIndex];
      if (parseChunk == null) {
        parseEndFlg = true;
      } else {
        parseToken = parseChunk.tokens[parseTokenIndex];
        if (parseToken == null) {
          parseEndFlg = true;
        } else {
          if (!coreferenceNextFlg) {
            phrases = [];
            chunk = { phrases: phrases, classes: [] };
            sentence.chunks.push(chunk);
            documentTree.chunks.push(chunk);
          }
        }
      }
    }
    if (coreferenceEndFlg || parseEndFlg) {
      if (!(coreferenceEndFlg && parseEndFlg)) console.log(`警告: 構文解析と照応解析の終了が同じではないです。coreferenceEndFlg: ${coreferenceEndFlg}, parseEndFlg: ${parseEndFlg}`);
      break;
    }
    if (parseToken.form !== coreferenceToken && !tokenAlert) {
      console.log(`警告: 構文解析のtoken(${parseToken.form})と照応解析のtoken(${coreferenceToken})が異なっています。`);
      tokenAlert = true;
    }
    let token = { id: parseToken.id, form: parseToken.form, classes: [] };
    documentTree.tokens.push(token);
    let word = { tokens: [ token ], classes: [] };
    if (['冠名詞', '名詞', 'Number'].includes(parseToken.pos)) {
      while(true) {
        let tmpParseToken = parseChunk.tokens[parseTokenIndex + 1];
        if (tmpParseToken && ['冠名詞', '名詞', '名詞接尾辞', 'Number', '助数詞', '助助数詞'].includes(tmpParseToken.pos)) {
          parseTokenIndex++;
          coreferenceTokenIndex++;
          parseToken = parseChunk.tokens[parseTokenIndex];
          coreferenceToken = coreferenceSentence[coreferenceTokenIndex];
          if (parseToken.form !== coreferenceToken && !tokenAlert) {
            console.log(`警告: 構文解析のtoken(${parseToken.form})と照応解析のtoken(${coreferenceToken})が異なっています。`);
            tokenAlert = true;
          }
          token = { id: parseToken.id, form: parseToken.form, classes: [] };
          documentTree.tokens.push(token);
          word.tokens.push(token);
        } else {
          break;
        }
      }
    }
    documentTree.words.push(word);
    let phrase = { words: [ word ], classes: [] };
    let parseBaseToken = parseToken;
    let dependencyLabels = parseToken.dependency_labels;
    if (dependencyLabels) {
      let i = 1;
      for(let dependencyLabel of dependencyLabels) {
        if (dependencyLabel.token_id !== parseBaseToken.id + i) {
          continue;
        }
        if (parseChunk.tokens[parseTokenIndex + 1] == null || coreferenceSentence[coreferenceTokenIndex + 1] == null){
          break;
        }
        if (!['aux', 'auxpass', 'cop'].includes(dependencyLabel.label)) {
          break;
        }
        parseTokenIndex++;
        coreferenceTokenIndex++;
        parseToken = parseChunk.tokens[parseTokenIndex];
        coreferenceToken = coreferenceSentence[coreferenceTokenIndex];
        if (parseToken.form !== coreferenceToken && !tokenAlert) {
          console.log(`警告: 構文解析のtoken(${parseToken.form})と照応解析のtoken(${coreferenceToken})が異なっています。`);
          tokenAlert = true;
        }
        token = { id: parseToken.id, form: parseToken.form, classes: [] };
        word = { tokens: [ token ], classes: [] };
        documentTree.words.push(word);
        documentTree.tokens.push(token);
        phrase.words.push(word);
        i++;
      }
    }
    chunk.phrases.push(phrase);
    documentTree.phrases.push(phrase);
    parseTokenIndex++;
    coreferenceTokenIndex++;
  }
  documentTree.sentences.forEach( sentence => {
    sentence.chunks.forEach( chunk => {
      chunk.phrases.forEach( phrase => {
        phrase.words.forEach( word => {
          word.form = word.tokens.map( token => token.form ).join('');
          word.token_id_from = word.tokens[0].id;
          word.token_id_to = word.tokens.slice(-1)[0].id;
        });
        phrase.form = phrase.words.map( word => word.form ).join('');
        phrase.token_id_from = phrase.words[0].token_id_from;
        phrase.token_id_to = phrase.words.slice(-1)[0].token_id_to;
      });
      chunk.form = chunk.phrases.map( phrase => phrase.form ).join('');
      chunk.token_id_from = chunk.phrases[0].token_id_from;
      chunk.token_id_to = chunk.phrases.slice(-1)[0].token_id_to;
    });
    sentence.form = sentence.chunks.map( phrase => phrase.form ).join('');
    sentence.token_id_from = sentence.chunks[0].token_id_from;
    sentence.token_id_to = sentence.chunks.slice(-1)[0].token_id_to;
  });
  
  let allSentences = documentTree.sentences;
  let allChunks = allSentences.map( s => s.chunks ).flat();
  let allPhrases = allChunks.map( c => c.phrases ).flat();
  let allWords = allPhrases.map( c => c.words ).flat();
  let allTokens = allWords.map( p => p.tokens ).flat();

  const getEqFormElem = str => {
    let ret = [];
    documentTree.sentences.forEach( sentence => {
      if (sentence.form === str) {
        ret.push(sentence);
      } else {
        sentence.chunks.forEach( chunk => {
          if (chunk.form === str) {
            ret.push(chunk);
          } else {
            chunk.phrases.forEach( phrase => {
              if (phrase.form === str) {
                ret.push(phrase);
              } else {
                phrase.words.forEach( word => {
                  if (word.form === str) {
                    ret.push(word);
                  } else {
                    word.tokens.forEach( token => {
                      if (token.form === str) {
                        ret.push(word);
                      }
                    });
                  }
                });
              }
            });
          }
        });
      }
    });
    return ret;
  };

  sentiment.emotional_phrase.forEach(ePhrase => {
    let targets = getEqFormElem(ePhrase.form);
    if (targets.length === 0) {
      console.log(`警告: emotional_phrase(${ePhrase.form})が存在しません。`);
    }
    targets.forEach( t => {
      if (!t.classes.includes(`emotion`)) t.classes.push(`emotion`);
      if (!t.classes.includes(`emotion-${ePhrase.emotion}`)) t.classes.push(`emotion-${ePhrase.emotion}`);
    });
  });

  keyword.forEach((k, i) => {
    let targets = getEqFormElem(k.form);
    if (targets.length === 0) {
      console.log(`警告: keyword(${k.form})が存在しません。`);
    }
    targets.forEach( t => {
      if (!t.classes.includes(`keyword`)) t.classes.push(`keyword`);
      if (!t.classes.includes(`keyword-${i + 1}`)) t.classes.push(`keyword-${i + 1}`);
    });
  });

  // begin_pos, end_posを持ってはいるが、文言一致を試す
  // ne⇒得点王、word⇒大会得点王等が一致しないが、wordとしては長いほうが適しているため
  // ロンゲスト一致のようなもので無理やり表示する方がいいかもしれない。
  ne.forEach((n) => {
    let targets = getEqFormElem(n.form);
    if (targets.length === 0) {
      console.log(`警告: ne(${n.form})が存在しません。`);
    }
    targets.forEach( t => {
      if (!t.classes.includes('ne')) t.classes.push('ne');
      if (!t.classes.includes(`ne-${n.class}`)) t.classes.push(`ne-${n.class}`);
    });
  });

  // sentenceには前後に空白等が含まれてしまっているため、前後空白を除去して、sentenceのみ比較する
  summaries.filter( summary => summary.form.length > 0).forEach( summary => {
    let targets = [];
    let replaceSummary = summary.form.replace(/。$/, '');
    documentTree.sentences.forEach(sentence => {
      let tmpSentence = sentence.form.replace(/^\s+/, '').replace(/\s+$/, '');
      // upperCaeしないと、サマリー側の小文字が大文字になっている場合がある。
      if (summary.form.toUpperCase() === tmpSentence.toUpperCase() || replaceSummary.toUpperCase() === tmpSentence.toUpperCase()) {
        targets.push(sentence);
      }
    })
    if (targets.length === 0) {
      console.log(`警告: summary(${summary.form})が存在しません。`);
    }
    targets.forEach( t => {
      if (!t.classes.includes(`summary-${summary.sent_len}`)) t.classes.push(`summary-${summary.sent_len}`);
    });
  });

  fs.writeFileSync(`${folderName}/90_documentTree_row.json`, JSON.stringify(documentTree, null, '  '));
  fs.writeFileSync(`${folderName}/91_documentTree_sentences.json`, allSentences.map(s => s.form).join('\n'));
  fs.writeFileSync(`${folderName}/92_documentTree_chunks.json`, allChunks.map(c => c.form).join('\n'));
  fs.writeFileSync(`${folderName}/93_documentTree_phrases.json`, allPhrases.map(p => p.form).join('\n'));
  fs.writeFileSync(`${folderName}/94_documentTree_words.json`, allWords.map(w => w.form).join('\n'));
  fs.writeFileSync(`${folderName}/95_documentTree_tokens.json`, allTokens.map(t => t.form).join('\n'));
  
  const getClassTag = (baseClass, element) => {
    return `class="${baseClass} ${element.classes.join(' ')}"`
  }
  
  // hmlt作成処理
  fs.writeFileSync(`${folderName}/${folderName}.html`, `
<html>
  <head>
    <meta charset="UTF-8">
    <title>${folderName}</title>
    <style>
      span.sentence {
        margin-right: 1em;
      }
      span.chunk {
        margin-right: 0.12em
      }
      span.phrase {
        margin-right: 0.08em
      }
      span.word {
        margin-right: 0.04em
      }
    </style>
    <style>
      span.summary-1 {
        background:linear-gradient(transparent 20%, rgb(215, 46, 60, 0.3) 70%, transparent 100%);
      }
      span.summary-2 {
        background:linear-gradient(transparent 20%, rgb(230, 73, 18, 0.3) 70%, transparent 100%);
      }
      span.summary-3 {
        background:linear-gradient(transparent 20%, rgb(220, 137, 20, 0.3) 70%, transparent 100%);
      }
    </style>
    <style>
      span.ne {
        color: rgb(64,64,64);
        font-weight: bold;
      }
      span.ne-ORG, span.ne-PSN { /* 組織名,人名 */
        color: rgb(128,64,0);
        font-weight: bold;
      }
      span.ne-LOC { /* 場所 */
        color: rgb(0,0,160);
        font-weight: bold;
      }
      span.ne-DAT, span.ne-TIM { /* 日付,時刻 */
        color: rgb(128,64,64);
        font-weight: bold;
      }
      span.ne-MNY { /* 金額 */
        color: rgb(128,128,64);
        font-weight: bold;
      }
    </style>
    <style>
      span.phrase,span.word,span.token {
        display: inline-block;
      }
      span.keyword{
        line-height: 1;
        font-size: 0.9em;
      }
      span.keyword-1 span.token:first-child::first-letter,span.keyword-1.token::first-letter{
        font-size: 1.6em;
      }
      span.keyword-2 span.token:first-child::first-letter,span.keyword-2.token::first-letter{
        font-size: 1.6em;
      }
      span.keyword-3 span.token:first-child::first-letter,span.keyword-3.token::first-letter{
        font-size: 1.6em;
      }
      span.keyword-4 span.token:first-child::first-letter,span.keyword-4.token::first-letter{
        font-size: 1.6em;
      }
      span.keyword-5 span.token:first-child::first-letter,span.keyword-5.token::first-letter{
        font-size: 1.4em;
      }
      span.keyword-6 span.token:first-child::first-letter,span.keyword-6.token::first-letter{
        font-size: 1.4em;
      }
      span.keyword-7 span.token:first-child::first-letter,span.keyword-7.token::first-letter{
        font-size: 1.4em;
      }
      span.keyword-8 span.token:first-child::first-letter,span.keyword-8.token::first-letter{
        font-size: 1.4em;
      }
    </style>
    <style>
      span.emotion {
      }
      span.emotion-P {
        border-bottom: dashed 2px red;
      }
      span.emotion-PN {
        border-bottom: dashed 2px gray;
      }
      span.emotion-N {
        border-bottom: dashed 2px blue;
      }
    </style>
  </head>
  <body>
${documentTree.sentences.map( s => '<span ' + getClassTag('sentence', s) + '>' +
  s.chunks.map( c => '<span ' + getClassTag('chunk', c) + '>' +
    c.phrases.map( p => '<span ' + getClassTag('phrase', p) + '>' +
      p.words.map( w => '<span ' + getClassTag('word', w) + '>' +
        w.tokens.map( t => '<span ' + getClassTag('token', t) + '>' +
          t.form +
          '</span>').join('') +
        '</span>').join('') +
      '</span>').join('') + 
    '</span>').join('') +
  '</br></span>').join('')}
  </body>
</html>
`);
  return documentTree;
}

const getAccessToken = () => {
  return new Promise((resolve, reject) => {
    request(
      {
        url: ACCESS_TOKEN_PUBLISH_URL,
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        json: {
          grantType: "client_credentials",
          clientId: CLIENT_ID,
          clientSecret: CLIENT_SECRET,
        },
      },
      (error, response, body) => {
        if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
          if (typeof body !== 'object') body = JSON.parse(body);
          resolve(body.access_token);
        } else {
          if (error) {
            console.log(`request fail. error: ${error}`);
          } else {
            console.log(`request fail. response.statusCode: ${response.statusCode}, ${body}`);
          }
          reject(body);
        }
      },
    );
  });
}

const getCommonRequest = (url ,fileName, getRequestJsonFunc) => (accessToken, folderName, document) => {
  let promise;
  promise = new Promise((resolve, reject) => {
    let filePath = `${folderName}/${fileName}`;
    if (fs.existsSync(filePath)) {
      resolve(JSON.parse(fs.readFileSync(filePath, 'utf-8')));
    } else {
      request(
        {
          url: `${DEVELOPER_API_BASE_URL}${url}`,
          method: 'POST',
          headers: { 'Content-Type': 'application/json;charset=UTF-8', Authorization: `Bearer ${accessToken}`},
          json: getRequestJsonFunc(document),
        },
        (error, response, body) => {
          if (!error && (response.statusCode === 200 || response.statusCode === 201)) {
            if (typeof body !== 'object') body = JSON.parse(body);
            if (body.status === 0) {
              fs.writeFileSync(`${filePath}`, JSON.stringify(body.result, null, '  '));
              resolve(body.result);
            } else {
              console.log(`request coreference fail. error: ${body.message}`);
              reject(body);
            }
          } else {
            if (error) {
              console.log(`request ${url} fail. error: ${error}`);
            } else {
              msg = (typeof body !== 'object') ? body : JSON.stringify(body);
              console.log(`request ${url} fail. response.statusCode: ${response.statusCode}, ${msg}`);
            }
            reject(body);
          }
        }
      );
    } 
  });
  return promise;

}

const getParse = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/parse', '10_parse_row.json', document => {
    return { sentence: document };
  })(accessToken, folderName, document);
}

const getNe = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/ne', '20_ne_row.json', document => {
    return { sentence: document };
  })(accessToken, folderName, document);
}

const getCoreference = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/coreference', '30_coreference_row.json', document => {
    return {
      document: document,
      do_segment: true,
    };
  })(accessToken, folderName, document);
}

const getKeyword = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/keyword', '50_keyword_row.json', document => {
    return {
      document: document,
      max_keyword_num: 8,
    };
  })(accessToken, folderName, document);
}

const getSentiment = (accessToken, folderName, document) => {
  return getCommonRequest('nlp/v1/sentiment', '60_sentiment_row.json', document => {
    return { sentence: document };
  })(accessToken, folderName, document);
}

const get3Summary = async (accessToken, folderName, document) => {
  let summary1 = await getSummary(accessToken, folderName, document, 1);
  let summary2 = await getSummary(accessToken, folderName, document, 2);
  let summary3 = await getSummary(accessToken, folderName, document, 3);
  let summary2Part = summary2.replace(summary1, '');
  let summary2Array = summary2Part.split('。').filter(v => v.length > 0).map(v => v + '。');
  let summary3Part = summary3.replace(summary1, '');
  summary2Array.forEach(s => {
    summary3Part = summary3Part.replace(s, '');
  });
  let summary3Array = summary3Part.split('。').filter(v => v.length > 0).map(v => v + '。');
  // summary1と、summary2とsummary3は。で区切られた、1文、2文、3文にはなっているが、
  // summary2にsummary1が含まれないこともある。
  return [ [ { form: summary1, sent_len: 1 } ] , summary2Array.map(s => { return { form: s, sent_len: 2}; } ), summary3Array.map(s => { return { form: s, sent_len: 3}; } )].flat();
}

const getSummary = (accessToken, folderName, document, i) => {
  return getCommonRequest('nlp/beta/summary', `40_summary_${i}_row.json`, document => {
    return {
      document: document,
      sent_len: i,
    };
  })(accessToken, folderName, document);
}

main();
```

# 結果

## すもももももももものうち

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/df0256c0-52b4-16dd-5945-31211c77cbf4.png)

↓

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/3650fb90-97a8-78d5-da40-8bf32beced5f.png)

# 外郎売

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/dd6a489a-d044-540e-4d12-2b4ad3b5419c.png)

↓

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/911a829d-19dc-f108-bb50-4fe674107295.png)

# 「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム

(出典): [「たけのこの里」を「きのこの山」に『正しく』自動で修正して差し上げるプログラム - Qiita](https://qiita.com/honehoney/items/a8c7ba2c53a70c88502e)

※ COTOHA APIの複数のAPIを使うため、文字数や文数で適当なサンプルを見つけるのに苦戦する中、丁寧な説明と、ネガ/ポジも含んだ文章として利用させていただきました。  **[マジ感謝ね](https://qiita.com/umaniel_/items/19ff60ab09b2fc5f4810)**


![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/ef110b86-c83a-228c-ce0f-713b3b0891d7.png)


↓

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352865/10ea77ca-e38e-fc14-3152-749532088902.png)

# 感想

記事では触れていないが、前処理での正規化に対応するなど、
結果を見ながら**トライ&エラー**繰り返した。

結果、**自然言語処理** 素人が、
**無理やり複数の結果をマージ**したため、
**1割程度は、作成した木構造とAPIの結果が一致せず**に、
結果を捨てている

また、本来はCodePen等で入力結果をパースし、
照応解析を利用して先行詞をツールチップ(title)で表示するなど、
動的な対応も入れたかったが、~~疲れた~~諦めた。

CSSのスタイルも、もう少し工夫すれば格段と見栄えはよくなると思う。
~~よくフロントエンドエンジニアと名乗れたレベル~~

この記事がお蔵入りしなかったのは、

 - **COTOHA APIが簡単に触れた**
 - **CSS** や **JavaScript** タグでCOTOHA APIの記事がなかった
 - **複数API** の組合せて利用している記事が少なかった。~~一方で、自然言語処理の難しさを垣間見た~~
 - **[キャンペーン](https://zine.qiita.com/event/collaboration-cotoha-api/)** をやっているから

であり、このような **[キャンペーン](https://zine.qiita.com/event/collaboration-cotoha-api/)** を実施している運営様にも **[マジ感謝ね](https://qiita.com/umaniel_/items/19ff60ab09b2fc5f4810)**

本物の **フロントエンドエンジニア** による **COTOHA API**の記事が書かれることを望む。