環境 Windows Python3.7
これからやること
Beautiful Soup 4(以後bs4)を使ってスクレイピング。結果をJSONファイルで出力。
WEBから分析用のデータを収集する。
データ分析の勉強をしたかったが、そもそも良い感じのデータが無かったためスクレイピングで入手することに。
スクレイピング対象
- ニコニコチャート(http://www.nicochart.jp/)
 - ニコニコ動画(https://www.nicovideo.jp/)
 
ニコニコ動画ゲームカテゴリランキングの上位100に入った動画と投稿者の情報を取得する。
Q. なぜニコニコ動画?
A. 私も投稿者だからです(宣伝)。https://www.nicovideo.jp/user/35233530
取得したい情報
- 動画情報
- 動画ID
 - 投稿日時
 - 再生時間
 - 過去最高ランク
 - タグ
 
 - 投稿者情報
- 投稿者ID
 - ユーザー登録時のニコニコ動画のバージョン
 - フォロワー数
 - 投稿動画数
 
 
Beautiful Soupについて
事前準備
BeautifulSoupをインストール
$ pip install beautifulsoup4
基本的な使い方
import urllib.request
from bs4 import BeautifulSoup
url="対象のURL"
html = urllib.request.urlopen(url)
soup = BeautifulSoup(html, "html.parser")
    
# 引数はどれか1つ以上でOK。最初に見つかったタグが返される。
hoge = soup.find("HTMLタグ名",class_="タグのクラス名",id="タグのID") 
# 条件に一致する全てのタグが返される。
hoge = soup.find_all("HTMLタグ名",class_="タグのクラス名",id="タグのID") 
# あるタグの子要素に限定して検索することも可能。
hogehoge=hoge.find("HTMLタグ名",class_="タグのクラス名",id="タグのID")
# タグのテキストを取得
print(hoge.text)
# タグの属性値を取得
print(hoge["href"]) 
エラー対策
ネットワークエラーの対策。
503エラーが発生することがあるので。
時間を空けてリトライ。それでもダメなら諦め。
try:
    html = urllib.request.urlopen(_Url)
except urllib.error.HTTPError as e:
    print(e.code)
    sleep(60)
    html = urllib.request.urlopen(_Url)
soup = BeautifulSoup(html, "html.parser")
タグが見つからないエラーの対策。
HTMLが上手く読めてない可能性があるので時間を空けてリトライ。
それでもダメなら諦め。
try:
    tag_panel=soup.find(class_="クラス名1").find(class_="クラス名2")
except:
    try:
        print("error")
        sleep(60)
        #再読み込み
        html = urllib.request.urlopen(_Url)
        soup = BeautifulSoup(html, "html.parser")
        tag_panel=soup.find(class_="クラス名1").find(class_="クラス名2")
        result=tag_panel.text
    except:
        print("error")
        result = None
スクレイピングの注意点
- サイトの規約を読みましょう。
 - 相手のサーバーに負担をかけるため、sleep等でリクエストの間隔を空けましょう。1秒以上は空けたほうが良さげ。
 
JSON出力
辞書型からJSONファイルで出力。エンコードはUTF-8で出力。Shift_Jisは文字化けしたり何かと厄介。
ensure_ascii=Falseも忘れずに。
ほんとはwith句でやると安全。
fw = open("ファイル名",'w',encoding='utf-8')
json.dump(_dictionary,fw,indent=4,ensure_ascii=False)
fw.close()
実際にやってみた
エラー対策は頻発する箇所のみ実施。
# %%
import urllib.request
import re
import datetime
import json
import sys
from bs4 import BeautifulSoup
from time import sleep 
# ニコニコチャートからスクレイピング
### ランキングページを開く
def setRankPage(_dateStr):
    url="http://www.nicochart.jp/ranking/game/"+_dateStr
    html = urllib.request.urlopen(url)
    print(url)
    soup = BeautifulSoup(html, "html.parser")
    return soup
## 動画IDと動画URLと投稿者名、IDとURLの取得
def getRankInfo(_soup,_num):
    rank=_soup.find(id="rank"+str(_num))
    
    link=rank.find(class_="title").find("a")
    infoUrl="http://www.nicochart.jp/"+str(link["href"])
    videoId=re.findall('\d+',link["href"])[0]
    print(infoUrl,videoId)
    ##投稿者名とURLの取得
    nameInfo=rank.find("em",class_="user")
    if not nameInfo:
        return {"videoId":videoId,"videoUrl":infoUrl,"userName":None,"userUrl":None}
    nameInfo=nameInfo.find("a")
    nameUrl=str(nameInfo["href"])
    userId=re.findall('\d+',nameUrl)[0]
    nameUrl += "/video"
    name=rank.find("em",class_="name").find("a").text
    print(name,nameUrl)
    return {"videoId":videoId,"videoUrl":infoUrl,"userName":name,"userId":userId,"userUrl":nameUrl}
### 動画URLからタグ、投稿日時、最高ランク、再生時間の取得
def getVideoInfo(_infoUrl):
    try:
        html = urllib.request.urlopen(_infoUrl)
    except urllib.error.HTTPError as e:
        try:
            print(e.code)
            sleep(60)
            html = urllib.request.urlopen(_infoUrl)
        except:
            return {"tags":None,"date":None,"length":None,"maxRank":None}
    soup = BeautifulSoup(html, "html.parser")
    ##タグの取得
    try:
        tag_panel=soup.find(class_="tab-panel").find(class_="cloud")
        tags=tag_panel.find_all("a",class_="word")
        tagWords=list(map(lambda tag: tag.text, tags))
    except:
        try:
            print("error")
            sleep(60)
            html = urllib.request.urlopen(_infoUrl)
            soup = BeautifulSoup(html, "html.parser")
            tag_panel=soup.find(class_="tab-panel").find(class_="cloud")
            tags=tag_panel.find_all("a",class_="word")
            tagWords=list(map(lambda tag: tag.text, tags))
        except:
            print("error")
            tagWords=None
    print(tagWords)
    ##投稿日時の取得
    date=soup.find("dd",class_="first-retrieve").find("span").text
    ##再生時間の取得
    length=soup.find("span",class_="length").text
    ##最高ランクの取得
    nicoRank=soup.find(class_="point-data").find_all("td",class_="fav")
    nicoRank=list(map(lambda rank:rank.find_all("a"),nicoRank))
    nicoRank=list(map(lambda rank:sys.maxsize if len(rank)==0 else int(rank[len(rank)-1].text),nicoRank))
    nicoRank=min(nicoRank)
    print(nicoRank)
    return {"tags":tagWords,"date":date,"length":length,"maxRank":nicoRank}
### 投稿者情報から投稿者の基本情報を取得
# 投稿者URLからフォロワー数、投稿動画数、ユーザーID、ニコ動Verを取得。
def getUserInfo(_nameUrl):
    try:
        html = urllib.request.urlopen(_nameUrl)
    except urllib.error.HTTPError as e:
        print(e.code)
        return {"followerNum":None,"videoNum":None,"nicoVer":None}
    soup = BeautifulSoup(html, "html.parser")
    ##基本情報の取得
    followerNum=soup.find(class_="stats channel_open_mb8").find_all(class_="num")[1].text
    followerNum=followerNum.replace(',',"")
    videoNum=soup.find(class_="content",id="video").find("h3").text
    videoNum=re.findall('\d+', videoNum)
    if videoNum:
        videoNum=videoNum[0]
    else:
        videoNum=None
    idInfo=soup.find(class_="accountNumber").find("span").text
    nicoVer=re.findall('(?<=\().*?(?=\))',idInfo)[0]
    print(followerNum,videoNum,nicoVer)
    return {"followerNum":followerNum,"videoNum":videoNum,"nicoVer":nicoVer}
### ランキングから動画URLと投稿者情報の取得
def getRankDataDay(_dateStr):  
    soup=setRankPage(_dateStr)
    rankData=[]
    for rankNum in range(1,100):
        print("//////",rankNum,"//////")
        rankData.append(getRankInfo(soup,rankNum))
    print(rankData)
    ###動画一覧、投稿者一覧の作成
    videoData={}
    userData={}
    user_videoData={}
    num=0
    for rank in rankData:
        num +=1
        videoId=rank["videoId"]
        videoData[videoId]={"videoUrl":rank["videoUrl"]}
        if not "userId" in rank:
            continue
        userId=rank["userId"]
        user_videoData[videoId]={"userId":rank["userId"]}
        if not userId in userData:
            userData[userId]={"userName":rank["userName"],"userUrl":rank["userUrl"]}
    ###動画一覧にタグ情報を追加
    for video in videoData.values():
        print("/////////",video["videoUrl"])
        videoInfo=getVideoInfo(video["videoUrl"])
        video.update(videoInfo)
        sleep(2)
    ###投稿者一覧に投稿者情報を追加
    for user in userData.values():
        userInfo=getUserInfo(user["userUrl"])
        user.update(userInfo)
        sleep(1)
    
    print(videoData)
    print(userData)
    return {"videoData":videoData,"userData":userData,"user_videoData":user_videoData}
### Json出力
def outputJson(_dateStr,_data,_folder):
    fw = open(_folder+ _dateStr +".json",'w',encoding='utf-8')
    json.dump(_data,fw,indent=4,ensure_ascii=False)
    fw.close()
    return
### 指定期間分のデータを取得。指定日の前日から取得開始。
date = datetime.date(2019,2,2)
count=365
for num in range(count):
    date -=datetime.timedelta(days=1)
    dateStr=date.strftime("%Y%m%d")
    rankDataDay=getRankDataDay(dateStr)
    outputJson(dateStr,rankDataDay["videoData"],"VideoData/")
    outputJson(dateStr,rankDataDay["userData"],"UserData/")
    outputJson(dateStr,rankDataDay["user_videoData"],"User_VideoData/")
結果
データ収集が完了。JSON形式で出力。
次回はこのデータをSQLデータベースにシュウウウウウウ!!!!超!!!エキサイティン!!!!!。
参考
https://www.crummy.com/software/BeautifulSoup/bs4/doc/#installing-beautiful-soup