【Ubuntu】Python の CaboCha で係り受け関係の表出と形態素解析を行う

Ubuntu 20.04 で CaboCha を使える様にした時の手順を記録しておきます。

まだインストールしていなければ build-essential をインストールします。

$ sudo apt install build-essential

MeCab のインストール

$ sudo apt install mecab
$ sudo apt install libmecab-dev
$ sudo apt install mecab-ipadic

この時点でコマンド「mecab」を実行して何か文章を入力すると MeCab が形態素解析をしてくれます。

$ mecab
今日はいい天気ですね。
今日	名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
いい	形容詞,自立,*,*,形容詞・イイ,基本形,いい,イイ,イイ
天気	名詞,一般,*,*,*,*,天気,テンキ,テンキ
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
ね	助詞,終助詞,*,*,*,*,ね,ネ,ネ
。	記号,句点,*,*,*,*,。,。,。

mecab-ipadic-neologd のインストール

新語に対応した NEologd 辞書(mecab-ipadic-neologd)をインストールします。

$ cd /var/lib/mecab/dic
$ sudo git clone https://github.com/neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ sudo bin/install-mecab-ipadic-neologd

「/usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd」にインストールされたので、MeCab 実行時に「-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd」を渡してあげると mecab-ipadic-neologd を辞書として使うことができます。

$ mecab -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd
霜降り明星が好きです。
霜降り明星	名詞,固有名詞,人名,一般,*,*,霜降り明星,シモフリミョウジョウ,シモフリミョウジョー
が	助詞,格助詞,一般,*,*,*,が,ガ,ガ
好き	名詞,形容動詞語幹,*,*,*,*,好き,スキ,スキ
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。	記号,句点,*,*,*,*,。,。,。
EOS

上では「霜降り明星」が一つの名詞として認識されていますが、デフォルトの辞書だと下記の様に「霜降り」と「明星」が別々の言葉として認識されています。

$ mecab
霜降り明星が好きです。
霜降り	名詞,一般,*,*,*,*,霜降り,シモフリ,シモフリ
明星	名詞,一般,*,*,*,*,明星,ミョウジョウ,ミョージョー
が	助詞,格助詞,一般,*,*,*,が,ガ,ガ
好き	名詞,形容動詞語幹,*,*,*,*,好き,スキ,スキ
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
。	記号,句点,*,*,*,*,。,。,。
EOS

CRF++ のインストール

CRF++-0.58.tar.gz

$ cd
$ wget "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7QVR6VXJ5dWExSTQ" -O CRF++-0.58.tar.gz
$ tar zxvf CRF++-0.58.tar.gz
$ cd CRF++-0.58
$ ./configure
$ sudo ldconfig
$ rm CRF++-0.58.tar.gz

CaboCha のインストール

cabocha-0.68.tar.bz2 をダウンロードします。Google Drive に配置されているので少し複雑です。

$ FILE_ID=0B4y35FiV1wh7SDd1Q1dUQkZQaUU
$ FILE_NAME=cabocha-0.69.tar.bz2
$ curl -sc /tmp/cookie "https://drive.google.com/uc?export=download&id=${FILE_ID}" > /dev/null
$ CODE="$(awk '/_warning_/ {print $NF}' /tmp/cookie)"  
$ curl -Lb /tmp/cookie "https://drive.google.com/uc?export=download&confirm=${CODE}&id=${FILE_ID}" -o ${FILE_NAME}

cabocha-0.68.tar.bz2 を解凍、インストールしていきます。

$ bzip2 -dc cabocha-0.69.tar.bz2 | tar xvf -
$ cd cabocha-0.69
$ ./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
$ make
$ make check
$ sudo make install
$ sudo ldconfig

先程の MeCab を同じ様に、コマンド「cabocha」を実行して何か文章を入力すると CaboCha が係り受けを出してくれます。

$ cabocha
今日はいい天気ですね。
      今日は---D
          いい-D
    天気ですね。
EOS

Pythonへのバインディング

Python の仮想環境を立ち上げた状態で下記を実行します。

$ cd python
$ python setup.py install
$ pip install mecab-python3

下記の通り Python で CaboCha が使える様になりました。

>>> import CaboCha
>>> sentence = '霜降り明星(しもふりみょうじょう)は、2018年『M-1グランプリ』14代目王者。'
>>> c = CaboCha.Parser('-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd')
>>> print(c.parseToString(sentence))
        霜降り明星-----D      
              (しも-D |      
                  ふり-D      
      みょうじょう)は、-----D
                    2018年-D |
           『M-1グランプリ』-D
                  14代目王者。
EOS

【Python】CaboCha のツリーを XML から JSON に変換する

CaboCha のツリーを扱いたいのですがデフォルトでは JSON でのアウトプットがない様なので、xmltodict を利用して XML 形式から JSON 形式に変換します。

XML での表出

まず、XML の表出は下記の様になります。

import CaboCha

c = CaboCha.Parser()

tree = c.parse('今日は天気がとても良いですね。')
xmltree = tree.toString(CaboCha.FORMAT_XML)
print(xmltree)

XML アウトプット

