Python爬虫实战:爱奇艺番剧更新日历制作全攻略
2026/9/13 20:03:44 网站建设 项目流程

1. 项目概述与追番场景拆解

先说说我为什么会对这个项目产生兴趣。

作为一个追番多年的老二次元,我每个月最头疼的事情就是“到底更新到哪一集了”。以前全靠微博超话、贴吧置顶、甚至朋友之间互相提醒,信息散得到处都是。后来我实在受不了,决定用自己吃饭的家伙—— Python 爬虫——把这个需求彻底解决掉。于是就有了这个项目:aqy番剧更新日历爬虫

先给不熟悉的朋友解释一下这个项目到底做了什么。

简单来说,它做的事情就三件:

  1. 从 aqy 网站上抓取番剧的更新信息(包括剧名、更新集数、更新时间等)。
  2. 把这些信息按照“星期几更新”的方式重新组织,形成一张清晰的周更日历。
  3. 用最简单的方式把结果展示出来,让追番的人一眼就能看到今天有什么新番更新。

这个项目解决的核心痛点是:追番党每天都要重复打开好几个平台、来回搜索“某部番更新了吗”,非常浪费时间。而有了这个爬虫之后,只需要跑一次脚本,就能拿到本周所有番剧的更新计划,不仅高效,而且数据一目了然。

如果你目前正处于 Python 入门阶段、或者刚学会 requests 库但手痒想找个真实项目练手,又或者你本身就是一个爱追番的程序员,那么这个项目会非常适合你。它不需要复杂的框架,不依赖 Scrapy 、不依赖数据库,只要 Python 环境能跑起来,你就能在半小时内看到效果。

我在写这个爬虫的时候,考虑的不只是“能不能抓到数据”,还考虑了“如果网站改版了怎么办”“如果请求频率太高被限制了怎么办”“数据怎么展示才更方便自己使用”这些实际问题。也正是因为这些考虑,这个项目从单纯的爬虫脚本,逐渐变成了一个完整的“个人追番数据工具”。

下面我会把整个项目的设计思路、核心代码、实操过程、遇到的各种坑,全部拆开揉碎讲一遍。如果你也想做一个属于自己的追番日历,这篇文章可以直接当教程来用。

2. 技术选型与整体设计思路

2.1 为什么用 requests 而不是 Scrapy

可能有人会问:既然做爬虫,为什么不直接用 Scrapy 这种框架?这里我说一下我的实际想法。

Scrapy 确实强大,有并发、有中间件、有自带的选择器,适合做大规模分布式采集。但对于这个项目来说,它属于典型的“杀鸡用牛刀”。我们只需要抓一个站的番剧列表,数据量撑死几十条,完全没有必要引入一个重型框架。

requests的好处是:足够轻量,逻辑直观,而且可以非常灵活地处理 response。尤其是当我们需要测试某个接口、临时调试某个字段的时候,直接写 5 行代码就能跑一次请求,根本不用去折腾 Scrapy 的项目结构。

另外,用 requests 还有一个隐藏的好处——对于初学者来说,整个请求流程是可见的。你能清晰地看到 headers 怎么构造、cookies 怎么传递、数据从哪里来。这种“每一步都在自己掌控之中”的感觉,对建立爬虫的直观理解特别重要。

当然,如果你后续想把这个爬虫扩展成抓取多个平台、多部番剧的工具,那时候再考虑迁到 Scrapy 也不迟。项目起步阶段,保持简单才是王道。

2.2 数据来源分析与接口定位

很多人第一次做爬虫的时候,第一反应是去解析网页的 HTML。其实这是一个思维定势。现在的主流网站,尤其是视频平台,绝大多数数据都是通过接口(API)动态加载的。你去解析 HTML 的话,往往只能得到一个空壳,真正的数据都是 JS 异步加载进来的。

所以我一上来就做了两件事:

第一,打开 aqy 的番剧频道页面,按 F12 打开开发者工具。 第二,切换到 Network 选项卡,刷新页面,然后观察有哪些网络请求是返回 JSON 数据的。

果然,没花多久就找到了一个非常关键的接口。它的返回结构里包含了这个频道页推荐的所有番剧信息,而且每个条目都带有seriesepisodeupdateTime之类的字段。看到这里基本就可以确定,这个接口就是我们要找的数据源

找到接口之后,还需要仔细分析它的参数。通常这类接口会有分页参数、类型参数、排序参数等。我这次的场景比较简单:只需要把默认页面的热门番剧信息抓下来就够用了,所以没有做额外的翻页处理。

