追剧助手from IMDB

2024-01-23 Views Pyhton | AI2581字12 min read

使用AI工具完成代码编写,实现从IMDB网站抓取信息。

1. 前言

今年是AI元年,自Chat GPT之后,国内各大互联网公司也推出了自家的AI大语言模型。我连Python编程的函数和语法都没入门,日常也就使用VBA辅助来处理工作中的重复事项,所以今天分享的代码完全来自于AI所写。
从上学的时候养成了看美剧的习惯,入门美剧是超人前传和神盾局特工。以前Series Guide还能正常使用的时候,一直用来记录追剧进度,后来也用过人人开发的软件。但是到现在就一直没有心水的软件,要么就是功能对于我来说不够轻量,要么就是用不上的功能太多。所以一直是在便签中记录。乘着AI的风,索性奴役它来为我服务一次。

2. 软件对比

这次写追剧助手前,分别试用了文心一言和通义千问一段时间,过程中也发现两家的模型各有优劣,下面就我个人使用体验分享一下。

对比项目 文心一言 通义千问
数据同步 多端同步,相互可以打开记录 网页和app无法互通
软件体验 有社区性质,安装包较大 功能专注,安装包小
上下文理解 有时候会忘了之前的对话,连续性差点,对话的时候直接提示前文会改善 连续性较好,无提示也能连续
代码质量 注释和代码常混到一行,需手动换行 注释和代码完美分开
对话字符限制 网页和app均为2000字符 网页10000字符,app 2000字符

3. 正文代码

  • 库的导入。在代码的前六行是引用了Python库,官方库安装速度较慢,甚至有时候连不上,所以安装时可以使用镜像源代替,比如清华源。
    安装单个库:
pip install re -i https://pypi.tuna.tsinghua.edu.cn/simple

安装多个库:

pip install bs4 datetime requests wcwidth pytz -i https://pypi.tuna.tsinghua.edu.cn/simple
  • 程序使用方法:
    代码第一段tv_shows字典存储的是每部美剧的信息,包括名称、IMDB ID和已观看剧集。
    修改tv_show字典中的内容,watched_episodes的格式可是是S01E01也可以写成S1E1,但是必须是大写。新剧可以写成S01E00形式。
  • 完整代码:
import requests
import re
from bs4 import BeautifulSoup
from datetime import datetime
import wcwidth
from pytz import timezone

# 创建一个字典来存储美剧信息
tv_shows = {
    "IronHeart钢铁之心": {"imdb_id": "tt13623126", "watched_episodes": "S01E00"},
    "Loki洛基": {"imdb_id": "tt9140554", "watched_episodes": "S02E06"},
    "What If假如": {"imdb_id": "tt10168312", "watched_episodes": "S02E03"},
    "Halo光环": {"imdb_id": "tt2934286", "watched_episodes": "S01E09"},
    "Superman & Lois超人与露易丝": {"imdb_id": "tt11192306", "watched_episodes": "S03E13"},
    "The Boys黑袍纠察队": {"imdb_id": "tt1190634", "watched_episodes": "S03E08"},
    "3 Body Problem三体": {"imdb_id": "tt13016388", "watched_episodes": "S01E00"}
}

