ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

动漫数据爬取与分析实战:从公开API到可视化呈现

动漫数据爬取与分析实战:从公开API到可视化呈现 做动漫数据爬取与分析这个项目起初并不是为了搞一个多复杂的数据平台。我在社区里看到太多人争论“这几年新番质量是不是下降了”“热血番是不是比恋爱番更容易拿高分”吵到最后谁都没数据。于是我就想干脆把动画条目、评分、类型、放送年份这些公开元数据抓下来做一轮统计和可视化让结论用数据说话。这个项目适合两类人一类是刚学完 Python 基础想练爬虫和数据分析但不知道拿什么练手的人另一类是追番多年、想看看全局趋势的爱好者。整个项目下来你会经历选数据源、写请求、解析字段、清洗入库、做分析、画图表这条完整链路能力上相当于把 requests、pandas、pyecharts、SQLite 这几个常用工具串起来实战了一遍。下面我从头开始说尽量把每一步为什么这么做的逻辑也讲清楚。1. 项目目标与整体设计思路1.1 想做这个项目先想清楚要回答什么问题“爬动漫数据”这个说法其实很模糊。动漫可以拆成动画和漫画动画里面又有 TV 动画、剧场版、OVA 之分。如果不把目标问题定义清楚后面的数据抓下来也会是一堆乱麻。我当时给自己定的问题是三个从 2000 年到最近一年新番数量整体走势是怎样的是不是真的“一年比一年多”不同题材热血、恋爱、奇幻、日常之间平均评分有没有明显差异评分人数能不能反映大众偏好高评分的动画通常具备哪些标签特征有没有“拿高分最多”的固定类型组合把这三个问题列出来之后整个项目的数据需求就清晰了我需要动画条目的 ID、标题、类型、放送日期、话数、评分、评分人数、标签列表。注意我刻意没有去碰视频资源、评论原文、用户头像这些内容。一方面它们体量大、解析成本高另一方面也涉及版权和使用协议。对于分析“趋势”这件事元数据已经完全够用了这也是这个项目能够在个人电脑上跑通的前提。这个设计思路其实可以迁移到很多领域。做影视分析、游戏销量分析、图书热门分析核心都是先定义“要回答什么”再反推“需要哪些字段”最后才写爬虫。我见过不少新手拿到目标站点就开始抓抓到一堆 HTML 之后发现字段不一致、日期格式乱七八糟反而浪费大量时间。所以我把“问题定义”放在整个项目的第一步这个顺序不要省。1.2 技术选型为什么是 requests pandas pyecharts 这套组合先说环境。Python 版本我用的是 3.8 以上编辑器用 VSCode 装个 Python 插件数据分析阶段切到 Jupyter Notebook 会更顺手。没装环境的朋友先到官网下载安装包安装时勾选 Add Python to PATH这样后续在命令行直接用 python 命令能省掉不少麻烦。这套环境本身没什么门槛重点是下面两个选择。采集层我选了 requests而不是 scrapy。原因是这个项目数据量在几千到几万条这个量级单机单线程加适当等待时间完全能跑完scrapy 的分布式调度、中间件这些能力在这个规模下属于过度设计。requests 足够直接代码写起来也容易理解对新手更友好。解析 HTML 我用 lxml BeautifulSoup前者快后者 API 直观两个配合是社区里最常见的组合。数据清洗和统计分析用 pandas这个不用多说按列处理缺失值、类型转换、分组聚合都是它的强项。可视化我主要用 pyecharts 做交互图表matplotlib 做静态图后面会具体说。为什么我不推荐一上来就上重型框架因为爬虫和分析两个阶段解耦之后你更容易定位问题。requests 拿到的数据是 JSON 或 HTML先存成中间文件再去跑 pandas 分析。如果分析结果不对你不必怀疑采集流程如果采集漏数据你也不必从头跑分析。这种“分段验证”的思路在实际项目里能省下很多调试时间。2. 数据源选型和采集前的准备工作2.1 数据源怎么选公开 API 优先网页解析兜底数据源是爬虫项目最容易踩坑的地方。很多人第一反应是去爬那些大而全的动画评分站点但这些站点通常反爬比较严格登录、验证码、频率限制一样不落个人学习项目很容易撞墙。我当时的取舍是优先找“提供公开接口、且允许低频访问”的数据源。我最后选的是番组计划Bangumi的开放接口它有公开的 API 文档动漫条目覆盖比较全字段也规整包括评分、评分人数、标签、放送日期这些我要的东西而且对低频的个人请求比较友好。用公开 API 的好处是响应直接返回 JSON不用去解析嵌套的 HTML字段结构一眼就能看明白开发效率比硬啃网页高很多。当然公开 API 不是万能的。有些动漫老条目字段不完整比如没有中文名、没有放送日期或者标签数量很少。这种时候我保留了“网页解析兜底”方案针对个别条目直接请求详情页用 BeautifulSoup 把缺失字段补回来。API 负责批量采集网页解析负责补漏两条腿走路。这里还要强调一个原则不要为了练技术去找需要登录、需要验证码、明显不希望被批量访问的目标。爬虫练手应该选公开、稳定的信息源既是对目标站点负责也避免把自己宝贵的练手时间耗在破解验证码上。数据源一旦选错后面所有工作都会变成赶工。我真的见过有人对着一个纯前端渲染的网站抓了一整天得到一堆空标签最后才发现数据是通过接口异步加载的。选对数据源这个项目就已经成功一半了。2.2 定义字段和分析用的原始表结构在写第一行代码之前我先把要收集的字段整理成了表格。这一步其实特别重要因为字段定义决定了后面数据清洗的工作量。我当时定下来的字段如下字段名含义类型示例subject_id动画条目唯一 IDint298name主标题str进击的巨人name_cn中文名称str进击的巨人type条目类型int2date放送日期str2013-04-07episodes话数int25score平均评分float8.8score_count评分人数int36542tags标签列表list[热血, 奇幻, 战斗]这里有几个字段需要提前想清楚。type 字段在 API 返回值里可能是一个数字含义需要查文档确认我当时把不需要的类型过滤掉只保留动画。date 字段在原始数据里可能是“2013-04-07”也可能是“未知”这种文本后续清洗压力很大。tags 是列表分析时要用 explode 展开才能做题材统计。所以建议采集阶段先保留原始值不要在半路做过多转换清洗环节再来统一处理。原始数据的落盘我建议先存一份 JSON 或者 CSV再做清洗。这样即使后续 SQLite 表结构调整原始数据还在可以随时重新跑分析流程。我自己的习惯是采集脚本只负责把数据追加写入一个 raw_anime.jsonl 文件每行一个 JSON 对象分析脚本再从文件读入 DataFrame。采集和分析两套脚本互不依赖调试起来特别省心。3. 数据采集请求、解析与落库的完整实现3.1 用公开 API 拉取动漫列表省掉大半反爬功夫选好接口之后代码实现其实比我预想的简单。以番组计划为例它的列表接口支持按类型、按热度排序分页查询我写了一段基础请求代码import requests import time API_URL https://api.bgm.tv/v0/subjects headers { User-Agent: my-anime-analysis/0.1 (personal study project) } params { type: 2, # 2 通常表示动画 sort: heat, # 按热度排序 page: 1, # 当前页码 limit: 25 # 每页数量 } resp requests.get(API_URL, paramsparams, headersheaders, timeout10) print(resp.status_code) print(resp.json())注意几个关键点。第一User-Agent 一定要设置很多接口对没有 UA 的请求会直接拒绝。第二有些接口需要在请求头里加一个客户端令牌申请流程很简单按文档创建应用拿到一个字符串填进来就行如果拿不到令牌就退回网页解析方案。第三分页参数要按文档调别想当然地写 pageSize、size不同接口起名差异很大我当时第一次就踩了“参数名不对导致永远返回第一页”的坑。我后来把请求封装成了一个带重试的函数def fetch_page(page): params[page] page for attempt in range(3): try: resp requests.get(API_URL, paramsparams, headersheaders, timeout10) if resp.status_code 200: return resp.json() else: print(f第 {page} 页请求失败状态码 {resp.status_code}) except requests.RequestException as e: print(f第 {page} 页异常{e}) time.sleep(2) return None重试间隔我设了 2 秒连续请求之间再加 0.5 到 1 秒的停顿。这个频率对个人学习项目足够安全也能把采集时间控制在可接受范围。官方虽然允许低频访问但你把请求打到对方服务器上尊重别人的资源是基本素养。采集几千条数据最多多等几分钟没必要为了省时间把速率拉满。3.2 网页解析兜底API 缺字段时怎么补API 拉下来的是规整数据但总有个别条目信息不全。这时候我写了另一个脚本专门用网页解析补字段。代码大概长这样import requests from bs4 import BeautifulSoup BASE_URL https://bgm.tv/subject/ HEADERS { User-Agent: my-anime-analysis/0.1 (personal study project) } def fetch_subject_detail(subject_id): url BASE_URL str(subject_id) resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) title_el soup.select_one(h1) title title_el.get_text(stripTrue) if title_el else info_items soup.select(li) # 具体解析逻辑根据页面结构调整一般在这里匹配“话数”“放送日”等字段 return { subject_id: subject_id, name_cn: title, }网页解析的稳定性取决于页面结构。真实项目里我建议先用浏览器开发者工具查看目标元素确认选择器能用之后再写进代码。这里有个小技巧把响应先保存成 html 文件在本地用 BeautifulSoup 反复试验选择器而不是每次都对线上发请求。这样调试速度更快也不会给对方服务器增加压力。还有一个非常容易翻车的点编码。如果页面返回的不是 UTF-8你需要手动设置 resp.encoding gbk 之类的编码否则字符串全是乱码。我习惯在拿到响应后打印前 200 个字符看一眼确认编码正常再继续解析这一步能省掉后面大量返工。3.3 请求节奏和异常重试我用过最稳的一套配置采集过程中遇到问题最多的不是解析逻辑而是网络请求本身。我总结出最顺手的配置超时设置为 10 秒重试最多 3 次重试间隔从 1 秒开始逐次递增比如 1 秒、2 秒、4 秒。这样既不会在偶发网络抖动时立刻失败也不会因为连续重试把对方服务器打到过载。另外我建议把爬取过程的所有日志输出到文件至少把失败页码记录下来。因为一次性采集几千页眼睛盯屏幕盯不过来。我在脚本里加了一个简单的失败列表跑完一轮之后把失败的页码再跑一轮直到没有失败为止。这种“增量补采”的思路比“失败一次就从头重跑”高效得多。采集完成之后我从原始列表里挑选了 3000 多条完整记录作为分析样本。样本量不用贪大关键是字段完整、来源可信。把样本导入 DataFrame 之后项目正式进入清洗和分析阶段。4. 数据清洗和存储别让脏数据污染你的分析结果4.1 清洗环节必须处理的四类问题采集回来的数据绝不是直接能用的我每次都会用下面四类问题过一遍数据。第一类型转换。原始的 date 是字符串score 可能是字符串score_count 可能带千分位逗号这些都需要统一转成正确的类型否则后面做数值运算会直接报错。 pandas 里用 pd.to_datetime 和 pd.to_numeric 处理命令很简单难在于你得先想清楚哪些字段必须转。第二缺失值处理。个别条目没有放送日期或没有评分。我的做法是评分缺失的条目保留下等统计数值时用 dropna 过滤放送日期缺失的条目如果无法从原始数据推断就直接丢掉否则按年份聚合时会多出一个“未知年份”分类干扰趋势判断。第三去重。数据源自身偶尔会返回重复条目分页拉取时也可能因为边界问题拉到上一页的最后一条。我用 subject_id 做唯一键去重drop_duplicates(subsetsubject_id) 一行搞定。第四文本归一化。标签里可能同时出现“热血”和“热血 番”或者英文标签我都统一转成小写、去掉首尾空格、再按全角半角做一次归一化。这个步骤在按标签统计时特别重要否则同一个题材会被拆成好几个分类统计结果完全失真。import pandas as pd df pd.read_json(raw_anime.jsonl, linesTrue) df[date] pd.to_datetime(df[date], errorscoerce) df[year] df[date].dt.year df[score] pd.to_numeric(df[score], errorscoerce) df[score_count] pd.to_numeric(df[score_count], errorscoerce) df df.drop_duplicates(subsetsubject_id) df df.dropna(subset[year])清洗完的数据我还做了几个 sanity check统计缺失比例、查看年份分布范围、打印前几行看看字段长什么样。不要直接跳到分析先通过肉眼确认数据基本合理再往下走。4.2 存储选型为什么我推荐 SQLite 而不是 CSV清洗后的数据最终存哪里我推荐 SQLite而不是纯 CSV。原因有三个一是 SQLite 是单文件数据库整个分析过程随手带走不需要单独安装数据库服务二是字段类型更可控查询时可以直接用 SQL 过滤比每次读 CSV 再用 pandas 过滤要方便三是当你扩展到几万条甚至几十万条数据时SQLite 的查询性能仍然足够快而 CSV 文件在反复读取、筛选时会明显变慢。建表语句大概是这样CREATE TABLE IF NOT EXISTS anime ( subject_id INTEGER PRIMARY KEY, name TEXT, name_cn TEXT, type INTEGER, date TEXT, year INTEGER, episodes INTEGER, score REAL, score_count INTEGER, tags TEXT );写入数据我用 pandas 的 to_sql 接口一句 df.to_sql(anime, conn, if_existsreplace, indexFalse) 就完成了。注意 tags 是列表SQLite 不支持数组我直接序列化成 JSON 字符串存进去分析时再解析回来。这种方式简单可靠不用引入额外的序列化框架。说白了存储方案不要整复杂。个人项目数据量级根本没到需要分布式数据库的程度SQLite 就是性价比最高的选择。等你以后真的需要多人协同、实时写入再考虑 MySQL 或 PostgreSQL 也不迟。过早引入重型组件只会增加环境配置成本对分析结果没有任何帮助。5. 数据分析与可视化从统计数字里看出门道5.1 分析维度怎么定围绕“年度、题材、评分”展开数据准备好了接下来就是最有意思的部分。我没有漫无目的地乱画图表而是对应开头那三个问题把分析拆成了四个维度年度数量趋势按 year 分组统计每个年份的动画条数看新番数量是不是逐年上涨。题材热度把 tags 字段用 explode 展开统计每个标签出现的次数找出最热门的题材方向。评分分布画评分直方图看大家的打分习惯是偏中庸还是偏极端。高分特征筛选评分前 100 的条目统计它们的标签分布回答“哪种题材最容易出高分”。这四个维度基本覆盖了“总体怎么样、什么受欢迎、质量怎么看”三个层面。做数据分析和“写需求文档”很像先定问题再定指标最后才定图表。如果你连自己要回答什么问题都不清楚画出来的图再多也只是装饰品并不解决问题。5.2 用 pandas 做聚合统计注意这几个坑实现起来其实就是几条聚合语句。下面这几个是每次分析都会用到的模板# 年度数量 year_count df.groupby(year).size() # 年度平均分 year_score df.groupby(year)[score].agg([mean, count]) # 标签热度 tag_count ( df.explode(tags) .groupby(tags) .size() .sort_values(ascendingFalse) .head(15) ) # 高分动画标签 top100 df.sort_values(score, ascendingFalse).head(100) top100_tag ( top100.explode(tags) .groupby(tags) .size() .sort_values(ascendingFalse) .head(10) )用 explode 展开列表之后再 groupby是处理标签这类“一对多”字段的标准姿势。这里有几个坑值得单独说。第一如果 tags 字段存在空列表explode 会把该行变成 NaN分析前要处理一下否则分组时会出现一个奇怪的“NaN”分类。第二groupby 之后要用 reset_index 把分组键变回普通列否则后续想 join 两个统计结果会很别扭。第三评分年度趋势这种统计一年只有个位数的样本时平均值波动非常大做结论的时候要看样本量不能只看平均分。我当时从这些数字里看到的最直观的现象是动画作品数量确实在逐年变多但近几年的平均评分并没有明显下滑在题材热度上热血、奇幻、日常这类词始终排在前列反而是很多人口中“烂大街”的题材恰恰是数量多、讨论度高的类型。这些结论单独看每一条都不算意外但放在一起就把很多“感觉”变成了可验证的客观事实。5.3 可视化输出中文字体、图表选型与发布样式数字统计完接下来画图。我先把最基础的年度数量柱状图用 matplotlib 画出来当“底线图”再用 pyecharts 做交互版本发布到自己的笔记里。先看基础版import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(12, 6)) ax.bar(year_count.index, year_count.values) ax.set_title(各年份动画数量趋势) ax.set_xlabel(年份) ax.set_ylabel(数量) plt.tight_layout() plt.savefig(year_count.png, dpi150)matplotlib 最需要关注的就是中文字体。Windows 上 SimHei 通常可用macOS 上一般用 PingFang SCLinux 上经常什么都没有。如果 run 出来是方框优先检查字体设置而不是怀疑数据。写代码前先做一行 plt.rcParams 是必须的否则后面所有图都要重画。pyecharts 的优势是生成交互式 HTML鼠标悬停能看具体数值发布到自己的博客或笔记里观感很好。它的接口风格和 matplotlib 完全不同链式调用Bar().add_xaxis().add_yaxis()第一次接触会不习惯但文档很全照着示例改数据就行。图表选型上我的经验是趋势用折线图占比用饼图数量对比用柱状图分布用直方图。别追求花哨。3D 图、动态图看着炫但可读性往往不如一张干净整洁的柱状图。分析项目最重要的是把信息传达清楚图表只是工具。6. 常见问题与排查技巧实录6.1 采集阶段最容易翻车的错误我把采集阶段遇到的问题整理成了一张速查表大半年后回头看仍然实用。现象可能原因解决办法请求返回 403User-Agent 缺失或过于常见设置完整 UA必要时添加 Referer中文乱码响应编码不是 UTF-8设置 resp.encoding resp.apparent_encoding超时频繁请求频率太高或网络波动降低频率增加重试超时设为 10 秒永远返回第一页分页参数名写错查文档确认 page 和 limit 参数名字段大量缺失条目本身不完整用网页解析兜底或直接过滤样本数据重复分页边界或源站重复用 subject_id 去重这里面最让我印象深刻的是 403。有一次我把 UA 设成了 Postman 默认值对方直接用 403 打回来换成带项目描述的 UA 之后就正常了。很多爬虫新手以为反爬是什么高科技其实大部分拦截都是因为请求头太假、频率太高。你用真实的 UA、合理的间隔、公开的接口很多问题根本不会遇到。另外“采集结果和预期差太多”的时候不要先怀疑反爬先怀疑自己的逻辑。可能是字段名写错了、页数算错了、过滤条件太严格。我当时为了找漏数据花了一下午最后发现是自己在遍历页码时少写了一页的上限。先验证基础逻辑再往“对方是不是防我”的玄学方向想。6.2 分析阶段出现“结果对不上”的排查思路分析阶段最常见的问题是“我明明爬了几千条数据怎么统计出来只有几百条”。这种问题的排查顺序是先看清洗步骤有没有误删再看原始数据到底有多少行最后看 join 或 groupby 的时候有没有键类型不一致。我踩过最隐蔽的坑是年份字段的类型。原始数据里 2013 年是字符串清洗时 pd.to_datetime 把个别非标准日期转成了 NaT然后用 dropna 删掉了结果导致 2013 年数量明显偏少。这类问题靠肉眼很难发现正确做法是在清洗后打印 groupby(year).size()看看每年数量是否符合常识。如果发现某一年突然变成零那大概率是清洗逻辑有问题而不是数据源真的没有当年作品。还有一种情况是评分人数和评分的关系“完全没规律”。这通常不是分析代码的问题而是你拿到的数据本身来自不同版本有些是旧版评分有些是重制版评分混合在一起自然没有规律。这种问题很难在代码层面解决只能在采集阶段通过过滤条件把版本限定清楚或者把版本字段单独存下来分析时控制变量。6.3 后续还能怎么扩展这套数据这套项目做下来本质上建立了一个“公开数据采集—清洗—分析—可视化”的完整流水线。后续想扩展方向其实非常多。可以把爬取范围从动画扩展到漫画、游戏条目可以做用户收藏数和评分之间的相关性分析也可以按声优、制作公司做维度统计甚至可以加一个定时任务每周自动更新一次评分变化看一部动画的口碑随时间怎么演变。如果想往工程化方向走可以把爬虫脚本包装成命令行工具用 argparse 接收参数再配置日志模块记录每次执行结果。数据量再大一点可以引入消息队列做异步处理。不过这些都是后话对大部分个人项目来说先把这套基础流程跑通比堆技术栈重要得多。我个人在实际操作中最深的感触是数据分析项目的价值不在于代码写得多么精巧而在于你能不能提出一个有意义的问题然后用干净的数据、清晰的逻辑去回答它。爬虫只是获取材料的工具分析才是真正产生价值的部分。这次项目之后我再看到网上那些“某类作品质量下滑”的讨论第一反应不再是站队而是想找数据验证一下。这种思维转变可能比学会几个 Python 库更有意义。
返回列表