神社 地域 にデジタルを

❝ワードクラウド ジェネレーター❞のコーディング

2023年06月04日 | 最終更新日:2023年07月14日 | コーディング |
Django4 python3.10 javascript wsl bootstrap pandas mecab wordcloud

当初、spaCyとGiNZAを用いて目的を達成しようと試みましたが、sudachidict-coreのインストールでつまずき断念。

そこで、mecabとmecab-ipadic-neologdを用いる形に方針を変えて試行錯誤し、どうにか想定とほぼ同じ結果が出力されるようになりました。

DBモデルにはデータを残さないで、結果のダウンロードにJavascriptを用いて制御しました。

また、デフォルトの出力は味気ないので色味を工夫。

マスク画像を3枚用意し、3パターンの画像の中からランダムにマスク画像を選ばれるようにし、結果の画像が3パターンになるよう工夫しました。

概要を記します。


事前準備(手順の概略)

MeCab他インストール

$ sudo apt-get install mecab mecab-ipadic-utf8 libmecab-dev swig

<Poetryで仮想環境を準備(例)>事前にPoetryをインストールしておく

仮想環境をフォルダ直下にする

poetry config virtualenvs.in-project true
~$ mkdir o_poetry_dj
~$ cd o_poetry_dj/
~/o_poetry_dj$ poetry init

以下の設問にEnter、yes、noを入力

This command will guide you through creating your pyproject.toml config.
Package name [o_poetry_dj]:※Enterキー
Version [0.1.0]:※Enterキー
Description []:※Enterキー
Author [user , n to skip]:※Enterキー
License []:※Enterキー
Compatible Python versions [^3.10]:※Enterキー
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "o_poetry_dj"
version = "0.1.0"
description = ""
authors = ["user "]
[tool.poetry.dependencies]
python = "^3.10"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Do you confirm generation? (yes/no) [yes] yes
poetry shell

<venvの場合(例)>※venvがインストールされていることを確認

python3 -m venv app_text_process
source app_o_ytdl/bin/activate

以後、仮想環境で作業

必要なパッケージをインストール

<poetryの場合>

(仮想名) poetry add mecab-python3 他

<pipの場合>

(仮想名) pip install mecab-python3 他

Djangoプロジェクトにアプリを作成

python manage.py startapp text_process

スーパーユーザーを作成

python manage.py createsuperuser

settings.py(config/settings.py)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    "text_process",    
    ...省略...

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates"],
        ...省略...

STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'

プロジェクトフォルダのsettings.pyに記述


