简介:基于Python爬虫与数据分析的实战项目压缩包,适合计算机相关专业学生、教师及入门开发者用于课程设计、毕业设计或项目初期演示。项目覆盖数据采集、清洗、可视化与基础建模的完整流程,包含斗鱼直播间数据爬取、京东图书信息采集、微信好友关系可视化、星巴克门店数据分析以及泰坦尼克号生存率预测等典型场景,并提供相应数据集与脚本。资源共十七个文件,以四个交互式分析脚本、三个表格数据集、三个压缩数据包为主,辅以爬虫脚本、配置文件、字体图片与说明文档,整体约七点零七兆,轻量易用。已有一百一十六人学习下载,内容按场景分模块组织,适合逐步跟练。借助说明与授权码可快速上手;交互式脚本中保留分析与可视化结果,能直观理解爬虫框架选型、数据清洗思路及绘图方法,也便于在此基础上二次开发或直接用于项目答辩。
1. 标题即需求:一个 Python 爬虫+数据分析项目的完整闭环
打开任何一个资源站,类似“基于python爬虫+数据分析实战项目文档详细+资料齐全.zip”的压缩包永远是下载量最高的那一类。原因很简单:爬虫是入口,数据分析是出路,而“文档详细+资料齐全”才是这类项目真正值钱的部分。实战场景里最常见的形态是竞品信息监控、行业报告数据采集、科研数据整理或者小批量运营报表生成——数据量从几千条到几十万条不等,单机跑得动,不需要上分布式,但也没法靠手工复制粘贴完成。
这类项目对工程师的真正要求不是“会写爬虫”,而是能独立完成一条从 URL 到结论的完整链路:选型、采集、清洗、分析、可视化、文档。这个标题背后真正想解决的问题,是如何让一个爬虫项目不烂尾在“数据抓到一半”或者“抓完不知道干什么”的状态。我会按自己做这类项目的习惯,把这套链路拆成五个部分讲清楚,每部分都能直接落到代码和命令上。
2. 选型与项目骨架:requests 是起点,Pandas 是终点,文档是交付物
2.1 为什么 requests + BeautifulSoup + Pandas 是绝大多数实战项目的最优起点
先说结论:单机、万级到十万级数据量、目标站点以静态 HTML 或简单 JSON 接口为主,requests + BeautifulSoup + Pandas 就是最稳的组合。requests 负责 HTTP 语义和会话管理,BeautifulSoup 处理 HTML 解析,Pandas 做清洗和分析。这个组合的调试成本低,任何一步出错都能用 print 或 logging 立刻定位。
什么时候不选 Scrapy?Scrapy 的优势在并发调度、中间件和分布式扩展,但这套机制的学习曲线和配置成本在数据量小于十万条时并不划算。Scrapy 里一个常见的坑是 Item Pipeline 里做数据清洗,调试时要在本地反复跑scrapy crawl,改动一次就要重启一次,效率反而不如脚本直接调函数。真正的分水岭是并发要求和增量采集复杂度:如果目标站点有强反爬、需要分布式节点、或者采集频率要稳定跑几天,再迁移到 Scrapy 也不迟。另一个必须明确的边界是动态渲染页面:requests 拿不到 Selenium 或 Playwright 渲染后的 DOM,所以看到页面内容来自 JS 异步加载时,先用 DevTools 的 Network 面板找 XHR 接口,找不到再上浏览器模拟。很多自称“爬虫实战”的项目文档连这个判断都没写,这是资料“不齐全”的典型表现。
数据分析端同理,Pandas 是事实标准。它覆盖了读取、清洗、聚合、透视、导出全流程,配合 Matplotlib 或 Seaborn 就能完成项目报告里 90% 的可视化需求。到此为止的选型可以总结为一句:requests 管下载,BeautifulSoup 管解析,Pandas 管分析,三个库足够撑起一个五脏俱全的数据分析实战项目。
2.2 项目目录与资料组织的 3 个好习惯
“资料齐全”和“文档详细”在真实项目里体现在目录结构上。我接手过不少交接项目,最怕的不是代码写得烂,而是 assets、output、test 这种目录名反复嵌套,跑起来的脚本散落在各个角落。按“数据流向”组织目录是最直观的做法。
crawler_analysis/ ├── README.md ├── requirements.txt ├── config/ │ └── settings.yaml # 目标URL、请求头、延时范围 ├── collector/ # 采集模块 │ ├── __init__.py │ ├── fetcher.py # 请求与重试逻辑 │ └── parser.py # HTML解析与字段提取 ├── analysis/ │ ├── __init__.py │ ├── cleaner.py # 清洗与类型转换 │ └── stats.py # 统计与可视化 ├── data/ │ ├── raw/ # 原始响应或CSV │ ├── processed/ # 清洗后的数据集 │ └── reports/ # 图表和结论 └── main.py # 主入口,一行命令跑全流程目录之间的依赖方向是单向的:collector 只写 data/raw,analysis 只读 data/raw 写 data/processed,main.py 只做编排。这样每个模块单独可测,出问题时能快速定位到具体环节。
requirements.txt 要锁定版本,但不锁太死。推荐写法是pandas>=2.0,<3.0这种范围约束,既能保证可复现,又不至于在换环境时因为版本过旧装不上。README 里必须写三样东西:安装命令、启动方式、输出物说明。安装命令一行pip install -r requirements.txt就够了;启动方式写明python main.py --fetch --analyze之类的实际命令;输出物说明要描述 data/processed 里每个文件的字段含义。做到这三项,这个项目就已经比 80% 的网盘资源包“资料齐全”了。
3. 采集端实战:用 requests 构造可监管的爬虫主循环
3.1 最小主循环:请求、解析、入库与限速
采集端的核心不是某个库,而是一个固定的循环模型:构造请求、拿到响应、解析字段、写入存储。我一般会先写一个最简单的同步版本,确认数据结构和目标站点没有变化,再考虑并发。下面这段代码是模拟采集公开的商品列表页,目标是拿到标题、价格和发布时间三个字段。
import csv import logging import time import requests from bs4 import BeautifulSoup logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger("collector") 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", "Accept": "text/html,application/xhtml+xml", } def fetch_page(session, url, retries=3): for attempt in range(retries): try: resp = session.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text logger.warning("status %s on %s, attempt %s", resp.status_code, url, attempt + 1) except requests.RequestException as exc: logger.error("request failed: %s, attempt %s", exc, attempt + 1) time.sleep(2 * (attempt + 1)) return None def parse_items(html): soup = BeautifulSoup(html, "html.parser") items = [] for card in soup.select(".item-card"): title_tag = card.select_one(".item-title a") price_tag = card.select_one(".item-price") date_tag = card.select_one(".item-date") items.append({ "title": title_tag.get_text(strip=True) if title_tag else None, "price": float(price_tag.get_text(strip=True).replace("¥", "")) if price_tag else None, "published_at": date_tag.get_text(strip=True) if date_tag else None, }) return items def main(): session = requests.Session() all_items = [] with open("data/raw/items_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "price", "published_at"]) writer.writeheader() for page in range(1, 6): url = f"https://example.com/list?page={page}" html = fetch_page(session, url) if not html: continue items = parse_items(html) writer.writerows(items) all_items.extend(items) logger.info("page %d collected, total %d", page, len(all_items)) time.sleep(1.5) logger.info("done, total items: %d", len(all_items)) if __name__ == "__main__": main()逐个说明关键点。requests.Session()要保留,它复用底层 TCP 连接,在翻页采集时能明显减少握手开销。fetch_page里做了重试,按照 2 秒、4 秒的退避间隔递增,这是对目标站点最基本的礼貌,也防止网络抖动导致丢数据。parse_items用 CSS 选择器提取节点,get_text(strip=True)去掉首尾空格,价格字段顺手做类型转换。写 CSV 时用utf-8-sig编码,Excel 打开不乱码。每页完成后time.sleep(1.5)是限速,具体值根据目标网站响应速度调整,一般 1 到 3 秒是安全区间。
这一步跑通之后,raw 目录下的 CSV 是纯原始数据,后续分析完全依赖这个文件,所以采集阶段的日志要留足信息:每个页面的状态码、成功条数、累计条数、耗时。这些线索在排查“为什么只有 80 条数据”时比代码本身更有用。
3.2 增量采集与断点续抓:让项目能反复运行而不重复劳动
一次性全量采集在实战里很少见。大部分数据分析项目需要每周或每天更新数据,所以爬虫脚本必须支持“上次抓到哪里,这次接着抓”的能力。最轻量的做法是用一个本地游标文件记录增量位置,我通常在 data/processed 下放一个crawler_state.json。
import json import os STATE_FILE = "data/processed/crawler_state.json" def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) return {"last_page": 0, "last_run": None} def save_state(state): os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2)last_page记录翻页位置,last_run记录上次运行时间。翻页类页面的逻辑就是把range(1, 6)改成range(state["last_page"] + 1, total_pages + 1);时间线类的页面则用last_run作为请求参数的起始时间。每次成功抓完一页就更新一次状态并落盘,这样就算脚本中途崩溃,重启后也不会从头开始。断点续抓的价值对数据分析项目尤其重要:分析端需要的是稳定的增量数据,而不是跑一次就报废的一次性数据。
3.3 反爬应对与并发设计:从单线程到 ThreadPoolExecutor 的平滑过渡
爬虫跑起来之后会遇到的第一道坎就是封 IP 或验证码。最常见的触发原因是请求频率太高和 User-Agent 太单一。UA 池是成本最低的规避手段,配合 fake_useragent 库每次请求随机换一个 UA 即可。此时 IP 代理才派上用场:要么找付费代理服务商的 API 拉取代理列表,要么自己维护一个代理池,每次请求随机挑一个。这里要提醒一句,代理的质量参差不齐,用之前必须做连通性校验,否则代理失效引发的超时重试比直接请求更慢。
并发设计是另一个高频问题。同样是采集 1000 个页面,for 循环串行可能要跑 30 分钟,换成线程池可能 5 分钟就完成。但对于“并发设计到底哪个好”这个问题,我的答案非常明确:取决于目标站点的承受能力和数据量级,而不是哪个技术听起来更先进。requests 单线程到 ThreadPoolExecutor 是最平滑的升级路径,线程池不会引入异步事件循环的心智负担,也足够应付 90% 的小规模实战。
from concurrent.futures import ThreadPoolExecutor, as_completed import threading lock = threading.Lock() session = requests.Session() def safe_fetch_and_parse(url): html = fetch_page(session, url) if not html: return [] items = parse_items(html) with lock: write_to_csv(items) return items urls = [f"https://example.com/list?page={page}" for page in range(1, 51)] with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(safe_fetch_and_parse, url): url for url in urls} for future in as_completed(futures): try: items = future.result() logger.info("collected %d items from %s", len(items), futures[future]) except Exception as exc: logger.error("task failed: %s", exc)max_workers不要贪大。单机爬虫 5 到 10 个线程足以跑满带宽和 CPU,再往上加只会增加被封风险。threading.Lock保护 CSV 写入是因为多个线程同时写文件会互相覆盖,这个锁只套在写文件那一步,不锁网络请求,对整体吞吐影响很小。另一个容易忽略的点是:线程池版本里session是线程共享的,requests 的 Session 内部维护连接池,但 cookie 和 header 是只读状态,实测并发场景下是安全的。
4. 数据分析端:用 Pandas 清出“能分析”的数据再可视化
4.1 从 CSV 到 DataFrame:字段类型与编码两个必踩的坑
采集端输出的 raw CSV 大概率是“半成品”:日期是字符串、价格里混着货币符号、可能存在空行和重复记录。数据分析的第一步不是画图,而是先把数据读进来并确认每列的类型、缺失率、唯一值数量。
import pandas as pd df = pd.read_csv("data/raw/items_raw.csv", encoding="utf-8-sig") print(df.info()) print(df.head(3)) print(df.describe(include="all"))df.info()会输出每列的非空数量和 dtype,这是判断清洗策略的依据。此时最常出现的两个问题,一个是日期列被读成 object 类型,另一个是价格列里混入了¥或逗号导致无法转 float。对应的处理方式是:
df["published_at"] = pd.to_datetime(df["published_at"], errors="coerce") df["price"] = ( df["price"] .astype(str) .str.replace("¥", "", regex=False) .str.replace(",", "", regex=False) .astype(float) )errors="coerce"的语义是遇到无法解析的时间就置为 NaT,而不是中断报错,这样能保留原始数据行,方便后续查日志定位问题。价格清理先转字符串再替换货币符号和千分位逗号,最后转 float。这两步做完,再跑一次df.info()确认类型正确,数据分析的底座才算是稳了。如果原始数据量特别大,比如几百万行,读取时建议加上dtype参数指定各列类型,避免 Pandas 自动推断带来的内存膨胀。
4.2 清洗规则:缺失值、去重与异常值截断
清洗的决策要写进项目文档里,而不是靠“感觉”。每种清洗策略背后都要有一个可解释的理由,这份理由比代码本身更能体现一个数据分析项目的质量。下面的表格是我做商品类数据分析项目时的默认清洗规则,可以直接抄进报告。
| 数据项 | 检查方法 | 处理策略 | 理由 |
|---|---|---|---|
| title 缺失 | df["title"].isna().sum() | 删除该行 | 标题是主体信息,缺失无分析价值 |
| price 缺失 | df["price"].isna().sum() | 用同页面均值填充 | 价格是关键指标,不宜丢行 |
| published_at 解析失败 | df["published_at"].isna().sum() | 删除该行 | 时间序列分析依赖完整时间标签 |
| 重复记录 | df.duplicated(subset=["title", "published_at"]).sum() | 保留第一条 | 同一商品同一天可能被采集两次 |
重复值的处理要特别注意,很多新手直接drop_duplicates()不加 subset 参数,结果把两天前和今天两条正常的记录也删掉了。正确做法是只针对业务唯一键去重,商品类数据的唯一键通常是标题加发布时间的组合。
异常值截断的逻辑是:先看描述性统计,df["price"].quantile([0.01, 0.99])拿到 1% 和 99% 分位数,把超出这个区间的值视为异常,再做截断处理。为什么要截断而不是删除?因为极端值可能来自秒杀或清仓行为,它们本身包含信息量,直接压到分位数值即可。
4.3 把“数据分析与可视化”落成两张图:趋势与构成
清洗完成后,剩下的工作是把数据变成别人能看懂的结论。大多数爬虫+数据分析项目的报告里,真正需要的是两类图:时间趋势折线图和类目占比条形图。下面代码用 Matplotlib 生成这两张图,保存到 reports 目录。
import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import pandas as pd plt.rcParams["font.sans-serif"] = ["SimHei", "Arial Unicode MS", "DejaVu Sans"] plt.rcParams["axes.unicode_minus"] = False # 按天聚合价格均值,观察价格走势 daily_avg = df.groupby(df["published_at"].dt.date)["price"].mean() fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(daily_avg.index, daily_avg.values, marker="o", linewidth=1.5) ax.set_title("商品日均价格走势") ax.set_xlabel("日期") ax.set_ylabel("平均价格") plt.xticks(rotation=45) plt.tight_layout() plt.savefig("data/reports/price_trend.png", dpi=150) plt.close()matplotlib.use("Agg")必须在 pyplot 被导入之前调用,这样脚本在无显示器的服务器上跑也不会报错,这个细节在 Linux 定时任务场景频繁翻车。字体设置要写后备字体列表,保证 Windows 和 Linux 都能找到可显示中文的字体。groupby(published_at.dt.date)把时间戳按天截断再求均值,这是时间序列分析最基础的降采样操作。dc:dpi 150 是印刷级清晰度,网页插入也足够。
第二张图建议画 Top 10 类目的数量占比,翻页类页面通常有类目标签,没有就用价格分桶近似替代。数据可视化不是为了炫技,而是让阅读报告的人一眼看出“数据采集是否有偏、样本是否覆盖完整、趋势是否合理”这三个问题。
5. 实战之后的可复核技巧:用校验和差异对比证明数据可信
数据采集完成不等于项目交付。真正让一个爬虫项目“资料齐全、文档详细”的最后一公里,是让所有人相信这份数据是可信的、可重复的。“可复核”有三个具体动作:响应校验、数据指纹、增量对比。
响应校验指的是在请求阶段,不仅判断状态码 200,还要判断返回内容是否真的包含目标结构。有的网站对爬虫返回 200 但内容是一个验证码页,如果解析阶段不做校验,抓下来的数据全是 null。常见的做法是在fetch_page返回前检查resp.text里是否包含一个标志性字符串,比如.item-card这个 class 名,不包含就重试或报警。
数据指纹用于判断这次采集和上次采集的数据集是否有结构性变化。用 MD5 对排序后的标题列表生成摘要,存到 crawler_state.json 里。下次运行后重新计算摘要,如果发生了变化,说明目标站点有新增、下架或改版;如果完全一致,就要考虑是不是被缓存或限流了。这一步的成本极低,但排查问题时的价值极高。
增量对比是把这次采集的数据和上次采的数据做差集,输出新增、减少、价格变动的明细。Pandas 做这个操作非常直接,按唯一键做 outer join 就能拿到三个状态。把对比结果写进 CSV,作为分析的边车文件保存在 reports 里,这样报告里的每一个结论都能溯源。项目文档里的“数据说明”部分,就写清楚这次采集的时间窗口、去重后的有效条目、和上次相比的增量变化,任何人拿着这份说明都可以重新跑一遍流程验证。这才是“资料齐全”的底层含义:不是文件的堆砌,而是每一个环节都可以被复核、被重新执行。
本文还有配套的精品资源,点击获取