def get_episode_info(imdb_id, watched_episodes):
    # 构造请求URL,用于获取指定IMDb ID的剧集信息
    episode_url = f"https://www.imdb.com/title/{imdb_id}/episodes"
    
    # 设置请求头,模拟浏览器发送请求
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
    }
    
    # 发送请求并获取响应
    response = requests.get(episode_url, headers=headers)
    
    # 检查请求是否成功(状态码为200表示成功)
    if response.status_code == 200:
        # 使用BeautifulSoup解析HTML响应
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # 从HTML中提取所有季节编号
        episode_elements = soup.select('.ipc-tab')

        # 创建一个新的字典来存储季节编号
        print("\n")
        season_numbers = {}
        for element in episode_elements:
            html_fragment = str(element)
            matches = re.findall(r'<li.*?>(\d+)</li>', html_fragment)
            if matches:
                for season_number in matches:
                    season_numbers[season_number] = None  # 这里你可以根据需要存储其他相关信息
                    print(f"{show}的季: {season_number}")

        # 初始化一个列表,用于存储剧集信息
        episode_infos = []
        
        # 打印表格标题
        print(f"Show\t\tIMDb ID\t\tWatched\t\tEpisodes\tRelease Date")

        # 标记是否已经找到已观看的剧集
        found_watched_episode = False
        
        # 遍历所有季节
        for season_number in sorted(season_numbers.keys(), reverse=True):
            if found_watched_episode:
                break
            
            # 构造请求URL,用于获取指定季节的剧集信息
            season_episode_url = f"https://www.imdb.com/title/{imdb_id}/episodes/?season={season_number}"
            
            # 发送请求并获取响应
            season_response = requests.get(season_episode_url, headers=headers)

            # 检查请求是否成功(状态码为200表示成功)
            if season_response.status_code == 200:
                # 使用BeautifulSoup解析HTML响应
                season_soup = BeautifulSoup(season_response.text, 'html.parser')
                
                # 从HTML中提取所有剧集的日期和编号
                episodes_dates = season_soup.select('article > div > div > div > div > span')
                episodes = season_soup.select('article > div > div > div > div > h4 > div > a > div')

                # 遍历所有剧集
                for episode_date, episode in zip(episodes_dates, episodes):
                    html_fragment = str(episode)
                    matches = re.findall(r'S(\d+)\.E(\d+)', html_fragment)
                    if matches:
                        season_number_str, episode_number_str = matches[0]
                        season_number = int(season_number_str)
                        episode_number = int(episode_number_str)

                        # 提取已观看剧集的季节和编号
                        watched_season, watched_episode = watched_episodes.replace("S", "").replace("E", ".").split('.')
                        watched_season_num = int(watched_season)
                        watched_episode_num = int(watched_episode)

                        # 判断当前剧集是否在已观看剧集之后
                        if watched_season_num < season_number or (watched_season_num == season_number and watched_episode_num < episode_number):
                            episode_date_text = episode_date.text.strip()
                            
                            try:
                                # 将剧集日期字符串转换为日期对象(假设日期是美国洛杉矶时间)
                                episode_date_obj = datetime.strptime(episode_date_text, "%a, %b %d, %Y")
                                
                                # 将美国洛杉矶时间转换为UTC时间
                                episode_date_utc = episode_date_obj.astimezone(timezone('US/Pacific'))

                                # 将UTC时间转换为北京时间
                                episode_date_bj = episode_date_utc.astimezone(timezone('Asia/Shanghai'))
                                
                                date_str = episode_date_bj.strftime("%Y-%m-%d")

                                # 获取当前日期对象(北京时间)
                                current_date_obj = datetime.now(timezone('Asia/Shanghai'))

                                # 判断剧集日期是否早于当前日期
                                if episode_date_bj < current_date_obj:
                                    episode_info = {
                                        'show': show,
                                        'imdb_id': imdb_id,
                                        'watched_episodes': watched_episodes,
                                        'episode_number': f"S{season_number}E{episode_number}",
                                        'episode_date': date_str  # episode_date_text
                                    }
                                    
                                    # 将剧集信息添加到列表中
                                    episode_infos.append(episode_info)
                                    
                                    # 打印剧集信息
                                    print(f"{show}\t{imdb_id}\t{watched_episodes}\t\tS{season_number}E{episode_number}\t\t{date_str}")
                                    
                                    # 如果找到了已观看的剧集,停止循环
                                    if watched_season_num == season_number and watched_episode_num == episode_number:
                                        found_watched_episode = True
                                        break
                            except ValueError:
                                # 如果无法解析日期或仅包含年份,跳过此剧集
                                pass

            else:
                # 如果请求失败,打印错误信息
                print(f"请求失败,状态码:{season_response.status_code}")

        return episode_infos
    else:
        # 如果请求失败,打印错误信息
        print(f"请求失败,状态码:{response.status_code}")
        return None

# 获取并更新剧集信息
all_episode_infos = {}
for show in tv_shows.keys():
    episode_infos = get_episode_info(tv_shows[show]["imdb_id"], tv_shows[show]["watched_episodes"])
    if episode_infos:
        all_episode_infos[show] = episode_infos

# 计算所有剧名的最大宽度(考虑多字节字符)
max_show_width = max(sum(wcwidth.wcwidth(c) for c in show) for show in tv_shows.keys())

