2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Nuxt で WordCloud を作る・そのためのデータを Python で作成する

Last updated at Posted at 2021-09-12

成果物

以前、こちらのサイトを参考にさせていただき、

こんなアプリを作りました。

これを応用しながら、現在開発中のアプリにも、WordCloud の表示を追加しました。

image.png

こちらは、一例ですが、色んな方の専門家会議等での発言に基づいて、WordCloud としてまとめています。

アプリケーション方式

このアプリ自体は、フロントを Nuxt で作って、Netlify にデプロイしています。
API を、Django REST Framework で作り、Heroku にデプロイしています。
DB は、Heroku と連携して使える、ClearDB MySQL に作成しています。

今回作ろうとしているのは、専門家会議の構成員一人一人についてのWordCloudです。
これを、リアルタイムで生成するのは無理だと思うので、上記アプリ構成外で、データ生成(ワードごとの重み付けデータ)を行います。
上記 sake-pairing は、定期バッチで生成しています。なぜなら、集計元のデータがTwiiterのツイートであり、これはリアルタイムで変わっていくものだからです。

しかし、今回は、集計元データは今の所自分で登録しているデータですので、データ登録に合わせて生成すれば問題ありません。
もう一点、ClearDB の制限が意外ときつい。(3600query/hour?)
最初、ローカルのプログラムでClearDBに接続していましたが、それだと接続制限にかかってしまいました。

ですので、ローカル開発環境でファイルを生成し、github にコミットすることでデプロイされ、Nuxt アプリ側からは、そのファイルを読み込んでWordCloud を生成する構成とします。

Python による、形態素解析、ワードカウント、ファイル生成

結論としては、こんなプログラムになりました。

create_wc.py
import json
import urllib.request

import tempfile
from janome.tokenizer import Tokenizer
import pandas as pd
import os

folder_name = "./wc"

def main():
  try:
    url = "http://localhost:8000/api/person/"
    res = urllib.request.urlopen(url)
    data = json.loads(res.read().decode('utf-8')) # 実行結果(json)をdata変数に格納

  except urllib.error.HTTPError as e:
     print('HTTPError: ', e)
  except json.JSONDecodeError as e:
     print('JSONDecodeError: ', e)

  for person in data:
    print(person['id']) # idのみ参照

    try:
      url = "http://localhost:8000/api/meeting_speech/"
      params = {
        'person': person['id'],
      }
      req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
      # res = urllib.request.urlopen(req)
      with urllib.request.urlopen(req) as res:
        speech_data = json.loads(res.read().decode('utf8'))

    except urllib.error.HTTPError as e:
      print('HTTPError: ', e)
    except json.JSONDecodeError as e:
      print('JSONDecodeError: ', e)

    if len(speech_data) > 0:
      speech = []
      # print(speech_data) 
      for skey in speech_data:
        # print(skey) 
        speech.append(skey['speech'])

      out = gen_wordcloud(speech)

      out.to_json(folder_name + '/' + person['id'] + '.json', orient = 'records', force_ascii=False)


def gen_wordcloud(texts):
    words = []
    for text in texts :
        words.extend(counter(text))

    sr = pd.Series(words)
    counts = sr.value_counts()
    df = pd.DataFrame(counts.rename_axis('name').reset_index(name='value'))

    return df

def counter(text):
    if not text:
      return ''
    
    t = Tokenizer()
    words = []

    try:
      tokens = t.tokenize(text)
      for token in tokens:
          #品詞から名詞だけ抽出
          pos = token.part_of_speech.split(',')[0]
          if (pos == '名詞') or (pos == '動詞') or (pos == '形容詞'):
              if token.base_form not in '#, [], する, (), ある, @, これ, こと, よう, いる, なる, さん, いう, ない, おる, れる, の, てる, もの, とこ, 料理, やる, つく, やつ, しまう, できる, それ, くる, とき, いく, せる, ところ, ため, たち, いく, くれる, わけ, 思う, いい, いただく, 行う, &':
                words.append(token.base_form)
    except:
      print('error!', text)


    return words


if __name__ == '__main__':
    main()

元データ(今回は発言データ)取得

直接DBからの取得ではなく、既存API経由で取得します。
人ごとにファイルを作成しますので、
http://localhost:8000/api/person/
で、人情報を取得します。

人のループで、
http://localhost:8000/api/meeting_speech/
で発言情報を取得します。

どちらもJSONで取得するので、

      with urllib.request.urlopen(req) as res:
        speech_data = json.loads(res.read().decode('utf8'))

の部分で、データとして扱える形を作ります。

speech = []
に文字列配列を作った上で、これを、gen_wordcloud に渡します。

形態素解析

