首页 / 资讯中心 / 文章详情

Python爬虫实战:动漫数据抓取、清洗与可视化全流程

Python爬虫实战:动漫数据抓取、清洗与可视化全流程 ★ FEATURED ARTICLE
简介这份资源面向Python爬虫与数据分析初学者以B站番剧为实战场景完整演示从数据采集到可视化呈现的项目流程。包内共23个文件以10张png可视化结果图、3个ipynb交互式笔记本、2个xlsx数据表及若干xml配置与md说明文档为主压缩包约3.58MB结构清晰便于按模块查阅。已有2984人学习下载适合作为爬虫入门的参考案例。资源先爬取总榜获取各番剧粗略信息与直达链接再逐条访问详情页补充数据随后完成数据清洗与处理并围绕追番人数、评分等维度展开分析最终以图表形式呈现历年动漫数据的变化趋势。配套readme对爬虫细节、数据处理、分析与可视化各环节均有说明读者可据此理解请求构造、字段提取与结果解读的完整思路并对照结果图复现分析过程。1. 从一份“追番清单”说起这个项目到底能干什么前阵子有个做运营的朋友找我说想看看某视频站上动漫区的热度分布问我能不能“爬一下”。我第一反应是这活儿不难但要做成能反复跑、能出图、还能改的东西就得有点工程结构了。这份「Python爬虫-动漫数据分析与可视化」资源干的正是这件事——用 Python 把动漫区的公开列表数据抓下来清洗成结构化表格再做排行、类型分布、评分区间这类可视化。它适合两类人一类是刚学完 requests 和 pandas、想找个完整链路练手的新手另一类是想拿它当模板换成别的榜单、别的字段做二次开发的老手。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序把它拆开讲。2. 技术栈与目录结构一个爬虫项目该有的骨架2.1 选型逻辑为什么是 requests pandas matplotlib这类“抓列表 → 存表格 → 出图”的项目最忌讳一上来就上重型框架。我一般会按数据量和反爬强度来定目标页面是服务端渲染的列表页翻页靠 URL 参数单页几十条总量在几千条量级这种场景用 requests 配 BeautifulSoup 完全够用没必要上 Scrapy 的调度器和中间件调试成本反而更高。数据落地用 pandas是因为后面清洗、去重、分组统计几乎一行搞定可视化用 matplotlib中文显示配一下字体就行不依赖浏览器环境。真正需要提前想清楚的是三件事请求头怎么伪装、翻页边界怎么判断、字段缺失怎么兜底。这三件事决定了脚本是“跑一次就废”还是“能反复跑”。资源里的实现思路也是围绕这三点展开的下面结合目录结构看。2.2 目录结构模块职责怎么分一个能维护的爬虫项目目录不该是“一个 py 文件从头写到尾”。我拆过的同类项目比较合理的组织是这样anime_spider/ ├── config.py # 请求头、基础URL、翻页上限、输出路径 ├── spider.py # 抓取层发请求、解析列表、翻页控制 ├── parser.py # 解析层把 HTML 节点转成 dict ├── storage.py # 存储层写 CSV / 追加去重 ├── analysis.py # 分析层pandas 清洗、分组统计 ├── visualize.py # 可视化层柱状图、饼图、评分分布 ├── data/ │ └── anime.csv # 抓取结果 └── output/ └── *.png # 图表输出这么分的核心目的是“抓取”和“解析”解耦。页面结构一变只改 parser.py想换存储格式只动 storage.py分析和可视化完全不碰网络层本地有 CSV 就能跑。新手最容易犯的错是把 requests 调用、正则解析、画图全塞一个文件里改一处牵全身血泪经验。2.3 依赖清单与版本约束依赖不多但版本要卡一下尤其是 pandas 2.x 之后部分 API 有变动pip install requests beautifulsoup4 pandas matplotlib lxmllxml是 BeautifulSoup 的解析器后端比默认的 html.parser 快不少页面结构不规范时容错也更好。matplotlib 画中文需要单独指定字体否则图上全是方块这个后面可视化那章会讲。pandas 建议 1.5 以上groupby和value_counts的行为比较稳定。提示如果目标环境不能联网装包提前把 wheel 包下好别等到跑脚本时才发现缺 lxml。3. 抓取与解析一个列表页从请求到结构化字段3.1 请求层请求头、超时与重试抓取层的第一原则是“像正常浏览器但别太过”。请求头里 User-Agent 是必须的Referer 视站点而定Cookie 一般不需要——公开列表页通常不校验登录态。超时一定要设不设的话某个请求卡住整个脚本就挂在那儿。重试我一般手写一个简单的循环不引第三方库够用且可控。import requests import time from config import BASE_URL, HEADERS, MAX_PAGE def fetch_page(page: int, retry: int 3) - str | None: 抓取单页 HTML失败重试 retry 次 url f{BASE_URL}?page{page} for attempt in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) # 状态码非 200 直接进入重试 if resp.status_code 200: resp.encoding resp.apparent_encoding # 防中文乱码 return resp.text except requests.RequestException as e: print(f第 {page} 页第 {attempt1} 次失败: {e}) time.sleep(1 attempt) # 退避避免连续打 return None这段的关键点有三个timeout10防止单请求挂死resp.apparent_encoding是根据内容猜编码比硬写 utf-8 稳很多站点的响应头编码声明是错的重试间隔用1 attempt做简单退避别用固定 sleep连续失败时给服务器一点喘息。MAX_PAGE放在 config 里是为了翻页边界可控——很多站点的页码超出范围会返回空列表或跳回首页不设上限容易死循环。3.2 解析层把 HTML 节点转成 dict解析层的职责单一输入一段 HTML输出一个字段齐全的 dict 列表。字段一般包括标题、类型、评分、播放量、链接。用 BeautifulSoup 的select比find_all链式调用可读性好选择器写错时也容易定位。from bs4 import BeautifulSoup def parse_list(html: str) - list[dict]: 解析列表页返回动漫条目列表 soup BeautifulSoup(html, lxml) items [] for node in soup.select(div.anime-item): # 选择器按实际页面调整 title node.select_one(h3.title) score node.select_one(span.score) items.append({ title: title.get_text(stripTrue) if title else , score: float(score.get_text(stripTrue)) if score else None, link: node.select_one(a)[href] if node.select_one(a) else , }) return items这里每个字段都做了“节点不存在就兜底”的处理这是解析层最重要的习惯。页面里总有那么几条缺评分或缺链接的脏数据不兜底的话get_text()直接抛 AttributeError整个批次就断了。score转 float 时如果文本里带“分”字要先replace(分, )这种细节得看实际页面。选择器div.anime-item是占位实际要对着页面 DOM 改改的时候用浏览器开发者工具复制选择器最省事。3.3 翻页控制与去重存储翻页有两种判断方式按固定页数上限或按“本页解析结果为空就停”。我一般两个都用——上限防死循环空页防无效请求。存储层用追加模式写 CSV同时维护一个已抓标题的集合做去重避免重复跑时数据翻倍。import csv, os from spider import fetch_page from parser import parse_list from config import MAX_PAGE, CSV_PATH def crawl_all(): seen set() if os.path.exists(CSV_PATH): # 断点续跑读已有标题 with open(CSV_PATH, encodingutf-8) as f: seen {row[title] for row in csv.DictReader(f)} with open(CSV_PATH, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[title, score, link]) if f.tell() 0: writer.writeheader() for page in range(1, MAX_PAGE 1): html fetch_page(page) if not html: break rows [r for r in parse_list(html) if r[title] not in seen] if not rows: break # 空页或全重复停止翻页 writer.writerows(rows) seen.update(r[title] for r in rows) print(f第 {page} 页写入 {len(rows)} 条)f.tell() 0判断文件是否为空决定要不要写表头这是追加模式下的常见写法。断点续跑靠启动时读已有标题进seen这样中途断了重跑不会重复。break放在“本页无新数据”处比单纯按页数停更稳——有些站点翻到后面会返回重复内容。4. 数据分析与可视化从 CSV 到能看的图4.1 pandas 清洗缺失值、类型与去重抓下来的 CSV 不能直接画图先过一遍清洗。评分列可能有空值播放量如果是“1.2万”这种文本要先转数值标题可能有重复。清洗逻辑集中在 analysis.py本地有 CSV 就能反复跑。import pandas as pd def load_clean(path: str) - pd.DataFrame: df pd.read_csv(path) df df.drop_duplicates(subsettitle) # 标题去重 df[score] pd.to_numeric(df[score], errorscoerce) # 脏值转 NaN df df.dropna(subset[score]) # 评分缺失的条目剔除 df df[df[score].between(0, 10)] # 过滤异常评分 return dferrorscoerce是把无法转换的值变成 NaN 而不是抛异常配合后面的dropna一起用是清洗数值列的标准套路。between(0, 10)过滤掉明显异常的评分比如解析错位抓到的年份数字。这几步做完数据才具备统计意义。4.2 统计口径排行、类型分布、评分区间统计部分要明确口径不然图出来自己都解释不清。排行按评分降序取前 20类型分布用value_counts评分区间用pd.cut分箱。分箱的边界要写死别用自动分箱否则每次跑出来的区间不一样图没法对比。def summarize(df: pd.DataFrame): top20 df.nlargest(20, score)[[title, score]] type_dist df[type].value_counts().head(10) if type in df else None bins [0, 6, 7, 8, 9, 10] labels [6, 6-7, 7-8, 8-9, 9-10] score_dist pd.cut(df[score], binsbins, labelslabels).value_counts().sort_index() return top20, type_dist, score_distnlargest比sort_values().head()在数据量大时略快语义也更清楚。pd.cut的bins左开右闭边界值归到右侧区间这个细节在写图注时要说明不然读者会对不上数。4.3 matplotlib 出图中文字体与布局可视化层最容易翻车的是中文显示。matplotlib 默认字体不含中文图上的标题全是方块。解决办法是显式指定系统里已有的中文字体并关掉负号显示问题。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] # 按系统实际字体改 matplotlib.rcParams[axes.unicode_minus] False # 负号正常显示 def plot_top20(top20): plt.figure(figsize(10, 6)) plt.barh(top20[title][::-1], top20[score][::-1]) # 反转使最高分在顶部 plt.xlabel(评分) plt.title(动漫评分 Top20) plt.tight_layout() plt.savefig(output/top20.png, dpi150) plt.close()[::-1]反转是为了让最高分排在横向柱状图顶部符合阅读习惯。tight_layout()防止标签被裁掉dpi150保证导出图清晰。plt.close()一定要加批量出图时不关 figure 会内存泄漏跑到后面越来越慢。5. 避坑与排查跑不通时先看这几条5.1 抓回来是空列表现象脚本不报错但 CSV 里一条数据都没有。原因多半是选择器写错或者页面是前端渲染的requests 拿到的 HTML 里根本没有数据节点。解决先把resp.text存成文件用浏览器打开看有没有目标内容没有就说明是动态渲染得换思路找接口或换工具别在 BeautifulSoup 上死磕。5.2 中文全是乱码现象标题列显示成æµ·è´¼ç这类字符。原因是响应编码判断错误。解决用resp.apparent_encoding覆盖resp.encoding或者在open()写文件时显式指定encodingutf-8。两处都要检查只改一处往往还是乱。5.3 翻页死循环或提前停现象脚本一直跑到页数上限或者第二页就停了。原因站点对超范围页码返回首页内容导致一直有数据或对连续请求做了限流返回空页。解决加“本页标题与上一页重复率过高就停”的判断同时把请求间隔调到 1 秒以上别用并发硬冲。5.4 图表中文变方块现象图能出但标题和坐标轴全是方框。原因matplotlib 没找到指定字体。解决先确认系统里装了什么中文字体Linux 下常见的是文泉驿把font.sans-serif改成实际存在的字体名找不到就装一个别硬写 SimHei。5.5 重复跑数据翻倍现象第二次运行后 CSV 行数变成两倍。原因追加模式没做去重。解决启动时把已有标题读进集合写入前过滤或者每次跑之前先备份旧文件、重新生成。我一般选前者断点续跑也靠它。6. 二次开发换站点、加字段、做增量拿到这份资源后最实际的改造方向是“换一个榜单”。核心改动集中在三处config.py 里的BASE_URL和HEADERS、parser.py 里的选择器和字段映射、analysis.py 里的统计口径。换站点时先手动请求一页把 HTML 存下来对着改选择器比边跑边调试快得多。字段扩展也类似——想加“更新时间”就在 parser 的 dict 里加一个键storage 的fieldnames同步加上pandas 那边自然多一列。增量更新是另一个高频需求。我的做法是给每条记录加一个抓取日期列每次跑只保留最新日期的数据参与分析历史数据留档。这样既能看趋势又不会因为重复抓取把统计搞乱。验证改造是否成功我一般跑三步先小页数MAX_PAGE2验证解析再全量跑验证存储最后本地读 CSV 跑分析和出图确认图表数字和 CSV 对得上。改造点改动文件验证方式换目标站点config.py、parser.py单页解析条数与页面实际一致新增字段parser.py、storage.pyCSV 表头与数据列对齐改统计口径analysis.py分组总数等于清洗后行数增量更新storage.py、analysis.py同一条目只保留最新记录有个坑我踩过不止一次改完 parser 忘了同步fieldnames结果DictWriter遇到多余键直接抛异常或者少写列导致后面 pandas 读出来错位。从那以后我每次改字段都强制走一遍“小页数试跑 → 打开 CSV 看表头 → 再全量”的流程宁可多花两分钟也不返工。希望这份拆解能帮你把这个爬虫模板真正跑起来、改成自己的。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站