まるぼ実験場

今年専門を卒業したけど就活サボってしまいました(関係ない職種でフリーター生活中)。どうすんだよこれ。

【Python/BeautifulSoup】スクレイピングで各話リストをつくるソースコード【ニコニコ大百科】

製作のきっかけ

推しアニメの各話リスト作りたいなーって話になり、せっかくキリの良い所を迎えたことだし作るか!と。
…って既に52話もあるんですが。1クール12話のアニメならともかくこれをコピペでも手動でやるのはしんどい、自動化できる部分自動化しよう。
などと呟きながら、過去のコードを参考にして適当に書き始めたのだった。
※このソースコードは今後のサイトリニューアルなどで使えなくなる可能性があります。

ソースコード

# coding: utf-8
from bs4 import BeautifulSoup
import re

def main():
    # htmlを指定
    nicohtml = ("ニコニコチャンネルのページを指定")
    dhtml = ["dアニメのページを指定",
            "複数ページにわたる場合は配列にして指定"]
    # 変数宣言
    nurlList = []   #動画URL(ニコニコ)
    nimgList = []   #サムネイル画像URL(ニコニコ)
    noList = []     #話数カウント
    titleList = []  #タイトル
    nIdList = []    #動画ID(ニコニコ)
    durlList = []   #動画URL(dアニメ)
    dimgList = []   #サムネイル画像URL(dアニメ)
    dIdList = []    #動画ID(dアニメ)
    # ニコニコデータ
    nurlList, nimgList, noList, titleList, nIdList = Niconico(nicohtml, nurlList, nimgList, noList, titleList, nIdList)
    # dアニデータ
    if(len(dhtml)>0):
        durlList, dimgList, dIdList = dAni(dhtml, durlList, dimgList, dIdList)
    # Tableタグ作成
    if(len(dhtml)>0):
        tableTag = TableMake(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList)
    else:
        tableTag = TableMakeN(noList, titleList, nIdList, nurlList, nimgList)
        
    # txt出力
    outputPath = "保存先ファイルを指定する"
    MakeTxt(outputPath, tableTag)

def Niconico(html, nurlList, nimgList, noList, titleList, nIdList):
    soup = BeautifulSoup(open(html, encoding="utf-8"), 'lxml', from_encoding="utf-8")
    
    # <a class ="g-video-link">~</a>を取得
    videoLinks = soup.find_all("a","thumb_anchor g-video-link")

    count = 0
    for videoLink in videoLinks:
        data = videoLink
        # タイトル
        title = data.get("title") 
        if (title == None):
            continue
        titletxt = re.findall("「(.*)」", str(title).replace('\u3000', ' '))
        # URL
        nurl = data.get("href")
        # 画像URL
        nimg = data.find("img")["data-original"]
        # 動画ID(ニコニコ)
        nId = nurl.split("/")
        # 話数名
        count += 1

        nurlList.append(nurl)
        nimgList.append(nimg)
        noList.append("第" + str(count) + "話")
        titleList.append(titletxt[0])
        nIdList.append(nId[-1])

    return nurlList, nimgList, noList, titleList, nIdList

def dAni(html, durlList, dimgList, dIdList):
    for page in html:
        soup = BeautifulSoup(open(page, encoding="utf-8"), 'lxml', from_encoding="utf-8")
    
        # <div class ="item_left">を取得
        items = soup.find_all("div", "item_left")
        for item in items:
            data = item
            # URL
            durl = data.find("a").get("href")
            # 画像URL
            dimg = data.find("img")["data-original"]
            # 動画ID
            dId = durl.split("/")

            durlList.append(durl)
            dimgList.append(dimg)
            dIdList.append(dId[-1])

    return durlList, dimgList, dIdList

