从IMDB上爬取MovieLens数据集中的详细电影信息

基于协同过滤的电影推荐系统

用这个数据集实现了一个小型的电影推荐网站,GitHub代码

数据集

数据集是MovieLens提供的ml-latest-small

https://grouplens.org/datasets/movielens/

试了几个数据集,这个数据集效果比较好

10万条评分记录,3600个用户对电影打的标签,9000部电影,600个用户

数据集的格式是这样的

link.csv :存放电影的imdb id和tmdb id

movies.csv :存放电影的id 名称 类型

ratings.csv :用户对电影的评分,范围是0.5~5

tags.csv :用户对电影打的标签

link.csv文件是这样的格式:

在这里插入图片描述

HTML页面分析

我刚看的时候不明白imdbID是什么意思,后面访问IMBD网站发现,这里的imdbID就是URL里面的标识符在这里插入图片描述
有了link.csv文件里面的imdbID,我们就可以访问到这部电影在IMDB上面的详情页面了(这个数据集也太爽了😄👍)
仔细看这个页面,红框里面是我们需要爬取的信息

这里就不分析了,不然篇幅有点长了,具体看代码就行。自己可以f12看一下页面的组织结构

爬虫代码

爬虫我用的库是requests和BeautifulSoup,其它库没有用的原因是不会,BS4用起来挺顺手的

需要注意的是,我们爬到的是海报的URL,所以在爬信息的过程中还需要再爬一次,发起请求把海报下载下来。下图中,src存储的就是海报的链接
src就是海报的链接
上述分析过程中可以知道,构造请求应该独立成一个函数,因为第一次爬电影信息要用,第二次爬电影海报要用
下面贴一下代码:
我都是把爬虫包装在一个类里面,这个类的函数结构:

在这里插入图片描述
解释一下为什么有white_lst和black_lst这两个奇怪的东西:
下面的爬虫代码我改了很多遍,它的异常处理结构是最后才改好的,也就是说中途会有报错,我需要把bug改好然后继续运行爬虫爬取剩下的电影信息。但是呢,没报错之前已经爬取了一些电影了,我就不需要重头爬了,因此我用一个white_lst来存放已经爬取完成的电影信息。
black_lst是用来存放爬取过程中出错的电影,例如ImdbID失效啊(很少),没有海报信息啊(有几部是这样的),整个数据集爬取完后,再处理这个black_lst里面的电影(最后我发现大概就十部不到的电影出错,于是我就手动处理了)
下图是运行日志中记录的爬取出错的电影
在这里插入图片描述

import requests
from bs4 import BeautifulSoup
import unicodedata
import logging
import csv
import time