print('\n\n\n追剧进度: ')
print(f"{'Show':<{max_show_width}}\tIMDb ID\t\tWatched\t\tStatus")

# 统计展示
for show in tv_shows.keys():
    if show in all_episode_infos:
        unwatched_episodes = len(all_episode_infos[show])
        status = f"待观看{unwatched_episodes}集"
    else:
        status = "未更新"

    # 计算剧名的填充空格数量
    padding = ' ' * (max_show_width - sum(wcwidth.wcwidth(c) for c in show))
    
    # 打印剧集统计信息
    print(f"{show}{padding}\t{tv_shows[show]['imdb_id']}\t{tv_shows[show]['watched_episodes']}\t\t{status}")

4. 安卓运行

写助手的初衷也是多平台都可以通用,PC端运行Python程序比较方便,安卓平台目前使用了Pydroid 3运行程序。Pydroid 3目前安装版本为5.00,有需要可以从下面的链接自取。

下载地址:https://www.alipan.com/s/trsJpuRV9uB
提取码: 8lq0

运行结果参考:

IronHeart钢铁之心的季: 1
Show            IMDb ID         Watched         Episodes        Release Date


Loki洛基的季: 1
Loki洛基的季: 2
Show            IMDb ID         Watched         Episodes        Release Date


What If假如的季: 1
What If假如的季: 2
What If假如的季: 3
Show            IMDb ID         Watched         Episodes        Release Date
What If假如     tt10168312      S02E03          S2E4            2023-12-25
What If假如     tt10168312      S02E03          S2E5            2023-12-26
What If假如     tt10168312      S02E03          S2E6            2023-12-27
What If假如     tt10168312      S02E03          S2E7            2023-12-28
What If假如     tt10168312      S02E03          S2E8            2023-12-29


Halo光环的季: 1
Halo光环的季: 2
Show            IMDb ID         Watched         Episodes        Release Date


Superman & Lois超人与露易丝的季: 1
Superman & Lois超人与露易丝的季: 2
Superman & Lois超人与露易丝的季: 3
Superman & Lois超人与露易丝的季: 4
Show            IMDb ID         Watched         Episodes        Release Date


The Boys黑袍纠察队的季: 1
The Boys黑袍纠察队的季: 2
The Boys黑袍纠察队的季: 3
The Boys黑袍纠察队的季: 4
Show            IMDb ID         Watched         Episodes        Release Date


3 Body Problem三体的季: 1
Show            IMDb ID         Watched         Episodes        Release Date



追剧进度:
Show                            IMDb ID         Watched         Status
IronHeart钢铁之心               tt13623126      S01E00          未更新
Loki洛基                        tt9140554       S02E06          未更新
What If假如                     tt10168312      S02E03          待观看5集
Halo光环                        tt2934286       S01E09          未更新
Superman & Lois超人与露易丝     tt11192306      S03E13          未更新
The Boys黑袍纠察队              tt1190634       S03E08          未更新
3 Body Problem三体              tt13016388      S01E00          未更新

[Program finished]

最后的追剧进度在控制台打印出来是对齐的,因为等宽字体的原因。

本来计划要在最后一列展示今天以后下一集的播放日期,但是奴役了通义千问一个晚上也还差点意思,所以看看后面能不能补充咯。

5. 后记

  • Q:为什么没有采用多线程或者异步请求加快程序运行速度?
    A:首先是技术限制,作为Python小白我不会写,即便是让通义千问写出来,我也不知道是否适用。
    其次是网络上一直提爬网站容易封IP,所以虽然运行速度慢,但作为自用也足够了,比上网站一部一部查快多了。
  • Q:程序能否长期稳定使用?
    A:取决于IMDB网站结构是否变化,或者对爬取网站有了什么新的要求。

总的来说,个人认为大语言模型的推出比什么元宇宙融合概念更具有跨时代的意义,也让技术小白在娱乐和工作中更加自如。搜索效率比百度等网页搜索更高(语言类);让不懂编程的人也可以把自己的想法变成现实;以前写文章材料,需要绞尽脑汁去用优雅的语言润色,现在甩给AI就可以得到答案。总的来说,搞文字它是在行的。
不过它们的出现是不是会限制个人的思考能力和再学习能力,相信最终时间会给出答案。

2023年12月29日

EOF