【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 を使って係り受け解析を行う方法を紹介します。

  1. MeCab、CRF++、CaboCha のインストール
    • MeCab のインストール
    • CRF++ と CaboCha のインストール
  2. CaboCha を使ってみる(Python 経由ではない)
  3. CaboCha の Python バインディング
    • 新たに仮想環境を作った場合
  4. Python で CaboCha を使う
    • 係り受け関係の出力
    • 形態素の出力
  5. NEologd 辞書で新語対応
    • NEologd 辞書のインストール
    • NEologd 辞書を使う

1. MeCab、CRF++、CaboCha のインストール

まず MeCab、CRF++ そして CaboCha をインストールするので、Python 仮想環境を起動した状態で下記を実行します。

MeCab のインストール

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

CRF++ と CaboCha のインストール

% brew install crf++
% brew install cabocha

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

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

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

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

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

3. 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

4. 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)
... 
名詞,副詞可能,*,*,*,*,今日,キョウ,キョー
助詞,係助詞,*,*,*,*,は,ハ,ワ
形容詞,自立,*,*,形容詞・アウオ段,基本形,良い,ヨイ,ヨイ
名詞,一般,*,*,*,*,天気,テンキ,テンキ
助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
助詞,終助詞,*,*,*,*,ね,ネ,ネ
記号,句点,*,*,*,*,。,。,。
>>> 

5. 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グランプリ」の部分が若干違いますね。

若干応用編として、YouTube のコメント欄を取得して形態素解析をしてみましたので下記に貼っておきます。

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

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

せっかく YouTube Data API でコメントを抽出できるので、今回は YouTube のコメント抽出の方法の紹介とともに、コメント欄にどんな単語が出てくるのか Python の MeCab で形態素解析をしたいと思います。

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

  1. YouTube Data API でコメント情報を抽出
  2. YouTube 動画のコメントを MeCab で処理する

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

1. YouTube Data API でコメント情報を抽出

まずはコメント情報を抽出するところから。コメント関連では CommentsThreads と Comments の二つがあり、両方とも JSON 形式で取得する事ができます。

CommentThreads

まず、CommentThreads では動画 ID やチャンネル 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"
      }
    }
  ]
}

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

次に、抽出したコメントに対して形態素解析をかけたいと思います。

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

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

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

MeCab の準備が済んだら下記の様に

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)

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

ローカル環境の Django を本番環境(Ubuntu)へデプロイする方法

Mac のローカル環境で作成した Django プロジェクトをさくらのVPSで用意した Ubuntu 環境へ反映する際の流れをメモしています。

▶︎ Django のおすすめ VPS(仮装専用サーバー)はOSから決めるべし

下記が完了していることを想定しています。

  • Web サーバーの設定が完了済み(筆者は Nginx を使用)
  • 仮想環境を作るディレクトリが決めてある
  1. データベースを本番環境にコピーする
    • ローカル環境での作業
    • 本番環境での作業
  2. ローカル環境のパッケージを本番環境で再現
    • requirements.txt の作成
    • requirements.txt を本番環境へコピー
    • 本番環境で Python 仮想環境を作成
    • requirements.txt によるパッケージインストール
  3. settings.py の編集
    • ファイルを分割する必要性
    • 開発環境の runserver 実行時に settings_dev.py を指定
    • settings.py(本番環境用)の編集
  4. Django ソースコードを本番環境に配置
    • 本番環境への git のインストール
    • リモートリポジトリから本番環境へファイルをコピー
  5. gunicorn のインストール
  6. データベースのログイン情報を別ファイル記述
  7. 静的ファイルの配置

1. データベースを本番環境にコピーする

検証環境で準備したデータベース、テーブル、そしてデータを本番環境でも反映します。

ここはうまくやれば一行スクリプト書いて終了する方法もある様ですが、色々とエラーが出て逆にめんどくさそうだったのでステップバイステップでやっていきます。

ローカル環境での作業

まずローカル環境でコマンド「mysqldump データベース名 > dump.txt」を実行し、dump ファイルを作成します。

% mysqldump データベース名 > dump.txt                                                              
%

それを FTP か何かで本番環境へアップロードします。

本番環境での作業

本番環境に接続し、本番環境の MySQL でデータベースを作成します。

mysql> create database データベース名;

そしてコマンド「sudo mysql データベース名 < dump.txt」を実行します。

$ sudo mysql データベース名 < dump.txt
$

これで本番環境の MySQL にもローカル環境と同じデータベース、テーブル、そしてデータがコピーされました。

2. ローカル環境のパッケージを本番環境で再現

ローカル環境の Python 関連パッケージを本番環境でも再現するため、ローカル環境で requirements.txt ファイルを作成し、それを元に本番環境でパッケージインストールをさせます。

requirements.txt の作成

ローカル環境の Python 仮想環境へ入り、コマンド「pip freeze > requirements.txt」を実行します。

% source bin/activate
% pip freeze > requirements.txt

requirements.txt を本番環境へコピー

コマンド「scp requirements.txt 本番環境のユーザー名@本番環境のホスト名(もしくは IP アドレス):保存先のディレクトリ」を実行します。

だいぶ長いですが下記の例だとすると。。。

  • 本番環境のユーザー名: vpsadmin
  • 本番環境のホスト名: xx1-234-56789.vs.sakura.ne.jp
  • 保存先のディレクトリ: /var/www/example.com/html

下記の様になります。

scp requirements.txt vpsadmin@xx1-234-56789.vs.sakura.ne.jp:/var/www/example.com/html

無事本番環境に requirements.txt がコピーされました。

$ ls
requirements.txt

本番環境で Python 仮想環境を作成

本番環境のディレクトリへ移動し、Python の仮想環境を作成します。

$ python3.9 -m venv pythonvenv
$ ls
pythonvenv  requirements.txt

requirements.txt によるパッケージインストール

本番環境の Python 仮想環境に入った状態で、コマンド「pip install -r requirements.txt」を実行します。

