LoginSignup
20
19

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-03

環境 Windows Python3.7

これからやること

Beautiful Soup 4(以後bs4)を使ってスクレイピング。結果をJSONファイルで出力。
WEBから分析用のデータを収集する。
データ分析の勉強をしたかったが、そもそも良い感じのデータが無かったためスクレイピングで入手することに。

スクレイピング対象

ニコニコ動画ゲームカテゴリランキングの上位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

20
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
19