Python3でWebスクレイピングとYouTube Data APIとMongoDB（ホロライブの動画配信予定を収集 その２）

## はじめに

前回（[https://qiita.com/kerobot/items/6009cb0db643bceaf4e9](https://qiita.com/kerobot/items/6009cb0db643bceaf4e9)）は、ホロライブの配信予定や動画情報をホロジュールと Youtube から収集するプログラムを作成しました。
今回は、収集した情報を MongoDB に登録するようにしてみます。

![img01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283306/0c554f2f-c1c7-9f00-3837-4054416ab9d8.png)

前回のプログラムをもとに MongoDB の処理を追加するだけですが、Poetry と pyenv を利用するため、プロジェクトの構成やプログラムの内容も全体的に変更します。

Azure Cosmos DB (Mongo API)  も調べ始めたりしたのですが、価格モデルがいまいち理解できず、意図しない課金を避けるために断念しました。Azure 仮想マシンの Debian に MongoDB を導入すればいいか。という程度で考えています。

## 最終的にやりたいこと

ここ最近、毎日のように閲覧している バーチャル YouTuber プロダクション「ホロライブ」などの、動画配信予定を定期的にデータベースに格納し、それを Web API で参照して閲覧する Android アプリを作りたい。

1. 動画配信予定を収集するためのプログラムの作成（前回）
2. 収集した動画配信予定を格納するためのデータベースの作成と収集の自動化（今回）
3. 格納した動画配信予定を参照するための Web API の作成
4. Web API を参照して動画配信予定を閲覧する Android アプリの作成

## やったこと

今回は、やりたいこと 2 の「収集した動画配信予定を格納するためのデータベースの作成と収集の自動化」のうち、データベースの作成までを行いました。

収集の自動化は、近いうちにあらためて Debian + Python3 + MongoDB で構築します。

1. 開発環境の準備
    * Python のバージョン管理やパッケージ管理のための Poetry と pyenv の導入
    * MongoDB のインストールと設定
    * MongoDB の接続確認

2. プロジェクトの準備
    * Poetry を利用してプロジェクトの作成とパッケージのインストール
    * pyenv を 利用して Python のバージョンをセット
    * git の初期化
    * .gitignore の作成
    * Web スクレイピングのための geckodriver のダウンロード
    * YouTube 動画情報を取得するための YouTube Data API v3 の有効化
    * .env の作成

3. プログラムの作成（前回のプログラムをもとに修正）
    * .env からの設定情報の取得
    * プログラム実行時のコマンドライン引数の取得
    * ホロライブプロダクション配信予定スケジュール『ホロジュール』の情報を取得
    * YouTube から動画の情報を取得
    * CSV ファイルの出力

4. プログラムの実行
    * lounch.json の作成
    * プログラムのデバッグ実行
    * 実行結果（MongoDB に登録されたドキュメント）
    * 実行結果（出力したCSVファイル）
    * ファイル構成

## 開発環境の準備

下記構成の環境を用意しました。

* Windows 10 Pro 1909 x64
* Python 3.8.5 x64
* PowerShell 7.0 x64
* Visual Studio Code 1.50.0 x64
* Git for Windows 2.27.0 x64
* MongoDB 4.4.1 x64

### Poetry と pyenv の導入

参考：[Windows 10 で Python のインストールから Poetry と pyenv の利用](https://qiita.com/kerobot/items/3f4064d5174676080585) 

```powershell
> poetry --version
Poetry version 1.1.0

> pyenv --version
pyenv 2.64.2
```

### MongoDB のインストールと設定

参考：[Windows 10 に MongoDB をインストールして Python から利用する](https://qiita.com/kerobot/items/e566559940b2c517188d) 

```powershell
> mongo --version
MongoDB shell version v4.4.1
Build Info: {
    "version": "4.4.1",
    "gitVersion": "ad91a93a5a31e175f5cbf8c69561e788bbc55ce1",
    "modules": [
        "enterprise"
    ],
    "allocator": "tcmalloc",
    "environment": {
        "distmod": "windows",
        "distarch": "x86_64",
        "target_arch": "x86_64"
    }
}
```

### MongoDB の接続確認とデータベースの作成

MongoDB に接続できることを確認します。

```powershell
> mongo localhost:27017/admin -u admin -p
MongoDB shell version v4.4.1
```

データベースを作成してロール（今回は dbOwner ）を設定しておきます。

```powershell
MongoDB > use holoduledb
MongoDB > db.createUser( { user:"owner", pwd:"password", roles:[{ "role" : "dbOwner", "db" : "holoduledb" }] } );
```

接続できることを確認したら、MongoDB のシェルを終了します。

```powershell
MongoDB > exit
```

## プロジェクトの準備

### poetry と pyenv

poetry を利用してプロジェクトを作成します。

```powershell
> poetry new holocrawler --name app
```

プロジェクトのディレクトリに移動します。

```powershell
> cd holocrawler
```

念のため、pyenv をアップデートしておきます。

```powershell
> pyenv update
```

pyenv でインストール可能な Python のバージョンを確認します。

```powershell
> pyenv install -l
```

ローカル（プロジェクトディレクトリ内）で利用する Python をセットします。

```powershell
> pyenv local 3.8.5
```

切り替わったかを確認します。

```powershell
> python -V
Python 3.8.5
```

セットした Python に切り替わらない場合は、rehash で shim コマンドを更新します。  

```powershell
> pyenv rehash
```

これにより、$HOME/.pyenv/shims/ 配下のコマンドが更新され、セットした Python に切り替わります。

poetry を利用してパッケージを pyproject.toml に追加しインストールします。

```powershell
> poetry add pylint
> poetry add beautifulsoup4
> poetry add requests
> poetry add selenium
> poetry add lxml
> poetry add google-api-python-client
> poetry add python-dotenv
> poetry add pymongo
```

パッケージがインストールされるとともに、pyproject.toml が下記のように更新されます。  
pyproject.toml にパッケージを記述したうえで poetry install を実行しても良いです。

```text
[tool.poetry.dependencies]
python = "^3.8"
pylint = "^2.6.0"
beautifulsoup4 = "^4.9.3"
requests = "^2.24.0"
selenium = "^3.141.0"
lxml = "^4.5.2"
google-api-python-client = "^1.12.3"
python-dotenv = "^0.14.0"
pymongo = "^3.11.0"
```

poetry を利用して Python のバージョンを確認してみます。

```powershell
> poetry run python -V
Python 3.8.5
```

### git の初期化

git を初期化しておきます。

```powershell
> git init
```

### .gitignore の作成

.gitignore ファイルを作成し、除外対象を設定しておきます。

```powershell
> ni .gitignore
```

```text
__pycache__/
*.egg-info/
*.egg
.env
.venv
env/
venv/
.python-version
/geckodriver.log
/geckodriver.exe
```

### Web スクレイピングのための geckodriver のダウンロード

Selenium で Firefox を利用するための [geckodriver](https://github.com/mozilla/geckodriver/releases/tag/v0.27.0) をダウンロードします。

1. geckodriver (geckodriver-v0.27.0-win64.zip) をダウンロードします。
2. geckodriver-v0.27.0-win64.zip を解凍し、geckodriver.exe を任意の場所に配置して PATH を通しておきます。

### YouTube 動画情報を取得するための YouTube Data API v3 の有効化

Google Developer Console でプロジェクトを作成し、YouTube Data API v3 を有効化します。

* Google Developer Console にログイン
* ダッシュボードでプロジェクトを作成
* ライブラリで YouTube Data API v3 を有効化
* 認証情報で認証情報を作成して APIキー を取得

### .env の作成

YouTube Data API v3 の APIキーや、MongoDB の接続情報などを設定しておくためのファイルを作成します。

```powershell
> ni .env
```

```text
HOLODULE_URL = "<Holodule URL>"
API_KEY = "<Youtube Data API Key>"
API_SERVICE_NAME = "youtube"
API_VERSION = "v3"
MONGODB_USER = "<user>"
MONGODB_PASSWORD = "<password>"
MONGODB_HOST = "<localhost:27017>"
```

## プログラムの作成

### .env から取得した設定情報を保持するクラス

app/settings.py ファイルを作成します。

```python
import os
import urllib.request
from dotenv import load_dotenv

class Settings:
    def __init__(self, envpath):
        # .env ファイルを明示的に指定して環境変数として読み込む
        self.__dotenv_path = envpath
        load_dotenv(self.__dotenv_path)
        # 環境変数から設定値を取得
        self.__holodule_url = os.environ.get("HOLODULE_URL")
        if self.__check_url(self.__holodule_url) == False:
            raise ValueError("指定したURLにアクセスできません。")
        self.__api_key = os.environ.get("API_KEY")
        self.__api_service_name = os.environ.get("API_SERVICE_NAME")
        self.__api_version = os.environ.get("API_VERSION")
        self.__mongodb_user = os.environ.get("MONGODB_USER")
        self.__mongodb_password = os.environ.get("MONGODB_PASSWORD")
        self.__mongodb_host = os.environ.get("MONGODB_HOST")

    # ホロジュールのURL
    @property
    def holodule_url(self):
        return self.__holodule_url

    # Youtube Data API v3 の APIキー
    @property
    def api_key(self):
        return self.__api_key

    # Youtube Data API v3 の APIサービス名
    @property
    def api_service_name(self):
        return self.__api_service_name

    # Youtube Data API v3 の APIバージョン
    @property
    def api_version(self):
        return self.__api_version

    # mongodb の ユーザー
    @property
    def mongodb_user(self):
        return self.__mongodb_user

    # mongodb の パスワード
    @property
    def mongodb_password(self):
        return self.__mongodb_password

    # mongodb の ホスト:ポート
    @property
    def mongodb_host(self):
        return self.__mongodb_host

    # 指定したURLにアクセスできるかをチェック
    def __check_url(self, url):
        try:
            with urllib.request.urlopen(url):
                return True
        except urllib.request.HTTPError:
            return False
```

### ホロジュールから取得した情報を保持するクラス

app/holodule.py ファイルを作成します。

```python
"""
ホロジュールの配信情報＋Youtubeの動画情報含む
"""

class Holodule:
    codes = {
        "ときのそら"  : "HL0001",
        "ロボ子さん" : "HL0002",
        "さくらみこ" : "HL0003",
        "星街すいせい" : "HL0004",
        "夜空メル" : "HL0101",
        "アキ・ローゼンタール" : "HL0102",
        "赤井はあと" : "HL0103",
        "白上フブキ" : "HL0104",
        "夏色まつり" : "HL0105",
        "湊あくあ" : "HL0201",
        "紫咲シオン" : "HL0202",
        "百鬼あやめ" : "HL0203",
        "癒月ちょこ" : "HL0204",
        "大空スバル" : "HL0205",
        "大神ミオ" : "HL0G02",
        "猫又おかゆ" : "HL0G03",
        "戌神ころね" : "HL0G04",
        "兎田ぺこら" : "HL0301",
        "潤羽るしあ" : "HL0302",
        "不知火フレア" : "HL0303",
        "白銀ノエル" : "HL0304",
        "宝鐘マリン" : "HL0305",
        "天音かなた" : "HL0401",
        "桐生ココ" : "HL0402",
        "角巻わため" : "HL0403",
        "常闇トワ" : "HL0404",
        "姫森ルーナ" : "HL0405",
        "獅白ぼたん" : "HL0501",
        "雪花ラミィ" : "HL0502",
        "尾丸ポルカ" : "HL0503",
        "桃鈴ねね" : "HL0504",
        "魔乃アロエ" : "HL0505"
    }

    def __init__(self):
        self.__video_id = ""
        self.__datetime = None
        self.__name = ""
        self.__title = ""
        self.__url = ""
        self.__description = ""

    # キー
    @property
    def key(self):
        _code = Holodule.codes[self.name] if self.name in Holodule.codes else ""
        _dttm = self.datetime.strftime("%Y%m%d_%H%M%S") if self.datetime is not None else ""
        return _code + "_" + _dttm if ( len(_code) > 0 and len(_dttm) > 0 ) else ""

    # video_id
    @property
    def video_id(self):
        return self.__video_id

    @video_id.setter
    def video_id(self, video_id):
        self.__video_id = video_id

    # 日時
    @property
    def datetime(self):
        return self.__datetime

    @datetime.setter
    def datetime(self, datetime):
        self.__datetime = datetime

    # 名前
    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, name):
        self.__name = name

    # タイトル（Youtubeから取得）
    @property
    def title(self):
        return self.__title

    @title.setter
    def title(self, title):
        self.__title = title

    # URL
    @property
    def url(self):
        return self.__url

    @url.setter
    def url(self, url):
        self.__url = url

    # 説明（Youtubeから取得）
    @property
    def description(self):
        return self.__description

    @description.setter
    def description(self, description):
        self.__description = description

    # ドキュメントへ変換
    def to_doc(self):
        doc = { 'key': str(self.key),
                'video_id': str(self.video_id),
                'datetime' : str(self.datetime.strftime("%Y%m%d %H%M%S")),
                'name' : str(self.name),
                'title' : str(self.title),
                'url' : str(self.url),
                'description' : str(self.description) }
        return doc
```

### ホロジュールと youtube の動画情報を取得して MongoDB へ登録するクラス

app/holocrawler.py ファイルを作成します。

```python
"""
【ホロライブ】ホロジュールと Youtube の動画情報を取得して MongoDB へ登録する

1. 事前に geckodriver をダウンロードして配置し PATH を設定しておく
   geckodriver https://github.com/mozilla/geckodriver/releases
2. Google の YouTube Data API v3 を有効化して API キーを取得しておく
   Google Developer Console https://console.developers.google.com/?hl=JA
3. .envファイルを作成し、URLやAPIキーを設定しておく
   参考 : .env.sample
4. pyproject.toml を利用して Python のパッケージを一括インストールしておく
   参考 : pyproject.toml
"""

import sys
import os
import csv
import re
import datetime
import time
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from apiclient.discovery import build  #pylint: disable=import-error
from apiclient.errors import HttpError #pylint: disable=import-error
from pymongo import MongoClient
from urllib.parse import quote_plus
from bson.objectid import ObjectId
from app.settings import Settings
from app.holodule import Holodule

class HoloCrawler:
    def __init__(self, settings):
        self.__driver = None
        self.__wait = None
        # ホロジュールの URL
        self.__holodule_url = settings.holodule_url
        # YouTube Data API v3 を利用するための準備
        self.__youtube = build(settings.api_service_name, settings.api_version, developerKey=settings.api_key, cache_discovery=False)
        # mongodbのユーザー
        self.__mongodb_user = quote_plus(settings.mongodb_user)
        # mongodbのパスワード
        self.__mongodb_password = quote_plus(settings.mongodb_password)
        # mongodbの接続情報
        self.__mongodb_host = "mongodb://%s/" % (settings.mongodb_host)

    # Firefoxプロファイルの設定
    def __setup_profile(self):
        profile = webdriver.FirefoxProfile()
        # その他（参考）
        # profile.set_preference("browser.download.folderList", 1)
        # profile.set_preference("browser.download.dir", "********")
        # profile.set_preference("browser.download.manager.showWhenStarting", False)
        # profile.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream-dummy")
        # profile.set_preference("browser.helperApps.alwaysAsk.force", False)
        # profile.set_preference("browser.download.manager.alertOnEXEOpen", False)
        # profile.set_preference("browser.download.manager.focusWhenStarting", False)
        # profile.set_preference("browser.download.manager.useWindow", False)
        # profile.set_preference("browser.download.manager.showAlertOnComplete", False)
        # profile.set_preference("browser.download.manager.closeWhenDone", False)
        return profile

    # Firefoxオプションの設定
    def __setup_options(self):
        options = Options()
        # ヘッドレスモードとする
        options.add_argument("--headless")
        return options

    # ホロジュールの取得
    def __get_holodule(self):
        # 取得対象の URL に遷移
        self.__driver.get(self.__holodule_url)
        # <div class="holodule" style="margin-top:10px;">が表示されるまで待機する
        self.__wait.until(EC.presence_of_element_located((By.CLASS_NAME, "holodule")))
        # ページソースの取得
        html = self.__driver.page_source.encode("utf-8")
        # ページソースの解析（パーサとして lxml を指定）
        soup = BeautifulSoup(html, "lxml")
        # タイトルの取得（確認用）
        head = soup.find("head")
        title = head.find("title").text
        print(title)
        # TODO : ここからはページの構成に合わせて決め打ち = ページの構成が変わったら動かない
        # スケジュールの取得
        holodule_list = []
        date_string = ""
        today = datetime.date.today()
        tab_pane = soup.find("div", class_="tab-pane show active")
        containers = tab_pane.find_all("div", class_="container")
        for container in containers:
            # 日付のみ取得
            div_date = container.find("div", class_="holodule navbar-text")
            if div_date is not None:
                date_text = div_date.text.strip()
                match_date = re.search(r"[0-9]{1,2}/[0-9]{1,2}", date_text)
                dates = match_date.group(0).split("/")
                month = int(dates[0])
                day = int(dates[1])
                year = today.year
                if month < today.month or ( month == 12 and today.month == 1 ):
                    year = year - 1
                elif month > today.month or ( month == 1 and today.month == 12 ):
                    year = year + 1
                date_string = f"{year}/{month}/{day}"
                # print(date_string)
            # ライバー毎のスケジュール
            thumbnails = container.find_all("a", class_="thumbnail")
            if thumbnails is not None:
                for thumbnail in thumbnails:
                    holodule = Holodule()
                    # Youtube URL
                    youtube_url = thumbnail.get("href")
                    if youtube_url is not None:
                        holodule.url = youtube_url
                        # print(holodule.url)
                    # 時刻（先に取得しておいた日付と合体）
                    div_time = thumbnail.find("div", class_="col-5 col-sm-5 col-md-5 text-left datetime")
                    if div_time is not None:
                        time_text = div_time.text.strip()
                        match_time = re.search(r"[0-9]{1,2}:[0-9]{1,2}", time_text)
                        times = match_time.group(0).split(":")
                        hour = int(times[0])
                        minute = int(times[1])
                        datetime_string = f"{date_string} {hour}:{minute}"
                        holodule.datetime = datetime.datetime.strptime(datetime_string, "%Y/%m/%d %H:%M")
                        # print(holodule.datetime)
                    # ライバーの名前
                    div_name = thumbnail.find("div", class_="col text-right name")
                    if div_name is not None:
                        holodule.name = div_name.text.strip()
                        # print(holodule.name)
                    # リストに追加
                    if len(holodule.key) > 0:
                        holodule_list.append(holodule)
        return holodule_list

    # Youtube 動画情報の取得
    def __get_youtube_video_info(self, youtube_url):
        # Youtube の URL から ID を取得
        match_video = re.search(r"^[^v]+v=(.{11}).*", youtube_url)
        video_id = match_video.group(1)
        # Youtube はスクレイピングを禁止しているので YouTube Data API (v3) で情報を取得
        search_response = self.__youtube.videos().list(
            # 結果として snippet のみを取得
            part="snippet",
            # 検索条件は id
            id=video_id,
            # 1件のみ取得
            maxResults=1
        ).execute()
        # 検索結果から情報を取得
        for search_result in search_response.get("items", []):
            # id
            vid = search_result["id"]
            # タイトル
            title = search_result["snippet"]["title"]
            # 説明
            description = search_result["snippet"]["description"]
            # IDとタイトルと説明を返却
            return (vid, title, description)
        return ("","","")

    # ホロジュールのスクレイピングと Youtube 動画情報から、配信情報リストの取得
    def get_holodule_list(self):
        try:
            # プロファイルのセットアップ
            profile = self.__setup_profile()
            # オプションのセットアップ
            options = self.__setup_options()
            # ドライバの初期化（オプション（ヘッドレスモード）とプロファイルを指定）
            self.__driver = webdriver.Firefox(options=options, firefox_profile=profile)
            # 指定したドライバに対して最大で10秒間待つように設定する
            self.__wait = WebDriverWait(self.__driver, 10)
            # ホロジュールの取得
            holodule_list = self.__get_holodule()
            # Youtube情報の取得
            for holodule in holodule_list:
                # video情報
                video_info = self.__get_youtube_video_info(holodule.url)
                # video_id
                holodule.video_id = video_info[0]
                # タイトル
                holodule.title = video_info[1]
                # 説明文（長いので100文字で切っている）
                holodule.description = video_info[2].replace("\r","").replace("\n","").replace("\"","").replace("\'","")[:100]
            # 生成したリストを返す
            return holodule_list
        except OSError as err:
            print("OS error: {0}".format(err))
        except:
            print("Unexpected error:", sys.exc_info()[0])
            raise
        finally:
            # ドライバを閉じる
            self.__driver.close()

    # 配信情報リストのCSV出力
    def output_holodule_list(self, holodule_list, filepath):
        try:
            # CSV出力(BOM付きUTF-8)
            with open(filepath, "w", newline="", encoding="utf_8_sig") as csvfile:
                csvwriter = csv.writer(csvfile, delimiter=",")
                csvwriter.writerow(["key", "video_id", "datetime", "name", "title", "url", "description"])
                for holodule in holodule_list:
                    csvwriter.writerow([holodule.key, holodule.video_id, holodule.datetime, holodule.name, holodule.title, holodule.url, holodule.description])
        except OSError as err:
            print("OS error: {0}".format(err))
        except:
            print("Unexpected error:", sys.exc_info()[0])
            raise
        finally:
            pass

    # 配信情報リストのDB登録
    def register_holodule_list(self, holodule_list):
        try:
            # MongoDB のコレクションからの削除と挿入
            client = MongoClient(self.__mongodb_host)
            db = client.holoduledb
            db.authenticate(name=self.__mongodb_user,password=self.__mongodb_password)
            collection = db.holodules
            for holodule in holodule_list:
                # video_id を条件としたドキュメントの削除
                video_id = holodule.video_id
                collection.delete_one( {"video_id":video_id} )
                # ドキュメントの挿入
                doc = holodule.to_doc()
                collection.insert_one(doc)
        except OSError as err:
            print("OS error: {0}".format(err))
        except:
            print("Unexpected error:", sys.exc_info()[0])
            raise
        finally:
            pass
```

### \_\_init__.py ファイルを作成

app/\_\_init__.py ファイルを作成し、main.py からディレクトリ名と同じ名前空間のモジュールを参照できるようにします。

```python
from . import settings
from . import holodule
from . import holocrawler

__version__ = '0.1.0'
```

### main モジュールを作成

main.py ファイルを作成します。

```python
import sys
import os
import argparse
from os.path import join, dirname
from app.settings import Settings
from app.holocrawler import HoloCrawler

RETURN_SUCCESS = 0
RETURN_FAILURE = -1

def main():
    # parser を作る（説明を指定できる）
    parser = argparse.ArgumentParser(description="ホロジュールのHTMLをSelenium + BeautifulSoup4 + Youtube API で解析して MongoDB へ登録")
    # コマンドライン引数を設定する（説明を指定できる）
    parser.add_argument("--csvpath", help="出力するCSVファイルのパス（任意）")
    # コマンドライン引数を解析する
    args = parser.parse_args()

    # ファイルパスの取得
    is_output = False
    csvpath = args.csvpath
    if csvpath is not None:
        # ディレクトリパスの取得と存在確認
        dirpath = os.path.dirname(csvpath)
        print(f"出力ディレクトリパス : {dirpath}")
        if os.path.exists(dirpath) == False:
            print("エラー : 出力するCSVファイルのディレクトリパスが存在しません。")
            return RETURN_FAILURE
        is_output = True

    try:
        # Settings インスタンス
        settings = Settings(join(dirname(__file__), '.env'))
        # HoloCrawler インスタンス
        holocrawler = HoloCrawler(settings)
        # ホロジュールの取得
        holodule_list = holocrawler.get_holodule_list()
        # ホロジュールの登録
        holocrawler.register_holodule_list(holodule_list)
        # ホロジュールの出力
        if is_output == True:
            holocrawler.output_holodule_list(holodule_list, csvpath)
        return RETURN_SUCCESS
    except:
        info = sys.exc_info()
        print(info[1])
        return RETURN_FAILURE

if __name__ == "__main__":
    sys.exit(main())
```

### GitHub に登録

GitHub にリポジトリを作成して登録しておきます。

```powershell
> git status
> git add .
> git commit -m "first commit."
> git remote add origin https://github.com/********/********.git
> git push -u origin main
```

## プログラムの実行

### lounch.json の作成

Visual Studio Code で launch.json を作成して出力ファイルパスを設定しておきます。

```json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Current File",
            "type": "python",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "args": ["--csvpath","c:\\temp\\holodule.csv"]
        }
    ]
}
```

### プログラムのデバッグ実行

Visual Studio Code でデバッグ実行します。

コマンドラインから実行する場合は、下記のように実行します。

```powershell
> poetry run python main.py --csvpath c:\temp\test.csv
```

### 実行結果（MongoDB に登録されたドキュメント）

MongoDB のコレクションに下記のようなドキュメントが登録されます。

```json
{ "_id" : ObjectId("5f843cc5c87f84988a7972c4"), "key" : "HL0403_20201011_000000", "video_id" : "y3WPN1uBusg", "datetime" : "20201011 000000", "name" : "角巻わため", "title" : "【マインクラフト】ついにこの時が来た。チェスト整理回！！！【角巻わため/ホロライブ４期生】", "url" : "https://www.youtube.com/watch?v=y3WPN1uBusg", "description" : "チェストォーーー！！！（チェストだけにね本日遊ばせていただいているゲーム(Minecraft)https://www.minecraft.net/ja-jp/※本ゲームは Mojang に確 認を得た上" }
{ "_id" : ObjectId("5f843cc6c87f84988a7972c5"), "key" : "HL0502_20201011_001400", "video_id" : "EgLwctUuu6M", "datetime" : "20201011 001400", "name" : "雪花ラミィ", "title" : "【Dead by Daylight】今日から始めるDBD生活【雪花ラミィ/ホロライブ】", "url" : "https://www.youtube.com/watch?v=EgLwctUuu6M", "description" : "ホロライブ5期生の雪花ラミィ（Yukihana Lamy ）です！怖い人からたくさん逃げて脱出するゲームしていきます。感想などは #らみらいぶ　#雪花ラミィ で呟いて頂けたら嬉しいです！୨୧┈┈┈┈┈┈" }
{ "_id" : ObjectId("5f843cc7c87f84988a7972c6"), "key" : "HL0G03_20201011_015800", "video_id" : "2Nsp0z1AmV8", "datetime" : "20201011 015800", "name" : "猫又おかゆ", "title" : "【雑談】ゲリラ！新居ではじめての夜更かし【ホロライブ/猫又おかゆ 】", "url" : "https://www.youtube.com/watch?v=2Nsp0z1AmV8", "description" : "ノープランだらだら雑談！🌠_____________________________________________________________________________ ※僕とみんなのお約" }
```

### 実行結果（出力したCSVファイル）

コマンドライン引数を指定して実行すると、下記のような CSV ファイルが出力されます。

```csv
HL0501_20201013_195900,jPP8y696Vkc,2020-10-13 19:59:00,獅白ぼたん,【Far Cry 5】信じて崇めて服従してくれるよねぇ？：＃02【ホロライブ/獅白ぼたん】,https://www.youtube.com/watch?v=jPP8y696Vkc,今日はフォールズエンドの攻略とドラブマン砦を攻略してハークを仲間にしたい感じ！本ゲームは Ubisoft に確認を得た上、Ubisoft Video Policy (https://www.ubiso
HL0305_20201013_195900,mEsDSRfV95U,2020-10-13 19:59:00,宝鐘マリン,【歌ってみた】青春を感じる!!古めの歌枠/Boomer Song【ホロライブ/宝鐘マリン】,https://www.youtube.com/watch?v=mEsDSRfV95U,サムネイラスト…火ノ❅ホロケット ホ18　@soraneko_hinoお借りしている音源↓・一味の有志の方の製作カラオケ・カラオケ歌っちゃ王　様▷https://www.youtube.com/cha
HL0504_20201013_200000,MJt3YMbAxkM,2020-10-13 20:00:00,桃鈴ねね,【making】✨お礼イラストメイキング✨【ホロライブ/桃鈴ねね】,https://www.youtube.com/watch?v=MJt3YMbAxkM,「せんちょーおすみつきねねは天才神！！！！」お名前呼ばれんかった！！！！って人はねねのリプ欄にアピールしにきてね(；＿；)ごめんね(；＿；)いつも応援ありが🍑※日本語以外の言語だと大いに読み間違えてい
...
```

### ファイル構成

```text
holocrawler
├ .venv
├ .vscode
|  └ launch.json
├ app
|  ├ __init__.py
|  ├ holocrawler.py
|  ├ holodule.py
|  └ settings.py
├ .env
├ .gitignore
├ .python-version
├ geckodriver.exe
├ geckodriver.log
├ main.py
├ poetry.lock
├ pyproject.toml
└ README.md
```

## おわりに

やりたいこと 2 の「収集した動画配信予定を格納するためのデータベースの作成と収集の自動化」のうち、データベースの作成までを行いました。

前回からの変化はあまりありませんでしたが、個人的には Poetry と pyenv を利用できたことが成果です。

次回は Azure 仮想マシン上での定期実行環境の構築や、MongoDB のドキュメントを参照するための Web API を作ってみようと思います。