(pythonvenv) $ pip install -r requirements.txt

すると requirements.txt の内容を元に Django を含めローカル環境で使っていたものと同じパッケージがインストールされます。

3. settings.py の編集

ファイルを分割する必要性

settings.py は本番環境と開発環境で内容が異なる部分が出てくるので、下記の様にファイルを分ける必要があります。(ファイル名は任意)

  • settings_common.py:開発環境と本番環境で共通の部分を記述
  • settings.py:本番環境でのみ適用する部分を記述
  • settings_dev.py:開発環境でのみ適用する部分を記述

settings.py と settings_dev.py それぞれに「from .settings_common import *」と記述し、共通部分を settings_common.py から読み込む様設定します。

開発環境の runserver 実行時に settings_dev.py を指定

runserver 実行時、デフォルトでは settings.py が設定ファイルとして読み込まれますが、今後開発環境(ローカル環境)では settings_dev.py を使う必要があります。

そのため、今後開発環境で runserver を実行する際は settings_dev.py を「runserver --settings=プロジェクトディレクトリ名.settings_dev」の形式で渡して実行します。

(pythonvenv) % python manage.py runserver --settings=some_project.settings_dev

settings.py(本番環境用)の編集

DEBUG の変更

False に変更

DEBUG = False

ALLOWED_HOSTS の変更

ドメイン名を設定。

ALLOWED_HOSTS = ['example.com']

Web サーバーで www.example.com から example.com のリダイレクトを行っていれば example.com のみを設定するだけで大丈夫です。

静的ファイルの本番環境での配置場所

STATIC_ROOT = '/var/www/examlpe.com/html/static'

4. Django ソースコードを本番環境に配置

本番環境への git のインストール(インストール済みの場合は不要)

本番環境でコマンド「sudo apt update」を実行しパッケージリストを更新、その後「sudo apt install git」で git をインストールします。

$ sudo apt update
$ sudo apt install git
Reading package lists... Done
Building dependency tree       
Reading state information... Done
git is already the newest version (1:2.25.1-1ubuntu3.1).
git set to manually installed.
0 upgraded, 0 newly installed, 0 to remove and 18 not upgraded.

リモートリポジトリから本番環境へファイルをコピー

プロジェクトフォルダをリポジトリに設定しているので、プロジェクトフォルダを配置したい場所へ移動した上で git clone を実行します。

$ git clone https://github.com/ユーザー名/リポジトリ名.git

これで本番環境にもプロジェクトフォルダがコピーされました。

ローカル環境からリモートリポジトリへファイルを反映させる方法はこちら↓

Github で開発ローカル → リモートリポジトリ → 本番ローカルに反映させる手順

5. gunicorn のインストール

本番環境でのみ必要となるものとして、アプリケーションサーバーがあります。今回は gunicorn を使います。

Python 仮想環境内でアプリケーションサーバーの gunicorn をインストールします。

(pythonvenv) $ pip install gunicorn

socket の待ち受けを開始します。これをしないとアクセス時に 502 Bad Gateway のエラーになります。

(pythonvenv) $ systemctl start example_sub.socket
(pythonvenv) $ systemctl start example_sub.service

今後作業する際は何かにつけて「pkill gunicorn」で gunicorn を終わらせた方が良いです。エラーが起きた時にいくらコードを修正しても治らないまま数時間経って、「pkill gunicorn」一発で治ったこともありました。

6. データベースのログイン情報を別ファイルに記述

MySQL のデータベースを使用したんですが、Git で管理する性質上、settings.py にデータベースのユーザー名とパスワード直接記述するのは危なそうなので、別ファイルに分けました。

Git で管理しない local_settings.py というファイル(ファイル名は自由)を作成し、そちらに機密情報をまとめます。

.gitignore ファイルに local_settings.py を含めた上で、local_settings.py を作ります。

# local_settings.py

DB_USER = 'DB ユーザー名'
DB_PASSWORD = 'DB パスワード'

その上で settings.py にそれら情報を import し、DATABASES の設定に使います。

# settings.py

from .local_settings import DB_USER as dbusr
from .local_settings import DB_PASSWORD as dbpw

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'sa',
        'USER': dbusr,
        'PASSWORD': dbpw,
        'HOST': '',
        'PORT': '',
    }
}

上記と別に、下記の様にしてサーバーの環境設定から取ってくる方法もあると思いますが、環境設定の上手い設定方法がわからなかったので今回はやっていないです。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'データベース名',
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': '',
        'PORT': '',
    }
}

7. 静的ファイルの配置

コマンド「python manage.py collectstatic」を実行し、settings.py で STATIC_ROOT に設定したパスに静的ファイルを集約します。

$ python manage.py collectstatic

これでとりあえず本番環境でも動くと思います。

▶︎ Django のおすすめ VPS(仮装専用サーバー)はOSから決めるべし

Django で URL のパラメータを取得する方法(クラスベースビュー)

  1. URL ディレクトリの文字列の利用の復習
  2. URL のパラメータの取得方法
    1. self.request.GET.get(パラメータキー) で取得
    2. 値が一つだけの場合
    3. 値が複数の場合
    4. 値を指定していない場合

1. URL ディレクトリの文字列の利用の復習

Django の ListView などでテンプレートに返すデータをフィルタリングする際、urls.py で設定した URL ディレクトリの文字列を取得してそれを元にすることがあると思います。

下記の様な感じで get_queryset 関数で self.kwargs から値を取り出す感じですね。

# urls.py
urlpatterns = [
    path('<str:some_code>/', views.SomeData.as_view(), name="some_data"),
]
# views.py
class SomeData(generic.ListView):
    ### 一部省略 ###
    def get_queryset(self):
        code = self.kwargs['some_code']
        return SomeDataModel.objects.filter(some_code_column=code)

ただ、今回はそうではなく URL のパラメータを「ドメイン名.com/?キー=値&キー=値」の形式で取得してそれをデータ取得に使いたいと思います。

2. URL のパラメータの取得方法