class Model():
    def __init__(self):
        # 请求头
        self.headers = {
            'User-Agent': 'Mozilla/5.o (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'
        }
        # 存放每一步电影的id和imdb的id
        self.movie_dct = {}
        # 存放已经处理完的movie id
        self.white_lst = []
        # 电影详情的初始url
        self.url = 'https://www.imdb.com/title/'
        self.movie_csv_path = '../ml-latest-small/links.csv'
        # 海报的保存路径
        self.poster_save_path = './poster'
        # 电影信息的保存文件
        self.info_save_path = './info/info.csv'
        # logging的配置,记录运行日志
        logging.basicConfig(filename="run.log", filemode="a+", format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
                            datefmt="%Y-%m-%d %H:%M:%S", level=logging.INFO)
        # 表示当前处理的电影
        self.cur_movie_id = None
        self.cur_imdb_id = None

    def get_white_lst(self):
        '''获取处理完的白名单'''
        with open('white_list') as fb:
            for line in fb:
                line = line.strip()
                self.white_lst.append(line)

    def get_movie_id(self):
        '''获取电影的id和imdb的id'''
        with open(self.movie_csv_path) as fb:
            fb.readline()
            for line in fb:
                line = line.strip()
                line = line.split(',')
                # 电影id 对应 imdbid
                self.movie_dct[line[0]] = line[1]

    def update_white_lst(self, movie_id):
        '''更新白名单'''
        with open('white_list', 'a+') as fb:
            fb.write(movie_id + '\n')

    def update_black_lst(self, movie_id, msg=''):
        with open('black_list.txt', 'a+') as fb:
            # 写入movie id 和imdb id,并且加上错误原因
            # msg=1是URL失效,msg=2是电影没有海报
            fb.write(movie_id + ' ' + self.movie_dct[movie_id] + ' ' + msg + '\n')

    def get_url_response(self, url):
        '''访问网页请求,返回response'''
        logging.info(f'get {url}')
        i = 0
        # 超时重传,最多5次
        while i < 5:
            try:
                response = requests.get(url, timeout=6)
                if response.status_code == 200:
                    logging.info(f'get {url} sucess')
                    # 正常获取,直接返回
                    return response
                # 如果状态码不对,获取失败,返回None,不再尝试
                logging.error(f'get {url} status_code error: {response.status_code} movie_id is {self.cur_movie_id}')
                return None
            except requests.RequestException:
                # 如果超时
                logging.error(f'get {url} error, try to restart {i + 1}')
                i += 1
        # 重试5次都失败,返回None
        return None

    def process_html(self, html):
        '''解析html,获取海报,电影信息'''
        soup = BeautifulSoup(html, 'lxml')
        # 名字和发布日期 如:Toy Story (1995)
        name = soup.find(class_='title_wrapper').h1.get_text()
        # 去掉html的一些/x20等空白符
        name = unicodedata.normalize('NFKC', name)
        # print(name)
        poster_url = ''
        try:
            # 海报的URL
            poster_url = soup.find(class_='poster').a.img['src']
            poster_re = self.get_url_response(poster_url)
            # 保存图片
            self.save_poster(self.cur_movie_id, poster_re.content)
        except AttributeError as e:
            # 如果没有海报链接,那么在黑名单中更新它
            # msg=2表示没有海报链接
            self.update_black_lst(self.cur_movie_id, '2')

        # 电影的基本信息   1h 21min | Animation, Adventure, Comedy | 21 March 1996 (Germany)
        info = []
        try:
            # 时长时间
            info.append(soup.find(class_='subtext').time.get_text().strip())
        except AttributeError as e:
            # 没有则添加空字符串
            info.append('')

        # 基本信息和详细发布时间 Animation, Adventure, Comedy | 21 March 1996 (Germany)
        for tag in soup.find(class_='subtext').find_all('a'):
            info.append(tag.get_text().strip())
        # 简介
        intro = soup.find(class_='summary_text').get_text().strip()
        intro = unicodedata.normalize('NFKC', intro)
        # 卡司。D W S C,分别表示 导演,编剧,明星,导演
        case_dict = {'D': [], 'W': [], 'S': [], 'C': []}
        for i, tags in enumerate(soup.find_all(class_='credit_summary_item')):
            for h4 in tags.find_all('h4'):
                title = h4.get_text()
                ch = title[0]
                for _, a in enumerate(h4.next_siblings):
                    if a.name == 'a':
                        case_dict[ch].append(a.get_text())

        for k, v in case_dict.items():
            # 去掉多余的信息,只保留关键人名。
            # 例如Pete Docter (original story by) | 6 more credits »。我们不需要|后面的字符
            if v and (v[-1].find('credit') != -1 or v[-1].find('full cast') != -1):
                case_dict[k] = case_dict[k][:-1]

        # 有时候导演名会用Creator代替
        if 'C' in case_dict.keys():
            case_dict['D'].extend(case_dict['C'])
        # id,电影名称,海报链接,时长,类型,发行时间,简介,导演,编剧,演员
        detail = [self.cur_movie_id, name, poster_url, info[0], '|'.join(info[1:-1]),
                  info[-1], intro,
                  '|'.join(case_dict['D']), '|'.join(case_dict['W']), '|'.join(case_dict['S'])]
        self.save_info(detail)

    def save_poster(self, movie_id, content):
        with open(f'{self.poster_save_path}/{movie_id}.jpg', 'wb') as fb:
            fb.write(content)

    def save_info(self, detail):
    	# 存储到CSV文件中
        with open(f'{self.info_save_path}', 'a+', encoding='utf-8', newline='') as fb:
            writer = csv.writer(fb)
            writer.writerow(detail)

    def run(self):
        # 开始爬取信息
        # 先读入文件
        self.get_white_lst()
        self.get_movie_id()
        for movie_id, imdb_id in self.movie_dct.items():
            if movie_id in self.white_lst:
                continue
            self.cur_movie_id = movie_id
            self.cur_imdb_id = imdb_id
            # 休眠,防止被封IP,大概3秒处理完一部电影的信息,如果注释掉,会减少大约2.5小时的运行时间
            # IMDB好像没有反爬机制,可以放心的注释掉
            time.sleep(1)
            response = self.get_url_response(self.url + 'tt' + self.cur_imdb_id)
            # 找不到电影详情页的url,或者超时,则仅仅保留id,之后再用另一个脚本处理
            if response == None:
                self.save_info([self.cur_movie_id, '' * 9])
                # 仍然更新白名单,避免重复爬取这些失败的电影
                self.update_white_lst(self.cur_movie_id)
                # 更新黑名单,爬完之后用另一个脚本再处理
                self.update_black_lst(self.cur_movie_id, '1')
                continue
            # 处理电影详情信息
            self.process_html(response.content)
            # 处理完成,增加movie id到白名单中
            self.update_white_lst(self.cur_movie_id)
            logging.info(f'process movie {self.cur_movie_id} success')


if __name__ == '__main__':
    s = Model()
    s.run()

运行时间

一部电影爬取大概要3~5秒的时间,因为中间额外的保存海报,所以会花1到2s的时间,写日志、写文件也需要一些时间。
最快时间: 9700 ∗ 3 / 3600 ≈ 8 h 9700*3/3600 \approx 8h 97003/36008h 所以最好放到云服务器上面跑。如果你会多进程加速的话也可以试着改进一下(顺便贴在评论教我一下😄)

百度网盘链接

ok看到这里,如果你不想花那么多时间自己跑,我提供我运行后的文件和代码,你可以直接使用
下图中的info.csv和poster,分别是电影的详细信息和电影的海报
在这里插入图片描述
海报的命名都是用imdbID来标识的
在这里插入图片描述
链接:https://pan.baidu.com/s/1qByOgO0sisL-lkn1hzsM-g
提取码:nd8b
复制这段内容后打开百度网盘手机App,操作更方便哦

  • 41
    点赞
  • 127
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值