<sentence>
 <chunk id="0" link="3" rel="D" score="-1.359140" head="0" func="1">
  <tok id="0" feature="名詞,副詞可能,*,*,*,*,今日,キョウ,キョー">今日</tok>
  <tok id="1" feature="助詞,係助詞,*,*,*,*,は,ハ,ワ">は</tok>
 </chunk>
 <chunk id="1" link="3" rel="D" score="-1.359140" head="2" func="3">
  <tok id="2" feature="名詞,一般,*,*,*,*,天気,テンキ,テンキ">天気</tok>
  <tok id="3" feature="助詞,格助詞,一般,*,*,*,が,ガ,ガ">が</tok>
 </chunk>
 <chunk id="2" link="3" rel="D" score="-1.359140" head="4" func="4">
  <tok id="4" feature="副詞,助詞類接続,*,*,*,*,とても,トテモ,トテモ">とても</tok>
 </chunk>
 <chunk id="3" link="-1" rel="D" score="0.000000" head="5" func="7">
  <tok id="5" feature="形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ">良い</tok>
  <tok id="6" feature="助動詞,*,*,*,特殊・デス,基本形,です,デス,デス">です</tok>
  <tok id="7" feature="助詞,終助詞,*,*,*,*,ね,ネ,ネ">ね</tok>
  <tok id="8" feature="記号,句点,*,*,*,*,。,。,。">。</tok>
 </chunk>
</sentence>

JSON での表出

xmltodict を使うので、インストールしていない場合はコマンド「pip install xmltodict」でインストールしてください。

import CaboCha
import xmltodict
import json

c = CaboCha.Parser()

tree = c.parse('今日は天気がとても良いですね。')
xmltree = tree.toString(CaboCha.FORMAT_XML)
jsonobj = xmltodict.parse(xmltree, attr_prefix='', cdata_key='surface', dict_constructor=dict)
print(json.dumps(jsonobj, indent=2, ensure_ascii=False))

JSON アウトプット

{
  "sentence": {
    "chunk": [
      {
        "id": "0",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "0",
        "func": "1",
        "tok": [
          {
            "id": "0",
            "feature": "名詞,副詞可能,*,*,*,*,今日,キョウ,キョー",
            "surface": "今日"
          },
          {
            "id": "1",
            "feature": "助詞,係助詞,*,*,*,*,は,ハ,ワ",
            "surface": "は"
          }
        ]
      },
      {
        "id": "1",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "2",
        "func": "3",
        "tok": [
          {
            "id": "2",
            "feature": "名詞,一般,*,*,*,*,天気,テンキ,テンキ",
            "surface": "天気"
          },
          {
            "id": "3",
            "feature": "助詞,格助詞,一般,*,*,*,が,ガ,ガ",
            "surface": "が"
          }
        ]
      },
      {
        "id": "2",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "4",
        "func": "4",
        "tok": {
          "id": "4",
          "feature": "副詞,助詞類接続,*,*,*,*,とても,トテモ,トテモ",
          "surface": "とても"
        }
      },
      {
        "id": "3",
        "link": "-1",
        "rel": "D",
        "score": "0.000000",
        "head": "5",
        "func": "7",
        "tok": [
          {
            "id": "5",
            "feature": "形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ",
            "surface": "良い"
          },
          {
            "id": "6",
            "feature": "助動詞,*,*,*,特殊・デス,基本形,です,デス,デス",
            "surface": "です"
          },
          {
            "id": "7",
            "feature": "助詞,終助詞,*,*,*,*,ね,ネ,ネ",
            "surface": "ね"
          },
          {
            "id": "8",
            "feature": "記号,句点,*,*,*,*,。,。,。",
            "surface": "。"
          }
        ]
      }
    ]
  }
}

さらに改良

上記でも JSON 形式で返ってきますが、chunk や tok 要素の中身が 1 つしかない時にリスト形式になっていない、feature がカンマ区切りの文字列(リスト形式でない)になっているなど少し不便です。

下記の様に処理を追加するとフォーマットを揃えることができます。

import CaboCha
import xmltodict
import json

c = CaboCha.Parser()

tree = c.parse('今日は天気がとても良いですね。')
xmltree = tree.toString(CaboCha.FORMAT_XML)
jsonobj = xmltodict.parse(xmltree, attr_prefix='', cdata_key='surface', dict_constructor=dict)

# 追記分 ↓
if jsonobj['sentence']: # sentence が存在する際に処理を行う
    if type(jsonobj['sentence']['chunk']) is not list: # chunk を必ずリスト形式にする
        jsonobj['sentence']['chunk'] = [jsonobj['sentence']['chunk']]
    
    for chunk in jsonobj['sentence']['chunk']:
        if type(chunk['tok']) is not list: # tok を必ずリスト形式にする
            chunk['tok'] = [chunk['tok']]
        
        for tok in chunk['tok']:
            feature_list = tok['feature'].split(',') # feature をリスト形式に変換
            tok['feature'] = feature_list
# 追記分 ↑

print(json.dumps(jsonobj, indent=2, ensure_ascii=False))

JSON アウトプット ver 2