1. self.request.GET.get(パラメータキー) で取得

get_queryset 関数をオーバーライドする際に self.request.GET.get(パラメータのキー) で取得できます。

例えば「http://127.0.0.1:8000/devicelist/?brand_nm=apple&category=laptop,phone」の様な URL の場合。

「?brand_nm=apple&category=laptop,phone」の部分をビューの処理に使いたいとします。

# views.py 内 get_queryset 関数
def get_queryset(self):
    brand = self.request.GET.get('brand_nm')
    return SomeDataModel.objects.filter(brand_nm=brand)

2. 値が一つだけの場合

値が一つだけの場合(brand_nm=apple)は「self.request.GET.get(キー)」でそのまま値が一つ取得できます。

print(self.request.GET.get('brand_nm'))
# apple

3. 値が複数の場合

複数の値を「,」で繋げて URL に渡した場合(category=laptop,phone)は、「,」が含まれただけの一つの文字列になっています。

print(self.request.GET.get('category'))
# laptop, phone

なので一旦 .split(',') を通してリストにしてあげる必要があります。

print(self.request.GET.get('category').split(','))
# ['laptop', 'phone']
print(self.request.GET.get('category').split(',')[0])
# laptop
print(self.request.GET.get('category').split(',')[1])
# phone

4. 値を指定していない場合

URL で渡していないキーを指定すると None が返ってきます。

print(self.request.GET.get('product_type'))
# None

Django の ModelForm でプルダウンをテーブルから取得する

やりたいこと:Django の ModelForm を作るときに、プルダウンメニューの選択肢(select option)をデータベースのテーブルから参照したい。

結論から言いますと Foreign Key を使って別のテーブルのユニークキーをフォームの選択肢として利用します。

  1. プルダウンにするカラムに models.ForeignKey を適用
  2. forms.py で ModelChoiceField を設定
  3. 任意のカラムの値を表出する様変更

1. プルダウンにするカラムに models.ForeignKey を適用

まず、models.py でプルダウン表示にしたい項目に models.ForeignKey を適用します。

別テーブルでユニークになっている値(つまりプルダウンの候補になる値)が入るという設定をします。

# models.py

class IgMstProduct(models.Model):
    id = models.AutoField(primary_key=True)
    product_nm = models.CharField(max_length=60, blank=True, null=True)
    brand_cd = models.ForeignKey('IgMstBrand',on_delete=models.SET_NULL, null=True, db_column='brand_cd')
    product_url = models.TextField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'ig_mst_product'

注意点

上記の設定時、「db_column」で DB 側で実際のカラム名を指定しないと、フォーム Submit 時に「1054, "Unknown column '〜_id' in 'field list'"」というエラーが返ってくると思います。

詳しくは調べていませんが、どうやら明示的に指定しないと Django が自動的に「〜_id」というカラム名で処理をしようとするらしいです。

2. forms.py で ModelChoiceField を設定

forms.py で、下記の様に対象の項目に対して forms.ModelChoiceField(queryset=モデル名.objects.all()) を設定してみると、それっぽいプルダウンが表示されます。

# forms.py

class IgProductForm(forms.ModelForm):
    brand_cd = forms.ModelChoiceField(queryset=IgMstBrand.objects.all())

    class Meta:
        model = IgMstProduct
        fields = ('product_nm', 'product_category', 'brand_cd', 'product_url',)

が、このままだとプルダウンの表示名が下記の様に「モデル名 object (1)」とかになるので変更します。

<!-- Chrome で「検証」した例 -->

<select name="brand_cd" required id="id_brand_cd">
  <option value="" selected>---------</option>
  <option value="1">モデル名 object (1)</option>
  <option value="2">モデル名 object (2)</option>
  <option value="3">モデル名 object (3)</option>
  <option value="4">モデル名 object (4)</option>
  <option value="5">モデル名 object (5)</option>
  <option value="6">モデル名 object (6)</option>
</select>

3. 任意のカラムの値を表出する様変更

同じく forms.py で ModelChoiceField を継承する別クラスを作成し、そこで label_from_instance 関数をオーバーライドし、return obj.表示したいカラム名 とすれば表示したいカラム名が表示名が変わります。

# forms.py

class CustomModelChoiceField(forms.ModelChoiceField):
    def label_from_instance(self, obj): # label_from_instance 関数をオーバーライド
         return obj.brand_nm # 表示したいカラム名を return


class IgProductForm(forms.ModelForm):
    brand_cd = CustomModelChoiceField(queryset=IgMstBrand.objects.all()) # 上記のクラスを参照する様変更

    class Meta:
        model = IgMstProduct
        fields = ('product_nm', 'product_category', 'brand_cd', 'product_url',)
<select name="brand_cd" required id="id_brand_cd">
  <option value="" selected>---------</option>
  <option value="1">Nikon</option>
  <option value="2">Canon</option>
  <option value="3">Sony</option>
  <option value="4">Fujifilm</option>
  <option value="5">Leica</option>
  <option value="6">Olympus</option>
</select>

Django の REST framework で API を作って JSON を返す方法

Django で REST API を作る場合、大まかな流れとしては Python 仮想環境内に作った Django プロジェクトに API 用の独自のアプリケーションを作り、その中にシリアライザ(データを JSON 形式に変換する機能)と View を作ります。

ここで紹介するのは単純にテーブル内のデータを全て GET で抽出してそのまま JSON 形式で返すだけのものです。POST や DELETE などは触れていません。

  1. 下準備
    • API 用 Django アプリケーションの作成
    • djangorestframework のインストール
    • urls.py の設定
    • models.py にモデルを記述
  2. シリアライザの作成
  3. ビューの作成
    • class based view の場合
    • function based view の場合
  4. 動作確認

下準備

API 用 Django アプリケーションの作成

API 用にアプリケーション「api」を作成します。

% python manage.py startapp api

settings.py の INSTALLED_APPS に追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'api.apps.ApiConfig', # 追記
]

djangorestframework のインストール

