WEBマンガのコメントでワードクラウドを作ってみたら、どんな漫画なのかが視覚的にわかっておもしろい

#はじめに
WEBマンガ、たまに読むんですが、いっぱいありすぎてどれを読めばいいのかわからなくなります。
どのマンガを読むかを選ぶひとつの指標として、コメントが使えないかというの考えました。
人気のある漫画はコメントが多いですし、コメントが少なくても面白いマンガも多い。
それで、いろいろとコメントを解析してみようと思っているんですが、その第一弾として、コメントをワードクラウド化してみたところ、コメントが視覚的に把握できて、興味をそそるマンガかどうかを直感的に見ることができました。

ワードクラウドをもとに新たな切り口でマンガを選ぶことで、作家さんが一生懸命書いたマンガの入り口になって、マンガ界の活性化に貢献できればという思いもこもってます。ちょっと大げさですかね。

#環境
python 3.7.6
selenium 3.141.0
ChromeDriver 80.0.3987.16
wordcloud 1.6.0
BeautifulSoup 4.8.2
mecab-python-windows 0.996.3

#対象
[ニコニコ静画](https://seiga.nicovideo.jp/manga/?track=home)

#WEBマンガワードクラウド参照サイト
結果を参照できるサイトを下記に作成しました。ワードクラウドをクリックするとそのマンガに遷移します。

[WEBマンガ ワードクラウド](https://comic.g-at.net/)

#ワードクラウド出力結果
以下が出力結果です。どんなマンガなのか気になってきませんか？
「美人」「好き」などのコメントで、ちょっと読んでみたいなぁなんて考えてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/5e039b8f-26e8-79ee-ba11-76eccd51187b.png)


#規約を確認
スクレイピングを行うので、規約を確認します。

＜[niconico利用規約](https://account.nicovideo.jp/rules/account)より抜粋＞
**5 禁止事項**
利用者による｢niconico｣の利用に関して、以下の行為が禁止されています。

- ニコニコ活動ガイドライン第３項及び第４項に掲げる行為又はこれらの行為に準じる行為(コメントの書き込みや動画等の投稿以外の手段を通じて行われる行為を含みます)
- 本利用規約の条項に違反する行為
- 公職選挙法に抵触する行為
- **｢niconico｣のサーバーに過度の負担を及ぼす行為**
- ｢niconico｣の運営を妨害する行為
- 児童買春・ポルノ、無修正ビデオ動画のダウンロードサイト等へのリンク掲載
- 運営会社の許諾を得ない売買行為、オークション行為、金銭支払やその他の類似行為
- 運営会社の許諾を得ない商品の広告、宣伝を目的としたプロフィール内容の公開、その他スパムメール、チェーンメール等の勧誘を目的とする行為
- 13歳以上の未成年者が法定代理人（親権者）の同意を得ずに、「niconico」を利用する行為
- 運営会社が不適切であると考える行為
- その他上記に準じる行為

ということで、過度な負荷をかけないよう注意して実施します。
連続走行させない、スリープを挟むなどを実施しながら実行してます。

#処理の流れ
以下の流れで処理を実行します。

1. ニコニコにログイン
2. ニコニコ静画を更新順で表示しマンガ一覧からURLリストを取得
3. 漫画の詳細に遷移
4. コメントを取得
5. コメントをWordCloudで処理

## ニコニコにログイン
ニコニコ静画を参照するにはログインが必要です。
ここでは、seleniumを使って、バックグラウンドでニコニコにログインします。

seleniumとChromeDriverはインストールしてある前提です。
[ChromeDriver](https://chromedriver.chromium.org/downloads)

#### ライブラリインポート
下記で必要ライブラリをインポートします。

```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import urllib.parse
```

#### WebDriver構築
オプションを設定して、ドライバーを構築します。
バックグラウンドで動作させるため、`--headless`オプションを指定しています。
また、`set_page_load_timeout`でタイムアウトを30秒に設定しています。

```python
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--window-size=1024,768')

driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
```

#### ログイン
まず、`https://account.nicovideo.jp/login?site=seiga&next_url=%2F`にアクセスします。
つぎに、メールアドレスとパスワードの項目をIDで取得して、それぞれを設定しています。
最後に、ログインボタンをクリックしています。
`[メールアドレス]`、`[パスワード]`はご自身のに変更してください。

```python
driver.get('https://account.nicovideo.jp/login?site=seiga&next_url=%2F')

e = driver.find_element(By.ID, "input__mailtel")
e.send_keys('[メールアドレス]')
e = driver.find_element(By.ID, "input__password")
e.send_keys('[パスワード]')

e = driver.find_element(By.ID, 'login__submit')
e.click()
```
requestsのpostを使ってもログインできるのですが、その場合は、ログイン画面から`auth_id`を取得してそれもポストする必要があります。そのあたりの処理がseleniumでは不要です。
また、画面表示後にJavaScript等で画面が更新される場合にもrequestsではいろいろと手間がかかりますが、seleniumだとそのあたりも気にせずに処理ができるのが便利です。

## ニコニコ静画を更新順で表示しマンガ一覧からURLリストを取得

#### 更新順でマンガの一覧を取得
下記の状態の時の静画の一覧を取得していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/7ba0c22c-2570-59a5-d910-97aa475c111d.png)
ページ遷移をしながら各ページのマンガ一覧のマンガのURLを一覧で取得します。負荷のことを考え、ここでは１～３ページまでを取得します。

```python
url_root = 'https://seiga.nicovideo.jp'
desc_urls = []

for n in range(1, 4):
    target_url = urllib.parse.urljoin(url_root, 'manga/list?page=%d&sort=manga_updated' % n)
    try:
        driver.get(target_url)
        html = driver.page_source.encode('utf-8')
        soup = BeautifulSoup(html, 'html.parser')

        # change to loop
        for desc in soup.select('.mg_description'):
            title = desc.select('.title')
            desc_urls.append(urllib.parse.urljoin(url_root, title[0].find('a').get('href')))
    except Exception as e:
        print(e)
        continue
```
`desc_urls`リストにマンガへのURLを保存します。
`target_url`に各ページへのURLを設定します。QueryString の `page=` に数字を設定することでページ制御されていますので、そこに取得したいページの数字を設定します。

`driver.get`でページを取得します。取得したら、`driver.page_source.encode('utf-8')`で中身のHTMLを取得し、扱いやすいように`BeautifulSoup`に設定しています。
`BeautifulSoup`に設定しなくても扱えますが、こっちのほうが慣れているのでこっちにしたという程度です。WebDriverはXPathとかも使えるので、そのままでも十分大丈夫だと思います。

`BeautifulSoup`の`select`はCSSセレクターなので、`.mg_description`を取得して、その中の`.title`とそこに設定されている `a` タグの `href` を取得しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/8929d324-0bc1-f43b-4ab4-ddc1e83ac4f4.png)

これで、ページ上のマンガのタイトルとURLの一覧が取得できました。

## 漫画の詳細に遷移

#### リスト内のURLでページを取得
`desc_urls`に保持したURLでページを取得します。取得は、`driver.get(desc_url)`でやってます。
取得したら同様にHTMLを取得して `BeautifulSoup`に設定します。

```python
for desc_url in desc_urls:
    try:
        driver.get(desc_url)

        html = driver.page_source.encode('utf-8')
        soupdesc =  BeautifulSoup(html, 'html.parser')
```

#### タイトルと著者を取得して確認
div タグで id が `ng_main_column`のエレメントを取得します。
その中の、`.main_title'クラスのエレメントを取得し、タイトルと著者を取得します。
print してみて、きちんと取得できているかを確認してみます。

```python
        maindesc = soupdesc.find('div', id = 'mg_main_column')

        titlediv = maindesc.select('.main_title')[0]

        title = titlediv.find('h1').text.strip()
        author = titlediv.find('span').text.strip()

        print(title)
        print(author)
```
HTMLの構造は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/ebecf406-fd41-782b-97ac-5e7753183957.png)

#### エピソード一覧からサブタイトルと詳細へのURLを取得して遷移
クラスが `.episode_item` のエレメントに各エピソードがあるので、そのリストを CSSセレクタの `select` で取得します。
複数のエレメントが取得されるので、それぞれのエレメントから、サブタイトルと詳細へのURLを取得します。

```python
        for eps in soupdesc.select('.episode_item'):
            eps_ttl_div = eps.select('.title')
            eps_title = eps_ttl_div[0].find('a')
            eps_url = urllib.parse.urljoin(url_root, eps_title.get('href'))
            eps_t = eps_title.text
            print(eps_t)

            try:
                driver.get(eps_url)
                html = driver.page_source.encode('utf-8')
                soupeps = BeautifulSoup(html, 'html.parser')
```
タイトルは、`.title`クラス、URLは a タグの `href` から取得しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/1c82a8a5-a75a-1594-e7ee-80e408ed0f34.png)
`driver.get(eps_url)`で詳細画面を取得しています。
取得したら、`BeautifulSoup`に設定します。

## コメントを取得

#### コメントのリストを取得し、その中のテキストを配列に設定

クラスが `.comment_list`のエレメントを取得し、その中の `.comment`をすべて取得しています。
その中の文字列を `c.text`で取得し、配列`comments_text`に設定しています。
配列への設定は、リスト内包表記を使っています。pythonの内包表記はチューリング完全だそうです。

```python
                crlist = soupeps.select('.comment_list')
                comments = crlist[0].select('.comment')
                comments_text = [c.text for c in comments]
```
コメント部分のHTML構成は以下のようになっています。`comment_viewer`の find でもできそうですね。この辺りはいい感じに指定していきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/951f142b-7241-bcf1-4311-f5ac63f94848.png)



## コメントをWordCloudで処理

#### MeCabで形態素解析
取得したコメントの文字列は、MeCabで形態素解析します。
インポート分を付け加えましょう。

```python
import MeCab
```
MeCab の `parse` で形態素解析をします。

```python
                m = MeCab.Tagger('')
                parsed = m.parse('。'.join(comments_text))
```
形態素解析した結果は、以下のようになります。

```
'流石\t名詞,形容動詞語幹,*,*,*,*,流石,サスガ,サスガ\nに\t助詞,副詞化,*,*,*,*,に,ニ,ニ\nなかっ\t形容詞,自立,*,*,形容詞・アウオ段,連用タ接続,ない,ナカッ,ナカッ\nた\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ\nｗｗｗ\t名詞,一般,*,*,*,*,*\n。\t記号,句点,*,*,*,*,。,。,。\nそれ\t名詞,代名詞,一般,*,*,*,それ,ソレ,ソレ\nは\t助詞,係助詞,*,*,*,*,は,ハ,ワ\n筆記具\t名詞,一般,*,*,*,*,筆記具,ヒッキグ,ヒッキグ\nで\t助詞,格助詞,一般,*,*,*,で,デ,デ\nは\t助詞,係助詞,*,*,*,*,は,ハ,ワ\nあり\t動詞,自立,*,*,五段・ラ行,連用形,ある,アリ,アリ\nませ\t助動詞,*,*,*,特殊・マス,未然形,ます,マセ,マセ\nん\t助動詞,*,*,*,不変化型,基本形,ん,ン,ン\n…\t記号,一般,*,*,*,*,…,…,…\n。\t記号,句点,*,*,*,*,。,。,。\nキシガイ\t名詞,一般,*,*,*,*,*\n。\t記号,...
```
`\n`が一行ごとなので、`splitlines` で一行ずつ取り出し、`\t`で区切られている右側の７番目から、形態素の基本形を取得します。
その際、助詞と助動詞、代名詞、そして「する」や「てる」などのいくつかの文字列を除外しています。
除外しないと、ワードクラウドを作ったときに、そればっかりが大きな文字れ表示されてしまいます。

```python
                words = ' '.join([x.split('\t')[1].split(',')[6] for x in parsed.splitlines()[:-1] if x.split('\t')[1].split(',')[0] not in ['助詞', '助動詞'] and x.split('\t')[1].split(',')[1] not in ['代名詞'] and x.split('\t')[1].split(',')[6] not in ['する', 'てる', 'なる', 'さん', 'そう', 'この', 'ある']])
```

#### WordCloudでワードクラウドを作成
WordCloudの `to_file` で、ワードクラウドを作成します。
`comic_titles` `comic_subtitles` `comic_images` `comic_urls` は配列で宣言してある変数で、後ほどHTMLを作成するときに使用します。それぞれ、タイトル、サブタイトル、画像名、URLを保持しています。

WordCloud構築時は、フォント、背景色、サイズを指定しています。フォントは、YouTubeでよく使われているらしい「ラノベPOP」というものを使っています。この辺りはお好みで指定してください。

`wordcloud.to_file` でファイルに出力しています。

```python
                if len(words) > 0:
                    try:
                        comic_titles.append(title)
                        comic_subtitles.append(eps_t)
                        comic_images.append('%d.png' % (comic_index))
                        comic_urls.append(eps_url)
                        wordcloud = WordCloud(font_path=r"C:\\WINDOWS\\Fonts\\ラノベPOP.otf", background_color="white", width=800,height=800).generate(words)
                        wordcloud.to_file("[保存したいパス]/wordcloud/%d.png" % (comic_index))
                        comic_index += 1
                    except Exception as e:
                        print(e)

```
出力結果は、最初にお見せしたものです。
これらでHTMLをつくってサイトに公開します。

#公開したサイト
https://comic.g-at.net/

上記URLにアクセスすると、下記のようなワードクラウドの一覧が表示されます。
ワードクラウドをクリックすると、そのマンガが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/508288/eaab9cd1-8ef5-2070-260d-367d47c967d0.png)

#おわりに
ジャンプ＋とかマンガワンとかのコメントって、結構辛辣なコメントが多いですが、ニコニコはみんな優しいコメントが多いです。やはり、コメント慣れしているんでしょうね。

ワードクラウドを作るだけではなくいろいろな分析をして、今まで出会うことが難しかった名作への扉を開くことができたらいいなと思います。