{
  "sentence": {
    "chunk": [
      {
        "id": "0",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "0",
        "func": "1",
        "tok": [
          {
            "id": "0",
            "feature": [
              "名詞",
              "副詞可能",
              "*",
              "*",
              "*",
              "*",
              "今日",
              "キョウ",
              "キョー"
            ],
            "surface": "今日"
          },
          {
            "id": "1",
            "feature": [
              "助詞",
              "係助詞",
              "*",
              "*",
              "*",
              "*",
              "は",
              "ハ",
              "ワ"
            ],
            "surface": "は"
          }
        ]
      },
      {
        "id": "1",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "2",
        "func": "3",
        "tok": [
          {
            "id": "2",
            "feature": [
              "名詞",
              "一般",
              "*",
              "*",
              "*",
              "*",
              "天気",
              "テンキ",
              "テンキ"
            ],
            "surface": "天気"
          },
          {
            "id": "3",
            "feature": [
              "助詞",
              "格助詞",
              "一般",
              "*",
              "*",
              "*",
              "が",
              "ガ",
              "ガ"
            ],
            "surface": "が"
          }
        ]
      },
      {
        "id": "2",
        "link": "3",
        "rel": "D",
        "score": "-1.359140",
        "head": "4",
        "func": "4",
        "tok": [
          {
            "id": "4",
            "feature": [
              "副詞",
              "助詞類接続",
              "*",
              "*",
              "*",
              "*",
              "とても",
              "トテモ",
              "トテモ"
            ],
            "surface": "とても"
          }
        ]
      },
      {
        "id": "3",
        "link": "-1",
        "rel": "D",
        "score": "0.000000",
        "head": "5",
        "func": "7",
        "tok": [
          {
            "id": "5",
            "feature": [
              "形容詞",
              "自立",
              "*",
              "*",
              "形容詞・アウオ段",
              "基本形",
              "良い",
              "ヨイ",
              "ヨイ"
            ],
            "surface": "良い"
          },
          {
            "id": "6",
            "feature": [
              "助動詞",
              "*",
              "*",
              "*",
              "特殊・デス",
              "基本形",
              "です",
              "デス",
              "デス"
            ],
            "surface": "です"
          },
          {
            "id": "7",
            "feature": [
              "助詞",
              "終助詞",
              "*",
              "*",
              "*",
              "*",
              "ね",
              "ネ",
              "ネ"
            ],
            "surface": "ね"
          },
          {
            "id": "8",
            "feature": [
              "記号",
              "句点",
              "*",
              "*",
              "*",
              "*",
              "。",
              "。",
              "。"
            ],
            "surface": "。"
          }
        ]
      }
    ]
  }
}

【Mac】Python の CaboCha で係り受け関係の表出と形態素解析を行う

Mac 環境で Python の CaboCha を使う方法を紹介します。

もろもろインストール

まずもろもろインストールが必要なので、Python 仮想環境を起動した状態で下記を実行します。

MeCab

% brew install mecab
% brew install mecab-ipadic
% pip install mecab-python3

CRF++ と CaboCha

% brew install crf++
% brew install cabocha

CaboCha を使ってみる(Python 経由ではない)

上記をインストールするとターミナルで直接であれば CaboCha が使える様になります。

コマンド「cabocha」を実行してそのまま「今日は良い天気ですね。」と入力すると下記の様に出力されます。

% cabocha
今日は良い天気ですね。
      今日は---D
          良い-D
    天気ですね。
EOS

ただ、ここまでは Python を立ち上げずに直接 Shellscript で CaboCha を使っただけです。

CaboCha の Python バインディング

cabocha-0.69.tar.bz2 のリンクがあるのでここからダウンロードします。

Downloads フォルダに圧縮ファイルがありますね。

% cd Users/ユーザー名/Downloads
% ls
cabocha-0.69.tar.bz2

ファイルを解凍して、configure、make、make install を行います。

% tar xfv cabocha-0.69.tar.bz2
% cd cabocha-0.69
% ./configure --prefix=/usr/local/cabocha/0_69 --with-charset=UTF8 --with-posset=IPA
% make
% make install

Python の仮想環境を立ち上げた状態で「cabocha-0.69」直下の「python」フォルダに移動して「sudo python setup.py install」を実行します。

% cd python
% sudo python setup.py install

こうすると import CaboCha できる様になります。

ただ、Downloads フォルダから cabocha-0.69 ファイルを削除してもできる意味をまだいまいち理解できていません。仮想環境の site-packages に CaboCha.py は作られたんですけどそれで間に合ってるんですかね。後でログをよくみてみます。。。

追加の仮想環境を作った場合

すでに一度上記の行程を経て CaboCha を使っている場合、新たに追加の仮想環境を作る際にはいくつか行程を飛ばすことができます。

解凍した「cabocha-0.69」がある状態で「cabocha-0.69/python」ディレクトリに入り、新たに作った仮想環境を起動し、下記を実行すれば OK です。

% pip install mecab-python3
% cd cabocha-0.69/python
% sudo python setup.py install

Python で CaboCha を使う

とりあえず Python を立ち上げて「import CaboCha」もできますし下記の処理も実行できました。

>>> import CaboCha
>>> c = CaboCha.Parser()
>>> sentence = '今日は良い天気ですね。'
>>> print(c.parseToString(sentence))
      今日は---D
          良い-D
    天気ですね。
EOS

係り受け関係の出力

>>> tree =  c.parse(sentence)
>>> print(tree.toString(CaboCha.FORMAT_TREE))
      今日は---D
          良い-D
    天気ですね。
EOS

>>> print(tree.toString(CaboCha.FORMAT_LATTICE))
* 0 2D 0/1 -1.140323
今日	名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
* 1 2D 0/0 -1.140323
良い	形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ
* 2 -1D 0/2 0.000000
天気	名詞,一般,*,*,*,*,天気,テンキ,テンキ
です	助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
ね	助詞,終助詞,*,*,*,*,ね,ネ,ネ
。	記号,句点,*,*,*,*,。,。,。
EOS