def TableMake(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList):
    #htmlタグ生成
    Table = []
    Table.append("<table><tbody><tr>")
    nowidth = len(noList) // 10 * 8 + 16 # 話数で長さ調整
    Table.append("<th width =\"" + str(nowidth) + "\" style=\"text-align: center; vertical-align: middle;\">話数</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">サブタイトル</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">動画</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">dアニメ</th>")
    Table.append("</tr>")
    for no, title, nid, did, nurl, durl, nimg, dimg in zip(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList):
        Table.append("<tr>")
        Table.append("<td style=\"vertical-align: middle; text-align: center; \">" + no + "</td>")
        Table.append("<td style=\"vertical-align: middle;\">" + title + "</td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + durl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + dimg + "\" alt=\"dアニメ\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("</tr>\n")
    Table.append("</tbody></table>")

    return Table

def TableMakeN(noList, titleList, nIdList, nurlList, nimgList):
    #htmlタグ生成
    Table = []
    Table.append("<table><tbody><tr>")
    nowidth = len(noList) // 10 * 8 + 16 # 話数で長さ調整
    Table.append("<th width =\"" + str(nowidth) + "\" style=\"text-align: center; vertical-align: middle;\">話数</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">サブタイトル</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">動画</th>")
    Table.append("</tr>")
    for no, title, nid, nurl, nimg in zip(noList, titleList, nIdList, nurlList, nimgList):
        Table.append("<tr>")
        Table.append("<td style=\"vertical-align: middle; text-align: center; \">" + no + "</td>")
        Table.append("<td style=\"vertical-align: middle;\">" + title + "</td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("</tr>\n")
    Table.append("</tbody></table>")

    return Table

def MakeTxt(outputFile, Tabledata):
    with open(outputFile, mode='w') as f:
        f.writelines(Tabledata)

if __name__ == "__main__":
    main()

コードの解説

def Niconico()

ニコニコチャンネルのページから各話情報を取得するためのメソッド。

    # <a class ="g-video-link">~</a>を取得
    videoLinks = soup.find_all("a","thumb_anchor g-video-link")

今回欲しいデータは、タイトル、サムネイルのURL、動画URL。
f:id:kikyou_kiki:20210124235727p:plain
これらはサムネイルに設定されている部分(上のthumb_anchor g-video-link)を取得すれば事足りるので、このタグを取得できる条件を設定します。

        # タイトル
        title = data.get("title") 
        if (title == None):
            continue

これは上記の検索条件だと上の方にある無料配信になっている動画の所が出力に余計に入ってきてしまうため、それを弾くために入れている条件。

titletxt = re.findall("「(.*)」", str(title).replace('\u3000', ' '))

「と」の間のタイトルを取得する処理。正規表現を使っています。今回の例の場合、
妖怪学園Y Nとの遭遇(妖怪ウォッチJam) 第1話「衝撃!! 初恋の人は○○だった」の「」内の部分、『衝撃!! 初恋の人は○○だった』だけを取得できます。
あと全角スペースが\u3000に置き換わっているのでついでに置き換えます。

        # URL
        nurl = data.get("href")
        # 動画ID(ニコニコ)
        nId = nurl.split("/")

        nIdList.append(nId[-1])

URLを/区切りでカットした配列を作り、末尾のみを取得します。
これで動画IDのみを取得できます。(が今回は取得しただけで使っていない)
サムネイルじゃなくて埋め込みコードを使いたいときは使えるかも。

        noList.append("第" + str(count) + "話")

ループで話数のテキストを作っています。
もし取得したいアニメのエピソードが、#〇とか第〇羽とかだったらここを書き換えます。

def dAni()

上記のdアニメページ版。
大体同じですが、こちらは複数ページを取得する可能性があるので、ループ内でページごとにbeautifulSoupで取得。

        # <div class ="item_left">を取得
        items = soup.find_all("div", "item_left")

こちらは画像URL、URLのデータを取得できる部分を指定します。

def TableMake()&def TableMakeN()

ここでhtmlタグを作成します。
dアニメのURLが指定されているがされていないかで分岐します。

<th style=\"text-align: center; vertical-align: middle;\">

でヘッダー行の書式を指定。

Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + durl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + dimg + "\" alt=\"dアニメ\" width=\"65\" height=\"50\" /></a></td>")

特にここのstyle="~"やwidth="~",height="~"は出力結果を見ながらパラメータ調整します。
ちなみにここのa属性のstyle="~”を指定しないと、
f:id:kikyou_kiki:20210125000347p:plain
左のように右下にアイコンがある感じになります。

使い方・実行サンプル

諸注意

このソースコードを実行したことによる問題について、筆者は責任を負いかねますので使用する場合は自己責任で。
あと妖怪学園Yのページ以外では動作確認していません。(オイ

各話リストが欲しいアニメのニコニコチャンネルのページを確認します

https://ch.nicovideo.jp/youkai-watch2020
こんな感じのページですね。
dアニメストア支店のデータも必要な場合は検索して持ってきます、
https://ch.nicovideo.jp/search/妖怪学園Y?&mode=s&sort=f&order=a&type=video&channel_id=ch2632720&page=1
検索条件は『投稿が古い順』に設定します。
筆者はリアルタイム性が必要ないスクリプトの場合、サーバーに負担をかけないようページを『名前を付けて保存』してからスクレイピングを実行する派なので、これらを保存します。
marubodiary.hateblo.jp
保存したHTMLを使う場合、こちらも参考資料として。

ファイルを指定します

保存したhtmlファイルの保存先を、

    # htmlを指定
    nicohtml = ("ニコニコチャンネルのページを指定")
    dhtml = ["dアニメのページを指定",
            "複数ページにわたる場合は配列にして指定"]

dアニメストアの方のhtmlがない場合は空にする。
出力結果のtxtファイルを保存する場所を

    # txt出力
    outputPath = "保存先ファイルを指定する"

にファイル名含むフルパスで指定します。拡張子はtxt推奨。

実行します

実行すると指定したところに話数、サブタイトル、動画のサムネイル(リンク付き)のTableタグが出力されたtxtファイルが出力されます。
これを大百科のHTMLエディタに貼り付けてみると…
dアニメ指定あり
f:id:kikyou_kiki:20210124235526p:plain
dアニメ指定なし
f:id:kikyou_kiki:20210124235532p:plain

感想

今思えば変数クラス化しときゃ良かったわ。後付けしつつ適当に作ったせいでその辺の設計ガバってた。
各話リスト作る過程でmarginとか画像サイズとかの部分をいろいろ試行錯誤してたのですが、
実行したら52話分一発でレイアウトとかを変えられるのはすごく楽だった。自動化して良かった。一個一個やってたら発狂してるとこだったわ。
せっかく作ったものの、筆者は一般会員で全く活用出来そうにないので、好きだけど話数が多すぎて編集するのがしんどい…ってアニメがあるニコ百編集者様方、ぜひこのソースを活用して頂ければ。
GUI化して設定項目とか分かりやすく設定できるようにしたら需要あるかなぁ?そもそもの目的がニッチすぎるか。

あと妖怪学園Yを見てください。このアニメが無かったらこのソースコードは生まれなかった。
せめてOPだけでもいいので(ry

学園が炎上するところから始まる公式紹介ムービーも紹介しておきますね。