コマンド「pip install djangorestframework」を実行します。

% pip install djangorestframework
Collecting djangorestframework
  Downloading djangorestframework-3.12.4-py3-none-any.whl (957 kB)
     |████████████████████████████████| 957 kB 4.1 MB/s 
Requirement already satisfied: django>=2.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from djangorestframework) (3.2)
Requirement already satisfied: pytz in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (2021.1)
Requirement already satisfied: sqlparse>=0.2.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (0.4.1)
Requirement already satisfied: asgiref<4,>=3.3.2 in /Users/ユーザー名/PythonProjects/仮想環境名/lib/python3.8/site-packages (from django>=2.2->djangorestframework) (3.3.4)
Installing collected packages: djangorestframework
Successfully installed djangorestframework-3.12.4
%

settings.py の INSTALLED_APPS に追加します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'api.apps.ApiConfig',
    'rest_framework', # 追記
]

urls.py の設定

アプリケーション「api」ディレクトリに url.py を作成した上で、プロジェクトレベルの url.py から参照します。

from django.contrib import admin
from django.urls import path, include # 追記

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')), # 追記
]

models.py にモデルを記述

API で使用したいモデルを models.py に記述します。

今回すでにデータベース上にテーブルを用意してあるのでコマンド「python manage.py inspectdb テーブル名」でモデル情報を抽出します。

% python manage.py inspectdb yt_analysis_07
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey and OneToOneField has `on_delete` set to the desired behavior
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models


class YtAnalysis07(models.Model):
    channel_id = models.CharField(max_length=40, blank=True, null=True)
    channel_name = models.TextField(blank=True, null=True)
    view_count = models.BigIntegerField(blank=True, null=True)
    like_count = models.IntegerField(blank=True, null=True)
    dislike_count = models.IntegerField(blank=True, null=True)
    favorite_count = models.IntegerField(blank=True, null=True)
    comment_count = models.IntegerField(blank=True, null=True)
    video_count = models.IntegerField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'yt_analysis_07'
% 

クラス文を models.py にコピーしますが、その際に 1 つ「primary_key=True」のフィールドを持つ必要があるそうです。

シリアライザの作成

Python のリスト・辞書形式のデータを JSON に変換するためのシリアライザを作成します。

アプリケーションフォルダ(今回は api/ )に「serializers.py」というファイルを作成します。

from rest_framework import serializers
from .models import YtAnalysis07 # モデルをインポート

class APISerializer(serializers.ModelSerializer):
    class Meta:
        model = YtAnalysis07 # 使用するモデル
        fields = '__all__' # 処理対象にするフィールド(今回は全項目)

ビューの作成

API のビューを作成します。ざっくり言うとモデルからデータを抽出し、シリアライザに渡して JSON に変換し、その JSON データをレスポンスとして返します。

class based view の場合

from rest_framework import viewsets
from .serializers import APISerializer
from .models import YtAnalysis07

class DataListView(viewsets.ModelViewSet):
    queryset = YtAnalysis07.objects.all() # モデルからデータを抽出するクエリセット
    serializer_class = APISerializer # 使用するシリアライザ

urls.py の設定は下記の様にします。

from django.urls import path
from . import views

app_name = 'api'
urlpatterns = [
    path('data-list/', views.DataListView.as_view({'get': 'list'}), name="data-list"),
]

function based view の場合

from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import APISerializer
from .models import YtAnalysis07

@api_view(['GET']) # GET のみに対応
def dataList(request):
    api_data = YtAnalysis07.objects.all() # モデルからデータを抽出する
    serializer = APISerializer(api_data, many=True) # シリアライザにデータを渡す
    return Response(serializer.data) シリアル可されたデータを return で返す

urls.py の設定は下記の様にします。

from django.urls import path
from . import views

app_name = 'api'
urlpatterns = [
    path('data-list/', views.dataList, name="data-list"),
]

動作確認

設定した URL をブラウザに打ち込むと下記の様な画面が表示されます。JSON データも見えます。

ブラウザで URL を叩くと上記の様な画面で表示されますが、例えばターミナルで URL を requests.get すると下記の様に JSON 形式のデータだけが返ってきます。

>>> import requests
>>> r = requests.get('http://127.0.0.1:8000/api/data-list/')
>>> r.text
'[{"channel_id":"UCLkAepWjdylmXSltofFvsYQ","channel_name":"BANGTANTV","view_count":22423409,"like_count":7325674,"dislike_count":20936,"favorite_count":0,"comment_count":381315,"video_count":9},{"channel_id":"UCE_M8A5yxnLfW0KghEeajjw","channel_name":"Apple","view_count":19044060,"like_count":600159,"dislike_count":21023,"favorite_count":0,"comment_count":0,"video_count":10},

これを Django のテンプレートから JavaScript で呼び出すなどして活用します。

Mac brew install mysqlclient でエラー pip install mysql で成功

環境

OS: macOS Big Sur バージョン 11.2.2
Python: Python 3.8.3

brew install mysqlclient でエラー

Python 仮想環境で「brew install mysqlclient」を実行したら下記のエラーが発生しました。結論から言うと「pip install mysqlclient」の間違いだったことに後で気づきました。

が、それに気づく前に「pip install mysql」を実行して無事解決したので書いておきます。

エラーログ

==> Searching for similarly named formulae...
These similarly named formulae were found:
mysql-client                                                                                  mysql-client@5.7
To install one of them, run (for example):
  brew install mysql-client
Error: No available formula or cask with the name "mysqlclient".
==> Searching for a previously deleted formula (in the last month)...
Error: No previously deleted formula found.
==> Searching taps on GitHub...
Error: No formulae found in taps.

とりあえず一番初めのエラー「Error: No available formula or cask with the name "mysqlclient".」が気になる。

調べてみると、「pip install mysql」を実行してその後に「pip install mysqlclient」を実行するという書き込みを発見。

ただ実際は「pip install mysql」だけで成功しました。

pip install mysql で成功

仮想環境内でコマンド「pip install mysql」を実行。