形態素の出力

形態素の文字列

>>> for i in range(tree.size()):
...     print(tree.token(i).surface)
... 
今日
は
良い
天気
です
ね
。
>>> 

形態素の情報

>>> for i in range(tree.size()):
...     print(tree.token(i).feature)
... 
名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
助詞,係助詞,*,*,*,*,は,ハ,ワ
形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ
名詞,一般,*,*,*,*,天気,テンキ,テンキ
助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
助詞,終助詞,*,*,*,*,ね,ネ,ネ
記号,句点,*,*,*,*,。,。,。
>>> 

NEologd 辞書で新語対応

デフォルトでは「IPA 辞書」という辞書が使用されますが、新語に対応するには「NEologd 辞書」が多く使用されている様です。

NEologd 辞書のインストール

通常の辞書「ipadic」が格納されているディレクトリに移動します。おそらく「/usr/local/lib/mecab/dic」もしくはそれに似た場所にあると思います。

% /usr/local/lib/mecab/dic
% ls
ipadic

git clone で「mecab-ipadic-neologd」を作成します。

% git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
% ls
ipadic			mecab-ipadic-neologd

「mecab-ipadic-neologd」フォルダに移動し、コマンド「./bin/install-mecab-ipadic-neologd -n -a」を実行します。

% cd mecab-ipadic-neologd
% ./bin/install-mecab-ipadic-neologd -n -a

途中「Do you want to install mecab-ipadic-NEologd? Type yes or no.」と聞かれるので「yes」と入力します。

これでインストール完了です。

NEologd 辞書を使う

CaboCha、MeCab を使用する際、デフォルトでは IPA 辞書が使用されるので、明示的に NEologd 辞書を指定する必要があります。

実行時に「-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd」を渡すのですが、下記コードの様に「CaboCha.Parser('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')」としてあげれば OK です。

import CaboCha

sentence = '霜降り明星(しもふりみょうじょう)は、2018年『M-1グランプリ』14代目王者。'

# IPA 辞書
c = CaboCha.Parser()
print('IPA 辞書:')
print(c.parseToString(sentence))

# NEologd 辞書
c = CaboCha.Parser('-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd')
print('NEologd 辞書:')
print(c.parseToString(sentence))

上記を実行すると下記のアウトプットが返ってきます。

IPA 辞書:
      霜降り明星---D            
            (しも-D            
              ふりみ-D          
      ょうじょう)は、---------D
                  2018年---D   |
                      『M--D   |
               1グランプリ』-D |
                        14代目-D
                          王者。
EOS

NEologd 辞書:
        霜降り明星-----D      
              (しも-D |      
                  ふり-D      
      みょうじょう)は、-----D
                    2018年-D |
           『M-1グランプリ』-D
                  14代目王者。
EOS

「しもふりみょうじょう」や「M-1グランプリ」の部分が若干違いますね。

【Mac】YouTube 動画のコメント欄を Python の MeCab で形態素解析にかける

せっかく YouTube Data API でコメントを取得できるので、コメント欄にどんな単語が出てくるのか見れる様にしたい。

今回はとりあえず Mac ローカル環境で触りだけやってみます。

ちなみに API キーの取得やライブラリのインストールがまだの場合は下記記事をどうぞ。

YouTube Data API でコメント情報を取得

まずはコメント情報を取得するところから。コメント関連では CommentsThreads と Comments の二つがあります。

CommentThreads

CommentThreads では動画 ID やチャンネル ID をもとにコメントを取得することができます。

例えば動画 ID 「fdsaZ8EMR2U」のコメント 5 件のデータを取る場合はこんな感じ。

# -*- coding: utf-8 -*-

# Sample Python code for youtube.commentThreads.list
# See instructions for running these code samples locally:
# https://developers.google.com/explorer-help/guides/code_samples#python

import os

import googleapiclient.discovery

def main():
    # Disable OAuthlib's HTTPS verification when running locally.
    # *DO NOT* leave this option enabled in production.
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

    api_service_name = "youtube"
    api_version = "v3"
    DEVELOPER_KEY = "YOUR_API_KEY"

    youtube = googleapiclient.discovery.build(
        api_service_name, api_version, developerKey = DEVELOPER_KEY)

    request = youtube.commentThreads().list(
        part="id,replies,snippet",
        maxResults=5,
        videoId="fdsaZ8EMR2U"
    )
    response = request.execute()

    print(response)

if __name__ == "__main__":
    main()

下記の様な JSON が返ってきます。コメントの内容、投稿主のチャンネル名などが含まれています。そして「replies」にはコメントに対する返信も含まれます。