2.3 数据清洗、结构化存储与去重的思路

爬虫拿到原始数据之后,并不能直接拿来进行展示。原因很简单:接口返回的字段往往是嵌套结构,而且很多字段我们根本用不到。

举个例子,接口可能返回:

{ "id": 12345, "name": "某部番剧", "videoInfo": { "series": 12, "duration": 1450 }, "release": { "time": "2024-06-14 10:00:00" } }

这里面可能还有几十个别的字段,但我们真正关心的其实就是“剧名”“更新集数”“更新时间”。所以我单独写了一个parse_data函数,专门负责把原始 JSON 转换成干净的 Python 字典列表。这个步骤我称之为数据清洗。

清洗之后,我还会做一次去重。因为频道页接口有时候会有重复数据,如果不处理,最终日历上会同一部番出现好几遍,看起来很乱。这里用的方法是维护一个 set,以番剧 ID 为 key,重复的直接跳过。

清洗和去重做好之后,再把数据按照星期分组,存到最终的字典结构里。这一步就是后面生成更新日历的基础。

3. 核心代码实现与关键细节解析

3.1 环境准备与依赖安装

这个项目不需要安装任何花哨的第三方库。Python 版本建议 3.8 以上,主要依赖只有两个:

pip install requests

如果你打算最后用表格方式打印输出,还可以顺便装一个tabulate,不过也不用强制,后面我会说怎么用纯内置库实现。

确保你的 Python 环境没有问题:

python --version

如果这一步提示找不到命令,那你需要先把 Python 加到系统环境变量里。Windows 用户在安装 Python 时记得勾选“Add Python to PATH”。

3.2 请求模块:构造 headers 与获取数据

爬虫的第一步是模拟浏览器发出请求。但如果你直接裸用 Python 去请求,很多网站会选择拒绝你,因为他们识别到了“这不是一个正常用户”。

怎么解决?最简单的方法就是构造一个看起来正常的 User-Agent。我这里用的是 Chrome 浏览器的 UA:

import requests HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Referer": "https://www.iqiyi.com/dongman/" } def fetch_data(url): resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() return resp.json()

这里有两个容易被忽略但很重要的点:

第一,timeout一定要设置。如果不设置的话,当请求卡住时,你的脚本可能会无限期地等下去,非常被动。设成 10 秒就够用了。

第二,Referer字段在某些网站上会成为判断请求是否合法的依据之一。加上它,相当于告诉服务器“我是从你网站首页跳转过来的”,能有效降低被拦截的风险。

3.3 解析模块:从嵌套结构里提取关键信息

拿到 JSON 数据之后,接下来的任务就是从中提取出我们需要的字段。这一步我用的是纯字典操作,因为 requests 返回的 JSON 已经自动转换成了 Python 字典和列表。

先简化一下逻辑,假设我们拿到的是这样的结构:

{ "data": { "list": [ { "id": 1001, "title": "某部番剧", "play": { "latest": "第5集", "updateTime": "2024-06-15" } } ] } }

那么对应的解析函数可以这样写:

def parse_data(raw_data): results = [] items = raw_data.get("data", {}).get("list", []) for item in items: vid = item.get("id") title = item.get("title") latest = item.get("play", {}).get("latest") update_time = item.get("play", {}).get("updateTime") if not all([vid, title, update_time]): continue results.append({ "id": vid, "title": title, "latest": latest, "update_time": update_time }) return results

这段代码的核心思路是“逐层剥取”。先拿到data.list这个列表,然后遍历每个元素,再通过嵌套.get()的方式一层层取出想要的值。

注意这里我用了.get()而不是直接item["title"]。区别在于:当字段不存在的时候,.get()会返回 None,而直接下标访问会抛异常。实际抓取过程中,数据结构往往会因为各种原因产生缺失,用.get()可以保证程序不崩溃。

3.4 日历化:把时间信息转换为星期维度

数据清洗完成后,最关键的一步来了:把更新时间转换为星期几,并按星期分组。

Python 的标准库datetime提供了非常方便的方法来处理这个问题。我先把时间字符串解析成 datetime 对象,然后用weekday()方法得到对应的星期编号(0 是周一,6 是周日),再把这个编号映射成中文星期名。

