尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

城市评论爬虫与情感分析:从数据采集到建模的全流程实战

城市评论爬虫与情感分析:从数据采集到建模的全流程实战 简介一份面向爬虫与情感分析学习者的完整项目资源包围绕潍坊、淄博两座城市5万条旅游评论数据演示从爬虫采集、数据清洗到情感打分的全流程。资源既适合入门者观察真实爬虫与NLP结合的代码组织方式也可供旅游数据分析人员用于市场舆情与满意度研究。压缩包内共1988个文件大小约29.59MB核心内容包括评论文本CSV、Python脚本、以及用于说明步骤的txt和md文档大量js/ts/json等文件主要对应项目运行所需的依赖与配置png/jpg等图片可辅助理解可视化结果整体目录较完整便于直接对照学习。目前已有587人学习/下载。通过学习可以获得完整的情感分析处理思路如何针对城市评论进行数据抓取、文本预处理、情感倾向判断并整理出游客对两地评价、满意度以及不满意细节的结论为后续改进旅游服务或开展相关项目提供可复用的素材与参考。1. 爬5万条城市评论做情感分析这套方案到底值不值得搭去年帮一个做城市文旅调研的团队处理过类似需求他们想从公开点评平台上了解游客对某座城市各景区的真实情绪但人工一条条看根本看不过来。当时我用了两周时间跑了不到 5 万条评论完成从爬虫采集、SQLAlchemy 储存爬虫数据到情感分析打分的全流程。这套组合就是今天要讲的利用爬虫爬取 5 万条城市评论并对其进行情感分析。它适合谁——手里有城市维度评论数据需求、想搭一条可复用的文本分析流水线、但又不希望一开始就上分布式爬虫或大模型服务的开发者。5 万条不是随便拍的数字这个量级刚好卡在“单机爬虫能稳定采完”和“人工标注成本可接受”的甜点上既能让情感分析模型吃饱又不会让存储和计算资源失控。2. 城市评论采集的技术选型先定边界再动手2.1 三种采集路线的取舍requests、Selenium 与开放接口做城市评论爬虫第一步不是写代码而是确认目标站点的数据获取方式。常见路线有三条适用场景差别很大。第一条是纯 requests 请求。目标站点如果存在公开的、不需要登录就能访问的页面或者前端通过 JSON 接口异步加载数据那么 requests 加 headers 伪装就能解决问题。优点是速度快、资源消耗低5 万条评论如果用这条路线配合适当并发几小时内就能采完。缺点是很多站点在 2026 年的今天早就把这类简单接口封得差不多了要么加了签名参数要么返回结果经过加密requests 直连很容易被识别成爬虫。第二条是 Selenium 驱动真实浏览器。当目标页面数据在 DOM 里、但接口被加密或需要登录态时用 Selenium 模拟真人浏览是最省事的方案。尤其适合那些反爬策略激进、但又没有专门做反爬对抗的本地生活类站点。代价是速度和并发能力都远不如 requests采集 5 万条可能要跑一整天而且浏览器进程吃内存长时间挂机需要定时重启。第三条是走官方或第三方开放接口。有些平台确实提供了带鉴权的评论接口申请到 token 之后可以在额度范围内稳定拉数据。如果你面向的是有开放平台的业务场景这条路最省心但通常评论内容、条数、频次都会受限5 万条可能需要分批次拉很多天。我的建议是先花半天看目标站点的 network 面板确认数据的真正来源。如果能直接找到 JSON 接口且不需要复杂签名就用 requests接口不可用再降级到 Selenium。不要一上来就上重型方案也不要对 requests 有执念——爬虫代码写得再干净被识破了也是白搭。2.2 5 万条数据量对应的存储方案SQLAlchemy ORM 定义评论表采集端确定后存储要第一时间定下来否则采到一半数据越来越乱。5 万条评论用 SQLite 加 SQLAlchemy ORM 就够不需要上 MySQL 和 Redis。SQLAlchemy 的价值不仅是可以方便地切换数据库后端开发用 SQLite以后数据量大了可以切 PostgreSQL更重要的是它把表结构和 Python 对象绑定在一起写入评论时不需要手写 SQL 拼接。下面是我常用的评论表模型from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base declarative_base() class CityComment(Base): __tablename__ city_comments id Column(Integer, primary_keyTrue, autoincrementTrue) city Column(String(50), nullableFalse, indexTrue) # 城市名 poi_name Column(String(100), indexTrue) # 景点或商户名称 comment_text Column(Text, nullableFalse) # 评论文本 rating Column(Float, default0.0) # 平台原始评分 comment_time Column(DateTime, defaultdatetime.now) # 评论时间 fetch_time Column(DateTime, defaultdatetime.now) # 采集时间 source_url Column(String(500), uniqueTrue) # 评论页唯一标识 def __repr__(self): return fCityComment(city{self.city}, poi_name{self.poi_name}) engine create_engine(sqlite:///city_comments.db, echoFalse) Base.metadata.create_all(engine) Session sessionmaker(bindengine)代码逻辑说明source_url字段设为uniqueTrue是全网段最关键的一步——它用来做评论去重。爬虫采集过程中同一个评论可能因为分页切换、断点续跑被重复抓到没有唯一约束的话表里会混入大量重复数据直接影响后续情感分析统计的准确性。city和poi_name加索引是因为后面肯定要按照城市和景点维度做聚合查询。2.3 请求调度与合规边界采集策略上5 万条评论建议采取“单机加限速”方案。所谓限速不是一条条抓而是控制请求频率每抓完一个页面 sleep 1 到 2 秒同时把失败重试做在请求层而不是解析层。这里必须先说清楚合规问题爬虫爬到公开数据不等于可以随意使用。城市评论包含用户主观表达涉及个人信息和平台服务协议。具体到操作层面我只建议针对明确允许爬取的站点、有公开 API 的站点、或是拿到授权的内部数据源。菜鸟最容易踩的坑是看到别人抓了某平台评论就去照抄却没注意到目标站点的 robots.txt 和服务条款。另外采集频率别设得太激进5 万条数据用每秒 1-2 个请求的速度慢慢拉既稳定又不容易触发风控。抓下来的数据如果要商用必须先做匿名化处理去掉用户名、头像、用户 ID 等字段只保留评论内容和时间这是底线。3. 用 Python 爬虫落库到 SQLAlchemy采集循环、字段设计与断点续采3.1 采集主循环从列表页到详情页的解析流程城市评论的页面结构一般分两层列表页展示评论概要详情页或展开区域包含完整文本。实际采集时我通常直接抓列表页里已经渲染出来的评论内容没必要进详情页。下面是一个基于 requests 加 BeautifulSoup 的最小可运行示例import requests import time import random from bs4 import BeautifulSoup from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from models import CityComment, Base # 上面定义的表模型 engine create_engine(sqlite:///city_comments.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine) session Session() HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_page_comments(city: str, poi_name: str, page: int): # 示例接口地址实际需要根据目标站点替换 url fhttps://example.com/{city}/{poi_name}/comments?page{page} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) comments [] for item in soup.select(.comment-item): # 根据实际页面结构调整选择器 text item.select_one(.comment-text).get_text(stripTrue) rating item.select_one(.comment-rating).get(data-score, 0) comments.append({ city: city, poi_name: poi_name, comment_text: text, rating: float(rating), source_url: f{url}#{item.get(data-id)}, }) return comments # 采集循环遍历城市和景点再翻页抓取 cities [北京, 上海, 成都, 杭州] poi_list [故宫, 外滩, 宽窄巷子, 西湖] total 0 for city in cities: for poi in poi_list: for page in range(1, 30): # 每个景点抓 30 页 try: items fetch_page_comments(city, poi, page) if not items: break for item in items: # 利用 source_url 唯一约束去重 session.merge(CityComment(**item)) session.commit() total len(items) print(f[{city}/{poi}] page {page} ok, total{total}) time.sleep(random.uniform(1, 2)) except requests.exceptions.RequestException as e: print(fpage error: {e}, retry after 5s) time.sleep(5) continue参数说明session.merge()是关键——如果source_url已存在它会更新而不是插入天然完成去重如果不存在则插入新记录。这比先查一遍再插入要高效很多。time.sleep(random.uniform(1, 2))让请求间隔在 1 到 2 秒之间随机化避免规律性太强被识别。超时设为 10 秒网络抖动时快速失败重试不拖住整个循环。3.2 断点续采设计中断后不从头再来5 万条评论不是几分钟能采完的中间大概率会遇到断网、进程被杀、目标站点临时封 IP。如果设计不好每次中断后都从第一页重新爬时间和带宽都浪费严重。我采用的方案是维护一个独立的采集进度表class CrawlProgress(Base): __tablename__ crawl_progress id Column(Integer, primary_keyTrue) city Column(String(50)) poi_name Column(String(100)) page Column(Integer, default1) status Column(String(20), defaultpending) # pending/done/error updated_at Column(DateTime, defaultdatetime.now, onupdatedatetime.now)采集主循环改为先查进度表已经完成的城市和景点直接跳过未完成的从上次的页数继续。同时给city poi_name建联合唯一索引保证进度表每次更新都是同一个任务。另一个工程上的细节是每采集完一个页面就 commit 一次而不是攒一批再提交。SQLite 的写入性能足够支撑这个频率但好处是即便进程崩溃已提交的数据也不会丢。这个习惯陪我避开了很多返工。3.3 评论字段的清洗策略入库前先做一次粗洗很多评论文本里混合着表情符号、用户、链接、还有“这条评论来自某 APP”之类的尾巴。这些内容对后面的情感分析是噪音入库前最好做粗洗。import re def clean_comment(raw: str) - str: # 去掉 URL text re.sub(rhttps?://\S|www\.\S, , raw) # 去掉 用户 text re.sub(r\w, , text) # 去掉 APP 尾巴关键词 text re.sub(r来自\w*(客户端|APP|小程序), , text) # 去掉多余空白 text re.sub(r\s, , text).strip() return text这段粗洗逻辑不追求完美只处理高频噪音。注意不要在这里做去除停用词的操作——情感分析模型通常自带预处理而且像“不”“不是”这类否定词一旦被当成停用词去掉情感判断会完全颠倒。我见过不少人在爬虫阶段就把否定词洗掉了后面做出来的分析结果整个偏离。4. 把 5 万条评论喂给情感分析模型清洗、批量预测与置信度过滤4.1 情感分析的模型选择SnowNLP 还是大模型 API5 万条中文评论属于典型的短文本情感分类任务可选方案无非三类基于规则的情感词典、传统机器学习模型如朴素贝叶斯/SVM、深度学习或大模型。规则词典适合极少数专业领域比如“好吃”“难吃”这种餐饮高频词但城市评论涉及吃住行游购娱多个主题词典根本覆盖不过来。支持向量机这类方案需要先做特征工程准确率上限有限。当前从业者最常用的两条路线是本地跑 SnowNLP 或 bert 系列模型或者调用大模型 API 做批量打分。SnowNLP 的优势是零成本、离线可用、安装即用缺点是默认模型是在电商评论上训练的换到城市文旅场景准确率会下降。大模型 API 的效果明显更好上下文理解能力强能处理“景区很美但是人太多”这种转折句式但成本和时间需要考虑——5 万条评论逐条调用 API按目前主流价格大概在几十到几百元之间且要控制并发防止限流。我一般建议冷启动阶段用 SnowNLP 跑通全流程验证方案可行后再用少量标注数据对比大模型 API 是否值得替换。先让流水线转起来再谈精度。4.2 用 SnowNLP 批量预测并落库下面这段代码从 SQLite 读取已采集的评论用 SnowNLP 计算情感得分再把得分写回数据表from snownlp import SnowNLP from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from models import CityComment engine create_engine(sqlite:///city_comments.db) Session sessionmaker(bindengine) session Session() batch_size 500 offset 0 while True: comments session.query(CityComment) \ .filter(CityComment.sentiment_score.is_(None)) \ .offset(offset).limit(batch_size).all() if not comments: break for comment in comments: try: s SnowNLP(comment.comment_text) comment.sentiment_score s.sentiments # 0~1 之间的浮点数 except Exception as e: comment.sentiment_score 0.5 # 解析失败时给中性分 print(ferror: {comment.id}, {e}) session.commit() offset batch_size print(fprocessed: {offset}) session.close()逻辑说明这里用sentiment_score.is_(None)做增量判断模型跑过一遍的评论不会重复计算中途失败了也只需要从断点继续。SnowNLP 的sentiments属性输出 0 到 1 之间的值0 为极负面1 为极正面0.5 左右算中性。代码里把解析异常的评论默认设为 0.5是为了不让空值破坏后续统计。但必须说清楚SnowNLP 的 0.6 分与 0.8 分之间的差距在文旅场景里并不完全可靠。实际项目中情感分析不能只看分数绝对值更合理的做法是转换成三级分类负面小于 0.4、中性0.4 到 0.6、正面大于 0.6这样后续分析可以容忍一定的分数偏差。4.3 多模态情感分析的扩展方向如果你的城市评论数据不只是文本还包含图片和视频——这在 2026 年的本地生活平台其实是常态——那么可以关注多模态情感分析这个方向。多模态情感分析指的是把文本、语音、图像特征融合起来做综合判断比如一条评论文字是“风景如画”配图却是大片垃圾堆文字模型可能给出正面分但图像信号会把它拉回负面。文本情感分析和多模态情感分析的选型区别在于前者关注语言表达中的情绪倾向后者需要同时理解视觉内容、语音语调与文本的交叉印证。对于 5 万条评论的体量如果大多数评论带图可以先用文本模型粗筛一遍再对“图文明显不一致”的样本做多模态复核而不是一开始就全量跑多模态模型——推理成本会高很多。4.4 置信度过滤把不确定的样本单独拎出来情感分析模型输出的分数背后还有置信度概念。很多入门教程不会提这一点但在真实项目里5 万条评论里总有几百条是模型拿不准的。处理方法是把分数落在 0.45 到 0.55 之间的评论单独切出来不参与聚合统计或者进入人工标注队列。uncertain session.query(CityComment) \ .filter(CityComment.sentiment_score.between(0.45, 0.55)) \ .count() print(flow confidence samples: {uncertain})这个数量如果超过总样本的 15%说明模型与目标场景的匹配度有问题需要重新考虑训练数据或换大模型。如果只占 5% 左右直接把它们标为中性即可不影响整体结论。5. 城市评论爬虫与情感分析的 5 个常见坑从反爬绕过到数据污染5.1 Selenium 被识别为自动化工具现象用 Selenium 启动 Chrome 访问目标页面正常打开首页但一登录或一翻页就弹出滑块验证甚至直接返回 403。原因新版 Chrome 浏览器的navigator.webdriver属性默认暴露给了网页脚本一些反爬体系通过检测这个标志就能判断你是程序控制的浏览器进而拒绝服务或要求验证。解决启动参数里加排除自动化标志的配置。核心代码如下from selenium.webdriver import Chrome from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver Chrome(optionsoptions)注意这只是基础绕过2026 年的不少站点还会检测浏览器指纹、屏幕尺寸、鼠标轨迹。如果加了自动化标志仍然被识别就别硬刚了要么换接口路线要么降低采集频率伪装成低活跃真用户。5.2 评论长度截断导致关键否定词丢失现象爬下来的评论文本后半段被省略号或“展开”截断比如“这家服务态度一点也不好环境倒是”后面没了情感分析给它打了 0.8 的高分。原因很多前端页面出于布局考虑默认只显示评论的前 50 到 100 字完整内容需要点击“展开”。爬虫只抓了默认渲染的那一部分导致文本不完整。解决采集前先看页面有没有展开按钮有的话在解析阶段主动触发展开或者直接抓取详情页/接口中的完整字段。如果是 Selenium可以模拟点击展开后再提取文本。宁可少采一些完整评论也不要采几千条截断文本——数据质量差比数据量少更致命。5.3 热门城市数据倾斜看着有 5 万条重大城市占了 4 万现象最终统计“全国城市综合好评率”时结果被头部城市主导三四线城市因为样本量太少情感分析结果波动极大。原因采集循环是从城市列表顺序执行的热门城市评论多、翻页深冷门城市可能只抓了十来条就到底了。总量 5 万条有了但分布完全不均匀。解决采集阶段做配额。每个城市设定最小样本量比如 2000 条没达到的城市优先抓取已经超出配额的城市停止抓取。进度表里加一个quota_done字段调度逻辑变成优先处理欠配的城市。5.4 情感分析结果出现地域性误判现象模型把“一般般”“还将就”“凑合”全都判成中性甚至正面但这些词在本地人的真实表达里多少带点负面意味。原因SnowNLP 的底层词典来自电商评论而电商语境里的中性表达和旅游场景的口语表达有系统性差异。“还将就”在买东西场景里可能真的是还行但旅游场景说出来往往是失望。解决领域适配的两种方式——一是收集 1000 条左右的城市评论人工标注基于标注结果修正词典或微调模型二是在分析结果里跑一遍“消极词频抽查”看看特定负面词的实际占比是否符合预期。这个坑不建议绕直接面对才有价值。5.5 被反爬策略封禁后全链路阻塞现象采集进行到第三天某天早上发现连续 10 页返回 403紧接着所有采集任务停摆需要重新换 IP。原因对目标站点的请求频率看起来单看每页间隔有 1 到 2 秒但集中在一个小时内的请求总数还是明显超出人类行为特征。尤其是列表页和评论页交替请求时规律一旦被识别封禁就来了。解决封禁之后不要着急继续采先停一晚上。然后调整策略把请求频率降到每页 3 到 5 秒同时打乱访问顺序不按照城市列表顺序逐个翻页而是随机挑选城市和景点。还可以把请求分散到多个时间段比如白天采一批、半夜采一批不让请求流量过于集中在同一时间窗口。6. 让情感分析结果真正可信标注抽样、指标复核与上线前最后一测链路跑通以后最容易被忽略的是验证。5 万条评论的情感分析结果如果没有人抽查过你根本不知道模型输出在多大程度上反映真实的城市口碑。我自己的习惯是即使预算紧张也会抽 300 到 500 条人工标注。具体做法从最终落库的 5 万条评论里随机抽取样本人工判断真正的情感倾向是正面、中性还是负面然后与 SnowNLP 的自动分类做交叉比对计算三种错误率把负面判成正面、把正面判成负面、把中性判成情感极性。如果错误率超过 15%说明当前模型不能直接使用必须进入领域适配环节——这比盲目扩大数据量更迫在眉睫。小规模的抽检代码可以和清洗逻辑写在一起import random from sqlalchemy.orm import sessionmaker from models import CityComment session Session() sample random.sample(session.query(CityComment).all(), 400) manual_labels {} # 人工标注: {comment_id: pos/neu/neg} confusion {pos:pos: 0, pos:neg: 0, neu:pos: 0} for comment in sample: label manual_labels.get(comment.id) if not label: continue pred pos if comment.sentiment_score 0.6 else (neg if comment.sentiment_score 0.4 else neu) key f{label}:{pred} confusion[key] confusion.get(key, 0) 1 print(confusion)这段代码的思路是先抽样本、再关联人工标注和模型预测重点关注模棱两可的样本落在哪个类别里。manual_labels需要你自己标注实际操作中可以让两个人各自标一遍不一致的样本单独讨论消解主观偏差。最后一步是验证分析结果是否符合常识。比如杭州、成都的正面情感占比显著高于整体平均这符合常规认知但如果爬下来的数据显示某知名荒漠景区正面情感占比超过 95%大概率是采集样本偏了或模型误判了。我会把得分最高的 20 条和得分最低的 20 条评论打印出来逐条看一遍确认模型没有把反讽当正面、把失望当中性。这种人工目检花不了几分钟但对项目交付的价值远大于多爬几千条数据。爬虫加情感分析这套流程数据是地基模型是支架最后的验证才是交付标准。我的经验是前端时间花在工程上后端时间必须砸在数据质量上。布局不合理的存储结构可以改模型不对可以换但数据本身是脏的后面所有环节都会连锁翻车。希望这些思路和坑位盘点能帮你在动手前看清全局少走我走过的那些弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表