{
  "kind": "youtube#commentThreadListResponse",
  "etag": "tSw5WSiFS4IMMytcgoYXJ9zpu6I",
  "nextPageToken": "QURTSl9pMDFKVnZtTFl0NFZOdnhaZFpXaFBOcWU5aDA0QWM5bDVpYk5oVTd1WDQwSDY1cU11OVBOZHNnWFNOTmNJby1Db1JpWno2Qnd5bw==",
  "pageInfo": {
    "totalResults": 5,
    "resultsPerPage": 5
  },
  "items": [
    {
      "kind": "youtube#commentThread",
      "etag": "An3zz04lgE7jUVO7VXXmqfdwFjk",
      "id": "UgwDY44NUll4uiZXqqx4AaABAg",
      "snippet": {
        "videoId": "fdsaZ8EMR2U",
        "topLevelComment": {
          "kind": "youtube#comment",
          "etag": "Kty1w2F4dTbXmakl-ywdK28vLEg",
          "id": "UgwDY44NUll4uiZXqqx4AaABAg",
          "snippet": {
            "videoId": "fdsaZ8EMR2U",
            "textDisplay": "SO gooood",
            "textOriginal": "SO gooood",
            "authorDisplayName": "fatt musiek",
            "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwngpQ-20jVq0c-9aC-wDJ87aTKi2QvPLTRN2GXGRaw=s48-c-k-c0x00ffffff-no-rj",
            "authorChannelUrl": "http://www.youtube.com/channel/UCl3ha3zwY9p6CemIZZXIdXQ",
            "authorChannelId": {
              "value": "UCl3ha3zwY9p6CemIZZXIdXQ"
            },
            "canRate": true,
            "viewerRating": "none",
            "likeCount": 0,
            "publishedAt": "2021-05-22T18:48:34Z",
            "updatedAt": "2021-05-22T18:48:34Z"
          }
        },
        "canReply": true,
        "totalReplyCount": 0,
        "isPublic": true
      }
    },
    {
      "kind": "youtube#commentThread",
      "etag": "QVJH5RHTNij1fN5jRj_mNcDscHA",
      "id": "Ugx8sUuwqKqPVG9eSuJ4AaABAg",
      "snippet": {
        "videoId": "fdsaZ8EMR2U",
        "topLevelComment": {
          "kind": "youtube#comment",
          "etag": "Y_SsBrGGxQoLpcztQqND9wGarUc",
          "id": "Ugx8sUuwqKqPVG9eSuJ4AaABAg",
          "snippet": {
            "videoId": "fdsaZ8EMR2U",
            "textDisplay": "so what if really yuffie have met johnny hehe",
            "textOriginal": "so what if really yuffie have met johnny hehe",
            "authorDisplayName": "GregOrio Barachina",
            "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnjgJE6zBYksYQWt8TmKlMDYOyG0t-BHPNWWmvUUPQ=s48-c-k-c0x00ffffff-no-rj",
            "authorChannelUrl": "http://www.youtube.com/channel/UCUs2OJ4-KqYGS2EPJCDj7tQ",
            "authorChannelId": {
              "value": "UCUs2OJ4-KqYGS2EPJCDj7tQ"
            },
            "canRate": true,
            "viewerRating": "none",
            "likeCount": 0,
            "publishedAt": "2021-05-21T13:42:45Z",
            "updatedAt": "2021-05-21T13:42:45Z"
          }
        },
        "canReply": true,
        "totalReplyCount": 0,
        "isPublic": true
      }
    },
    {
      "kind": "youtube#commentThread",
      "etag": "ggLtp9jtNyrqb3JSvzkvUDon7gg",
      "id": "UgwP-4ucsrWh_iXJQMN4AaABAg",
      "snippet": {
        "videoId": "fdsaZ8EMR2U",
        "topLevelComment": {
          "kind": "youtube#comment",
          "etag": "Raxyf3_Zw3ksZyGeWDt7_HHW8SA",
          "id": "UgwP-4ucsrWh_iXJQMN4AaABAg",
          "snippet": {
            "videoId": "fdsaZ8EMR2U",
            "textDisplay": "The Aerith and Cloud scene is much more meaningful than the one with Tifa. Still a good scene but come on...Aerith just appearing amongst the flowers and getting to see her again...priceless",
            "textOriginal": "The Aerith and Cloud scene is much more meaningful than the one with Tifa. Still a good scene but come on...Aerith just appearing amongst the flowers and getting to see her again...priceless",
            "authorDisplayName": "Maxx Doran",
            "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnixfMDBxLt_TfUEjlpHhU-OvwE1vjCgpFBAVIMxjg=s48-c-k-c0x00ffffff-no-rj",
            "authorChannelUrl": "http://www.youtube.com/channel/UCXcLTX_9fNHLVAMr_plxeqQ",
            "authorChannelId": {
              "value": "UCXcLTX_9fNHLVAMr_plxeqQ"
            },
            "canRate": true,
            "viewerRating": "none",
            "likeCount": 0,
            "publishedAt": "2021-05-18T01:50:04Z",
            "updatedAt": "2021-05-18T01:50:04Z"
          }
        },
        "canReply": true,
        "totalReplyCount": 0,
        "isPublic": true
      }
    },
    {
      "kind": "youtube#commentThread",
      "etag": "ptUVfOGBkZDnUXKFoDaGnQ9Y-gw",
      "id": "UgzTN_4ek7syWNNbCrB4AaABAg",
      "snippet": {
        "videoId": "fdsaZ8EMR2U",
        "topLevelComment": {
          "kind": "youtube#comment",
          "etag": "L3vDroBKOAklgIqKSkcX_JvLn_g",
          "id": "UgzTN_4ek7syWNNbCrB4AaABAg",
          "snippet": {
            "videoId": "fdsaZ8EMR2U",
            "textDisplay": "You caught on to the magnify barrier idea so early. I was part way into hard mode before I thought of that.",
            "textOriginal": "You caught on to the magnify barrier idea so early. I was part way into hard mode before I thought of that.",
            "authorDisplayName": "Justin Edwards",
            "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwngz1mU5zD3QHSRVU3jXTEZApnkYsmAzCKFXxUyD1w=s48-c-k-c0x00ffffff-no-rj",
            "authorChannelUrl": "http://www.youtube.com/channel/UCO-oPQJCpNw87M6YbcuuFMw",
            "authorChannelId": {
              "value": "UCO-oPQJCpNw87M6YbcuuFMw"
            },
            "canRate": true,
            "viewerRating": "none",
            "likeCount": 0,
            "publishedAt": "2021-05-10T07:25:36Z",
            "updatedAt": "2021-05-10T07:25:36Z"
          }
        },
        "canReply": true,
        "totalReplyCount": 0,
        "isPublic": true
      }
    },
    {
      "kind": "youtube#commentThread",
      "etag": "vfaqu09YbjpC_akz6riq0_XpSCw",
      "id": "UgygOOysmSAraKnx81h4AaABAg",
      "snippet": {
        "videoId": "fdsaZ8EMR2U",
        "topLevelComment": {
          "kind": "youtube#comment",
          "etag": "k8vIS0anrGkqCcFfyj0gnrUwXQI",
          "id": "UgygOOysmSAraKnx81h4AaABAg",
          "snippet": {
            "videoId": "fdsaZ8EMR2U",
            "textDisplay": "Y'know Max, you COULD have just run 5k steps in Aerith's garden, checked what the Materia did, and moved on. Or, maybe, look up an online guide, since by now I'm sure SOMEONE has posted one.",
            "textOriginal": "Y'know Max, you COULD have just run 5k steps in Aerith's garden, checked what the Materia did, and moved on. Or, maybe, look up an online guide, since by now I'm sure SOMEONE has posted one.",
            "authorDisplayName": "Soma Cruz the Demigod of Balance",
            "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnilg2dkOBvJqeTbW34CBoxURHLWv78fnbCRkArv=s48-c-k-c0x00ffffff-no-rj",
            "authorChannelUrl": "http://www.youtube.com/channel/UCNaiemmWvNbzfaQm5e3hyqA",
            "authorChannelId": {
              "value": "UCNaiemmWvNbzfaQm5e3hyqA"
            },
            "canRate": true,
            "viewerRating": "none",
            "likeCount": 0,
            "publishedAt": "2021-05-07T02:21:29Z",
            "updatedAt": "2021-05-07T02:21:29Z"
          }
        },
        "canReply": true,
        "totalReplyCount": 1,
        "isPublic": true
      },
      "replies": {
        "comments": [
          {
            "kind": "youtube#comment",
            "etag": "oMSJ1drDJreTmguX72NWydzfbcY",
            "id": "UgygOOysmSAraKnx81h4AaABAg.9N10GKEX1209N2h_vQGc9Y",
            "snippet": {
              "videoId": "fdsaZ8EMR2U",
              "textDisplay": "\u003ca href=\"https://www.youtube.com/watch?v=fdsaZ8EMR2U&t=38m03s\"\u003e38:03\u003c/a\u003e These things don't stagger? But each time they clone, they lose health, and the clones are much weaker. Damn, I see why you had trouble with these, Max.",
              "textOriginal": "38:03 These things don't stagger? But each time they clone, they lose health, and the clones are much weaker. Damn, I see why you had trouble with these, Max.",
              "parentId": "UgygOOysmSAraKnx81h4AaABAg",
              "authorDisplayName": "Soma Cruz the Demigod of Balance",
              "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnilg2dkOBvJqeTbW34CBoxURHLWv78fnbCRkArv=s48-c-k-c0x00ffffff-no-rj",
              "authorChannelUrl": "http://www.youtube.com/channel/UCNaiemmWvNbzfaQm5e3hyqA",
              "authorChannelId": {
                "value": "UCNaiemmWvNbzfaQm5e3hyqA"
              },
              "canRate": true,
              "viewerRating": "none",
              "likeCount": 0,
              "publishedAt": "2021-05-07T18:08:01Z",
              "updatedAt": "2021-05-07T18:08:01Z"
            }
          }
        ]
      }
    }
  ]
}

