ワードクラウドで捉える「こち亀」作風の変遷

## キーワードで再発見した「こち亀」の魅力
こち亀は40年間も長期連載していた、思い出の詰まった作品です。
そんなこち亀へのリスペクトと私自身の復習のために、こち亀を徹底的に分析してみました。意外な発見もたくさんあり、こち亀を新たな視点から楽しめたデータ分析となりました。
　具体的な分析内容としては、「ワードクラウド」とよばれる頻度の高いキーワードを大きく表示する視覚化技法を用いて、各時期の象徴的なキーワードを抽出する形をとりました。なお、分析過程でAmazon単行本ページのdescriptionを抽出し、自然言語処理をかけてみました。

（※）Amazon規約を精査しまして、非ログイン状態であればスクレイピングは問題ないことを確認しています。

### 1~40巻：1977/7/9 ~ 1986/5/9
![zero_group_wc_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/31e1a2ce-7596-07fa-eeea-021236908bc7.png)
この時期は、「暴走」「拳銃」「追撃」など尖ったワードが並びます。「本田」君の最盛期で、初期は登場が多かったことが示されています。単行本の表紙も、銃を構えたりバイクに乗る絵柄が他の時期よりも目立ちます。

### 41~80巻：1986/7/10 ~ 1993/6/4
![zero_group_wc_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/2d10ba65-ff8e-225e-174d-3d961dd1da41.png)
「浅草」「神輿」「上野」というキーワードが並ぶように、お祭りや下町の描写が比較的多い時期でした。50巻あたりから表紙絵が劇画寄りから優しいトーンに変わっており、柔らかな作風への変化を感じます。

### 81巻~120巻：1993/6/4 ~ 2000/7/4
![zero_group_wc_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/98291bb9-9915-0781-50b5-ebda09af9fff.png)
「麗子」「マリア」が頻出しており、２大ヒロインの全盛期を感じさせます。「婦警」というキーワードも目立つように、100~120巻は女性が表紙を飾る機会が多くなりました。その中で、マッチョ揃い踏みの108巻表紙が逆に目を引きます。「ゲーム」「たまごっち」など娯楽の変化も窺えます。

### 121~160巻：2000/9/4 ~ 2008/6/4
![zero_group_wc_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/8303df18-2e93-d5d1-aaca-7c43baf085cd.png)
ニューヒロイン「纏」の登場が目覚ましいです。両さんが最も「結婚」に近づいたのもこの時期でした。「大阪」「京都」の話題も多く、関西へのお出かけがとても活発でした。これまで見慣れない人物名が増えているように、新キャラが多く登場するにぎやかさもこの時期の特徴です。

### 161~201巻：2008/9/4 ~ 2021/10/4
![zero_group_wc_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/ed2e0f80-b6c2-70e7-31eb-ab3aeb695444.png)
「部長」が堂々の最頻出キーワード。フィナーレも近づくこの時期は、最初期からみられた部長とのお馴染みの掛け合いも見せてくれました。「夏」「海」も注目すべきポイントで、季節描写の豊かなこち亀の中でも、やはり真っ赤な太陽に白い砂浜、青い海が、そのド真ん中にくるのではないでしょうか。


## 技術ハイライト

これまでのアウトプットには、下記の手順で進めました。デバイスはMacです。
最初はローカル環境でMeCabを試しましたが、循環importの不具合が解消されず、仮想環境を立ててMeCabを実行する方針に切り替えました。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323732363536392f39353838373535622d386639392d363732382d643863352d3265303264313730623036332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/63cc3b21-7d36-fc54-2767-edad62d45cfe.png)