(venv) % pip install mysql
Collecting mysql
  Downloading https://files.pythonhosted.org/packages/bf/5f/b574ac9f70811df0540e403309f349a8b9fa1a25d3653824c32e52cc1f28/mysql-0.0.2.tar.gz
Collecting mysqlclient (from mysql)
  Downloading https://files.pythonhosted.org/packages/3c/df/59cd2fa5e48d0804d213bdcb1acb4d08c403b61c7ff7ed4dd4a6a2deb3f7/mysqlclient-2.0.3.tar.gz (88kB)
     |████████████████████████████████| 92kB 5.7MB/s 
Installing collected packages: mysqlclient, mysql
  Running setup.py install for mysqlclient ... done
  Running setup.py install for mysql ... done
Successfully installed mysql-0.0.2 mysqlclient-2.0.3
WARNING: You are using pip version 19.2.3, however version 21.0.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
(venv) %

「pip install mysql」で mysqlclient もカバーされている?

ログをみるとちらほら「mysqlclient」について書かれているのが見えると思います。

そして「Successfully installed mysql-0.0.2 mysqlclient-2.0.3」の一文も。これってストレートに「mysql と mysqlclient が無事インストールされました。」ってことですよね?

% pip install mysqlclient

なので python を立ち上げて「import MySQLdb」を実行してみたら特にエラーも出ず。

「pip install mysqlclient」を実行しても何も変わらず

ここまで来てやっと「brew install 〜」ではなく「pip install 〜」を実行すべきだったと気づいて「pip install mysqlclient」を実行してみましたが、すでに完了しているとのことで何も変わりませんでした。

(venv) % pip install mysqlclient
Requirement already satisfied: mysqlclient in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (2.0.3)
(venv) %

Python の YouTube Data API v3 でチャンネル・動画情報を取得する方法

YouTube Data API v3 では、YouTube 上の様々な情報を取得する事ができます。こちらの記事では API キーの作成からチャンネル、動画情報の取得までを解説します。

  1. API キーの作成
  2. ライブラリのインストール
  3. API リクエストを試してみる
  4. 取得できる情報

1. API キーの作成

「認証情報」のタブを開きます。

「+認証情報を作成」をクリックし「API キー」を選択。

黒く塗りつぶしてますが、ここに API キーが表示されます。

2. ライブラリのインストール

仮想環境内で Google APIs Client Library for Python をインストールします。

コマンド「pip install --upgrade google-api-python-client」を実行します。

(sample_venv) % pip install --upgrade google-api-python-client
Collecting google-api-python-client
  Downloading https://files.pythonhosted.org/packages/b7/56/bc7314cd180d1420e4ef11202dc9548ec22237a0a6de1aaf37b460ee7753/google_api_python_client-2.0.2-py2.py3-none-any.whl (6.5MB)
     |████████████████████████████████| 6.5MB 9.0MB/s 