Comments

Comments ではコメント ID を直接指定してデータを取得します。上の例で取得した 5 つのコメント ID を「id」に指定してデータを取得してみます。

# -*- coding: utf-8 -*-

# Sample Python code for youtube.comments.list
# See instructions for running these code samples locally:
# https://developers.google.com/explorer-help/guides/code_samples#python

import os

import googleapiclient.discovery

def main():
    # Disable OAuthlib's HTTPS verification when running locally.
    # *DO NOT* leave this option enabled in production.
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

    api_service_name = "youtube"
    api_version = "v3"
    DEVELOPER_KEY = "YOUR_API_KEY"

    youtube = googleapiclient.discovery.build(
        api_service_name, api_version, developerKey = DEVELOPER_KEY)

    request = youtube.comments().list(
        part="id,snippet",
        id="UgwDY44NUll4uiZXqqx4AaABAg,Ugx8sUuwqKqPVG9eSuJ4AaABAg,UgwP-4ucsrWh_iXJQMN4AaABAg,UgzTN_4ek7syWNNbCrB4AaABAg,UgygOOysmSAraKnx81h4AaABAg"
    )
    response = request.execute()

    print(response)

if __name__ == "__main__":
    main()

下記の様な JSON が返ってきます。CommentThreads の方ではコメントが投稿された動画の ID や、コメントに対する返信も含まれていましたが、Comments の方には含まれません。

