ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python爬虫实战:爱奇艺番剧更新日历制作全攻略

Python爬虫实战:爱奇艺番剧更新日历制作全攻略 1. 项目概述与追番场景拆解先说说我为什么会对这个项目产生兴趣。作为一个追番多年的老二次元我每个月最头疼的事情就是“到底更新到哪一集了”。以前全靠微博超话、贴吧置顶、甚至朋友之间互相提醒信息散得到处都是。后来我实在受不了决定用自己吃饭的家伙—— Python 爬虫——把这个需求彻底解决掉。于是就有了这个项目aqy番剧更新日历爬虫。先给不熟悉的朋友解释一下这个项目到底做了什么。简单来说它做的事情就三件从 aqy 网站上抓取番剧的更新信息包括剧名、更新集数、更新时间等。把这些信息按照“星期几更新”的方式重新组织形成一张清晰的周更日历。用最简单的方式把结果展示出来让追番的人一眼就能看到今天有什么新番更新。这个项目解决的核心痛点是追番党每天都要重复打开好几个平台、来回搜索“某部番更新了吗”非常浪费时间。而有了这个爬虫之后只需要跑一次脚本就能拿到本周所有番剧的更新计划不仅高效而且数据一目了然。如果你目前正处于 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 数据的。果然没花多久就找到了一个非常关键的接口。它的返回结构里包含了这个频道页推荐的所有番剧信息而且每个条目都带有series、episode、updateTime之类的字段。看到这里基本就可以确定这个接口就是我们要找的数据源。找到接口之后还需要仔细分析它的参数。通常这类接口会有分页参数、类型参数、排序参数等。我这次的场景比较简单只需要把默认页面的热门番剧信息抓下来就够用了所以没有做额外的翻页处理。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 浏览器的 UAimport 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, headersHEADERS, timeout10) 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, headersheaders, tablefmtgrid))两种方式我都用过很久纯内置的方法轻量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。再检查是否有Referer、Origin等字段需要补充。如果还是不行可以尝试加上 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 爬虫能做的事情远比想象中多。希望这篇文章能给你带来一些启发和参考。
返回列表