gen_wordcloud から、すかさず、counter に渡して、単語ごとに分割します。
janome.tokenizer、何もわからなくても、勝手に分割して、品詞判別までしてくれる。
素晴らしいですね。

で、token.base_form に単語が入っているので、それを配列に突っ込んでいくわけですが、一部、意味のない単語を排除していきます。結果を見ながら、どんどん足していくわけですが、こういうの上手くできると良いなと思います。

出現数カウント

counter で単語一覧ができたので、pandas でカウントします。
これは、pandas の素晴らしさで簡単にできるわけですが、これをファイルにするところは、ちょっと悩みました。
ただ、これも素晴らしいことに、padas の DataFrame には、to_json という操作ができる。
引数、force_ascii=False はtrueじゃなくてfalse というところに気をつければ、できるというわけです。

また、その前に、

df = pd.DataFrame(counts.rename_axis('name').reset_index(name='value'))

とすることで、単語の列に「name」、カウントの列に「value」という名前をつけることができます。

Nuxt での、WordCloud 生成

データができたらNuxt側で読み込みます。

最初、axios で読み込むサンプルを見て、ただ、nuxt.config.ts で

nuxt.config.ts
  axios: {
    baseURL: process.env.BASE_URL || 'http://localhost:8000/api/',
  },

のように定義していて、自動的に、API側を見にくようになっていた、すなわち、上記構成だと、Heroku 側に醜ようになっていたので、そちらにファイルを置こうとしました。

そのために、まず、staticfiles に置くのに苦労しましたが、それに成功しても、CORS に引っかかるという状況だったので、Nuxt 側の静的ファイルとして置くことにしました。

(余談)Django における静的ファイルの配置

最初、ファイルを「staticfiles」に直接置いても参照できず、

python manage.py collectstatic

しても、反応しないなと悩んでいたのですが、アプリ側フォルダ内に、「static」フォルダを作成し、そこにファイルを置いた上で、collectstatic をすると、staticfiles にファイルがコピーされて、三勝可能になるのでした。

assets フォルダの参照

では、実際に行った方式です。Nuxt 構成の、assets フォルダに、ファイルを置き、それを参照するようにします。

こちらも、結論としては、このようになります。

<template>
・・・
      <wordcloud
        :data="words"
        name-key="name"
        value-key="value"
        :show-tooltip="false"
      />
・・・
</template>
<script lang="ts">
// 個人発言リスト
import { reactive, computed, toRefs, useFetch, useContext, defineComponent } from '@nuxtjs/composition-api';

import { useMeetingSpeech } from '@/compositions';

// import jsonData from '@/assets/wordcloud/1e987981-415a-4e50-ab5f-2a9e35243056.json'
// import jsonData from `@/assets/wordcloud/${props.personId}.json`

export default defineComponent({
  name: 'PersonSpeechList',

  props: {
    personId: {
      type: String,
      required: true,
    },
  },
  setup(props, { root }) {
    console.log('personId', props.personId);
    const { state: meetingSpeechState, getMeetingSpeechList } = useMeetingSpeech();

    const itemsPerPageArray = [4, 8, 12];
    const state = reactive({
      search: '',
      filter: {},
      sortDesc: false,
      words: [
        { name: "発言", value: "100" },
        { name: "Wordcloud", value: "20" },
        { name: "議事録", value: "30" },
        { name: "政府会議体", value: "40" },
      ],
    });
    const page = 1;
    const itemsPerPage = 60;
    const sortBy = 'name';
    const keys = [ 'meeting_date', 'name'];

    try {
      const jsondata = require(`~/assets/wordcloud/${props.personId}.json`)
      // console.log('jsondata', jsondata);
      if (jsondata) {
        state.words = jsondata
      }
    } catch {
      console.log('no jsondata');
    }

    const fetchData = async (offset = 0, personId = '') => {
      console.log('personId', personId);
      await getMeetingSpeechList({ offset: offset, person: personId });

      // const jsondata = await import(`~/assets/wordcloud/${props.personId}.json`)
      // const jsondata = await fetch(`/wordcloud/${props.personId}.json`).then(res =>
      //   res.json()
      // )
      // console.log('jsondata', jsondata);

      // state.words = jsondata

    };

    const { fetchState } = useFetch(() => fetchData(0, props.personId));

    return {
      itemsPerPageArray,
      ...toRefs(state),
      page,
      itemsPerPage,
      sortBy,
      keys,
      ...toRefs(meetingSpeechState),
    };
  },
});
</script>

途中の悪戦苦闘の後もコメントで残してあります。

requireで、動的文字列を使えるので、これで人ごとに読み込みファイルを切り替えることができる、ということです。

以上

WordCloud は見た目のインパクトがあるので、効果的に使えると良いですね。

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?