bangumi游戏信息与评论爬取与整理(基于urllib与beautifulsoup)

爬虫结构

项目原址

注:项目原址中已包含游戏相关的数据,如有研究需要,可以下载。不可用于商业用途!

bangumi网站机器人协议

User-agent: *

Disallow: /pic/
Disallow: /img/
Disallow: /js/

网站格式

  • 以游戏《素晴日》为例,该游戏页面有如下结构:
    1. 概览页面网址为:https://bangumi.tv/subject/259061
    2. 角色页面网址为:https://bangumi.tv/subject/259061/characters
    3. 制作人员页面网址为; https://bangumi.tv/subject/259061/persons
    4. 吐槽页面网址为: https://bangumi.tv/subject/259061/comments
  • 因而不难推测,对于bangumi上的一款游戏,基本的页面结构如下:
    1. 概览页面网址为:https://bangumi.tv/subject/XXXXXX
    2. 角色页面网址为:https://bangumi.tv/subject/XXXXXX/characters
    3. 制作人员页面网址为: https://bangumi.tv/subject/XXXXXX/persons
    4. 吐槽页面网址为: https://bangumi.tv/subject/XXXXXX/comments
      其中XXXXXX为每一部作品的id

爬取内容

  1. 概览页面
    • 左侧简要信息
    • 中部所有tag标签及各标签的标记人数
    • 右部的评分、rank
  2. 角色页面
    • cv原文姓名
    • cv中文姓名
    • 是主角还是配角
  3. 制作人员页面
    • 每位成员原文名称
    • 每位成员中文名称
    • 该成员担任的职务
  4. 吐槽页面
    • 评论用户的id
    • 评论时间
    • 用户评分
    • 评论内容

流程

  • 总体
Created with Raphaël 2.2.0 开始 人为确定爬取范围:游戏-所有游戏 换页 是否为最后一页? 爬取该页所有游戏信息 结束 爬取该页所有游戏信息 yes no
#-*- coding: UTF-8 -*- 
#爬取bangumi上所有游戏的信息
import single_item_frame as sif#见下方'爬取该页所有游戏信息'部分的代码
from urllib.request import urlopen
from bs4 import BeautifulSoup
import pandas as pd
import re
import time 

basement = r'https://bangumi.tv'#单款产品基础网址
start_page = r'https://bangumi.tv/game/browser'#单页面基础网址
present_page = r'https://bangumi.tv/game/browser?page=1'#当前地址


#数据读取
try:
    raw_data_total_left = pd.read_csv('raw_data_total_left.csv',encoding='utf-8')
except:
    raw_data_total_left = pd.DataFrame(columns=['game_id','attr','thing'])#游戏信息初始化,id,属性,内容
    print('left文件不存在,创建一个')

try:
    raw_data_total_mid = pd.read_csv('raw_data_total_mid.csv',encoding='utf-8')
except:
    raw_data_total_mid = pd.DataFrame(columns=['game_id','tag_name','tag_num'])
    print('mid文件不存在,创建一个')

try:
    raw_data_total_right = pd.read_csv('raw_data_total_right.csv',encoding='utf-8')
except:
    raw_data_total_right = pd.DataFrame(columns=['game_id','score','rank'])
    print('right文件不存在,创建一个')
    
try:
    raw_data_characters = pd.read_csv('raw_data_characters.csv',encoding='utf-8')
except:
    raw_data_characters = pd.DataFrame(columns=['game_id','cv_id','charcter_type','name','other_name'])#角色信息初始化,声优id,角色类型,声优原名,别名
    print('characters文件不存在,创建一个')
    
try:
    raw_data_persons = pd.read_csv('raw_data_persons.csv',encoding='utf-8')
except:
    raw_data_persons = pd.DataFrame(columns=['game_id','person_id','work','name','other_name'])#制作人员信息初始化,职务、名字、别名
    print('persons文件不存在,创建一个')

try:
    raw_data_comments = pd.read_csv('raw_data_comments.csv',encoding='utf-8')
except:
    raw_data_comments = pd.DataFrame(columns=['game_id','user_id','issue_time','user_score','content'])#存储数据的dataframe#评论信息初始化
    print('comments文件不存在,创建一个')

