【Python】Beautiful SoupでスクレイピングしてJSON出力でデータ収集

環境　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をインストール

```sh
$ pip install beautifulsoup4
```

###基本的な使い方
```python
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エラーが発生することがあるので。
時間を空けてリトライ。それでもダメなら諦め。

```python
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が上手く読めてない可能性があるので時間を空けてリトライ。
それでもダメなら諦め。

```python
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句でやると安全。

```python
fw = open("ファイル名",'w',encoding='utf-8')
json.dump(_dictionary,fw,indent=4,ensure_ascii=False)
fw.close()
```

#実際にやってみた
エラー対策は頻発する箇所のみ実施。

```python
#%%
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