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

Python爬虫实战:大众点评评论抓取、数据清洗与消费分析报告

Python爬虫实战:大众点评评论抓取、数据清洗与消费分析报告 ★ FEATURED ARTICLE
简介这份资源面向计算机相关专业学生与开发者提供一套完整的大众点评评论采集与消费分析实战项目可作为毕业设计、课程设计或技术研究的参考案例。项目包含爬虫源码、数据清洗脚本与消费分析报告覆盖从评论抓取、图片采集到数据预处理、情感分析与可视化建模的完整链路适合具备一定Python基础、希望快速复现或二次开发的人群。压缩包共61个文件约90KB以20个py脚本为核心辅以js、xml、json、md等配置与说明文件另有bat、sh启动脚本和yaml配置目录结构清晰便于按模块查阅。目前已有154人学习下载。资源不仅提供可稳定运行的完整代码还配套设计文档与多环境测试说明读者可据此理解爬虫调度、MongoDB存储、OCR识别与SHAP解释等关键实现并在此基础上修改扩展功能用于毕设、课设或原型验证遇到环境配置问题还可获得远程指导支持。1. 从一份「大众点评评论爬虫源码」说起它到底能跑出什么很多人第一次搜「Python大众点评评论爬虫源码」脑子里想的是一份解压就能跑的压缩包双击之后评论哗哗往下掉。现实是这类项目真正值钱的部分从来不是那几十行 requests 请求而是它背后串起来的一条完整链路抓取、清洗、分析、出报告。标题里四个词——爬虫、数据清洗、消费分析报告、实战案例——其实是一条数据流水线的四个工位缺一个最后那份报告就是废纸。我见过太多人卡在中间评论抓下来了几万条躺在 CSV 里字段乱七八糟评分是「4.5分」这种带单位的字符串人均消费是「¥120/人」时间戳是「2024-03-15 更新」这种混着中文的格式。这时候你拿 pandas 直接 groupby出来的全是 object 类型算不了均值画不了图。所以这篇不打算只讲怎么发请求而是把从抓取到出消费分析报告的整条路走一遍重点放在数据清洗和指标设计上。适合两类人刚学完 requests 和 pandas、想找个真实数据集练手的新手以及做过爬虫但没认真处理过脏数据的熟手。下面所有代码都是能直接改改就用的骨架不依赖任何不存在的私有库。2. 抓取层怎么搭requests 分页 字段落库的最小闭环2.1 为什么选 requests 而不是上 Scrapy 或分布式先说选型。大众点评这类站点的评论页本质是「列表页 详情页」结构单店评论量通常在几百到几千条分页参数清晰。这种量级用 Scrapy 属于杀鸡用牛刀分布式爬虫更是没必要——你面对的不是百万级页面而是几十家店的评论聚合。requests 会话保持 分页循环代码量控制在 100 行以内调试成本最低。真正要花心思的是三件事请求头伪装、分页终止条件、字段抽取的容错。大众点评对未登录请求有较严的限制常见做法是带上完整的浏览器请求头尤其是User-Agent、Referer和Cookie。Cookie 需要你自己在浏览器登录后复制这一步没有捷径也不建议用任何自动化登录方案容易触发风控。import requests import time import random import csv from parsel import Selector # 比 lxml 更顺手语法接近 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Referer: https://www.dianping.com/, Cookie: 你的Cookie放这里, # 从浏览器开发者工具复制 } def fetch_page(shop_id, page): 抓取单店某一页评论返回 HTML 文本 url fhttps://www.dianping.com/shop/{shop_id}/review_all/p{page} try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f[失败] shop{shop_id} page{page} err{e}) return None def parse_reviews(html): 从列表页 HTML 抽取评论字段返回字典列表 sel Selector(texthtml) items [] for node in sel.css(.review-list .review-item): items.append({ user: node.css(.user-name::text).get(default).strip(), score: node.css(.score::text).get(default).strip(), comment: .join(node.css(.review-words::text).getall()).strip(), date: node.css(.review-date::text).get(default).strip(), cost: node.css(.cost::text).get(default).strip(), }) return items这段代码的逻辑很直白fetch_page负责拿 HTMLparse_reviews负责抽字段。关键参数有三个。timeout10是必须的不设超时一个卡住的请求能把整个脚本挂死。default保证字段缺失时不会抛异常脏数据留到清洗层处理抓取层只负责「尽量拿全」。random和time.sleep配合做请求间隔我一般设 2 到 5 秒随机太快必被封。2.2 分页终止与断点续爬别让一次中断白跑分页循环最容易翻车的地方是终止条件。很多教程写「当返回的评论数为 0 就停」但大众点评在超出范围时可能返回上一页内容或空列表你得同时判断「本页条数」和「是否与上一页重复」。更稳的做法是设一个最大页数上限配合去重集合。def crawl_shop(shop_id, max_pages50): seen set() all_rows [] for page in range(1, max_pages 1): html fetch_page(shop_id, page) if not html: break rows parse_reviews(html) if not rows: print(fshop{shop_id} 第{page}页无数据停止) break new_count 0 for r in rows: key (r[user], r[date], r[comment][:30]) if key not in seen: seen.add(key) all_rows.append(r) new_count 1 if new_count 0: print(fshop{shop_id} 第{page}页全重复停止) break time.sleep(random.uniform(2, 5)) return all_rows def save_csv(rows, pathraw_reviews.csv): if not rows: return with open(path, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys()) writer.writeheader() writer.writerows(rows)encodingutf-8-sig这个细节很多人忽略用 Excel 打开 CSV 时中文乱码就是因为少了 BOM 头。断点续爬的思路是把seen集合和all_rows定期落盘脚本崩了下次读回来接着跑。我一般每抓 5 页存一次用json存中间态比 CSV 方便因为集合不能直接序列化得转成列表。提示抓取频率和并发数直接决定你会不会被封。单 IP 单线程、间隔 2 秒以上是能长期稳定跑的下限。别一上来就开多线程先跑通单线程再说。3. 数据清洗把「4.5分」「¥120/人」变成能算的数值3.1 脏数据的四种典型形态和对应处理抓下来的原始数据字段类型几乎全是字符串而且混着各种符号。我把它归成四类每类有固定的处理套路。第一类是带单位的数值比如评分「4.5分」「4.5」人均「¥120/人」「120元」。处理方式是正则提取数字部分。第二类是缺失值评论里经常没有人均消费字段是空字符串或「暂无」。这类不能简单填 0因为 0 和「未知」在消费分析里含义完全不同我一般填NaN后续统计时用dropna或单独标注。第三类是时间格式不统一「2024-03-15」「3月15日」「2024.03.15」都可能出现统一转成datetime。第四类是文本噪声评论里的换行、多余空格、表情符号影响后续分词和关键词统计。import pandas as pd import numpy as np import re def clean_score(x): 4.5分 - 4.5, 空值 - NaN if not isinstance(x, str) or not x.strip(): return np.nan m re.search(r(\d(\.\d)?), x) return float(m.group(1)) if m else np.nan def clean_cost(x): ¥120/人 - 120.0, 暂无 - NaN if not isinstance(x, str): return np.nan if 暂无 in x or not x.strip(): return np.nan m re.search(r(\d(\.\d)?), x) return float(m.group(1)) if m else np.nan def clean_date(x): 多种日期格式统一 if not isinstance(x, str): return pd.NaT x x.strip() x re.sub(r[年月], -, x).replace(日, ) return pd.to_datetime(x, errorscoerce) df pd.read_csv(raw_reviews.csv) df[score_num] df[score].apply(clean_score) df[cost_num] df[cost].apply(clean_cost) df[date_dt] df[date].apply(clean_date) df[comment] df[comment].str.replace(r\s, , regexTrue).str.strip()errorscoerce是 pandas 里最实用的参数之一转换失败的直接变NaT不会让整个脚本崩掉。清洗完一定要做一次体检看看每列的空值率和分布不然你后面算出来的均值可能是被几个异常值带偏的。print(df[[score_num, cost_num]].describe()) print(空值率\n, df[[score_num, cost_num, date_dt]].isna().mean())3.2 去重、异常值和时间窗口对齐清洗完基础字段还有三件事要做。去重不能只按评论内容因为不同用户可能发一样的「好吃」得用「用户 日期 内容前 30 字」组合键。异常值方面人均消费出现 9999 这种基本是商家填的占位符或者用户乱填我一般按分位数截断超过 99 分位的直接置为NaN。时间窗口对齐是为了做趋势分析把date_dt转成「年-月」或「年-周」方便按月聚合。# 去重 df df.drop_duplicates(subset[user, date, comment], keepfirst) # 异常值截断 q99 df[cost_num].quantile(0.99) df.loc[df[cost_num] q99, cost_num] np.nan # 时间窗口 df[year_month] df[date_dt].dt.to_period(M).astype(str) df df.dropna(subset[date_dt]) # 时间缺失的行对趋势分析无用这里有个血泪经验dropna的时机很关键。如果你在清洗早期就把所有含空值的行删掉可能损失一半数据因为人均消费缺失太常见了。正确做法是分字段处理——做评分分析时用有评分的行做消费分析时用有人均的行各取所需而不是一刀切。注意to_period(M)转出来是 Period 类型直接存 CSV 会变成「2024-03」想保留这个格式没问题但如果要再转回时间戳记得用to_timestamp()。4. 消费分析报告从清洗后的表到能看的结论4.1 该算哪些指标为什么不是越多越好数据清洗完很多人第一反应是「把所有能算的都算一遍」结果报告里堆了二十个指标没人看得懂。消费分析报告的核心是回答三个问题这家店贵不贵、大家满不满意、什么时候人多。对应到指标就是人均消费分布、评分与消费的关系、评论量的时间趋势。三个方向每个方向两三个指标足够了。人均消费分布不能只看均值因为消费数据天然右偏少数高消费会把均值拉高。我一般同时给中位数和四分位数。评分与消费的关系用分组聚合把人均消费分成几档看每档的平均评分能发现「是不是越贵评分越高」这种反直觉结论。时间趋势按月聚合评论数能看出旺季淡季。# 人均消费分布 cost_stats df[cost_num].describe(percentiles[0.25, 0.5, 0.75]) print(cost_stats) # 消费分档 vs 平均评分 df[cost_bin] pd.cut(df[cost_num], bins[0, 50, 100, 200, 500, np.inf], labels[50以下, 50-100, 100-200, 200-500, 500以上]) score_by_cost df.groupby(cost_bin, observedTrue)[score_num].agg([mean, count]) print(score_by_cost) # 月度评论趋势 monthly df.groupby(year_month).size().reset_index(namereview_count) print(monthly.tail(12))pd.cut的分箱边界不是拍脑袋定的得看你的数据实际分布。如果大部分消费集中在 80 到 150那分箱就该围绕这个区间细化。observedTrue在新版 pandas 里必须加否则空的分箱也会出现在结果里干扰阅读。4.2 把结论落成图表和一句话摘要指标算出来只是半成品报告要能让人一眼看懂。我的习惯是每个方向配一张图加一句话结论。图用 matplotlib 或 seaborn 都行重点是别追求花哨柱状图、箱线图、折线图三件套覆盖 90% 场景。一句话结论要具体比如「人均 100-200 区间的评分最高达到 4.6说明中端价位性价比感知最强」而不是「消费与评分存在相关性」这种废话。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 中文显示 plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 3, figsize(15, 4)) df[cost_num].dropna().hist(axaxes[0], bins30) axes[0].set_title(人均消费分布) score_by_cost[mean].plot(kindbar, axaxes[1]) axes[1].set_title(各消费档平均评分) monthly.set_index(year_month)[review_count].plot(axaxes[2]) axes[2].set_title(月度评论量趋势) plt.tight_layout() plt.savefig(report.png, dpi150)SimHei是 Windows 自带中文字体Linux 上可能没有换成WenQuanYi Micro Hei或直接指定字体文件路径。dpi150保证导出图清晰报告里插图够用。这三张图加上前面的指标表就是一份能交付的消费分析报告的最小形态。提示如果要做成可交互的报告pandas 的to_excel配合 openpyxl 能直接输出带格式的表格比 CSV 更适合给非技术同事看。5. 避坑与排查那些让脚本半夜挂掉的细节5.1 请求返回 200 但内容是空的现象resp.status_code是 200但parse_reviews抽出来 0 条。原因通常是触发了反爬服务器返回了一个验证页或空壳页状态码却是正常的。解决办法是先打印resp.text[:500]看实际内容如果出现「验证」「滑动」等字样说明需要降低频率或更换 Cookie。我一般会在解析前加一个判断如果页面里没有预期的 CSS 选择器就记录警告并跳过。5.2 清洗后数据量骤减现象原始 5000 条清洗完只剩 800 条。原因多半是dropna用得太狠或者日期解析失败率过高。排查方法是分步骤统计每列的空值率找出「杀手字段」。如果是日期格式太杂导致to_datetime大量失败就扩展clean_date的正则把更多格式纳入。记住一个原则清洗的目标是「让数据可用」不是「让数据完美」能保留的尽量保留。5.3 分组聚合结果里出现 NaN 分组现象groupby之后多出一行索引是NaN的组。原因是分组字段本身有空值pandas 默认会把空值也当成一组。解决办法是在分组前dropna(subset[分组字段])或者用groupby(..., dropnaTrue)新版 pandas 支持。这个坑在按消费分档时特别常见因为pd.cut对NaN会返回NaN类别。5.4 中文乱码在 Excel 和代码里反复横跳现象代码里打印正常存成 CSV 用 Excel 打开乱码或者反过来。根因是编码不一致。写 CSV 用utf-8-sig读的时候也用utf-8-sig保持一致。如果中间经过 Excel 编辑再读回来可能变成gbk这时候用chardet检测一下实际编码再读。别小看这个我见过有人因为乱码以为数据抓错了白白重抓一遍。5.5 时间戳时区导致的趋势错位现象月度趋势图里某个月的数据明显偏少或偏多。原因是to_datetime默认按本地时区解析如果原始数据里混了不同时区的时间聚合就会错位。稳妥做法是解析时统一指定utcTrue再转成你需要的时区。对于评论数据一般统一到东八区即可但显式处理比默认行为可靠。6. 进阶把一次性脚本变成可复用的分析模板跑通一遍之后你会发现真正耗时的不是写代码而是每次换一家店就要改一堆硬编码。我的做法是把整条链路拆成配置驱动用一个config.yaml存店铺 ID 列表、请求间隔、分箱边界、输出路径主脚本读配置跑循环。这样换目标只需要改配置不用动代码。import yaml with open(config.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) for shop in cfg[shops]: rows crawl_shop(shop[id], max_pagescfg[max_pages]) save_csv(rows, fraw_{shop[name]}.csv) # 后续清洗和分析复用同一套函数对应的config.yaml长这样shops: - id: 12345678 name: 某火锅店 - id: 87654321 name: 某日料店 max_pages: 50 request_interval: [2, 5] cost_bins: [0, 50, 100, 200, 500, 99999] output_dir: ./output配置化的好处不只是省事更重要的是可复现。你把配置和代码一起存档三个月后想重跑改个日期就能对比「这家店评分是不是掉了」。验证方法也很简单拿两家已知情况的店跑一遍看算出来的中位数人均和你在点评页面上看到的区间是否吻合吻合就说明清洗逻辑没跑偏。最后说个我自己的习惯。每次做完一个爬虫分析项目我会把清洗函数单独抽成一个clean_utils.py下次遇到类似数据直接 import。这些函数攒到十几个之后你会发现大部分脏数据处理都是重复劳动真正需要动脑的只有指标设计和结论提炼。爬虫和分析的门槛从来不在代码而在你愿不愿意花时间把数据看仔细。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站