urls.py(config/urls.py)

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [ path('admin/', admin.site.urls), path("text_process/", include("text_process.urls")), ...省略... if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

プロジェクトフォルダのurls.pyに記述


フォルダ構成
├── text_process/
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── __init__.py
│   ├── migrations/
│   ├── models.py
│   ├── __pycache__/
│   ├── tests.py
│   ├── urls.py
│   ├── utils_news.py
│   ├── utils.py
│   └── views.py

前処理と形態素解析などの、必要な自然言語処理のためのutils.pyを配置します


utils.py

前処理、形態素解析、除外ワードなどの設定を記述します

import MeCab
import re


    
# 前処理
def clean_text(text):
    replaced_text = '\n'.join(s.strip() for s in text.splitlines()[
                              :] if s != '')  # skip header by [2:]
    # replaced_text = replaced_text.lower()
    replaced_text = re.sub(r'[【】]', ' ', replaced_text)       # 【】の除去
    replaced_text = re.sub(r'[()()]', ' ', replaced_text)     # ()の除去
    replaced_text = re.sub(r'[[]\[\]]', ' ', replaced_text)   # []の除去
    replaced_text = re.sub(r'[@@]\w+', '', replaced_text)  # メンションの除去
    replaced_text = re.sub(r"page:\s\d", "", replaced_text)
    replaced_text = re.sub(r"広告\n", "", replaced_text)
    # replaced_text = re.sub("写真:cora/pixta", "", replaced_text)
    replaced_text = re.sub(r"写真:\w+/\w+", "", replaced_text)
    replaced_text = re.sub(r"写真:.+\n", "", replaced_text)
    replaced_text = re.sub(r"写真:\w+\s.{2}", "", replaced_text)
    replaced_text = re.sub(r"写真:.+/\w+", "", replaced_text)
    replaced_text = re.sub(r"写真:.+\/\w+", "", replaced_text)
    replaced_text = re.sub(r"写真提供:.+\n", "", replaced_text)
    replaced_text = re.sub(r"\w+/.+", "", replaced_text)
    # replaced_text = re.sub(r"\s\w{2}?\n", "", replaced_text)
    replaced_text = re.sub(r"コピー\n", "", replaced_text)
    replaced_text = re.sub(r"✔", "", replaced_text)
    replaced_text = re.sub(r"印刷\n", "", replaced_text)
    replaced_text = re.sub("印刷する", "", replaced_text)
    replaced_text = re.sub("AA", "", replaced_text)
    replaced_text = re.sub("写真:PIXTA", "", replaced_text)
    replaced_text = re.sub("次ページ.*?\n", "", replaced_text)
    replaced_text = re.sub("Photo:AFP/JIJI", "", replaced_text)
    replaced_text = re.sub(r"シェア.*?\n", "", replaced_text)
    replaced_text = re.sub(r"〔photo〕gettyimages\n", "", replaced_text)
    replaced_text = re.sub(r"\〔PHOTO\〕gettyimages\n", "", replaced_text)
    replaced_text = re.sub(r"facebook\n", "", replaced_text)
    replaced_text = re.sub(r"hatena\n", "", replaced_text)
    replaced_text = re.sub(r"mail\n", "", replaced_text)
    replaced_text = re.sub(r"Photo:\w+\n", "", replaced_text)
    replaced_text = re.sub(r"[A-Za-z]+\s.*[A-Za-z]\n", "", replaced_text)
    # replaced_text = re.sub("[ァ-ヴ][ァ-ヴー・]+\d", "", replaced_text)
    replaced_text = re.sub("\d{1,3}\n", "", replaced_text)
    replaced_text = re.sub(r"◆.*\n", "", replaced_text)
    replaced_text = re.sub(r"Getty.+\n", "", replaced_text)
    # replaced_text = re.sub(r"\d\/", "", replaced_text)
    replaced_text = re.sub(r".+\d\/.+", "", replaced_text)
    replaced_text = re.sub(r"[0-9]+\n", "", replaced_text)
    replaced_text = re.sub(r"[ァ-ヴ][ァ-ヴー・]+へ\n", "", replaced_text)
    replaced_text = re.sub(r"(.*)\n(\1)", r"\1\n", replaced_text)
    replaced_text = re.sub(r"この記事の画像.*\n", "", replaced_text)
    replaced_text = re.sub(r"メール.{1,6}?\n", "", replaced_text)
    replaced_text = re.sub(r"著者.{1,6}\n", "", replaced_text)
    replaced_text = re.sub(r"◎新潮社.+\n", "", replaced_text)
    # replaced_text = re.sub(r"・.*「.+」.+「.+」.*\n", "", replaced_text)
    replaced_text = re.sub(r"会員限定.+\n", "", replaced_text)
    # replaced_text = re.sub(r"©.+?\n", "", replaced_text)
    replaced_text = re.sub(r"©\w+\n", "", replaced_text)
    replaced_text = re.sub(r"〔PHOTO〕\w+\n", "", replaced_text)
    replaced_text = re.sub("iStock.", "", replaced_text)
    replaced_text = re.sub(r"photo.+\n", "", replaced_text)
    replaced_text = re.sub("©", "", replaced_text)
    replaced_text = re.sub("gettyimages", "", replaced_text)
    replaced_text = re.sub(
        r'https?:\/\/.*?[\r\n ]', '', replaced_text)  # URLの除去
    replaced_text = re.sub(r' ', ' ', replaced_text)  # 全角空白の除去
    return replaced_text


# 基礎処理
def base_process(text):
    
    m = MeCab.Tagger()
    
    # 形態素解析
    node = m.parseToNode(text)
    words=[]
    while(node):
#         print(node.surface, node.feature)
        if node.surface != "":  # ヘッダとフッタを除外
            word_type = node.feature.split(",")[0]
            
            if word_type in ["名詞", "動詞","形容詞"]:
                words.append(node.surface)  # node.surface は「表層形」
            # 動詞(の原型),形容詞,副詞もリストに加えたい場合は次の2行を有効にする
            # if word_type in [ "動詞", "形容詞","副詞"]:
            #     words.append(node.feature.split(",")[6]) # node.feature.split(",")[6] は形態素解析結果の「原型」
        node = node.next
        if node is None:
            break
    return words          
    
# 除外ロジック
def is_target_word(word):
    # ひらがなのみから構成される単語を除外
    pattern = re.compile(r"[あ-ん]+")
    if pattern.fullmatch(word):
        return False
    
    # 一文字単語は除外
    if len(word) == 1:
        return False
    return True


# 正規単語の登録
def words_clearte(s_words):
    tr_token = []
    for word in s_words:
        if is_target_word(word):
            tr_token.append(word)
    
    return tr_token

clean_textは、試行錯誤中。ご容赦ください。


urls.py

from django.urls import path
from .views import generate_cloud_image, news_home

app_name = "text_process"


urlpatterns = [
    path("", generate_cloud_image, name="cloud"),
    
]

forms.py


from django import forms

class DataForm(forms.Form):
    
    text_area = forms.CharField(max_length=7000, label="テキストをペースト 7000字まで", widget=forms.Textarea)

テキストエリアを持つシンプルなフォームを作ります。


views.py

import wordcloud
import random

from django.shortcuts import render

from .forms import DataForm, NewsChoiceForm

from .utils import clean_text, words_clearte, base_process

from collections import Counter
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')
from PIL import Image
import urllib.parse
import io
import base64
import random

from django.conf import settings
from matplotlib.colors import LinearSegmentedColormap


colors = ["#BCBCBC", "#F1F1F1", "#A1A1A1", "#EFEFEF", "#8A8A8A", "#A2A2A2"]
cmap = LinearSegmentedColormap.from_list("mycmap", colors)


f_path = "/usr/share/fonts/truetype/fonts-japanese-gothic.ttf"
b_path1 = settings.BASE_DIR / "reference" / "wordcloud_mask/rappa.png"
b_path2 = settings.BASE_DIR / "reference" / "wordcloud_mask/ougi.png"
b_path3 = settings.BASE_DIR / "reference" / "wordcloud_mask/daimonzi.png"

mask_list = [b_path3, b_path2, b_path1]


# Custom Color Function
def grey_color_func(word, font_size, position, orientation, random_state=None,
                    **kwargs):
    return "hsl(0, 0%%, %d%%)" % random.randint(60, 100)


def generate_cloud_image(request):    
    form = DataForm()
    
    if request.method=="POST":
        
        
        form = DataForm(request.POST or None)    
        if form.is_valid():
            mask_val = random.choice(mask_list)
            # print(mask_val)
            b_mask = np.array(Image.open(mask_val))
            
            user_area = form.cleaned_data.get("text_area")
        
            c_text = clean_text(user_area)
            b_text = base_process(c_text)
            w_text = words_clearte(b_text)
            
            # 単語をカウント
            rl = " ".join(map(str, w_text)).replace("\n", "")
            c = Counter(rl.split())
            count = c.most_common(50)
            
            
            wordc = wordcloud.WordCloud(
                font_path=f_path, 
                background_color="black",
                mask=b_mask,
                width=1000,
                height=1000,
                colormap=cmap
            ).generate(rl)
            
            plt.figure(figsize=[10, 10], dpi=100)
            plt.imshow(wordc.recolor(color_func=grey_color_func, random_state=3), 
                       interpolation="bilinear")
            
            plt.axis("off")
            image = io.BytesIO()
            plt.savefig(image, format="png")            
            image.seek(0)
            string = base64.b64encode(image.read())
            image_64 = "data:image/png;base64," + urllib.parse.quote_plus(string)            
            
            context = {            
                "image": image_64,
                "count": count,
            }
            return render(request, "text_process/cloud.html", context)
        
    return render(request, "text_process/cloud.html", {"form": form})

views.py

  • wordcloudの色設定。25,26行目
  • fontのパス。25行目
  • マスク画像。30~32行目以降
  • base64形式にする。85行目

以下のサイトを参考にさせていただきました

参照:
https://baronchibuike.medium.com/how-to-integrate-wordcloud-to-your-django-web-application-9bb76af0a28
参照:
https://www.datacamp.com/tutorial/wordcloud-python

templates/base.html

※雛形のイメージhtmlです 用途に合わせて手直してください


{% load static %}
<!doctype html>
<html lanh=ja>
    <head>
        <!-- Require meta tags -->
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0", shrink-to-fit=none>
        
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        
        <!-- custom css & js -->
        <link rel="stylesheet" href="{% static 'style.css' %}">
        <script src="{% static 'main.js' %}" defer></script>
        
        
        <title>Spiner | {% block title %}{% endblock title %}</title>
    </head>
    <body>
        <div class="container mt-3">
            {% block content %}
            {% endblock content %}
        </div>
        <!-- Optional Javascript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS-->
        <script
        src="https://code.jquery.com/jquery-3.6.3.min.js"
        integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU="
        crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
    </body>
</html>

templates/text_process/cloud.html

base.htmlを継承した表示用のhtmlを作成します。

{% extends 'base.html' %}
{% load widget_tweaks %}
{% load static %}

{% block title %}ワードクラウド ジェネレーター{% endblock title %}

{% block head_scripts %}
    {% comment %} <script src="{% static 'js/word_cloud.js' %}" defer></script> {% endcomment %}
{% endblock head_scripts %}

{% block contents %}
    <div class="row">
        <div class="col">

            <h3 class="mb-3">ワードクラウド ジェネレーター</h3>
            <p>適当な文章をペーストする・・・生成をクリック</p>

            <form action="{% url 'text_process:cloud' %}" method="post" autocomplete="off">
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        {{ field.label_tag }}
                        {% render_field field class="form-control" %}
                        {% if field.help_text %}
                        <small class="form-text text-muted">
                            {{ field.help_text }}
                        </small>
                        {% endif %}
                    </div>
                    {% endfor %}

                {% comment %} <input type="text" class="form-control" name="text_area"> {% endcomment %}
                <button type="submit" class="btn btn-dark rounded-capsule mt-3">クラウド生成</button>
            </form>
             {% if image %}
                <img id="parse_img" style="max-width: 100%;height: auto;" src="{{ image }}" alt="">
                {% comment %} <a download="word_cloud.png" href="{{ image }}">画像をDownload</a></p> {% endcomment %}
                <p class="text-center"><button type="button" id="btn_d" class="btn">画像をDownload</button></p>
                <p>{{ count }}</p>
             {% endif %}            
        </div>
    </div>
{% endblock contents %}

{% block endscripts %}
    {% if image %}
        <script>
            // Base64をクリックでダウンロードさせる

            let imgElem = document.querySelector("#parse_img")
            let result = imgElem.getAttribute("src")


            document.getElementById("btn_d").addEventListener("click", ()=> {
                let type = 'image/png'
                let filename = 'wordCloud.png'
                let data = result
                const a = document.createElement('a');
                a.href = data;
                a.setAttribute('download', filename);
                document.body.appendChild(a);
                a.click();
            })
        </script>
    {% endif %}
{% endblock endscripts %}
私の願い

私は神社の宮司です。神社や地域を担う次世代の人々に対し、何かを残してお役に立ててもらいたいとの願いが、強く芽生えました。個業としての神社や、小規模な地域社会に、恩恵が届くのが遅くなりそうな「デジタル」の分野。門外漢として奮闘した実体験から得た経験則を、わずかずつでも残し未来につなぎたいと願うばかりです。

More

若宮 住吉神社

サイトへ

最近の投稿

Copyright ©All rights reserved | by omo fun

Made withby Toshio Omote