简介基于 Scrapy 框架的大众点评爬虫面向需要采集大众点评城市、分类与店铺信息的 Python 开发者。项目演示了浏览器登录与手动过验证、代理设置、字体反爬处理等常见反爬应对思路并给出城市列表、分类列表、店铺基本信息等结构化抓取流程。资源共 19 个文件压缩包大小约 21KB核心为 13 个 Python 脚本覆盖爬虫、管道、中间件、工具函数等模块并有清晰拆分的数据定义、项目配置与依赖清单另有 requirements.txt 依赖清单、README.md 使用说明与 scrapy.cfg 项目配置便于本地搭建环境后直接运行。爬虫对数据依赖顺序有明确约定先爬取城市和分类再爬取店铺链接最后爬取店铺详情目录中同时保留 .gitkeep 与 LICENSE项目结构完整。目前已有 596 人学习下载适合想通过完整案例快速上手 Scrapy 并处理登录、代理与字体反爬的开发者。1. 基于 Scrapy 框架的大众点评爬虫先想清楚要拿什么再考虑写代码很多人一听到大众点评爬虫第一反应是赶紧把 Scrapy 项目脚手架搭起来结果第一周全耗在验证码和封 IP 上连一条评论都没存进数据库。大众点评反爬的核心逻辑并不复杂它允许你看但绝不让你批量看。爬虫跟它的博弈本质是「访问频率像不像人」。Scrapy 作为异步爬虫框架并发拉满能到几十路请求但用在点评上恰恰要反着调参数——限速、重试、Cookie 轮换比框架本身的抓取能力更重要。这个标题的落地路径我建议分四步走先分析页面结构和接口再搭 Scrapy 工程然后处理登录态和反爬最后把数据通过 SQLAlchemy 落到 MySQL。适合的人群是已经会用 Scrapy 抓静态站、想啃下动态渲染和高反爬站点的人。本篇会直接给出可复现的 Item、Pipeline、中间件代码并标注我实际调过的一组合适参数。2. 大众点评的反爬底牌先看懂页面再选 Scrapy 还是 Playwright2.1 点评页面的真实结构列表页是静态的评论是动态 iframe写爬虫最忌讳拿到 URL 就去写 parse 函数。大众点评不同模块的渲染方式完全不同这一点决定了你用 Scrapy 直接请求还是要上 Playwright 渲染。先说结论榜单页和商户列表页比如「美食」分类下的商家列表返回的是服务端渲染的 HTML评论详情页则把内容塞进一个动态 iframe。你在浏览器里看到的评论正文其实不是当前 document 里的 DOM而是 iframe 子文档的内容。用 Requests 或 Scrapy 直接请求评论页拿到的是空壳页面里面只有一个 iframe 的 src 指向一个带 token 的 URL。这类问题用 Python 爬虫的视角看有三种解法。第一种是直接请求 iframe 的真实地址但它的 URL 里有时效性很强的参数通常基于服务器时间戳和商户 ID 做签名签名算法在 JS 里加密。第二种是解析页面里预先注入的 window.INITIAL_STATE之类的全局变量点评在部分页面上会内联 JSON 数据。第三种是干脆引入 Playwright 渲染整个页面等 iframe 加载完再取内容。我实际测试下来评论区用第一种最省资源你需要的是抓包找到那个 iframe URL 的拼接规则而它的核心参数是 shopId 和时间戳签名部分并不总是出现很多城市站点的评论区 URL 是可以直接构造的。Scrapy 和 Playwright 在这个场景的正确分工是Scrapy 负责抓取、解析、去重、调度、存储Playwright 只负责「补渲染」。在 Scrapy 的 downloader middleware 里挂一个 Playwright 渲染器当检测到响应里没有目标选择器、且 URL 命中需要渲染的规则时自动用 Playwright 重新加载页面。这种做法比全站 Playwright 慢但比肉眼等它跑完要快得多而且能躲掉很多「检测 Headless 浏览器」的脚本。2.2 用抓包确认数据接口打开开发者工具的三个关键观察点我一般会在 Chrome 无痕窗口里打开目标城市的目标分类页F12 切到 Network 面板然后按 F5 重新加载。三个关键观察点按优先级排一是过滤 XHR 和 Fetch 请求看有没有返回 JSON 的接口二是看 doc 类型的首个响应确认页面是服务端渲染三是看 iframe 相关请求点评评论页的 iframe URL 通常带有 shop_id 和 s 开头的签名参数。一个比较典型的发现是列表页首屏的数据几乎都在 HTML 里翻页则走一个异步 POST 接口参数里有 cityId、cateId 和 pageNum返回值是 HTML 片段。也就是说列表页不需要 Playwright直接用 Scrapy 的 FormRequest 翻页就行。评论页则复杂一些iframe 的 src 指向的路径里带一个shopId和一个形如tokenxxx的参数这个 token 在页面源码里可以找到一般是一个全局变量的值。直接把 token 拼到请求里响应就是干净的 HTML。建议把抓包结果整理成一张表贴到项目文档里因为后面写解析和中间件时你总会回来翻它。记录字段至少包括请求类型doc/xhr/iframe、URL 模板、关键参数、返回格式、是否带 Cookie 依赖、是否需要渲染。有了这张表Scrapy 的下载中间件该怎么配置就清楚了。2.3 动态 iframe 的抓取策略什么时候用 Playwright什么时候纯 Scrapy先说一个判断标准如果你构造出的 iframe URL 直接返回 200 且有评论内容就没必要上 Playwright。用 PyCharm 的 HTTP client 或者直接在终端里curl一下这个 URL看返回的 HTML 里有没有comment-list这类关键词就能快速确认。如果返回 403 或者跳登录再考虑渲染方案。Playwright 接入 Scrapy 的做法是安装scrapy-playwright插件并在 settings.py 里配置DOWNLOAD_HANDLERS。它最大的价值不是解决反爬而是执行页面里的 JavaScript让 iframe 里的内容真正出现在 DOM 里。代价是资源占用高一个浏览器实例要占几百 MB 内存并发级别只能降到 1 到 2。所以我只建议在「页面数据必须靠运行时渲染才能拿到」时才启用绝不全线开启。下面给一个最小配置示例放在 Scrapy 项目的 settings.py 里# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } PLAYWRIGHT_BROWSER_TYPE chromium PLAYWRIGHT_LAUNCH_OPTIONS { headless: True, args: [--no-sandbox, --disable-gpu] } DOWNLOADER_MIDDLEWARES { dianping.middlewares.RenderMiddleware: 543, }RenderMiddleware 的写法是在 process_request 里通过meta传入渲染开关# middlewares.py from scrapy import signals class RenderMiddleware: async def process_request(self, request, spider): if request.meta.get(render) is True: request.meta[playwright] True request.meta[playwright_include_page] True return None这样在 spider 里发请求时只需要对真正需要渲染的 URL 设置meta{render: True}其余请求仍走 Scrapy 原生下载器吞吐量不会整体下降。参数说明--no-sandbox是 Linux 服务器上跑 Chromium 的常规参数不加上去很多系统会直接报错PLAYWRIGHT_LAUNCH_OPTIONS里的headless建议设成 True调试时才改 False渲染请求必须playwright_include_page为 True不然你拿不到 page 对象也就没法等 iframe 里的内容。3. 搭一个能跑的 Scrapy 项目从 Items 到数据库落库的最小骨架3.1 项目结构和 Item 设计先定义好要存什么Scrapy 项目的标准结构大家都熟spiders 目录放爬虫items.py 定义字段pipelines.py 做数据清洗和存储middlewares.py 写下载中间件和爬虫中间件settings.py 做全局配置。点评爬虫在 items.py 里最少要定义三个 ItemShopItem 存商户基础信息CommentItem 存评论内容UserItem 存评论用户信息。不要图省事只写一个字典到处传后面存数据库时你会发现字段名对不上改起来非常痛苦。我用 SQLAlchemy 存储爬虫数据所以会把每个 Item 映射到一张表。先看 Item 定义# items.py import scrapy class ShopItem(scrapy.Item): shop_id scrapy.Field() shop_name scrapy.Field() address scrapy.Field() avg_price scrapy.Field() rating scrapy.Field() review_count scrapy.Field() category scrapy.Field() crawl_time scrapy.Field() class CommentItem(scrapy.Item): comment_id scrapy.Field() shop_id scrapy.Field() user_id scrapy.Field() content scrapy.Field() rating scrapy.Field() comment_time scrapy.Field() crawl_time scrapy.Field()字段说明shop_id是点评站内的商户 ID所有后续请求都以它为核心比如评论页 URL 里的shopId参数review_count是商户页展示的评论总数用来估算需要翻多少页crawl_time加在每一个 Item 上是方便以后做增量更新时排查数据时效性。comment_id不一定能拿到有些评论没有全局唯一 ID可以用shop_id 时间戳 用户ID拼接生成这一逻辑放在 Pipeline 里做。Item 设计完成之后spider 里只需要把字段填充好然后yield item不要自己塞进数据库。这也是我建议用 Scrapy 而不是纯 Requests 写爬虫的原因——框架把调度、去重、异常重试、持久化都拆成了模块数据流是单向且清晰的。3.2 Spider 核心解析逻辑XPath 取数最常见的翻车点点评的 HTML 结构不算复杂字段基本都能靠 XPath 取到。商户列表页每一条商户信息包在一个div里标题、星级、人均、评论数都在这个 div 内的固定层级。翻车点通常在「取不到」而不是「取错」因为页面里混着广告位和推荐位它们的 DOM 结构和正常商户一致但缺少部分字段。所以解析时不要用//div[classitem]一把梭要先定位到真实列表容器。我给的写法是用response.xpath定位列表根节点然后遍历每条子节点在循环里单独取各字段。部分字段取不到就设默认值比如人均价格取不到就填 0之后存储时再过滤。核心代码# spiders/shop_list.py import scrapy from dianping.items import ShopItem class ShopListSpider(scrapy.Spider): name shop_list def start_requests(self): base_url https://www.dianping.com/{city}/food city_codes [bj, sh, gz] for code in city_codes: yield scrapy.Request(urlbase_url.format(citycode), callbackself.parse_list) def parse_list(self, response): # 定位真实商户列表容器避开广告位和推荐位 container response.xpath(//div[idshop-all-list]/ul/li) if not container: self.logger.warning(未找到商户列表节点可能触发了验证码或页面改版) return for li in container: item ShopItem() item[shop_name] li.xpath(.//h4/text()).get() item[shop_id] self.extract_shop_id(li) item[address] li.xpath(.//span[classaddr]/text()).get() item[avg_price] li.xpath(.//span[classmean-price]/text()).get() item[rating] li.xpath(.//span[contains(class,star-rating)]/data-star).get() yield item # 翻页处理 next_url response.xpath(//a[contains(class,next)]/href).get() if next_url: yield scrapy.Request(urlresponse.urljoin(next_url), callbackself.parse_list)参数说明city_codes是城市拼音缩写点评的 URL 结构是主域名后直接跟城市码这个变量可以扩展成整个城市列表># pipelines.py from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, UniqueConstraint from sqlalchemy.orm import sessionmaker, declarative_base from datetime import datetime Base declarative_base() class Shop(Base): __tablename__ shops id Column(Integer, primary_keyTrue, autoincrementTrue) shop_id Column(String(32), nullableFalse) shop_name Column(String(128)) address Column(String(256)) avg_price Column(Float) rating Column(Float) crawl_time Column(DateTime, defaultdatetime.now) __table_args__ (UniqueConstraint(shop_id, nameuq_shop_id),) class Comment(Base): __tablename__ comments id Column(Integer, primary_keyTrue, autoincrementTrue) comment_id Column(String(64), nullableFalse) shop_id Column(String(32), nullableFalse) content Column(String(2000)) rating Column(Float) comment_time Column(DateTime) crawl_time Column(DateTime, defaultdatetime.now) __table_args__ (UniqueConstraint(comment_id, nameuq_comment_id),) class DatabasePipeline: def __init__(self, db_url): self.engine create_engine(db_url, pool_size10, pool_recycle3600) Base.metadata.create_all(self.engine) self.Session sessionmaker(bindself.engine) classmethod def from_crawler(cls, crawler): return cls(db_urlcrawler.settings.get(MYSQL_CONNECTION_STRING)) def process_item(self, item, spider): session self.Session() try: if isinstance(item, ShopItem): row Shop(**dict(item)) session.merge(row) elif isinstance(item, CommentItem): row Comment(**dict(item)) session.add(row) session.commit() except Exception as e: session.rollback() spider.logger.error(f存储失败: {e}, item{dict(item)}) finally: session.close() return item这段代码里需要重点说明的是session.merge(row)而不是session.add(row)。merge 会先按主键查一次保底逻辑是「存在就更新不存在才新增」天然兼容「断点续爬后重复抓取同一家店」的场景。评论表不设更新时间字段是因为评论内容极少变化增量爬虫只抓新增评论就行。pool_recycle3600是为了防止 MySQL 的 wait_timeout 把连接回收掉导致隔一段时间后报 MySQL server has gone away 的经典错误。设置了MYSQL_CONNECTION_STRING的样例值一般是mysqlpymysql://user:password127.0.0.1:3306/dianping?charsetutf8mb4注意要加charsetutf8mb4否则点评正文里的 emoji 表情会在写入时报 Incorrect string value。这一步“后悔药”很重要等爬到一半发现库里有几千条乱码再改字符集数据和表结构都会很麻烦。4. 登录态、Cookie 与请求头把爬虫伪装成「正常人」4.1 Cookie 的获取和注入手动登录一次长期复用大众点评的很多数据接口对未登录态只返回部分内容尤其是评论全文和店铺电话这种敏感字段。解决思路不是让爬虫自动登录——点评登录有滑块验证码自动化成本极高——而是手动在浏览器登录一次导出 Cookie 存成文件Spider 启动时加载并注入请求头。这个方案不优雅但非常实用只要你不频繁更换出口 IPCookie 能稳定用很久。导 Cookie 的路径通常是开发者工具的 Network 面板里找到任意一个请求复制它的Cookie请求头粘贴到本地文件cookies.txt。Scrapy 里注入 Cookie 我习惯放在start_requests里统一处理而不是写在 settings 里因为 Cookie 可能过期需要随时换。代码很简单# spiders/shop_list.py import json from pathlib import Path def load_cookies(): path Path(__file__).parent.parent / cookies.json with open(path, r, encodingutf-8) as f: return json.load(f) def make_cookie_header(cookie_list): return ; .join([f{item[name]}{item[value]} for item in cookie_list])用json文件存 Cookie 的好处是 Django/Chrome 插件导出的cookies.txt格式不统一而开发者工具的 Copy as cURL 可以一键生成 JSON 格式的 Cookie 列表。组装成Cookie: name1value1; name2value2的请求头字符串Scrapy 会在每个请求上自动携带。注意点评会在多次请求后下发新的 Cookie 来追踪你的会话所以爬虫必须在中间件里捕获Set-Cookie响应头并更新本地文件否则爬久了会被判断为异常访问。这个逻辑我放在下载中间件的process_response里等看到验证码再处理就晚了。4.2 User-Agent 轮换scrapy-fake-useragent的配置与坑UA 轮换是必修课。Scrapy 默认的 UA 是Scrapy/版本号一眼假。点评的反爬系统对 UA 的检测非常敏感同一 UA 高频访问短时间内就会触发风控。我用的方案是scrapy-fake-useragent中间件它维护了一个 UA 池随机返回浏览器 UA。插件在 settings.py 里加一行即可启用DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, scrapy_fake_useragent.middleware.RandomUserAgentMiddleware: 400, } FAKEUSERAGENT_PROVIDERS [ scrapy_fake_useragent.providers.FakeUserAgentProvider, scrapy_fake_useragent.providers.FakerProvider, scrapy_fake_useragent.providers.AiohttpProvider, ]第一个坑是插件的RANDOM_UA_TYPE字段默认是random会随机选 PC 或 Mobile 的 UA。大众点评对移动端 UA 返回的是触屏版页面DOM 结构和 PC 版完全不同XPath 会全部失效。所以必须显式指定RANDOM_UA_TYPE desktop只轮换桌面端浏览器 UA。第二个坑是 UA 和 Cookie 的绑定关系。点评风控会记录「UA Cookie IP」三元组如果频繁切换 UA 但 Cookie 不变会被判定为账号在异动而强制要求登录。建议的做法是一个 Cookie 文件配一个固定的 UA 白名单比如只随机从 Chrome 的 5 个版本中选择别用全量 UA 池。4.3 请求频率控制Scrapy 的 AUTO_THROTTLE 和并发参数怎么搭配Scrapy 默认是火力全开的这在大众点评这种站点等于自杀。合理的设置是开自动限速并且手动压低并发数到一个非常保守的值。以下是实战验证过的配置# settings.py CONCURRENT_REQUESTS 4 CONCURRENT_REQUESTS_PER_DOMAIN 2 CONCURRENT_REQUESTS_PER_IP 2 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 3.0 AUTOTHROTTLE_MAX_DELAY 30.0 AUTOTHROTTLE_TARGET_CONCURRENCY 2.0 DOWNLOAD_DELAY 2.0 RANDOMIZE_DOWNLOAD_DELAY True RETRY_ENABLED True RETRY_TIMES 2 RETRY_HTTP_CODES [403, 429, 500, 502, 503]参数解释CONCURRENT_REQUESTS_PER_IP 2的含义是同一出口 IP 下最多同时处理 2 个请求这个值控制了封 IP 风险的根本命脉AUTOTHROTTLE_START_DELAY是初始延迟如果网站响应快且没出现限流Scrapy 会自动缩短延迟RETRY_HTTP_CODES把 403 也纳入重试因为点评偶尔会对单次请求误判重试一次可能就正常了。实际跑下来的感受是列表页 4 个并发、每秒约 1 个请求的速度最稳。评论接口频率要更保守建议单独给评论爬虫设置DOWNLOAD_DELAY 5.0因为评论页的 token 时效很短请求频率太快会拿到过期 token 导致大量 404。这些配置不是死的我建议先用小规模数据试出阈值再全局铺开“玄学参数”也是基于响应时间和错误码的反馈去调整的。4.4 动态 iframe 的请求头Referer 和 Origin 不能漏点评的评论 iframe 请求有一个隐蔽的反爬点它校验Referer必须是评论所属的商户页。如果用浏览器抓包看到 iframe 的请求复制Referer字段它的值是商户详情页的 URL 而不是评论页自身的 URL。Scrapy 里给评论请求设置一个不正确的 Referer或者完全不设置都会触发 403。在 spider 里构造评论请求时要手动加上yield scrapy.Request( urlcomment_url, callbackself.parse_comments, headers{ Referer: fhttps://www.dianping.com/shop/{shop_id}, X-Requested-With: XMLHttpRequest, Accept: text/html,application/xhtmlxml } )X-Requested-With这个头是模拟 AJAX 请求的标准做法有些接口会单独校验它的存在。Accept字段要和浏览器保持一致不要用 Scrapy 默认的*/*。Origin 头一般只在 POST 接口里校验GET 请求通常不强制但如果你碰到跨域才出现的 403可以补一个和 Referer 同源的 Origin 试试。5. 大众点评爬虫避坑清单从 IP 封禁到「诚实的 User-Agent」5.1 爬到一半突然全是验证码现象连续正常抓取一个多小时后所有请求返回的不是列表数据而是一张滑块验证码页面XPath 全部取空日志里出现验证码或risk字段。原因触发点评风控阈值的维度很多最常见有两个。一是同一出口 IP 的单位时间请求数超过阈值即便低频也不代表没风险长时间累计的高请求量同样会触发二是 Cookie 对应的账号被标记为「异常活跃」此时验证码不再针对 IP 而是针对账号。判断是哪种情况看验证码页面 URL 里是否带account或session参数即可。解决如果是 IP 维度最有效的方式是降低并发和增加DOWNLOAD_DELAY并且让爬虫跑一段时间就歇一阵可以加一个「定时暂停」爬虫每抓完 300 个页面后time.sleep(600)。如果是账号维度停掉爬虫手动清理该 Cookie 对应账号的登录态重新登录再导出新 Cookie。验证码本身不能绕过任何声称能自动过滑块的库都不建议用于生产一是成功率低二是代价是封号风险。5.2 评论 iframe 的 token 总是过期现象评论页构造的 URL 在浏览器里打开正常但在 Scrapy 里请求返回 403 或 404response 里提示 token 失效。原因token 是商户页服务端下发、经过 JavaScript 加密的签名有效期通常在几秒到几分钟之间。如果你的爬虫先抓商户列表再逐个进详情页取 token再回头请求评论接口中间流程一旦超过 token 有效期必然失效。另一种常见情况是 token 和请求的shopId绑定商户详情页解析时取错了 ID 也会报同样错误。解决缩短「取 token」和「发评论请求」之间的间隔。在同一个回调函数里完成两件事解析详情页拿到 token 和 shopId 后立刻构造评论请求 yield 出去不要经过 item 管道或外部存储中转。如果一定要异步处理就把 token 存到内存缓存并在 1 分钟内消费完。另外注意 token 有时效性就意味着它不能复用每个商户评论请求都必须用对应详情页返回的新 token不能像 Cookie 一样全局共享。5.3 数据库里出现大量重复数据现象第一次全量爬了 1 万条评论断点续爬后再次运行库里出现大量重复记录comment_id相同的记录有两条甚至更多。原因去重逻辑的实现位置不对。只在内存字典里做去重爬虫重启后内存清空之前见过的 ID 全部失效Scrapy 的去重机制DUPEFILTER_CLASS默认也只对 request 去重对数据内容不去重。数据库里的UniqueConstraint加上了但session.merge只对主键生效对UniqueConstraint字段不一定能正确触发更新。解决最保险的做法是在 Pipeline 的process_item里手动查一次该comment_id是否存在不存在才add存在就跳过。为了性能不用每次都SELECT可以维护一个进程内的 ID 集合每次先从数据库加载一轮已有的 ID然后只查集合内没有的。核心逻辑是「启动时全量加载运行时增量判断定期追加新 ID 到集合」。这个集合会在长时间运行下越来越大但评论量在十万量级内内存完全扛得住。5.4 用selenium反而更容易被检测现象某天为了快速抓验证码绕过逻辑把 Spider 的下载器换成了 Selenium结果请求频率比 Scrapy 慢了一大截而且没过多久就被封号了。原因Selenium 启动的 Chrome 指纹特征太明显。navigator.webdriver属性为 True、浏览器窗口尺寸异常、缺少某些插件环境、加载时间过长等都被反爬系统作为识别信号。大众点评的风控系统对这类「伪人类」的判定比纯 HTTP 请求更严格因为正常的浏览器行为不会存在 10 秒以上的机械间隔。解决不推荐在主流程里用 Selenium只用来处理特定区块的渲染。既想保留 Scrapy 的调度和存储又想拿到渲染后的数据就用 Playwright 方案因为scrapy-playwright的浏览器上下文控制比 Selenium 细粒度得多。如果不得不用 Selenium记得要在启动参数里加上--disable-blink-featuresAutomationControlled但这只是基础操作防不了高级检测。血泪经验是能用 HTTP 解决的问题千万别上真浏览器。5.5 文本编码和表情符号造成入库失败现象评论内容里包含 emoji 表情时SQL 写入报错Incorrect string value: \xF0\x9F\x98\x82 for column而且只有部分评论失败看起来像随机翻车。原因MySQL 的 utf8 字符集最多支持 3 字节的 UTF-8 字符而 emoji 是 4 字节需要 utf8mb4 字符集。创建表时如果用的是默认字符集没有单独指定DEFAULT CHARSETutf8mb4就会在写入时报错。错误发生在INSERT阶段所以表现为「前面几条成功遇到表情就中断」。解决建表时显式指定字符集。如果表已经建好用ALTER TABLE语句转换字符集再修改数据库连接串加?charsetutf8mb4。注意表和字段两级都要设置只改表默认字符集而字段还是 utf8 依然会报错连接串不加charsetutf8mb4时ORM 会在写入前就把字符串里的 4 字节字符转成问号数据不报错但内容已经损坏这种更隐蔽。6. 数据能用才算完校验、去重与增量更新的最后一公里前面的工程能跑通后最重要的问题变成了「这个数据敢不敢给业务用」。校验要从三个层面做数量对得上、内容不脏、时效有保障。数量校验最简单就是把库里每个商户的评论数和详情页显示的总数对比如果差异超过 5%说明有翻页遗漏或去重误伤。内容质量校验要抽查评论正文字段的长度分布点评的评论最短几十字如果出现大量None或空字符串大概率是解析 XPath 时因为页面模板变更取空了。去重这块我补充一个生产级做法不要只依赖comment_id的唯一约束要给评论表加一个(shop_id, user_id, comment_time)联合唯一索引。原因很简单有的评论根本没有 ID 或者 ID 是页码相关的临时值会误判成新评论。联合索引能抵抗这种「ID 不可靠」的边界情况代价是插入前多一次查询。对点评评论这个量级单商户几千条可以忽略性能损耗。增量更新是最后也是最重要的一个环节。第一次全量爬完后后续每天只需要爬新评论否则老评论反复抓取既浪费流量又容易被封。做法是在评论表里记录crawl_time增量抓取时把「最新评论时间」作为分页过滤条件只请求当天有变化的商户。还有一个更稳妥的技巧用scrapy.Request里dont_filterFalse的默认去重机制配合 URL 里的随机参数打断重复判断这样同一商户页即使重复进入也能重新解析。这一招的效果是「既不重复入库又不漏掉新评论」。我自己的习惯是每周跑一次全量校验每天跑增量抓取并用一个定时任务检查「商户总数和评论总数」的增长曲线如果某天评论数突然翻倍多半不是业务爆发而是爬虫逻辑出了 bug别高兴得太早。一套爬虫做得再好只要数据校验环节没人盯最后给到业务手里的数字随时可能变成黑匣子。这套方案做完大众点评的数据就从「页面上的碎片信息」变成了「库里可检索可分析的结构化资产」后续接报表、做商家运营分析都能顺理成章。Scrapy 本身只是个执行者真正有价值的是你为这个目标站点定制的请求频率、Cookie 策略和数据建模。希望这套流程能帮你少踩几个坑。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
Ventoy启动主题定制:theme.txt、字体与图标排错指南 插上一个 U 盘,里面躺着十几个系统镜像,开机按快捷键,屏幕上蹦出一排白字黑底的菜单——这是大多数人做多合一启动盘时的第一印象。Ventoy 把这件事的门槛压到了最低:装一次,之后镜像文件直接往里拷,开机自… · 2026/11/2 14:42:57
智能安全生产系统:从责任落实到风险预警的数字化实践 1. 项目背景与核心痛点 安全生产责任制是企业管理的基石,但传统管理方式存在三大致命缺陷:纸质台账易丢失、责任追溯困难、考核流于形式。某化工企业2022年因责任落实不到位导致的安全事故,直接经济损失超3000万元,暴露出人工管理… · 2026/11/2 14:42:57
BERT多任务实战:意图识别、领域分类与槽位填充 简介:围绕BERT的意图识别与自然语言理解实战资源,面向NLP入门者及需要落地领域分类、意图识别、槽位填充任务的开发者。内容对应SMP2019ECDT任务,完整覆盖数据预处理、BERT微调、顶部分类层与序列标注设计、模型评估和推理输出,可… · 2026/11/2 14:42:57
STM32G4电机控制入门:FOC配置与Workbench实战避坑 简介:《基于STM32G4电机控制基础篇》是一份面向STM32G4平台电机控制初学者与嵌入式工程师的PDF技术资料,重点解决ST电机控制生态不熟悉、FOC工程搭建与代码移植路径不清等问题。资料围绕ST电机控制生态、MC SDK生成STM32G4电机FOC代码、基于CubeMX移植电… · 2026/11/3 8:23:07
SaltStack配置四大逻辑断点与高频报错实战排障指南 1. 这不是运维手册,而是一份“盐罐子”翻车实录你刚在服务器上敲下salt * state.apply,终端却突然跳出一串红色报错——Minion did not return. [No response];或者更魔幻的:Authentication error occurred.;又或者干脆… · 2026/11/3 8:23:07
转录组分析如何实现可复现、可验证、可解释的标准化流程 1. 项目概述:为什么说“转录组分析原来如此简单”不是标题党“转录组分析_转录组分析原来如此简单”这个标题乍看像流量套路,但在我带过二十多个生物信息实操班、帮某高校实验室重建三代测序分析流程、给某IVD公司开发自动化RNA-seq质控模块的十多年里&a… · 2026/11/3 8:23:07
SAP EWM零基础入门:仓库结构、主数据与收货发货配置指南 简介:SAP EWM110(课程版本17)基础定制参与者手册英文版,面向SAP EWM顾问、仓库管理实施人员及参加官方讲师培训的学员,用于系统掌握Extended Warehouse Management的核心配置与实施要点。压缩包内含1个PDF文件… · 2026/11/3 8:23:07
MQTT协议详解:物联网通信核心与实践指南 1. MQTT协议概述:物联网时代的通信基石MQTT(Message Queuing Telemetry Transport)协议已经成为物联网领域事实上的标准通信协议。作为一名在工业物联网领域工作多年的架构师,我亲眼见证了MQTT从一个小众协议发展成为支撑全球数百… · 2026/11/3 8:23:07
实测 Opus 5.5 与 GPT-6 Sol:从代码到创意的真实差距 一口气等来了 Opus 5.5 和 GPT-6 Sol 同时发布,这放在过去我完全不敢想。一个上午同时刷出新模型不是没遇到过,但两个旗舰级模型赶在同一天发布,还是全球首发,我身边几乎所有做内容、做开发的朋友都在问同一个问题:到底… · 2026/11/3 8:22:07
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46