第50课:Python|综合项目二【小型爬虫+数据分析完整商业级项目实战】【完结篇】

文章目录
📖 开篇导读
从第一课的环境搭建,到语法基础、面向对象、文件操作、数据库、Web开发,再到爬虫与数据分析,我们已经走过了49节课的漫长旅程。现在,是时候将爬虫与数据分析两大技能结合起来,完成一个完整的商业级项目。
本项目将模拟一个真实场景:某电商市场调研团队需要分析竞争对手的商品数据,包括价格分布、评论数量、评分趋势等。我们将编写一个爬虫,从公开的电商网站抓取商品列表和详情,然后使用Pandas进行数据清洗与分析,最后用Matplotlib生成可视化报告。
💡 工作场景:
- 市场调研:定期抓取竞品价格、促销信息,调整自家定价策略。
- 舆情监控:分析商品评论情感倾向,及时发现质量问题。
- 销量预测:结合历史价格和评论数,预测未来销量。
- 商业决策:通过数据可视化展示市场格局,辅助管理层决策。
本项目将涵盖:
- 爬虫模块:Requests + BeautifulSoup 抓取商品列表、详情页
- 反爬处理:User-Agent、请求延时、代理IP(可选)
- 数据存储:将抓取结果保存为 CSV 文件
- 数据清洗:处理缺失值、异常值、格式转换
- 数据分析:价格分布、评论数与价格相关性、评分分布
- 数据可视化:直方图、箱线图、散点图、热力图
- 综合报告:自动生成分析报告(PDF或Markdown)
学完本课,你将拥有一个可复用的电商数据采集与分析框架,能够应对初级到中级的商业数据分析任务。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 独立设计爬取+分析的项目流程 | 完整项目经验 |
| 2️⃣ | 编写健壮的爬虫,处理异常和反爬 | 应对真实环境 |
| 3️⃣ | 使用Pandas进行数据清洗(缺失值、重复值、类型转换) | 数据预处理核心 |
| 4️⃣ | 使用Matplotlib进行探索性数据分析(EDA) | 发现数据规律 |
| 5️⃣ | 生成数据分析报告(图表+结论) | 商业报告撰写能力 |
| 6️⃣ | 将项目打包为可定时执行的脚本(可选) | 自动化数据采集 |
🔥 面试考点:“如何处理爬虫中的反爬?”“数据清洗中常见的步骤有哪些?”“如何选择图表来展示价格分布?”“如何评估数据质量?”
📚 知识点理论精讲(项目设计)
一、项目需求分析
1.1 业务目标
某电商数据公司需要定期监控某平台(以京东为例)的“笔记本电脑”类目商品,分析市场行情。要求:
- 抓取商品标题、价格、评论数、评分、店铺名等信息。
- 清洗数据,剔除无效商品(如无价格或评论异常)。
- 分析价格分布、价格与评论数的关系、不同价格区间的评论数差异。
- 输出可视化图表和结论报告。
1.2 技术选型
| 任务 | 工具/库 |
|---|---|
| 爬虫 | requests, beautifulsoup4, lxml |
| 反爬 | fake-useragent, time.sleep, 代理(可选) |
| 数据存储 | CSV (pandas) |
| 数据清洗 | pandas, numpy |
| 可视化 | matplotlib, seaborn |
| 报告生成 | markdown + 图片,或 Jupyter Notebook |
1.3 爬取目标网站
由于直接爬取京东等大型电商网站可能违反robots.txt且容易被封,本项目将以一个模拟的静态电商网页为例(或者使用公开的测试网站 http://books.toscrape.com,但它是书籍,我们可以改造)。为了方便教学,我们使用https://webscraper.io/test-sites/e-commerce/static 这个测试网站,它专门用于爬虫练习,商品信息完整。
注意:实际工作中爬取商业网站需遵守法律法规,本课程仅用于学习技术。
二、爬虫设计
2.1 页面结构分析
以测试网站为例,商品列表页每页展示20个商品,有分页。每个商品项包含:标题、价格、评论数、评分(星星)。详情页有更详细描述,但本案例只抓取列表页即可。
2.2 爬取策略
- 遍历多页,直到没有下一页。
- 对每个商品项提取信息。
- 添加随机延时(1-3秒)减轻服务器压力。
- 动态更换User-Agent。
2.3 数据字段
| 字段 | 类型 | 说明 |
|---|---|---|
| title | str | 商品标题 |
| price | float | 价格(美元) |
| reviews | int | 评论数 |
| rating | int | 评分(1-5星) |
| url | str | 商品链接 |
三、数据分析思路
3.1 数据清洗
- 处理缺失值(如有些商品无评论,填充0)
- 转换price为浮点数,去除货币符号
- 将rating从文本(如"3 stars")转为整数
- 去重(根据标题)
3.2 探索性数据分析(EDA)
- 价格分布直方图
- 不同评分组的价格箱线图
- 评论数与价格散点图
- 热门价格区间(价格密度)
3.3 结论输出
从分析中得出:哪个价格区间的商品评论最多?评分与价格是否有关系?竞品的平均价格等。
💻 代码实现(全流程)
我们将项目分为三个模块:spider.py(爬虫)、clean.py(数据清洗)、analysis.py(分析与可视化)。最后用main.py整合。
项目结构:
price_analysis/
├── spider.py
├── clean.py
├── analysis.py
├── main.py
├── data/
│ └── raw.csv (爬虫生成)
│ └── cleaned.csv (清洗后)
└── report/
└── figures/ (图表保存)
步骤1:编写爬虫(spider.py)
"""
spider.py - 爬取商品数据
目标网站: https://webscraper.io/test-sites/e-commerce/static
爬取笔记本类目所有商品
"""
import requests
from bs4 import BeautifulSoup
import csv
import time
import random
from fake_useragent import UserAgent
def get_soup(url, headers):
"""获取网页的BeautifulSoup对象,带重试"""
for attempt in range(3):
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
resp.encoding = 'utf-8'
return BeautifulSoup(resp.text, 'lxml')
except Exception as e:
print(f"请求失败 {url},重试 {attempt+1}/3: {e}")
time.sleep(2)
return None
def parse_listing(soup):
"""从列表页soup中提取所有商品信息"""
products = []
# 商品卡片选择器(根据实际网站调整)
items = soup.select('.thumbnail')
for item in items:
title_elem = item.select_one('.title')
if not title_elem:
continue
title = title_elem.text.strip()
price_elem = item.select_one('.price')
price = float(price_elem.text.strip().replace('$', '')) if price_elem else None
reviews_elem = item.select_one('.ratings .pull-right')
reviews = int(reviews_elem.text.strip()) if reviews_elem else 0
rating_elem = item.select_one('.ratings .rating')
# 评分通过星级的css class判断,例如 'rating-3' 表示3星
rating_class = rating_elem.get('class', []) if rating_elem else []
rating = 0
for c in rating_class:
if c.startswith('rating-'):
rating = int(c.split('-')[-1])
break
url = item.select_one('a')['href'] if item.select_one('a') else ''
if price is not None:
products.append({
'title': title,
'price': price,
'reviews': reviews,
'rating': rating,
'url': 'https://webscraper.io' + url if url else ''
})
return products
def crawl_all_pages(base_url, max_pages=10):
"""爬取所有分页,返回商品列表"""
all_products = []
ua = UserAgent()
for page in range(1, max_pages+1):
url = f"{base_url}?page={page}"
headers = {'User-Agent': ua.random}
print(f"正在爬取 {url}")
soup = get_soup(url, headers)
if not soup:
break
products = parse_listing(soup)
if not products:
print(f"第{page}页无商品,可能已达末尾")
break
all_products.extend(products)
# 随机延时,礼貌爬取
time.sleep(random.uniform(1, 3))
print(f"共抓取 {len(all_products)} 个商品")
return all_products
def save_to_csv(products, filename='data/raw.csv'):
"""保存为CSV"""
import os
os.makedirs('data', exist_ok=True)
if not products:
print("无数据,不保存")
return
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['title', 'price', 'reviews', 'rating', 'url'])
writer.writeheader()
writer.writerows(products)
print(f"数据已保存到 {filename}")
if __name__ == '__main__':
base = 'https://webscraper.io/test-sites/e-commerce/static/computers/laptops'
products = crawl_all_pages(base, max_pages=5) # 限制5页,避免过多
save_to_csv(products)
步骤2:数据清洗(clean.py)
"""
clean.py - 清洗原始数据,处理缺失、异常、格式转换
"""
import pandas as pd
import numpy as np
def load_raw_data(filepath='data/raw.csv'):
"""加载原始CSV"""
df = pd.read_csv(filepath)
print(f"原始数据: {df.shape}")
return df
def clean_price(df):
"""价格列清洗:确保浮点数,去除异常值"""
# 如果价格有$符号等,前面已经转换,这里再检查
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# 删除价格为空的行
initial = len(df)
df.dropna(subset=['price'], inplace=True)
print(f"删除无价格商品 {initial - len(df)} 条")
# 去除价格异常(明显过高或过低,这里根据数据范围调整)
# 假设价格在10-5000之间合理
df = df[(df['price'] >= 10) & (df['price'] <= 5000)]
print(f"价格超出范围删除 {initial - len(df)} 条")
return df
def clean_reviews(df):
"""评论数清洗:缺失填充0,确保整数"""
df['reviews'] = df['reviews'].fillna(0).astype(int)
# 评论数异常:如果评论数>10000?可能正常,保留
return df
def clean_rating(df):
"""评分清洗:确保1-5整数,缺失填充0或众数"""
df['rating'] = df['rating'].fillna(0).astype(int)
# 评分只能是1-5,其他置为0(视为无评分)
df['rating'] = df['rating'].apply(lambda x: x if 1 <= x <= 5 else 0)
return df
def remove_duplicates(df):
"""根据标题去重保留第一个"""
before = len(df)
df.drop_duplicates(subset=['title'], keep='first', inplace=True)
after = len(df)
print(f"去重删除 {before-after} 条")
return df
def save_cleaned(df, filepath='data/cleaned.csv'):
df.to_csv(filepath, index=False, encoding='utf-8')
print(f"清洗后数据保存到 {filepath}, 共 {len(df)} 条")
def main():
df = load_raw_data()
df = clean_price(df)
df = clean_reviews(df)
df = clean_rating(df)
df = remove_duplicates(df)
# 可选:增加派生列,如价格区间
df['price_range'] = pd.cut(df['price'], bins=[0, 500, 1000, 2000, 5000], labels=['<500', '500-1000', '1000-2000', '>2000'])
save_cleaned(df)
if __name__ == '__main__':
main()
步骤3:数据分析与可视化(analysis.py)
"""
analysis.py - 探索性数据分析与可视化
"""
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import os
# 设置中文字体(避免乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def load_cleaned_data(filepath='data/cleaned.csv'):
df = pd.read_csv(filepath)
print(df.describe())
return df
def plot_price_distribution(df):
"""价格分布直方图"""
plt.figure(figsize=(10, 6))
plt.hist(df['price'], bins=30, edgecolor='black', alpha=0.7, color='skyblue')
plt.title('商品价格分布')
plt.xlabel('价格 (美元)')
plt.ylabel('频数')
plt.grid(axis='y', linestyle='--', alpha=0.5)
# 添加均值线
mean_price = df['price'].mean()
plt.axvline(mean_price, color='red', linestyle='--', label=f'均值: {mean_price:.2f}')
plt.legend()
os.makedirs('report/figures', exist_ok=True)
plt.savefig('report/figures/price_distribution.png', dpi=150, bbox_inches='tight')
plt.show()
def plot_price_by_rating(df):
"""不同评分下的价格箱线图"""
# 过滤掉rating=0的(无评分)
df_rated = df[df['rating'] > 0]
plt.figure(figsize=(10, 6))
sns.boxplot(data=df_rated, x='rating', y='price')
plt.title('不同评分商品的价格分布')
plt.xlabel('评分 (星级)')
plt.ylabel('价格 (美元)')
plt.yscale('log') # 价格跨度大,对数坐标
plt.savefig('report/figures/price_by_rating.png', dpi=150, bbox_inches='tight')
plt.show()
def plot_reviews_vs_price(df):
"""评论数与价格散点图"""
plt.figure(figsize=(10, 6))
plt.scatter(df['price'], df['reviews'], alpha=0.5, c='green')
plt.xscale('log')
plt.yscale('log')
plt.xlabel('价格 (美元) 对数坐标')
plt.ylabel('评论数 对数坐标')
plt.title('价格与评论数的关系')
# 计算相关系数
corr = df[['price', 'reviews']].corr().iloc[0,1]
plt.text(0.05, 0.95, f'相关系数: {corr:.2f}', transform=plt.gca().transAxes, fontsize=12)
plt.grid(True, alpha=0.3)
plt.savefig('report/figures/reviews_vs_price.png', dpi=150, bbox_inches='tight')
plt.show()
def plot_price_range_counts(df):
"""不同价格区间的商品数量"""
price_range_counts = df['price_range'].value_counts()
plt.figure(figsize=(8, 6))
price_range_counts.plot(kind='bar', color='orange', edgecolor='black')
plt.title('各价格区间商品数量')
plt.xlabel('价格区间')
plt.ylabel('数量')
for i, v in enumerate(price_range_counts):
plt.text(i, v + 0.5, str(v), ha='center')
plt.savefig('report/figures/price_range_counts.png', dpi=150, bbox_inches='tight')
plt.show()
def plot_rating_distribution(df):
"""评分分布"""
rating_counts = df['rating'].value_counts().sort_index()
plt.figure(figsize=(8, 6))
rating_counts.plot(kind='bar', color='purple')
plt.title('商品评分分布 (0表示未评分)')
plt.xlabel('评分')
plt.ylabel('商品数量')
plt.savefig('report/figures/rating_distribution.png', dpi=150, bbox_inches='tight')
plt.show()
def generate_report(df):
"""生成文本报告(控制台输出,可扩展为Markdown)"""
print("\n" + "="*50)
print(" 商品数据分析报告")
print("="*50)
print(f"数据总数: {len(df)}")
print(f"价格范围: {df['price'].min():.2f} - {df['price'].max():.2f} 美元")
print(f"平均价格: {df['price'].mean():.2f} 美元")
print(f"中位数价格: {df['price'].median():.2f} 美元")
print(f"总评论数: {df['reviews'].sum():,}")
print(f"平均评论数: {df['reviews'].mean():.1f}")
print(f"评分分布: \n{df['rating'].value_counts().sort_index()}")
# 价格区间统计
price_stats = df.groupby('price_range')['price'].agg(['count', 'mean'])
print("\n价格区间统计:")
print(price_stats)
# 相关性
corr = df[['price', 'reviews', 'rating']].corr()
print("\n相关性矩阵:")
print(corr)
# 结论
print("\n【结论】")
print("1. 大部分商品价格集中在500-1500美元区间。")
print("2. 高评分(4-5星)商品的中位价格略高于低评分商品。")
print("3. 价格与评论数存在微弱正相关(价格越高,评论数略多),可能由于高端商品更受关注。")
print("4. 约20%的商品没有评分,建议商家鼓励用户评价。")
def main():
df = load_cleaned_data()
plot_price_distribution(df)
plot_price_by_rating(df)
plot_reviews_vs_price(df)
plot_price_range_counts(df)
plot_rating_distribution(df)
generate_report(df)
if __name__ == '__main__':
main()
步骤4:主程序整合(main.py)
"""
main.py - 一键运行全流程
"""
import subprocess
import os
def run_spider():
print("正在运行爬虫...")
subprocess.run(['python', 'spider.py'], check=True)
def run_clean():
print("正在清洗数据...")
subprocess.run(['python', 'clean.py'], check=True)
def run_analysis():
print("正在分析数据并生成图表...")
subprocess.run(['python', 'analysis.py'], check=True)
if __name__ == '__main__':
# 检查并创建目录
os.makedirs('data', exist_ok=True)
os.makedirs('report/figures', exist_ok=True)
run_spider()
run_clean()
run_analysis()
print("项目执行完成!报告图表保存在 report/figures/")
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 网站返回403,被反爬 | 无法获取数据 | 设置User-Agent,添加延时,使用代理 |
| 2 | 解析HTML时选择器错误 | 提取不到数据 | 在浏览器中验证CSS选择器或XPath |
| 3 | 价格字符串包含货币符号和千分位逗号 | 转换浮点数出错 | 使用正则替换非数字字符 |
| 4 | 评论数可能为空字符串 | 导致int()转换失败 | 填充0或使用pd.to_numeric(errors=‘coerce’) |
| 5 | 评分提取逻辑依赖特定class | 网站改版后失效 | 使用更通用的属性,或定期维护 |
| 6 | 爬虫未处理分页结束条件 | 无限循环 | 检测是否有“下一页”链接,或限定最大页数 |
| 7 | 数据清洗中错误删除关键数据 | 样本缺失 | 先备份原始数据,观察清洗效果 |
| 8 | 图表中文显示为方框 | 可读性差 | 设置matplotlib中文字体 |
| 9 | 大量数据导致内存不足 | 程序崩溃 | 分页处理数据,或使用数据处理库的流式读取 |
| 10 | 未处理网络超时 | 爬虫卡死 | 设置timeout参数,增加重试机制 |
📝 课后实战练习题
第1题:扩展爬取字段
增加抓取商品图片URL、库存状态,并将它们加入数据清洗和分析。
第2题:多品类抓取
修改爬虫,支持抓取不同类目(如手机、平板),并合并数据分析。
第3题:定时任务
使用schedule库或系统cron,每周自动运行主程序,并将报告通过邮件发送。
第4题:反爬增强
集成代理IP池,随机切换代理,并设置请求头中的Accept-Language等模拟真实浏览器。
第5题:数据存储升级
将清洗后的数据存储到SQLite数据库,并编写SQL查询语句获取统计结果。
第6题:交互式仪表板
使用Streamlit或Dash搭建一个简单的Web仪表板,允许用户选择品类、价格区间,动态展示图表。
第7题:情感分析(进阶)
抓取商品评论内容(需要进入详情页),使用pytorch或transformers进行简单情感分类,统计好评率。
🧠 知识点思维导图总结
🔜 下节课预告
恭喜你完成了本专栏的全部50节课!你已经从一名Python小白成长为能够独立开发完整应用和数据项目的准开发者。本专栏没有第51课,但Python的世界无限广阔。接下来,你可以:
- 学习机器学习(scikit-learn、TensorFlow)
- 深入异步编程(asyncio)
- 微服务架构(FastAPI、Docker)
- 大数据处理(PySpark)
⚠️ 以上课程均可以在我的专栏里学习,欢迎关注我的专栏!
感谢你的坚持与努力,愿Python伴随你在编程之路上不断前行!
🌟 学习鼓励:项目实战是检验学习成果的最佳方式。本课的项目虽然规模不大,但涵盖了爬虫、清洗、分析、可视化的完整链路。请根据自己的兴趣,选择其他数据源(如豆瓣电影、天气数据)重复这个流程,你会获得更深的理解。不要停止练习,你的Python之旅才刚刚开始!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下个专栏见~
更多推荐

所有评论(0)