Collecting google-auth<2dev,>=1.16.0 (from google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/b5/ef/13d3b65c41ddde1a57e89de6adfc610093d109ebd472545a33e76dcdd8bf/google_auth-1.28.0-py2.py3-none-any.whl (136kB)
     |████████████████████████████████| 143kB 10.8MB/s 
Collecting google-api-core<2dev,>=1.21.0 (from google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/66/21/891884b4fe2eda194e5cdc9ea617b63ab41e0aca595c865492380e2c10b3/google_api_core-1.26.2-py2.py3-none-any.whl (93kB)
     |████████████████████████████████| 102kB 11.4MB/s 
Collecting httplib2<1dev,>=0.15.0 (from google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/15/7e/51e5bd333c0afa1c7bdbf98eb3b0ccf5167e2b1ecc8b4d13e9cc29291f81/httplib2-0.19.0-py3-none-any.whl (95kB)
     |████████████████████████████████| 102kB 11.4MB/s 
Collecting uritemplate<4dev,>=3.0.0 (from google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/bf/0c/60d82c077998feb631608dca3cc1fe19ac074e772bf0c24cf409b977b815/uritemplate-3.0.1-py2.py3-none-any.whl
Collecting google-auth-httplib2>=0.0.3 (from google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/ba/db/721e2f3f32339080153995d16e46edc3a7657251f167ddcb9327e632783b/google_auth_httplib2-0.1.0-py2.py3-none-any.whl
Requirement already satisfied, skipping upgrade: six<2dev,>=1.13.0 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from google-api-python-client) (1.15.0)
Collecting cachetools<5.0,>=2.0.0 (from google-auth<2dev,>=1.16.0->google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/bb/72/8df2e0dc991f1a1d2c6869404e7622e8ee50d80bff357dbb57c3df70305b/cachetools-4.2.1-py3-none-any.whl
Collecting pyasn1-modules>=0.2.1 (from google-auth<2dev,>=1.16.0->google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/95/de/214830a981892a3e286c3794f41ae67a4495df1108c3da8a9f62159b9a9d/pyasn1_modules-0.2.8-py2.py3-none-any.whl
Collecting rsa<5,>=3.1.4; python_version >= "3.6" (from google-auth<2dev,>=1.16.0->google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/e9/93/0c0f002031f18b53af7a6166103c02b9c0667be528944137cc954ec921b3/rsa-4.7.2-py3-none-any.whl
Requirement already satisfied, skipping upgrade: setuptools>=40.3.0 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from google-auth<2dev,>=1.16.0->google-api-python-client) (41.2.0)
Collecting packaging>=14.3 (from google-api-core<2dev,>=1.21.0->google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/3e/89/7ea760b4daa42653ece2380531c90f64788d979110a2ab51049d92f408af/packaging-20.9-py2.py3-none-any.whl (40kB)
     |████████████████████████████████| 40kB 8.7MB/s 
Requirement already satisfied, skipping upgrade: requests<3.0.0dev,>=2.18.0 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from google-api-core<2dev,>=1.21.0->google-api-python-client) (2.25.1)
Collecting googleapis-common-protos<2.0dev,>=1.6.0 (from google-api-core<2dev,>=1.21.0->google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/55/08/796a6bc0b550e2b7116041c953d3d5100016abea106131d71e5651826e7b/googleapis_common_protos-1.53.0-py2.py3-none-any.whl (198kB)
     |████████████████████████████████| 204kB 11.0MB/s 
Collecting protobuf>=3.12.0 (from google-api-core<2dev,>=1.21.0->google-api-python-client)
  Downloading https://files.pythonhosted.org/packages/62/be/f610958bf64f2a64b6b0b3403d17564af53bbfad5dad69f69b164cca038d/protobuf-3.15.6-cp38-cp38-macosx_10_9_x86_64.whl (1.0MB)
     |████████████████████████████████| 1.0MB 10.8MB/s 
Requirement already satisfied, skipping upgrade: pytz in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from google-api-core<2dev,>=1.21.0->google-api-python-client) (2021.1)
Requirement already satisfied, skipping upgrade: pyparsing<3,>=2.4.2 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from httplib2<1dev,>=0.15.0->google-api-python-client) (2.4.7)
Collecting pyasn1<0.5.0,>=0.4.6 (from pyasn1-modules>=0.2.1->google-auth<2dev,>=1.16.0->google-api-python-client)
  Using cached https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl
Requirement already satisfied, skipping upgrade: certifi>=2017.4.17 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from requests<3.0.0dev,>=2.18.0->google-api-core<2dev,>=1.21.0->google-api-python-client) (2020.12.5)
Requirement already satisfied, skipping upgrade: chardet<5,>=3.0.2 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from requests<3.0.0dev,>=2.18.0->google-api-core<2dev,>=1.21.0->google-api-python-client) (4.0.0)
Requirement already satisfied, skipping upgrade: urllib3<1.27,>=1.21.1 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from requests<3.0.0dev,>=2.18.0->google-api-core<2dev,>=1.21.0->google-api-python-client) (1.26.4)
Requirement already satisfied, skipping upgrade: idna<3,>=2.5 in /Users/ユーザー名/PythonProjects/sample_venv/lib/python3.8/site-packages (from requests<3.0.0dev,>=2.18.0->google-api-core<2dev,>=1.21.0->google-api-python-client) (2.10)
Installing collected packages: cachetools, pyasn1, pyasn1-modules, rsa, google-auth, packaging, protobuf, googleapis-common-protos, google-api-core, httplib2, uritemplate, google-auth-httplib2, google-api-python-client
Successfully installed cachetools-4.2.1 google-api-core-1.26.2 google-api-python-client-2.0.2 google-auth-1.28.0 google-auth-httplib2-0.1.0 googleapis-common-protos-1.53.0 httplib2-0.19.0 packaging-20.9 protobuf-3.15.6 pyasn1-0.4.8 pyasn1-modules-0.2.8 rsa-4.7.2 uritemplate-3.0.1
(sample_venv) %

3. API リクエストを試してみる

まずはブラウザの URL バーから

まずは Python 関係なく、単純にブラウザの URL バーに下記の URL を入れてみます。(APIキーはご自分のを入れてください)

https://youtube.googleapis.com/youtube/v3/channels?part=snippet%2CcontentDetails%2Cstatistics&id=UC_x5XG1OV2P6uZZ5FSM9Ttw&key=APIキー

すると下記の JSON テキストが表示されました。ID「UC_x5XG1OV2P6uZZ5FSM9Ttw」の Google Developers チャンネルの情報が取れました。

Python での実行

同じことを Python でやろうとすると下記の様なコードになるそうです。(Google のサイト参照

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 = "API キー"

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

    request = youtube.channels().list( # channels().list
        part="snippet,contentDetails,statistics", # 取得したい情報の指定
        id="UC_x5XG1OV2P6uZZ5FSM9Ttw" # チャンネル ID を指定(複数も可)
    )
    response = request.execute()

    print(response)

if __name__ == "__main__":
    main()

「part」に渡しているのが取得するデータの種類です。この記事の最後に取得できる part のリストを載せておきます。

動画情報の場合は下記の通り。個人的に好きで見てた FF 7 リメイクのプレイ動画です。 動画ID「fdsaZ8EMR2U」で指定しています。

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 = "API キー"

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

    request = youtube.videos().list( # videos().list
        part="contentDetails", # 取得したい情報の指定
        id="fdsaZ8EMR2U" # 動画 ID を指定(複数も可)
    )
    response = request.execute()

    print(response)

if __name__ == "__main__":
    main()

動画情報の part の内下記は投稿主のみアクセスできるそうです。

  • fileDetails
  • processingDetails
  • suggestions

そして急上昇ランキングを取る場合はこう。動画 ID の指定は消して「chart="mostPopular"」、「regionCode="JP"」、「maxResults=50」 を追記します。

JP(日本)の mostPopular(急上昇ランキング)から 50 件のデータを取るという意味です。

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 = "API キー"

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

    request = youtube.videos().list(
        part="snippet,contentDetails,statistics", # 取得したい情報の指定
        chart="mostPopular", # chart に「mostPopular」を指定
        maxResults=50, # 最大件数
        regionCode="JP" # 地域コード
    )
    response = request.execute()

    print(response)

if __name__ == "__main__":
    main()

実行すると下記の様な内容の dict データが取れるはずです。(実際は整形されていない状態)

{
  "kind": "youtube#videoListResponse",
  "etag": "jUM1iWDvm5ncH7VdFCrks9TZYQY",
  "items": [
    {
      "kind": "youtube#video",
      "etag": "jtNs4gdyS4wCzxvZKpEA0GbXJ0M",
      "id": "GOBeIaj7Ml0",
      "snippet": {
        "publishedAt": "2021-03-27T11:00:43Z",
        "channelId": "UCpOjLndjOqMoffA-fr8cbKA",
        "title": "【コムドット】生意気な後輩YouTuberと学校貸し切って本気でドロケイした結果【アバ溜りドット】",
        "description": "まだまだ道は開けない!!\n\nアバンティーズ\nhttps://www.youtube.com/user/avntisdouga\n\nコムドット \nhttps://www.youtube.com/channel/UCRxPrFmRHsXGWfAyE6oqrPQ\n\nフォーエイト とのドロケイ動画はこちらです!\nhttps://youtu.be/cbrQUkl6iD8\n\n動画がいいなと思っていただけたら高評価押してください!再生数よりもこちらを参考にして動画を作りたい!!そして少しでも面白いなと思っていただけたらぜひ【チャンネル登録】お願いします!\n\n水溜りボンドLINE公式アカウント!\nhttps://lin.ee/cVkWzGW\n最新情報不定期配信中!僕らからのメッセージも届きますよ!\nぜひ友だち登録してください!\n\nしゃべる!水溜りボンドLINEスタンプ\nhttps://00m.in/AcJcI\n水溜りボンドLINEスタンプ\nhttps://00m.in/2mzGU\n水溜りボンドのグッズはこちら!\nhttps://muuu.com/users/c7bf52c2b9fb5c5a\nおすすめでございます!!\n\n6年間1度も休まず毎日投稿しておりました!これからも少しでもみなさんが楽しんでいただければ幸いです。\n\n\n\n☆水溜りボンドを初めて知った方に是非見ていただきたい動画はこちら!☆\nhttps://www.youtube.com/watch?v=BvRvt6Nxny8\n\n●水溜りボンドの日常!僕らのサブチャンネルです。毎日比較的ゆったりやってます〜\nhttps://www.youtube.com/channel/UCaTFX7ckdowpnwHWcJGH2KQ\n\n\n●水溜りボンドのオールナイトニッポン0の詳細はこちらから\nぜひフォローお願いいたします!!\nhttps://twitter.com/mizuann0\n●限定動画や専用の絵文字など。チャンネルメンバーシップはこちらから!\nhttps://www.youtube.com/channel/UCpOjLndjOqMoffA-fr8cbKA/join\n\n◆Twitter\nカンタのアカウント\nhttps://twitter.com/kantamizutamari\nトミーのアカウント\nhttps://twitter.com/miztamari_nikki\n\n■インスタグラム\nカンタのアカウント\nhttps://bit.ly/2UckzeP\nトミーのアカウント\nhttps://www.instagram.com/tommymizutamaribond/\nーーーーーー\nよく使用している友人のBGM作曲者さん↓\nhttps://twitter.com/gt_k_2014?lang=ja\n素材提供 Music-Note.jp(ミュージックノート)\n他の楽曲提供:Production Music by http://www.epidemicsound.com\n素材提供 PIXTA\n\n#水溜りボンド\n#ドロケイ\n#コムドット",
        "thumbnails": {
          "default": {
            "url": "https://i.ytimg.com/vi/GOBeIaj7Ml0/default.jpg",
            "width": 120,
            "height": 90
          },
          "medium": {
            "url": "https://i.ytimg.com/vi/GOBeIaj7Ml0/mqdefault.jpg",
            "width": 320,
            "height": 180
          },
          "high": {
            "url": "https://i.ytimg.com/vi/GOBeIaj7Ml0/hqdefault.jpg",
            "width": 480,
            "height": 360
          },
          "standard": {
            "url": "https://i.ytimg.com/vi/GOBeIaj7Ml0/sddefault.jpg",
            "width": 640,
            "height": 480
          },
          "maxres": {
            "url": "https://i.ytimg.com/vi/GOBeIaj7Ml0/maxresdefault.jpg",
            "width": 1280,
            "height": 720
          }
        },
        "channelTitle": "水溜りボンド",
        "tags": [
          "水溜りボンド",
          "都市伝説",
          "検証",
          "実験",
          "科学",
          "お笑い",
          "面白",
          "面白い",
          "企画",
          "楽しい",
          "ワクワク",
          "カンタ",
          "トミー"
        ],
        "categoryId": "24",
        "liveBroadcastContent": "none",
        "defaultLanguage": "ja",
        "localized": {
          "title": "【コムドット】生意気な後輩YouTuberと学校貸し切って本気でドロケイした結果【アバ溜りドット】",
          "description": "まだまだ道は開けない!!\n\nアバンティーズ\nhttps://www.youtube.com/user/avntisdouga\n\nコムドット \nhttps://www.youtube.com/channel/UCRxPrFmRHsXGWfAyE6oqrPQ\n\nフォーエイト とのドロケイ動画はこちらです!\nhttps://youtu.be/cbrQUkl6iD8\n\n動画がいいなと思っていただけたら高評価押してください!再生数よりもこちらを参考にして動画を作りたい!!そして少しでも面白いなと思っていただけたらぜひ【チャンネル登録】お願いします!\n\n水溜りボンドLINE公式アカウント!\nhttps://lin.ee/cVkWzGW\n最新情報不定期配信中!僕らからのメッセージも届きますよ!\nぜひ友だち登録してください!\n\nしゃべる!水溜りボンドLINEスタンプ\nhttps://00m.in/AcJcI\n水溜りボンドLINEスタンプ\nhttps://00m.in/2mzGU\n水溜りボンドのグッズはこちら!\nhttps://muuu.com/users/c7bf52c2b9fb5c5a\nおすすめでございます!!\n\n6年間1度も休まず毎日投稿しておりました!これからも少しでもみなさんが楽しんでいただければ幸いです。\n\n\n\n☆水溜りボンドを初めて知った方に是非見ていただきたい動画はこちら!☆\nhttps://www.youtube.com/watch?v=BvRvt6Nxny8\n\n●水溜りボンドの日常!僕らのサブチャンネルです。毎日比較的ゆったりやってます〜\nhttps://www.youtube.com/channel/UCaTFX7ckdowpnwHWcJGH2KQ\n\n\n●水溜りボンドのオールナイトニッポン0の詳細はこちらから\nぜひフォローお願いいたします!!\nhttps://twitter.com/mizuann0\n●限定動画や専用の絵文字など。チャンネルメンバーシップはこちらから!\nhttps://www.youtube.com/channel/UCpOjLndjOqMoffA-fr8cbKA/join\n\n◆Twitter\nカンタのアカウント\nhttps://twitter.com/kantamizutamari\nトミーのアカウント\nhttps://twitter.com/miztamari_nikki\n\n■インスタグラム\nカンタのアカウント\nhttps://bit.ly/2UckzeP\nトミーのアカウント\nhttps://www.instagram.com/tommymizutamaribond/\nーーーーーー\nよく使用している友人のBGM作曲者さん↓\nhttps://twitter.com/gt_k_2014?lang=ja\n素材提供 Music-Note.jp(ミュージックノート)\n他の楽曲提供:Production Music by http://www.epidemicsound.com\n素材提供 PIXTA\n\n#水溜りボンド\n#ドロケイ\n#コムドット"
        },
        "defaultAudioLanguage": "ja"
      },
      "contentDetails": {
        "duration": "PT23M58S",
        "dimension": "2d",
        "definition": "hd",
        "caption": "false",
        "licensedContent": true,
        "contentRating": {},
        "projection": "rectangular"
      },
      "statistics": {
        "viewCount": "337770",
        "likeCount": "17557",
        "dislikeCount": "544",
        "favoriteCount": "0",
        "commentCount": "1650"
      }
    },
 

4. 取得できる情報(part)

確認できた範囲で、どの part にどんな項目が含まれているのか書き出しておきます。

取得できるチャンネル情報

チャンネルの part ではチャンネル名(title)、チャンネル登録者数(subscriberCount)、投稿動画数(videoCount)など、チャンネル単位の情報が取得できます。

part 名称配下の項目
auditDetails投稿主のみアクセス可
brandingSettingschannel, image
contentDetailsrelatedPlaylists
contentOwnerDetails
idチャンネル ID
localizations
snippettitle, description, customUrl, publishedAt, thumbnails, localized, country
statisticsviewCount, subscriberCount, hiddenSubscriberCount, videoCount
statusprivacyStatus, isLinked, longUploadsStatus, madeForKids
topicDetailstopicIds, topicCategories

取得できる動画情報

動画の part では動画タイトル(title)、動画再生回数(viewCount)、いいね数(likeCount)など、 動画単位の情報が取得できます。

part 名称配下の項目
contentDetailsduration, dimension, definition, caption, licensedContent, contentRating, projection
fileDetails投稿主のみアクセス可
id動画 ID
liveStreamingDetailsactualStartTime, scheduledStartTime, concurrentViewers, activeLiveChatId
localizations
playerembedHtml
processingDetails投稿主のみアクセス可
recordingDetails
snippetpublishedAt, channelId, title, description, thumbnails, channelTitle, tags, categoryId, liveBroadcastContent, localized, defaultAudioLanguage
statisticsviewCount, likeCount, dislikeCount, favoriteCount, commentCount
statusuploadStatus, privacyStatus, license, embeddable, publicStatsViewable, madeForKids
suggestions投稿主のみアクセス可
topicDetailstopicCategories

チャンネル情報や動画情報をリクエストする際、一度に指定できる id の数は 50 までの様です。

【Django】static ファイルの配置と設定

プロジェクト単位かアプリケーション単位かで若干ファイルの配置場所と設定方法が違いますが、まずは settings.py の INSTALLED_APPS に「'django.contrib.staticfiles'」の記述があることを確認します。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # この記述があることを確認

    'sample_django_app.apps.SampleDjangoAppConfig'
]

プロジェクト単位で使う static の場合

プロジェクトフォルダ(manage.py がある階層)に「static」フォルダを作成し、ファイルを配置。

settings.py の「STATICFILES_DIRS」にフルパスを追加することで、Django が静的ファイルを検索する際の検索対象となります。

collectstatic コマンド実行時のファイル取得元にもなります。

STATICFILES_DIRS = [
    os.path.join(BASE_DIR,'static'),
]

アプリケーション単位で使う static の場合

アプリケーションフォルダ直下に「static/アプリケーション名」のフォルダを作成し、ファイルを配置。

settings.py に「STATIC_URL = '/static/'」の記述があることを確認。

STATIC_URL = '/static/'

参考情報

項目メモ
STATICFILES_DIRScollectstatic が静的ファイルを追加検索する対象のディレクトリ(特定のアプリケーションに属さないものが対象)
STATIC_ROOTcollectstatic 実行時に静的ファイルの集約先となるディレクトリ。下記STATIC_URL の参照先。
STATIC_URL公式ドキュメントには "URL to use when referring to static files located in STATIC_ROOT."(STATIC_ROOT にある静的ファイルを参照する時に使う URL)とあるので、「STATIC_ROOT を表向きの URL としては何と表すか」という事だと理解しています。
参照:Django Documentation - Static Files

また、Django プロジェクト内の静的ファイルを探索するファイルファインダーと呼ばれる仕組みがあり、デフォルトでは下記2つが有効になっています。

  • FileSystemFinder - STATICFILES_DIRS で指定されたディレクトリを探索する
  • AppDirectoriesFinder - アプリケーションレベルの static ディレクトリを探索する

忘備録

アプリケーションレベルの static フォルダを作る際、お作法として直下にアプリケーション名のフォルダを作ってその中に静的ファイルを入れます。

例えば、「app_dir」というアプリケーションに対して「/js/app_script.js」というファイルを作る場合、「app_dir/static/app_name/js/app_script.js」となります。

本番環境で collectstatic コマンドを実行する際、下記の様に static ファイルがコピーされます。

そして、テンプレート上でアプリケーションレベルの静的ファイルを指定する時には「href="{% static 'app_name/css/style.css' %}"」という様にアプリケーション名を合わせて指定することになります。

上記例のテンプレート変数「static」は表向きの URL としては STATIC_URL が紐付き、実際は STATIC_ROOT で指定したディレクトリを参照します。