from datetime import datetime WEEKDAY_MAP = { 0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日" } def build_calendar(anime_list): calendar = {day: [] for day in WEEKDAY_MAP.values()} for anime in anime_list: try: dt = datetime.strptime(anime["update_time"], "%Y-%m-%d") except (ValueError, TypeError): continue weekday = WEEKDAY_MAP[dt.weekday()] calendar[weekday].append(anime) return calendar

这里有一个值得说的小细节:strptime的格式字符串必须和实际时间的格式完全匹配。如果接口返回的是“2024/06/15”这种格式,你就不能再用%Y-%m-%d,而是要用%Y/%m/%d。最好的办法是先把返回的字符串打印出来看一眼,再去写格式,否则很容易掉进ValueError的坑。

3.5 展示模块:不用第三方库也能做表格

数据整理好了,最后要解决的是展示问题。

最直观的方式当然是把结果按星期分组打印出来。如果你不想安装额外的库,使用 Python 内置的字符串格式化方法就够了:

def print_calendar(calendar): for day, anime_list in calendar.items(): print(f"\n===== {day} =====") for anime in anime_list: latest = anime.get("latest", "未知") print(f" {anime['title']} - {latest}")

如果是写给自己看,这样已经相当清爽了。但我个人更喜欢用tabulate,因为它可以自动对齐列宽,输出效果更像一张表格:

from tabulate import tabulate def print_calendar_table(calendar): rows = [] for day, anime_list in calendar.items(): for anime in anime_list: rows.append([day, anime["title"], anime.get("latest", "未知")]) headers = ["星期", "番剧名称", "最新集数"] print(tabulate(rows, headers=headers, tablefmt="grid"))

两种方式我都用过很久,纯内置的方法轻量,tabulate 的方式好看。你可以根据喜好选择。

4. 完整实操过程与运行效果

4.1 整理代码项目结构

为了让代码结构清晰一点,我没有把所有内容堆在同一个文件里,而是用一个简单的模块化组织:

anime_calendar/ ├── main.py # 入口文件 ├── fetcher.py # 负责请求接口 ├── parser.py # 负责解析数据 └── calendar.py # 负责生成日历与展示

这样划分的好处是,每个文件只承担一个职责。后续如果接口换了,只需要改fetcher.py;如果数据字段变了,只需要改parser.py。这也是实际项目中常用的分层思想,只是规模小一点而已。

当然,对于一个快速脚本来说,你也可以把全部代码放进一个main.py。我建议第一次练手的人直接写单文件,步骤更少,出现导入问题时也更容易排查。

4.2 带异常处理与重试机制的主流程

爬虫项目里,异常处理非常关键。网络请求随时可能失败,服务器也可能偶尔返回 5xx 错误。为了让脚本更健壮,我在主流程里加入了简单的重试机制:

import time from fetcher import fetch_data from parser import parse_data from calendargram import build_calendar, print_calendar_table def main(): url = "https://接口地址" for attempt in range(3): try: raw_data = fetch_data(url) break except Exception as e: print(f"请求失败:{e},第 {attempt + 1} 次重试") time.sleep(2) else: print("请求多次失败,请检查网络或接口地址") return anime_list = parse_data(raw_data) calendar = build_calendar(anime_list) print_calendar_table(calendar) if __name__ == "__main__": main()

这段代码有个地方值得说一下:for...else...结构。当循环里没有触发 break 时,else 块会执行。也就是说,如果三次请求都失败了,就会进入最后的错误提示分支。

这种写法比设置一个success标志位要简洁不少,也是在真实爬虫项目里比较常见的一种重试写法。

4.3 运行效果预览

假设脚本运行成功,打印结果大概是这样的:

+--------+----------------------+--------------+ | 星期 | 番剧名称 | 最新集数 | +========+======================+==============+ | 周一 | 威力工坊 | 第8集 | +--------+----------------------+--------------+ | 周一 | 大战斗破 | 第10集 | +--------+----------------------+--------------+ | 周四 | 星之梦语 | 第3集 | +--------+----------------------+--------------+ | 周六 | 夏日回忆簿 | 第6集 | +--------+----------------------+--------------+ | 周日 | 魔王学院 | 第12集 | +--------+----------------------+--------------+

看到这个结果的那一刻,你会觉得前面所有的代码都没白写。原本需要去网站反复刷新才能确认的更新情况,现在一条命令就全部搞定了。

5. 常见问题与排坑记录

5.1 请求返回 403 或 403 Forbidden

这是我做爬虫时遇到最多的错误,尤其在访问有反爬策略的网站时特别常见。

403 表示服务器理解了请求,但拒绝执行。出现这个错误,绝大多数情况都是因为请求头不够“拟人化”。

排查思路如下:

  • 先检查User-Agent是否设置成完整的浏览器 UA。
  • 再检查是否有RefererOrigin等字段需要补充。
  • 如果还是不行,可以尝试加上 Cookie,把浏览器里的登录态或临时会话标识复制过来。

我自己的习惯是:先在浏览器里打开目标接口,在 Network 面板里选中那个请求,然后右键选择“Copy as cURL”。接着用一些工具把 curl 命令转换成 Python requests 代码,这样可以拿到非常完整的请求头信息。当然,这种方式适合一次性排查,不推荐长期把 Cookie 硬编码在代码里。

5.2 JSON 解析时出现 KeyError 或字段缺失

JSON 结构里经常出现某个条目缺字段的情况。特别是番剧列表页,不同剧目的信息完整度可能不一样。有的有更新时间,有的可能暂时没有排期。

我前面用.get()的方式能处理好大部分场景。但有一种情况比较隐蔽:字段确实存在,但值是None或者空字符串。

比如周期性的时间字段为Nonestrptime就会报错。所以我在build_calendar里面包了一层try...except...,失败就直接跳过这条数据。宁可不展示这条信息,也不能让整个脚本崩溃。

5.3 接口返回数据为空,怎么判断是不是爬虫问题

很多时候,你发现脚本运行正常,但结果却空空如也。这时候不要急着怀疑代码,先从这几个角度排查:

  • 用浏览器直接访问接口地址,看返回的数据是否正常。
  • 如果浏览器正常但 Python 请求结果异常,八成是请求头或参数有差异。
  • 还有一种可能:不同地区、不同账号状态看到的推荐内容不一样。这会导致接口参数相同但结果不同。

我这里补充一个比较实用的调试技巧:把接口返回的原始 JSON 保存到本地文件,然后再单独写一个 Python 脚本来做解析调试。这样就不需要每次跑脚本都去请求一次接口,既省时间又能快速定位解析问题。

5.4 被临时限流或封禁 IP 的处理经验

如果你短时间内对网站发起大量请求,很容易触发限流。表现就是前几次请求正常,后面突然全部超时或者被要求验证。

我的处理办法很简单:

  • 降低请求频率,在每次请求之间加上time.sleep(随机时间)
  • 做好可配置化,比如把请求间隔写成一个变量,方便随时调。
  • 如果必须大量抓取,建议做好多代理 IP 的支持,但本项目数据量很小,完全没有这个必要。

从做个人小工具的角度来看,我强烈不建议对这个项目做大规模的并发请求。我们只需要每天跑一次,根本不会对服务器造成任何压力,抱着“够用就好”的心态去做这个项目是最合适的。

6. 往更深层的方向思考

6.1 这个项目能扩展出什么

这个爬虫虽然简单,但它的架构可以扩展出很多有意思的功能。

比如,你可以把抓取的数据保存到 SQLite 数据库里,做一个长期积累的“追番历史记录”。再比如,你可以接入一个定时任务,每天早上自动运行一次,然后把更新日历推送到自己的微信或邮件里。这样连手动运行脚本这一步都省了。

我后来还做了一个小改动:把结果输出成 HTML 格式,然后用浏览器打开,做成了一个自用的“追番主页”。视觉上比黑底白字的控制台又舒服了不少。

6.2 对 Python 爬虫学习的建议

爬虫是 Python 里最适合快速获得成就感的领域之一,因为它把“数据获取”的过程变得可感知、可交互。但正因为它有趣,新手反而容易走偏:

  • 不要只盯着一个网站猛抓,多换几个不同类型的站点,理解不同的反爬策略。
  • 永远把“合法合规”放在第一位。个人学习爬虫没问题,但不要抓取用户隐私数据,也不要对目标网站造成访问压力。
  • 多做数据清洗和存储的训练。爬虫的核心难点往往不在“爬”,而在“处理”。

最后再说一个我自己的体会:最开始写这个爬虫的时候,我也只是抱着“试一下”的心态,没想到最后不仅解决了自己追番的痛点,还顺带复习了一遍 requests 的用法、JSON 解析的技巧、时间处理的细节,甚至养成了一看到接口就条件反射去分析的职业习惯。

追番只是一个小场景,但把这个场景里的“数据思维”通用化之后,你会发现 Python 爬虫能做的事情远比想象中多。希望这篇文章能给你带来一些启发和参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询