{
  "kind": "youtube#commentListResponse",
  "etag": "vwaB3KAa_Snb_GuTkkMYrlL7Jrg",
  "items": [
    {
      "kind": "youtube#comment",
      "etag": "E9ovRZPTGOUQzHb0AEiKA26EJxY",
      "id": "UgwDY44NUll4uiZXqqx4AaABAg",
      "snippet": {
        "textDisplay": "SO gooood",
        "textOriginal": "SO gooood",
        "authorDisplayName": "fatt musiek",
        "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwngpQ-20jVq0c-9aC-wDJ87aTKi2QvPLTRN2GXGRaw=s48-c-k-c0x00ffffff-no-rj",
        "authorChannelUrl": "http://www.youtube.com/channel/UCl3ha3zwY9p6CemIZZXIdXQ",
        "authorChannelId": {
          "value": "UCl3ha3zwY9p6CemIZZXIdXQ"
        },
        "canRate": true,
        "viewerRating": "none",
        "likeCount": 0,
        "publishedAt": "2021-05-22T18:48:34Z",
        "updatedAt": "2021-05-22T18:48:34Z"
      }
    },
    {
      "kind": "youtube#comment",
      "etag": "FG5oWvmMF39kDNl_rlnzb0bsSWM",
      "id": "Ugx8sUuwqKqPVG9eSuJ4AaABAg",
      "snippet": {
        "textDisplay": "so what if really yuffie have met johnny hehe",
        "textOriginal": "so what if really yuffie have met johnny hehe",
        "authorDisplayName": "GregOrio Barachina",
        "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnjgJE6zBYksYQWt8TmKlMDYOyG0t-BHPNWWmvUUPQ=s48-c-k-c0x00ffffff-no-rj",
        "authorChannelUrl": "http://www.youtube.com/channel/UCUs2OJ4-KqYGS2EPJCDj7tQ",
        "authorChannelId": {
          "value": "UCUs2OJ4-KqYGS2EPJCDj7tQ"
        },
        "canRate": true,
        "viewerRating": "none",
        "likeCount": 0,
        "publishedAt": "2021-05-21T13:42:45Z",
        "updatedAt": "2021-05-21T13:42:45Z"
      }
    },
    {
      "kind": "youtube#comment",
      "etag": "WQw2UyILXAhkYOAl-AKScZi1pCY",
      "id": "UgwP-4ucsrWh_iXJQMN4AaABAg",
      "snippet": {
        "textDisplay": "The Aerith and Cloud scene is much more meaningful than the one with Tifa. Still a good scene but come on...Aerith just appearing amongst the flowers and getting to see her again...priceless",
        "textOriginal": "The Aerith and Cloud scene is much more meaningful than the one with Tifa. Still a good scene but come on...Aerith just appearing amongst the flowers and getting to see her again...priceless",
        "authorDisplayName": "Maxx Doran",
        "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnixfMDBxLt_TfUEjlpHhU-OvwE1vjCgpFBAVIMxjg=s48-c-k-c0x00ffffff-no-rj",
        "authorChannelUrl": "http://www.youtube.com/channel/UCXcLTX_9fNHLVAMr_plxeqQ",
        "authorChannelId": {
          "value": "UCXcLTX_9fNHLVAMr_plxeqQ"
        },
        "canRate": true,
        "viewerRating": "none",
        "likeCount": 0,
        "publishedAt": "2021-05-18T01:50:04Z",
        "updatedAt": "2021-05-18T01:50:04Z"
      }
    },
    {
      "kind": "youtube#comment",
      "etag": "Vchl7kutnRgZb-uKYjMNMrJQ2qQ",
      "id": "UgzTN_4ek7syWNNbCrB4AaABAg",
      "snippet": {
        "textDisplay": "You caught on to the magnify barrier idea so early. I was part way into hard mode before I thought of that.",
        "textOriginal": "You caught on to the magnify barrier idea so early. I was part way into hard mode before I thought of that.",
        "authorDisplayName": "Justin Edwards",
        "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwngz1mU5zD3QHSRVU3jXTEZApnkYsmAzCKFXxUyD1w=s48-c-k-c0x00ffffff-no-rj",
        "authorChannelUrl": "http://www.youtube.com/channel/UCO-oPQJCpNw87M6YbcuuFMw",
        "authorChannelId": {
          "value": "UCO-oPQJCpNw87M6YbcuuFMw"
        },
        "canRate": true,
        "viewerRating": "none",
        "likeCount": 0,
        "publishedAt": "2021-05-10T07:25:36Z",
        "updatedAt": "2021-05-10T07:25:36Z"
      }
    },
    {
      "kind": "youtube#comment",
      "etag": "pbfhdpIgB5QCKnr4Inkm_U2wbjQ",
      "id": "UgygOOysmSAraKnx81h4AaABAg",
      "snippet": {
        "textDisplay": "Y'know Max, you COULD have just run 5k steps in Aerith's garden, checked what the Materia did, and moved on. Or, maybe, look up an online guide, since by now I'm sure SOMEONE has posted one.",
        "textOriginal": "Y'know Max, you COULD have just run 5k steps in Aerith's garden, checked what the Materia did, and moved on. Or, maybe, look up an online guide, since by now I'm sure SOMEONE has posted one.",
        "authorDisplayName": "Soma Cruz the Demigod of Balance",
        "authorProfileImageUrl": "https://yt3.ggpht.com/ytc/AAUvwnilg2dkOBvJqeTbW34CBoxURHLWv78fnbCRkArv=s48-c-k-c0x00ffffff-no-rj",
        "authorChannelUrl": "http://www.youtube.com/channel/UCNaiemmWvNbzfaQm5e3hyqA",
        "authorChannelId": {
          "value": "UCNaiemmWvNbzfaQm5e3hyqA"
        },
        "canRate": true,
        "viewerRating": "none",
        "likeCount": 0,
        "publishedAt": "2021-05-07T02:21:29Z",
        "updatedAt": "2021-05-07T02:21:29Z"
      }
    }
  ]
}