（1）Dockerで仮想環境構築（2）Selenium仮想環境にSafariブラウザ立ち上げ（3）WebDriverでスクレイピング（4）Docker仮想環境でのJupyterNotebook上で、自然言語処理（MeCab）とワードクラウド生成
### （1）Dockerで仮想環境構築
Dockerの構築はこちらをご参照ください。
[【入門】はじめての Docker Desktop for Mac のインストールと CentOS の仮想環境構築のセットアップ](https://qiita.com/gahoh/items/92217e0a887bb81e3155)
Dockerfile とdocker-compose.yml をローカルのホームディレクトリに設置

```python:Dockerfile
FROM python:3.9

WORKDIR /app

SHELL ["/bin/bash", "-c"]

RUN apt-get update &

RUN pip install --upgrade pip 

RUN pip install notebook
RUN pip install selenium
RUN pip install numpy
RUN pip install pandas
RUN pip install mecab-python3
RUN pip install unidic-lite
RUN pip install wordcloud
```
```python:docker-compose.yml
version: '3'
services:
  app:
    build: .
    volumes:
      - ./:/app
    ports:
      - 8888:8888
    tty: true
```
コンテナを起動します
```
docker-compose up -d
```
起動確認はこちら
```
docker-compose ps
```
### （2）Selenium仮想環境にSafariブラウザ立ち上げ
Seleniumの基本概念は、こちらがわかりやすいです。
[Selenium Gridの環境を構築する（docker-compose利用）](https://qiita.com/hiromitsu7/items/c1b6734edd0d154b268f)
selenium-server-standalone-3.5.3.jar をダウンロードし、ローカルのホームディレクトリに設置します。
[https://selenium-release.storage.googleapis.com/index.html?path=3.5](https://selenium-release.storage.googleapis.com/index.html?path=3.5)
nodeConfig.jsonを作成し、ローカルのホームディレクトリに設置します。
```python:nodeConfig.json
{
 "capabilities":
  [
   {
   "platform": "MAC",
   "version": "10.0",
   "browserName": "safari"
   }
  ],
  "maxSession": 5,
  "hub": "http://localhost:4444/grid/register",
  "port": 5051,
  "register": true
}
```
Selenium Grid Server(Hub) を立てます。
※javaが入っていない場合はインストール
```
java -jar selenium-server-standalone-3.5.3.jar -role hub
```
ここでは、http://10.70.248.78 がHubに指定されているので、
追加する端末（Node）はこの Hub URLに登録します。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323732363536392f37626236633131622d393339632d336137332d636230622d3965333765303135333861332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/10fcddfc-8fde-4cf1-d0c0-a8e46cac77d0.png)

次に、端末（Node）を立てます。
```
java -Dwebdriver.safari.driver=/usr/bin/safaridriver -jar selenium-server-standalone-3.5.3.jar -role node -nodeConfig nodeConfig.json -hub http://192.168.11.2:4444/grid/register
```
そして、ターミナルの Nodes should register to … にあるURLホスト名部分を利用して、ローカルブラウザURLに http://10.70.248.78:4444/grid/console  と入力。
Selenium仮想環境にSafariブラウザが立ち上がりました。

※ Hub URLのホスト名は実行タイミングで変化することがあります。
※ Safariブラウザを選択した理由は、（1）WebDriverがデフォルトで用意されており、ブラウザのバージョンをダウングレードしてWebDriverに合わせる必要がないこと（2）スクレイピング時の指定が簡単であること
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f323732363536392f38336264346131652d643334652d333361342d343939662d3464653266663934303431392e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726569/4b743cd2-2550-e088-d7d5-b5c833948561.png)

### （3）Docker仮想環境にJupyterNotebookを立ち上げ、スクレイピング実行
連載一覧ページから、各単行本のdescriptionを取得し、dataframeに格納します。
まずは、Docker仮想環境で jupyter notebook を立ち上げます。
以下のコマンドでjupyter notebookを起動します。

```
jupyter notebook --port=8888 --ip=0.0.0.0 --allow-root --NotebookApp.token=''
```
起動できたら、ローカルブラウザで http://localhost:8888/ にアクセスすると、
Docker環境で立ち上げた jupyter notebookを操作することができます。
スクレイピング実行前に、ローカルのSafari設定について、開発 > リモートオートメーションを許可　　を設定。
jupyter notebook上で下記のスクリプトを回し、WebdriverでSelenium仮想環境のSafariにスクレイピングを実行させます。

```python:scraping.ipynb
import sys
from selenium import webdriver
from time import sleep
import numpy as np
import pandas as pd
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

#Safariドライバ起動
SafariDriver = webdriver.Remote(command_executor='http://10.70.248.78:4444/wd/hub', 
                                desired_capabilities=DesiredCapabilities.SAFARI)
SafariDriver.get('https://www.amazon.co.jp/dp/B0758FFXPZ')

#スクレイピング実行
#cssセレクタはサイトコード変更によって有効でなくなる可能性があります。
css_selector = "li#seriesAsinListShowNextPage > span.a-list-item > a.a-link-normal"
for i in np.arange(20):
    element = SafariDriver.find_element(By.CSS_SELECTOR, css_selector)
    SafariDriver.execute_script("arguments[0].scrollIntoView();", element)
    sleep(1)
    SafariDriver.find_element(By.CSS_SELECTOR, css_selector).send_keys(Keys.ENTER)
    sleep(2)

dataset = pd.DataFrame(columns=['id', 'description'])
elem = SafariDriver.find_elements(By.CSS_SELECTOR, "div.a-expander-content > div.a-section > span.a-size-base")

for i, e in enumerate(elem):
    dataset = dataset.append({'id': i, 'description': e.text}, ignore_index=True)
```
### （4）Docker仮想環境でのJupyterNotebook上で、自然言語処理とワードクラウド生成
日本語フォントをダウンロード
```
sudo apt install fonts-ipaexfont
```
出力ワードクラウド画像を確認しつつ、特徴が際立つように除外キーワードを手動で入れることを繰り返して改善していきます。
```python:wordcloud.ipynb
# 除外キーワード
trim_words_ =  '【ページ数が多いビッグボリューム版！】|編|収録|スル|巻|他|イル|イク|前|ナル|クル|こと|ハジメル|ミル|ススメル|メザス|ナイ|アル|シマウ|'\
                            'イウ|ヤル|ツレル|フクム|ノル|ツケル|もの|ため|デル|キク|少年|ジャンプ|増量|ヒラク|エラブ|カウ|カク|シル|両|派出|所|やつ|亀|津|勘吉|ハタシテ|オシエル'

def trim_words(text):
    text = re.sub(trim_words_, '', text)
    return text
    
dataset['description'] = dataset['description'].apply(trim_words)

import re
import MeCab as mc

def strip_CRLF_from_Text(text):

    plaintext = re.sub('([ぁ-んー]+|[ァ-ンー]+|[\\u4e00-\\u9FFF]+|[ぁ-んァ-ンー\\u4e00-\\u9FFF]+)(\n)([ぁ-んー]+|[ァ-ンー]+|[\\u4e00-\\u9FFF]+|[ぁ-んァ-ンー\\u4e00-\\u9FFF]+)',
                       r'\1\3',
                       text)
    plaintext = plaintext.replace('\n', ' ').replace('\t', ' ')
    return plaintext
 
def mecab_wakati(text):

    t = mc.Tagger()

    node = t.parseToNode(text)
    sent = ""
    while(node):
        if node.surface != "": 
            word_type = node.feature.split(",")[0]
            if word_type in ["名詞"]:
                 sent += node.surface + " "  
            if word_type in [ "動詞", "形容詞","副詞"]:
                sent += node.feature.split(",")[6] + " " 
        node = node.next
        if node is None:
            break
    return sent

dataset['words'] = dataset['description'].apply(strip_CRLF_from_Text)
dataset['words2'] = dataset['description'].apply(mecab_wakati)

book_group = (dataset['id'] + 1) // 40
dataset_ = dataset.assign(book_group=book_group)

for i in np.arange(5):
    zero_group = dataset_.query('book_group==' + str(i))

    words_set = ''

    for words in zero_group['words2']:
        words_set = words_set + ' ' + words
    
    words_set = re.sub(trim_words_, '', words_set)
    
　　 # 日本語フォントにパスを通す
    zero_group_wc = WordCloud(max_font_size=60, font_path='/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf').generate(words_set)
    zero_group_wc.to_file("zero_group_wc_" + str(i) + ".png")
```