#记录已经获取的gameid
id_list_left = list(set(raw_data_total_left['game_id']))
id_list_mid = list(set(raw_data_total_mid['game_id']))
id_list_right = list(set(raw_data_total_right['game_id']))
id_list_characters = list(set(raw_data_characters['game_id']))
id_list_persons = list(set(raw_data_persons['game_id']))
id_list_comments = list(set(raw_data_comments['game_id']))


#累计爬取产品数
n = 0

#爬取该页所有信息
while True:
    print('正在爬' + present_page)

    #设置超时(timeout)重新请求
    for times in range(50):#默认最多重新请求50次,并假定50次内必定请求成功
        try:
            present_html = urlopen(present_page,timeout = 5)#设置timeout
            present_bs0bj = BeautifulSoup(present_html)
            break
        except:
            print('请求页面时,网址:\t'+ present_page + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出

    #先抓取该页上的所有游戏信息
    items_list = present_bs0bj.find('ul',{'id':"browserItemList",'class':"browserFull"}).findAll('a',{'class':'subjectCover cover ll'})#包含了该页面的所有游戏
    for each_item in items_list:
        fi_id = int(re.search(r'[0-9]+',each_item['href']).group(0))
        #先查看是否可爬
        browse_page = basement + each_item['href']
        temp = sif.one_item(browse_page)
        if temp.is_crawlable():
            #total信息有一列不存在就得加
            if (fi_id in id_list_left) + (fi_id in id_list_mid) + (fi_id in id_list_right) < 3:
                temp.info_total()
                if fi_id not in id_list_left:
                    raw_data_total_left = pd.concat([raw_data_total_left,temp.total_left])
                    print(str(fi_id)+'left数据不存在,故加入')
                if fi_id not in id_list_mid:
                    raw_data_total_mid = pd.concat([raw_data_total_mid,temp.total_mid])
                    print(str(fi_id)+'mid数据不存在,故加入')
                if fi_id not in id_list_right:
                    raw_data_total_right = pd.concat([raw_data_total_right,temp.total_right])
                    print(str(fi_id)+'right数据不存在,故加入')
            if fi_id not in id_list_characters:
                raw_data_characters = pd.concat([raw_data_characters,temp.characters])
                print(str(fi_id)+'characters数据不存在,故加入')
            if fi_id not in id_list_persons:
                raw_data_persons = pd.concat([raw_data_persons,temp.persons])
                print(str(fi_id)+'persons数据不存在,故加入')
            if fi_id not in id_list_comments:
                raw_data_comments = pd.concat([raw_data_comments,temp.comments])
                print(str(fi_id)+'comments数据不存在,故加入')
        
            n += 1
            if (n % 5 == 0):#每满五条输出一次
                print('已获取' + str(n) + '个产品,输出一次')#输出日志
                print("当前时间: ",time.strftime('%Y.%m.%d %H:%M:%S ',time.localtime(time.time())))
                raw_data_total_left.to_csv('raw_data_total_left.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
                raw_data_total_mid.to_csv('raw_data_total_mid.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
                raw_data_total_right.to_csv('raw_data_total_right.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
                raw_data_characters.to_csv('raw_data_characters.csv',index = False, encoding = 'utf-8')
                raw_data_persons.to_csv('raw_data_persons.csv',index = False, encoding = 'utf-8')
                raw_data_comments.to_csv('raw_data_comments.csv',index = False, encoding = 'utf-8')
        else:
            print(str(fi_id) + '不可爬,故跳过')
    #判断是否为最后一页
    is_last_page = present_bs0bj.find('div',{'class':'page_inner'})
    if '››' in is_last_page.get_text():#说明还有下一页
        next_page = is_last_page.findAll('a')
        for x in next_page:
            if x.get_text() == '››':#有"››"即为下一页连接
                present_page = start_page + x['href']
    else:#说明没有下一页
        break
raw_data_total_left.to_csv('raw_data_total_left.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
raw_data_total_mid.to_csv('raw_data_total_mid.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
raw_data_total_right.to_csv('raw_data_total_right.csv',index = False, encoding = 'utf-8')#原始爬取数据输出
raw_data_characters.to_csv('raw_data_characters.csv',index = False, encoding = 'utf-8')
raw_data_persons.to_csv('raw_data_persons.csv',index = False, encoding = 'utf-8')
raw_data_comments.to_csv('raw_data_comments.csv',index = False, encoding = 'utf-8')

  • 爬取该页所有游戏信息
Created with Raphaël 2.2.0 开始 换下一款游戏 是否为该页最后一款游戏? 爬取当前游戏信息 结束 爬取当前游戏信息 yes no
  • 爬取当前游戏信息
Created with Raphaël 2.2.0 开始 爬取游戏id 爬取概览页面信息 爬取角色页面信息 爬取制作人员页面信息 爬取吐槽页面信息 结束
  • 对应代码

本部分代码原址

#-*- coding: UTF-8 -*- 
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import pandas as pd

class one_item:
    def __init__(self, base_html):
        """
            初始化每一款游戏产品页面的基础地址
            形如
            https://bangumi.tv/subject/XXXXXX
        """
        self.base_html = base_html#初始化,赋予该款游戏产品的基址
        self.id = ''#id初始化
        self.total_left = pd.DataFrame(columns=['game_id','attr','thing'])#游戏信息初始化,id,属性,内容
        self.total_mid = pd.DataFrame(columns=['game_id','tag_name','tag_num'])
        self.total_right = pd.DataFrame(columns=['game_id','score','rank'])
        self.characters = pd.DataFrame(columns=['game_id','cv_id','charcter_type','name','other_name'])#角色信息初始化,声优id,角色类型,声优原名,别名
        self.persons = pd.DataFrame(columns=['game_id','person_id','work','name','other_name'])#制作人员信息初始化,职务、名字、别名
        self.comments = pd.DataFrame(columns=['game_id','user_id','issue_time','user_score','content'])#存储数据的dataframe#评论信息初始化
        
        #先获取id
        match = re.search(r'[0-9]+', self.base_html)
        self.id = match.group(0)
        
     
    def is_crawlable(self):
        """
            确认是否可爬
        """
        #设置超时(timeout)重新请求
        for times in range(50):#默认最多重新请求50次,并假定50次内必定请求成功
            try:
                html = urlopen(self.base_html, timeout = 5)#设置timeout
                bs0bj = BeautifulSoup(html)
                break
            except:
                print('在检测是否可爬时,请求网址:\t'+ self.base_html + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
        

        if len(bs0bj.findAll('img',{'src':r'/img/bangumi/404.png', 'class':'ll'})) != 0:#如果有该页面信息,则说明不可爬
            print(self.id + "页面信息不存在")
            return False
        else:
            print(self.id + "页面信息存在,可爬取")
            return True
    
    
    def info_total(self):
        """
            获取概览页面信息
        """
        #设置超时(timeout)重新请求
        for times in range(50):#默认最多重新请求50次,并假定50次内必定请求成功
            try:
                html = urlopen(self.base_html, timeout = 5)#设置timeout
                bs0bj = BeautifulSoup(html)
                break
            except:
                print('在获取产品概览信息时,请求网址:\t'+ self.base_html + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
        
        
        #左侧简要信息
        
        #先找产品名称
        name = bs0bj.find('a',{'property':'v:itemreviewed'}).get_text()
        #先加入名称信息
        self.total_left = self.total_left.append({'game_id':self.id, 'attr':'产品名称', 'thing':name}, ignore_index = True)#将该条信息加入dataframe中


        #再找其他信息
        brief_info_tag = bs0bj.find('ul',{'id':'infobox'})#<ul id="infobox">的标签在该页面中只有一个,故只需要使用find函数即可
        brief_info_list = brief_info_tag.findAll('li')
        for each_info in brief_info_list:
            seg_list = each_info.get_text().split(': ')#分割后一号为attr,二号位thing
            self.total_left = self.total_left.append({'game_id':self.id, 'attr':seg_list[0], 'thing':seg_list[1]}, ignore_index = True)#将该条信息加入dataframe中
        
        
        
        #中部所有标签
        mid_info_tag_LV1 = bs0bj.find('div',{'class':'subject_tag_section'})#无法直接定位,故采用二级式标签
        #查空
        if mid_info_tag_LV1 != None:#有此标签才会有内容
            mid_info_tag_LV2 = mid_info_tag_LV1.find('div',{'class':'inner'})
            mid_info_list = mid_info_tag_LV2.findAll('a')
            for x in mid_info_list:#贴的标签名称与所贴标签人数
                temp_tag = x.find('span').get_text()
                temp_num = x.find('small').get_text()
                self.total_mid = self.total_mid.append({'game_id':self.id, 'tag_name':temp_tag, 'tag_num':temp_num}, ignore_index = True)#数据加入


        
        #右部的评分、rank
        right_info_tag = bs0bj.find('div',{'class':'global_score'})#定位大页面
        score = right_info_tag.find('span',{'class':'number','property':'v:average'}).get_text()#爬取评分
        #爬取rank,要区分有rank和没有rank的情况
        if right_info_tag.find('small',{'class':'alarm'}) == None:#如果没有该标签,说明暂无rank
            rank = ''
        else:
            rank = right_info_tag.find('small',{'class':'alarm'}).get_text()#爬取rank
            rank = rank.replace('#','')#把#替换掉
        self.total_right = self.total_right.append({'game_id':self.id, 'score':score, 'rank':rank}, ignore_index=True)
        
    def info_characters(self):
        """
            获取角色页面信息
        """
        suffix = r'/characters'#角色信息页面后缀名
        characters_URL = self.base_html + suffix

        
        #单条声优的数据格式
        def gen_single_character_structure():
            """
                生成一个角色的数据结构
            """
            single_character_info_temp = {
                'game_id':self.id,
                'cv_id':'',
                'charcter_type':'',
                'name':'',
                'other_name':''
            }
            return single_character_info_temp
        
        
        #设置超时(timeout)重新请求
        for times in range(50):#默认最多重新请求50次,并假定50次内必定请求成功
            try:
                html = urlopen(characters_URL, timeout = 5)#设置timeout
                bs0bj = BeautifulSoup(html)
                break
            except:
                print('在获取产品角色信息时,请求网址:\t'+ characters_URL + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
        
        
        #开始查找
        character_info_tag = bs0bj.find('div',{'class':'column','id':'columnInSubjectA'})
        #查空
        if len(character_info_tag.contents) != 0:
            each_character_list = character_info_tag.findAll('div',{'style':'padding-left: 90px;','class':'clearit'})
            for each_character in each_character_list:
                character_type = each_character.find('span',{'class':'badge_job'}).get_text()#主配角
                #有的角色可能并未提供cv角色,故需要进一步判断
                character_col = each_character.find('div',{'class':'actorBadge clearit'})
                if (character_col) != None:
                    voice_id = character_col.find('p').find('a')['href'][8:]#cv的id
                    voice_name = character_col.find('p').find('a').get_text()
                    voice_other_name = character_col.find('p').find('small').get_text()
                    
                    temp_character_structure = gen_single_character_structure()#首先生成一个空的结构
                    temp_character_structure['cv_id'] = voice_id
                    temp_character_structure['charcter_type'] = character_type
                    temp_character_structure['name'] = voice_name
                    temp_character_structure['other_name'] = voice_other_name
                    
                    ###把当前的一条信息放入dataframe
                    self.characters = self.characters.append(temp_character_structure, ignore_index = True)#将该条信息加入dataframe中
                        
                        
#         print(self.characters)

    
    def info_persons(self):
        """
            获取制作人员信息
        """
        suffix = r'/persons'#制作人员后缀名
        persons_URL = self.base_html + suffix
        
        #单条工作人员的数据格式
        def gen_single_person_structure():
            """
                生成一个单条评论的数据结构
            """
            single_person_info_temp = {
                'game_id':self.id,
                'person_id':'',
                'work':'',
                'name':'',
                'other_name':''
            }
            return single_person_info_temp
        
        
        #设置超时(timeout)重新请求
        for times in range(50):#默认最多重新请求50次,并假定50次内必定请求成功
            try:
                html = urlopen(persons_URL, timeout = 5)#设置timeout
                bs0bj = BeautifulSoup(html)
                break
            except:
                print('在获取产品制作人员信息时,请求网址:\t'+ persons_URL + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
        

        #开始查找
        persons_info_tag = bs0bj.find('div',{'id':'columnInSubjectA','class':'column'})#包含所有工作人员及其任务的tag
        #查空
        if len(persons_info_tag.contents) != 0:
            each_person_list = persons_info_tag.findAll('div',{'style':'padding-left: 100px;'})
            for each_person in each_person_list:
                #该工作人的名字,含本名与中文名,按'  / '分割
                one_member = each_person.find('h2').get_text()
                one_member_list = one_member.split('  / ')#分割,可能会有一个或两个元素,取决于有没有中文名
                member_id = each_person.find('h2').find('a')['href'][8:]#找出此人id
                #该该工作人员的职务
                each_person_work_list = each_person.findAll('span',{'class':'badge_job'})
                for each_person_work in each_person_work_list:
                    
                    temp_person_structure = gen_single_person_structure()#首先生成一个空的结构
                    
                    temp_person_structure['person_id'] = member_id
                    temp_person_structure['work'] = each_person_work.get_text()
                    temp_person_structure['name'] = one_member_list[0]
                    if len(one_member_list) > 1:#说明有别名
                        temp_person_structure['other_name'] = one_member_list[1]
                    
                    ###把当前的一条评论放入dataframe
                    self.persons = self.persons.append(temp_person_structure, ignore_index = True)#将该条信息加入dataframe中




    
    def info_comments(self):
        """
            获取吐槽页面信息
        """
        suffix = r'/comments'#吐槽页面后缀名
        
        initial_page = self.base_html + suffix#初始页面
        present_page = self.base_html + suffix#当前页面

        
        #一款游戏的所有评论暂寸于下dataframe中
        #单条评论的数据格式
        def gen_single_comment_structure():
            """
                生成一个单条评论的数据结构
            """
            single_comment_info_temp = {
                'game_id':self.id,
                'user_id':'',
                'issue_time':'',
                'user_score':'',
                'content':''
            }
            return single_comment_info_temp
        
        
        for times in range(50):
            try:
                present_html = urlopen(present_page,timeout = 5)#设置timeout
                present_bs0bj = BeautifulSoup(present_html)
                break
            except:
                print('在获取产品评论信息时,请求网址:\t'+ present_page + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
        

        #开始查找
        #查空
        if len(present_bs0bj.find('div',{'id':'comment_box'}).contents) != 0:#所有得子节点用列表返回,无子节点代表列表长度为0#说明有子项,说明有评论
            #爬取当前页面内容+换页
            while True:
                for times in range(50):
                    try:
                        present_html = urlopen(present_page,timeout = 5)#设置timeout
                        present_bs0bj = BeautifulSoup(present_html)
                        break
                    except:
                        print('在获取产品评论信息时,请求网址:\t'+ present_page + '\t超时\n再尝试一次,已累计尝试' + str(times + 1) + '次')#出错输出
                        #
                total_comment_tag = present_bs0bj.find('div',{'id':'comment_box'})#包含了每页所有评论的总tag
                comment_list_tag = total_comment_tag.findAll('div',{'class':'text'})#该列表中包含了每条评论及其评分,以及评论用户名及评论时间

                for each_comment in comment_list_tag:#遍历该页中的每一条评论
                    temp_comment_structure = gen_single_comment_structure()#首先生成一个空的结构
                    
                    comment_user_id = each_comment.find('a')['href'][6:]#每条评论的用户名,存在了href属性中,只取第7位到最后一位为id
                    comment_time = each_comment.find('small').get_text()#每条评论的评论时间
                    comment_time = comment_time[2:]#时间以“@ ”开头,去除即可
                    comment_content = each_comment.find('p').get_text()#每条评论的内容
                    #每条评论对应的评分,分两种情况:一种是有评分的,一种是没有评分的
                    is_score = each_comment.find('span')#是否有评分的标记,如果没有,is_score返回None
                    if is_score == None:
                        comment_score = '0'#没有评分的记为0星方便后面数值化处理
                    else:#说明有评分
                        comment_score = is_score.find('span')['class'][1]#每条评论对应的评分,是个list,第二个为评分
                        comment_score = comment_score[5:]
                    #print(comment_score)
                    
                    temp_comment_structure['user_id'] = comment_user_id
                    temp_comment_structure['issue_time'] = comment_time
                    temp_comment_structure['user_score'] = comment_score
                    temp_comment_structure['content'] = comment_content
                    
                    ###把当前的一条评论放入dataframe
                    self.comments = self.comments.append(temp_comment_structure, ignore_index = True)#将该条评论加入dataframe中

                #跳页过程
                pages_tag = present_bs0bj.find('div',{'class':'page_inner'})#可以知道有多少页评论的tag在这里
                if (pages_tag != None):#==None时说明评论只有一页,而一页的评论是没有划页选项的
                    if ('››' in pages_tag.get_text()):#如果返回True说明当前页还有下一页
                        #跳到下一页
                        next_page = pages_tag.findAll(name = 'a')#返回一个列表,要在该列表中寻找下一页的连接
                        for x in next_page:
                            if x.get_text() == '››':#有"››"即为下一页连接
                                present_page = initial_page + x['href']         
                    else:#否则说明没有下一页
                        break
                else:#说明只有一页
                    break
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值