YouTube 動画のコメントを MeCab で処理する

基本編:単一コメント取得〜形態素解析

まず仮想環境で下記を実行し、MeCab を使える様準備します。

$ brew install mecab
$ brew install mecab-ipadic
$ pip install mecab-python3

上記が済んだら下記の様に

import os
import googleapiclient.discovery
import MeCab

def main():
    # Disable OAuthlib's HTTPS verification when running locally.
    # *DO NOT* leave this option enabled in production.
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

    api_service_name = "youtube"
    api_version = "v3"
    DEVELOPER_KEY = "YOUR_API_KEY"

    youtube = googleapiclient.discovery.build(
        api_service_name, api_version, developerKey = DEVELOPER_KEY)

    # コメント ID を指定してコメントを取得
    request = youtube.comments().list(
        part="id,snippet",
        id="UgzKZhtc3fX6JgX6p5p4AaABAg" # コメント ID 
    )
    response = request.execute()

    # 返された JSON からコメントの文章を取得し text に保存
    text = response['items'][0]['snippet']['textOriginal']
    
    m = MeCab.Tagger()
 
    node = m.parseToNode(text)
    text_after = []
    while node:
        words.append(node.surface)
        node = node.next

    print('処理前: '+str(text)) # MeCab 処理前の text
    print('処理後: '+str(text_after)) # MeCab 処理を行なった後の text
    
if __name__ == "__main__":
    main()

狩野英孝さんの動画のコメントで、上記のコードを実行すると下記の結果を返してくれます。

処理前: ここ最近英孝ちゃんの動画みてたらいつの間にか日付け変わってるんだけど笑
処理前: ['', 'ここ', '最近', '英孝', 'ちゃん', 'の', '動画', 'み', 'て', 'たら', 'いつの間にか', '日', '付け', '変わっ', 'てる', 'ん', 'だ', 'けど', '笑', '']

応用編:複数コメント取得〜頻出単語の表出

YouTube Data API でコメントスレッドを取得し、それを MeCab の処理に欠けて出現数の多い単語を抽出してみます。

import os
import googleapiclient.discovery
import MeCab
import collections

def main():
    # Disable OAuthlib's HTTPS verification when running locally.
    # *DO NOT* leave this option enabled in production.
    os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

    api_service_name = "youtube"
    api_version = "v3"
    DEVELOPER_KEY = "YOUR_API_KEY"

    youtube = googleapiclient.discovery.build(
        api_service_name, api_version, developerKey = DEVELOPER_KEY)

    # コメントスレッドの取得
    request = youtube.commentThreads().list(
        part="id,replies,snippet",
        maxResults=100, # 最大取得コメントスレッド数
        videoId="jsRR_ZimvAo", # 動画 ID
        order="relevance" # 関連性の高い順にコメントスレッドを取得
    )
    response = request.execute()

    comment_list = []
    for item in response['items']:

        comment_list.append(item['snippet']['topLevelComment']['snippet']['textOriginal'])
        if 'replies' in item.keys():
            for reply in item['replies']['comments']:

                comment_list.append(reply['snippet']['textOriginal'])
    
    # MeCab の処理
    comment_particles = []
    for comment in comment_list:
        m = MeCab.Tagger()
    
        node = m.parseToNode(comment)
        while node:
            if len(node.surface) > 0: # ''は処理から除外
                hinshi = node.feature.split(',')[0]
                if hinshi in ['名詞','形容詞']: # 名詞か形容詞に絞る
                    comment_particles.append(node.surface)
            
            node = node.next

    c = collections.Counter(comment_particles)
    
    # 出現数順に print
    for i in c.most_common(30):
        print(i)


if __name__ == "__main__":
    main()

お笑い芸人のさまぁ〜ずとぺこぱの動画なので下記の様な感じでタプルが返ってきます。

('シュウ', 23)
('ペイ', 23)
('さん', 21)
('笑', 17)
('好き', 11)
('ちゃん', 10)
('シュウペイ', 9)
('の', 9)
('ん', 9)
('ぺこぱ', 9)
('企画', 9)
('お前', 9)
('ツッコミ', 8)
('~', 8)
('寺', 7)
('かわいい', 7)
('最高', 7)
('松陰', 6)
('ショック', 6)
('回', 6)
('純粋', 6)
('人', 6)
('俺', 6)
('いい', 5)
('www', 5)
('w', 5)
('面白い', 5)
('ww', 5)
('❤', 5)
('ずさん', 4)

不自然なところで単語が切れてしまったりするのが気になりますが、とりあえずこんな風なことができるということで!