神社 地域 にデジタルを
❝ワードクラウド ジェネレーター❞のコーディング
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
├── 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 %}
投稿内容 ランダム表示
❝画像のトリミング❞のコーディング
最終更新:2023年07月14日
❝ワードクラウド ジェネレーター❞の使い方
最終更新:2023年07月14日
HTMLメールを活用してみた
最終更新:2023年07月14日
❝ユーチューブのダウンロード❞のコーディング
最終更新:2023年08月18日
python3.10 Django4 wsl bootstrap javascript
カテゴリ
私の願い
私は神社の宮司です。神社や地域を担う次世代の人々に対し、何かを残してお役に立ててもらいたいとの願いが、強く芽生えました。個業としての神社や、小規模な地域社会に、恩恵が届くのが遅くなりそうな「デジタル」の分野。門外漢として奮闘した実体験から得た経験則を、わずかずつでも残し未来につなぎたいと願うばかりです。
最近の投稿
- 簡易で安価なカメラで防犯・外出対策を
最終更新:2023年09月08日
- 神社のオリジナルTシャツを作ってみた
最終更新:2023年08月07日
- HTMLメールを活用してみた
最終更新:2023年07月14日
- 豊中市Graphの基データの説明
最終更新:2023年07月14日