标签

Scrapy

Scrapy 是一个快速、高层次的网页爬虫和网页抓取框架,用于抓取网站数据并从页面中提取结构化数据。它被广泛用于数据挖掘、监测和自动化测试等领域。Scrapy 是用 Python 开发的,并提供了一个简单但功能强大的 API,可以快速地编写爬虫。

Scrapy
查看更多相关内容
服务端5月31日 01:21
Scrapy 框架的核心组件是如何协同工作的?Scrapy 是 Python 生态里的爬虫框架,解决“怎么稳定抓取一批页面”。如果只是抓一两个静态页面,`requests + BeautifulSoup` 足够;一旦涉及队列、并发、重试和导出,Scrapy 的价值就明显了。 核心链路可以简化成一句话:Spider 产生请求,Scheduler 排队,Downloader 下载页面,Spider 解析响应,Item Pipeline 处理数据,Engine 调度全局。理解链路比背组件名更重要,因为很多问题都发生在边界上。 ## Scrapy 的核心组件怎么分工? Scrapy Engine 是中控层,负责让请求、响应和数据在各组件之间流转。Scheduler 保存待抓取请求,并按去重规则决定哪些请求入队。Downloader 发起网络请求,处理代理、超时、编码和响应返回。Spider 只关心两件事:从响应里提取数据,以及继续生成新的请求。Item Pipeline 则放在数据出口,适合做清洗、校验、去重、入库和导出。 一个最小 Spider 大概是这样: ```python import scrapy class ProductSpider(scrapy.Spider): name = "product" start_urls = ["https://example.com/products"] def parse(self, response): for card in response.css(".product-card"): yield { "name": card.css(".name::text").get(), "price": card.css(".price::text").get(), } next_url = response.css("a.next::attr(href)").get() if next_url: yield response.follow(next_url, callback=self.parse) ``` 这段代码只写了 Spider,但调度器、下载器、去重器和管道都已参与工作。Scrapy 的好处就在这里:业务写解析,工程化交给框架。 ## 追问 ### Scrapy 和 requests + BeautifulSoup 该怎么选? 如果页面数量少、抓取频率低、没有复杂队列,`requests + BeautifulSoup` 更轻,调试也直观。Scrapy 适合页面规模大、链路长、需要失败重试和持续运行的场景。取舍点主要是工程成本:Scrapy 起步配置多一点,但后期维护更稳。踩坑最多的是“小任务硬上 Scrapy”,最后项目结构比业务还复杂。 ### Scheduler、Downloader 和 Spider 的边界在哪里? Scheduler 不应该关心页面内容,它只负责请求排队和去重。Downloader 不应该写业务解析逻辑,它只负责拿到响应并处理网络层问题。Spider 才是解析 HTML、生成 Item 和下一批 Request 的地方。边界混乱会导致代码难测,比如把字段清洗写进下载中间件,后面换站点时很难复用。 ### Downloader Middleware 和 Item Pipeline 有什么区别? Downloader Middleware 处理的是请求和响应,常见用途是加代理、换 User-Agent、处理 Cookie、识别封禁响应。Item Pipeline 处理的是已经解析出来的数据,适合字段规范化、去重、入库和异常数据丢弃。一个简单判断是:还没拿到页面内容,用中间件;已经拿到结构化字段,用 Pipeline。不要把数据库写入放到中间件里,否则请求失败和数据失败会混在一起。 ### Scrapy 的异步并发是不是越大越好? 不是。Scrapy 基于 Twisted 事件循环,可以高并发抓取,但并发过高会带来封禁、超时、数据重复和本机资源耗尽。生产里通常要配合 `CONCURRENT_REQUESTS`、`DOWNLOAD_DELAY`、`AUTOTHROTTLE_ENABLED` 一起调。边界是目标站点的承受能力和反爬策略,不是你的机器能开多少连接。比较稳的做法是先小并发跑通,再根据错误率和响应时间逐步增加。 ```python # settings.py CONCURRENT_REQUESTS = 16 DOWNLOAD_DELAY = 0.5 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0 RETRY_TIMES = 3 ROBOTSTXT_OBEY = True ``` ### Scrapy 项目上线后最容易踩什么坑? 第一类是选择器太脆,页面结构一改就抓不到字段,所以关键字段要做空值校验和告警。第二类是去重规则没想清楚,带时间戳或追踪参数的 URL 会造成重复抓取。第三类是 Pipeline 入库没有幂等,爬虫重跑后数据重复。Scrapy 提供框架能力,但“抓取是否合法、数据是否可靠、失败是否可恢复”仍要靠项目设计兜底。
服务端5月31日 01:07
Scrapy 中间件有什么作用?适合哪些场景?Scrapy 中间件可以理解为爬虫流程里的拦截层:请求发出去之前能改,响应回来之后能查,异常发生时也能兜底。它主要分为下载器中间件和爬虫中间件,前者夹在引擎和下载器之间,常用来处理请求头、代理、重试、Cookie、响应状态;后者夹在引擎和 Spider 之间,更适合处理输入给 Spider 的响应和 Spider 产出的请求。中间件的价值是把通用逻辑从 Spider 里抽走,但边界也明显:业务字段解析不要塞进中间件,否则后面排查时会分不清数据到底在哪一步被改掉。判断一个逻辑要不要做成中间件,可以看它是否能被多个 Spider 复用,以及是否只依赖请求、响应、异常这些通用对象。 ## 追问 ### 下载器中间件和爬虫中间件有什么区别? 下载器中间件关注“请求能不能顺利拿到响应”,所以常见场景是加请求头、切代理、处理 403、记录耗时、对异常做重试。爬虫中间件关注“响应和请求如何进出 Spider”,比如过滤某些响应、统一补充 meta、处理 Spider 抛出的异常。取舍上,大多数反爬和网络层问题放下载器中间件更自然,业务解析前后的流程控制才考虑爬虫中间件。踩坑点是把两者职责混在一起,比如在下载器中间件里解析商品价格,短期能跑,长期会让代码很难测试。 ```python class TimingDownloaderMiddleware: def process_request(self, request, spider): request.meta["start_ts"] = time.time() def process_response(self, request, response, spider): cost = time.time() - request.meta.get("start_ts", time.time()) spider.logger.info("%s %s %.2fs", response.status, response.url, cost) return response ``` ### `process_request`、`process_response`、`process_exception` 分别怎么用? `process_request` 在请求进入下载器前执行,适合补请求头、代理、Cookie 或直接返回缓存响应。`process_response` 在响应回到引擎后执行,适合检查状态码、替换响应、对异常页面重新发请求。`process_exception` 只处理下载阶段抛出的异常,比如超时、连接失败、DNS 错误。边界是返回值会改变流程:返回 Request 会重新调度,返回 Response 会跳过下载,返回 None 才是继续交给下一个中间件;不理解这个规则,很容易写出重复请求或响应丢失的问题。 ```python class StatusRetryMiddleware: def process_response(self, request, response, spider): if response.status in {403, 429} and request.meta.get("retry_times", 0) < 2: new = request.copy() new.meta["retry_times"] = request.meta.get("retry_times", 0) + 1 new.dont_filter = True return new return response ``` ### 中间件优先级应该怎么配置? Scrapy 通过数字控制中间件顺序,下载器中间件的 `process_request` 按数字从小到大执行,`process_response` 则反过来。这个设计容易让人第一次配置时看反,尤其是多个中间件都在改代理、请求头和重试逻辑时。取舍上,通用基础逻辑可以靠前,例如设置默认 Header;依赖响应结果的统计、重试、清洗可以靠后。踩坑最多的是优先级和内置中间件冲突,比如自定义重试放错位置,导致 Scrapy 内置 RetryMiddleware 已经处理过一次,你又额外重试一次。 ```python # settings.py DOWNLOADER_MIDDLEWARES = { "myproject.middlewares.RandomHeaderMiddleware": 400, "myproject.middlewares.ProxyMiddleware": 410, "myproject.middlewares.StatusRetryMiddleware": 550, } ``` ### 哪些逻辑不适合写进中间件? 和具体页面结构强相关的字段解析不适合写进中间件,应该留在 Spider 或 Item Pipeline 里。中间件也不适合保存大量业务状态,例如把所有已抓商品、分类树、价格规则都塞进去,这会让它变成隐藏的业务中心。边界判断可以很简单:如果这个逻辑换一个 Spider 仍然有价值,它适合抽成中间件;如果只服务某个页面字段,就别放进去。实际项目里滥用中间件会让调试很痛苦,因为响应还没到 parse 方法就已经被改过,日志不全时很难还原现场。 ### 如何设计一个可维护的代理中间件? 代理中间件不要只做随机选择,还应该记录代理的失败次数、最近使用时间、适用域名和是否需要隔离登录态。轻量任务可以从列表里轮询,成本低也容易排查;高并发任务则需要独立代理池服务,负责健康检查和下线坏代理。取舍上,本地简单实现开发快,但多 Spider 共用时容易重复踩同一个坏代理;中心化代理池更稳定,却需要额外维护。常见坑是失败后立刻无限重试同一个请求,最后把调度队列拖慢,应该设置最大重试次数并对状态码做区分。 ```python class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_pool.get(domain=request.url.split('/')[2]) if proxy: request.meta["proxy"] = proxy def process_exception(self, request, exception, spider): proxy = request.meta.get("proxy") if proxy: spider.proxy_pool.mark_bad(proxy) ``` ## 结论 Scrapy 中间件适合承载跨 Spider 的通用流程逻辑,尤其是网络请求、反爬、重试、代理、日志和异常处理。写中间件时最重要的是职责边界和执行顺序:通用逻辑抽出来,业务解析留在业务层,返回值和优先级要明确。这样中间件才是扩展点,而不是另一个难排查的黑盒。生产环境里还要给关键中间件补日志和开关,遇到代理池抖动、目标站改规则或重试风暴时,可以快速关闭某一层,而不是停掉整套爬虫。
服务端5月31日 01:07
Scrapy 如何用选择器解析网页内容?Scrapy 解析网页内容主要靠 Selector,它把响应内容包装成可以用 CSS、XPath 和正则提取的对象。选择器写得好,爬虫会很稳定;选择器写得太依赖页面样式,前端一改 class 名就会全线失效。实际开发里不要纠结“CSS 一定比 XPath 简单”或者“XPath 一定更强”,更重要的是看页面结构、字段稳定性和后续维护成本。写选择器前先在 Scrapy shell 里试几条真实页面,比直接在代码里盲改更省时间,也能提前发现编码、重定向和空页面问题。列表页、详情页、分页、隐藏字段都可能需要不同写法,边界是:如果数据来自异步接口而不是 HTML,优先抓接口,别硬从渲染后的 DOM 里抠。 ## 追问 ### CSS 选择器和 XPath 应该怎么选? CSS 选择器写起来更直观,适合按 class、id、标签层级提取内容,团队里前端背景的人也容易维护。XPath 的优势是表达能力更强,能按文本、位置、祖先节点、兄弟节点做更精确的定位。取舍上,结构简单就用 CSS,遇到“找到包含某段文字的标题,再取它后面的价格”这类需求,用 XPath 会少绕很多弯。踩坑点是过度依赖层级路径,例如 `div > div > div:nth-child(2)`,页面插一个广告位就会错位。更稳的方式是先找业务语义明显的容器,再在容器内取标题、链接和价格,避免整页范围内抓到推荐位或页脚里的相似元素。 ```python def parse(self, response): for card in response.css("div.product-card"): yield { "title": card.css("h2::text").get(default="").strip(), "price": card.xpath(".//span[contains(@class, 'price')]/text()").get(), "url": response.urljoin(card.css("a::attr(href)").get()) } ``` ### `.get()`、`.getall()` 和 `.extract()` 有什么区别? 现在更推荐用 `.get()` 和 `.getall()`,它们语义更清楚:前者拿第一个结果,后者拿全部结果。`.extract()` 是旧写法,仍然能用,但新代码里没有必要继续混用,团队维护时容易让人误判返回类型。边界在于 `.get()` 没匹配到会返回 None,后续直接 `.strip()` 就会报错,所以要加默认值或单独判断。很多解析 bug 不是选择器错了,而是默认值没处理好,导致某一条脏数据让整个任务中断。字段进入 Item 前最好统一做清洗,例如去空白、补全 URL、规范日期格式,这些步骤比事后在数据库里修数据可靠得多。 ```python title = response.css("h1::text").get(default="").strip() tags = [x.strip() for x in response.css(".tag::text").getall() if x.strip()] ``` ### 如何让选择器更抗页面改版? 优先选择语义稳定的属性,比如 data-id、itemprop、aria-label、固定 URL 结构,而不是只看样式 class。很多网站的 class 是构建工具生成的,今天叫 `css-a1b2`,下次发布就变成另一个值,用它做选择器风险很高。取舍上,选择器写得宽一点能抗改版,但可能误抓广告、推荐位和隐藏模板;写得窄一点更准确,却更容易被结构变动打断。实战里可以先定位稳定容器,再在容器内做相对选择,避免全局搜索抓到重复字段。 ```python for item in response.css("article[data-id]"): title = item.css("[itemprop='headline']::text, h2::text").get(default="").strip() if not title: continue yield {"title": title} ``` ### 正则选择器适合用在哪些地方? 正则适合从一段文本里提取格式明确的值,比如脚本里的 JSON 字段、价格数字、日期、页面内嵌 ID。它不适合替代 HTML 解析,因为用正则匹配嵌套标签通常会变得脆弱又难读。边界是正则要尽量约束上下文,别写一个过宽的 `.*` 去吞整页内容,否则页面稍微变大就可能性能变差。踩坑最多的是拿到转义后的 JSON 字符串却没有反转义,或者把多个相似字段里的第一个误当成目标字段。 ```python import json raw = response.xpath("//script[contains(., 'window.__DATA__')]/text()").re_first(r"window\.__DATA__\s*=\s*(\{.*\})") if raw: data = json.loads(raw) ``` ### 解析结果怎么做质量检查? 选择器写完后,至少要检查必填字段是否为空、列表数量是否异常、URL 是否能补全、价格或日期格式是否符合预期。不要等数据入库后才发现标题全是空字符串,那时排查成本会高很多。取舍上,严格校验能尽早发现页面变动,但也可能丢掉少量字段不完整的正常数据;宽松校验吞吐高,却容易把坏数据带到下游。常见做法是必填字段缺失就丢弃并打日志,非核心字段允许为空,但要在统计里看到缺失比例。如果某次发布后缺失率突然升高,就应该暂停入库或降级为抽样抓取,先确认页面结构是否变化。 ## 结论 Scrapy 选择器的核心不是背语法,而是写出稳定、可读、可验证的提取逻辑。CSS、XPath、正则各有位置:CSS 负责常规结构,XPath 处理复杂关系,正则只提取文本里的明确模式。解析代码一旦进入生产,就要配合默认值、校验和日志,否则页面一次小改版就可能让数据悄悄变脏。
服务端5月31日 01:07
Scrapy 如何应对反爬虫机制?Scrapy 处理反爬,不是简单地把 User-Agent 换成浏览器就结束了。更稳的做法是先判断网站的限制来自哪里:是请求频率过高、Cookie 会话缺失、IP 被限流,还是前端渲染和验证码把页面内容挡住了。Scrapy 能做的是把请求行为变得更接近正常访问,并把失败、限速、代理、登录态这些环节放进可维护的配置和中间件里。上线前最好先用小样本跑通完整链路,确认列表页、详情页、翻页和异常页都能被识别,否则反爬策略还没生效,解析层就已经把脏页面当成正常数据了。真正的边界也要说清楚:遇到强验证码、设备指纹、复杂 JS 加密时,Scrapy 本身不是万能钥匙,继续硬撞通常只会浪费代理和时间。 ## 追问 ### 只改 User-Agent 能绕过反爬吗? 不能把 User-Agent 当成反爬方案的全部,它最多解决一部分“默认爬虫特征太明显”的问题。很多网站会同时检查 Accept、Accept-Language、Referer、Cookie、请求间隔,甚至会观察同一 IP 的访问路径是否像真人。取舍上,随机请求头实现成本低,适合轻度限制的网站;但如果每次请求头组合都乱跳,反而可能显得更异常。踩坑最多的是只改 UA、不保留会话,结果登录页、列表页、详情页之间的 Cookie 对不上,服务端直接返回空数据或 403。还有一种隐蔽情况是服务端返回状态码 200,但正文其实是风控提示页,解析器照样能跑,却会把错误内容写进数据库。 ```python # middlewares.py import random class RandomHeaderMiddleware: agents = [ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/123 Safari/537.36" ] def process_request(self, request, spider): request.headers.setdefault("User-Agent", random.choice(self.agents)) request.headers.setdefault("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") ``` ### 请求速度应该怎么控制? Scrapy 的 DOWNLOAD_DELAY、RANDOMIZE_DOWNLOAD_DELAY 和 AutoThrottle 都能控制速度,但它们解决的问题不一样。固定延迟简单可预测,适合小站点和低并发任务;AutoThrottle 会根据响应延迟动态调整,更适合站点负载变化明显的场景。边界是 AutoThrottle 不是“越开越安全”,如果目标站本身响应很快但限制按分钟计数,它可能仍然跑得太快。实际项目里建议从保守参数开始,看 429、403、超时比例再调,不要一上来把 CONCURRENT_REQUESTS 拉满。如果任务有时效要求,可以按域名拆分队列,让高价值页面优先抓取,而不是所有 URL 使用同一套并发策略。 ```python # settings.py CONCURRENT_REQUESTS = 8 CONCURRENT_REQUESTS_PER_DOMAIN = 4 DOWNLOAD_DELAY = 1.5 RANDOMIZE_DOWNLOAD_DELAY = True AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 1 AUTOTHROTTLE_MAX_DELAY = 10 AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 ``` ### 代理池什么时候该用,什么时候不该用? 代理池适合 IP 维度限流明显的网站,例如同一 IP 连续访问几十次后开始返回 403 或验证码。它不适合拿来掩盖所有问题,因为代理质量差会带来超时、脏 IP、地区不一致、TLS 指纹异常等新麻烦。取舍上,少量稳定代理加合理限速,通常比大量廉价代理随机切换更可靠。踩坑点是登录态和代理绑定:如果登录请求用 A 代理,后续详情页突然换到 B 代理,服务端很可能判定会话异常。 ```python class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_pool.pick() request.meta["proxy"] = proxy ``` ### 登录、Cookie 和验证码怎么处理? 需要登录的网站先用 FormRequest 或接口请求建立会话,让 Scrapy 的 CookieMiddleware 保持后续请求状态。简单验证码可以接第三方识别服务,但这会增加成本和失败率,也可能触碰站点规则边界。更推荐的思路是先确认数据是否有公开接口、RSS、站点地图或授权数据源,不要默认把验证码当成必须绕过的障碍。常见坑是登录成功后没有检查响应内容,只看状态码 200 就继续抓,最后抓到一堆登录页 HTML。 ```python def start_requests(self): yield scrapy.FormRequest( url="https://example.com/login", formdata={"username": "user", "password": "pwd"}, callback=self.after_login ) def after_login(self, response): if "退出登录" not in response.text: self.logger.warning("login failed") return yield scrapy.Request("https://example.com/profile") ``` ### 反爬失败时怎么判断问题出在哪? 不要只看异常类型,要同时记录状态码、响应长度、重定向地址、命中的代理、请求头和重试次数。403 常见于权限或规则拦截,429 多半是频率问题,200 但内容为空则可能是登录态、JS 渲染或被返回了假页面。取舍上,日志越详细越容易定位,但也要避免记录密码、Cookie 等敏感信息。一个实用做法是对异常响应保存少量样本 HTML,排查完及时清理,避免把脏数据继续送进解析流程。日志里还可以记录指纹字段,例如代理、UA、下载延迟和重试次数,这样才能判断是单个代理坏了,还是整套策略被目标站识别。 ## 结论 Scrapy 的反爬处理重点是分层:请求头和 Cookie 解决基础识别,限速解决频率,代理解决 IP 限制,中间件负责把这些策略做成可复用逻辑。遇到强验证码、设备指纹和复杂加密时,要先评估成本和合规边界,而不是把所有问题都交给代理池。
服务端5月31日 01:07
Scrapy 数据流从请求到入库是如何运转的?Scrapy 的数据流可以理解成一条异步流水线:Spider 产生请求,Engine 负责协调,Scheduler 排队和去重,Downloader 取回响应,Spider 再解析响应并产出新的请求或 Item,最后 Item 进入 Pipeline。真正的核心不是某一个组件,而是 Engine 在这些组件之间来回转发数据。 启动爬虫后,Engine 会向 Spider 索要初始请求,并交给 Scheduler。Scheduler 保存请求,Downloader Middleware 可以在下载前改请求头、代理或 cookie,Downloader 拿到响应后再经过 Middleware 返回给 Spider。Spider 的回调函数解析页面,如果产出 Request,就回到 Scheduler;如果产出 Item,就交给 Pipeline。这个循环持续到队列清空、运行被停止或触发关闭条件。 ```text Spider -> Engine -> Scheduler -> Engine -> Downloader Downloader -> Engine -> Spider -> Item Pipeline Spider -> new Request -> Scheduler ``` 常见配置会影响这条数据流的节奏: ```python CONCURRENT_REQUESTS = 16 DOWNLOAD_DELAY = 0.5 RETRY_ENABLED = True RETRY_TIMES = 2 DOWNLOADER_MIDDLEWARES = { 'myproject.middlewares.ProxyMiddleware': 350, } ITEM_PIPELINES = { 'myproject.pipelines.StorePipeline': 300, } ``` ## 追问 ### Engine 在 Scrapy 数据流里到底做什么? Engine 是调度中枢,它不负责解析页面,也不负责真正下载,而是决定请求和响应该交给谁。它向 Scheduler 要请求,把请求交给 Downloader,再把响应交给 Spider。取舍是这种拆分让组件边界清楚,也让中间件和扩展有插入点。边界是业务代码通常不直接操作 Engine,调试时更多看日志、信号和各组件输入输出。 ### Scheduler 和 Downloader 的关系是什么? Scheduler 只管请求排队、优先级和去重,Downloader 只管把某个请求变成响应。Engine 从 Scheduler 取出下一个请求,再交给 Downloader 执行下载。踩坑点是把“为什么这个 URL 没抓”都归因于 Downloader,其实它可能早就在 Scheduler 阶段被去重过滤了。排查时应先看 dupefilter 日志,再看下载状态码和异常。 ### Middleware 在数据流中适合做哪些事? Downloader Middleware 适合处理请求发送前和响应返回后的横切逻辑,比如代理、User-Agent、重试、异常响应处理。Spider Middleware 更靠近解析层,适合处理进入或离开 spider 的 response、request、item。取舍是 Middleware 很强,但滥用会让数据流变得不透明。边界建议是:和网络请求相关的放 Downloader Middleware,和解析结果相关的放 Spider Middleware,不要把入库逻辑塞进去。 ### Scrapy 为什么能同时处理很多请求? Scrapy 基于 Twisted 的异步事件模型,等待网络响应时不会阻塞整个进程,可以继续处理其他请求。它适合 I/O 密集型爬取,尤其是大量网页下载场景。边界是解析函数里如果写了 CPU 很重的代码,或者调用同步阻塞接口,异步优势会被抵消。踩坑最多的是在回调里直接做大文件处理、慢 SQL 或 `time.sleep()`,结果并发配置再高也跑不起来。 ### 数据流排查应该从哪里开始? 先看请求有没有进入 Scheduler,再看是否被去重,然后看 Downloader 返回的状态码,最后看 Spider 是否产出 Item 或新 Request。这个顺序比直接盯着 pipeline 更有效,因为很多问题根本没走到入库阶段。取舍是全链路日志会增加噪音,但在调试复杂爬虫时非常值。生产环境可以只保留关键统计,例如请求数、去重数、非 200 响应数、item 数和 drop 数。
服务端5月31日 01:07
Scrapy Pipeline 管道如何清洗、去重并入库?Scrapy Pipeline 是 Item 离开 spider 后进入存储系统前的处理链。它适合做数据清洗、字段校验、去重、补全、入库、发送消息等工作。和 spider 相比,Pipeline 更靠近数据出口,所以它不应该关心页面怎么解析,而应该关心“这条数据是否可信、如何保存、失败后怎么办”。 Pipeline 可以配置多个,Scrapy 会按优先级从小到大依次执行。每个 `process_item` 要么返回 item 交给下一个管道,要么抛出 `DropItem` 丢弃数据。这里的取舍很实际:把所有逻辑写进一个 Pipeline 简单,但后期很难复用;拆得太细又会增加配置和排查成本。通常可以按职责拆成校验、去重、存储三类。 ```python # pipelines.py from itemadapter import ItemAdapter from scrapy.exceptions import DropItem class ValidatePipeline: def process_item(self, item, spider): data = ItemAdapter(item) if not data.get('title') or not data.get('url'): raise DropItem('missing title or url') data['title'] = data['title'].strip() return item class DedupePipeline: def open_spider(self, spider): self.seen = set() def process_item(self, item, spider): url = ItemAdapter(item).get('url') if url in self.seen: raise DropItem(f'duplicate url: {url}') self.seen.add(url) return item ``` ```python # settings.py ITEM_PIPELINES = { 'myproject.pipelines.ValidatePipeline': 100, 'myproject.pipelines.DedupePipeline': 200, 'myproject.pipelines.StorePipeline': 300, } ``` ## 追问 ### Pipeline 和 Item Loader 都能清洗数据,应该放哪边? Loader 更适合处理字段级别的格式问题,比如去空格、取第一个文本、把价格字符串转数字。Pipeline 更适合处理整条数据的业务判断,比如字段是否完整、是否重复、是否需要入库。取舍点是复用范围:页面解析相关的清洗放 Loader,跨 spider 的出口规则放 Pipeline。常见坑是两个地方都清洗同一个字段,最后线上数据异常时没人说得清是哪一步改坏的。 ### 多个 Pipeline 的执行顺序怎么定? `ITEM_PIPELINES` 里的数字越小越早执行,所以校验通常放前面,存储放后面。这样缺字段或明显错误的数据可以尽早丢掉,避免浪费数据库连接和网络请求。边界是有些字段需要存储前才生成,比如数据库 ID 或消息队列 trace id,就不能在前置校验里强依赖。实践里优先级不要随手写,最好留出间隔,比如 100、200、300,后面插新步骤更方便。 ### Pipeline 里连接数据库要注意什么? 连接对象应在 `open_spider` 中初始化,在 `close_spider` 中关闭,避免每条 item 都新建连接。每条都建连接会非常慢,还容易把数据库打到连接数上限。取舍是长连接性能好,但要处理断线重连和批量提交失败。边界是小脚本写 CSV 可以简单处理,生产入库则要考虑唯一键、事务、重试和错误日志。 ### 去重应该放内存、Redis 还是数据库? 单机小任务用内存 set 最简单,速度快,也不需要额外服务。多进程、多机器或长周期任务就应该用 Redis 或数据库唯一约束,因为本地 set 无法跨节点共享。取舍是 Redis 去重快但需要维护 key 生命周期,数据库去重可靠但写入压力更大。踩坑点是只用 URL 去重,有些站同一内容存在移动端、PC 端、带追踪参数的多个 URL,需要先做 URL 规范化。 ### Pipeline 抛出 DropItem 后还会发生什么? 抛出 `DropItem` 后,这条 item 不会进入后续 Pipeline,Scrapy 会把它记录到 dropped 统计里。这个机制适合丢弃缺字段、重复、明显异常的数据。边界是不要把暂时性错误也直接 Drop,比如数据库超时、接口偶发失败,这类问题更适合重试或落失败队列。否则数据会“安静地丢失”,日志里看得到,但业务侧很难补回来。
服务端5月31日 01:07
Scrapy 分布式爬虫如何用 Redis 稳定实现?Scrapy 本身是单进程内的爬虫框架,要做分布式,关键是把“请求队列”和“去重集合”从本地内存搬到所有节点都能访问的地方。最常见方案是 scrapy-redis:多个 spider 实例共享 Redis 里的 request queue 和 dupefilter set,每个节点从同一个队列取任务,处理完再把新请求推回队列。 这套方案的好处是改造成本低,单机 Scrapy 项目不用重写成全新的调度系统。代价是 Redis 变成核心依赖,网络抖动、队列堆积、去重 key 设计不当都会影响全局吞吐。分布式爬虫不是“机器越多越快”,目标站限速、代理质量、数据库写入能力、队列序列化开销都会成为边界。 ```python # settings.py SCHEDULER = 'scrapy_redis.scheduler.Scheduler' DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter' SCHEDULER_PERSIST = True REDIS_URL = 'redis://:password@127.0.0.1:6379/0' ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 300, } ``` ```python from scrapy_redis.spiders import RedisSpider class ProductSpider(RedisSpider): name = 'product' redis_key = 'product:start_urls' def parse(self, response): yield {'url': response.url, 'title': response.css('h1::text').get()} ``` 启动后向 Redis 写入入口地址即可: ```bash redis-cli lpush product:start_urls https://example.com/list ``` ## 追问 ### scrapy-redis 到底替换了 Scrapy 的哪些部分? 它主要替换调度器和去重器,让请求不再只存在本地内存队列中,而是序列化后放进 Redis。这样多个进程甚至多台机器可以消费同一个任务池,宕机后也能继续从队列恢复。取舍是调度性能会受 Redis 网络和序列化影响,小规模任务未必比单机更快。边界是下载器、解析函数、pipeline 仍然是每个节点本地执行,scrapy-redis 不会自动帮你解决数据库写入冲突。 ### 分布式爬虫怎么做去重才可靠? 默认 RFPDupeFilter 会根据请求指纹去重,通常由 URL、方法、请求体等信息计算而来。它适合大多数 GET 页面,但对带时间戳、分页参数顺序混乱、POST body 动态变化的请求可能误判或漏判。踩坑点是把 session、随机参数也算进指纹,队列看似很忙,实际一直重复抓同一批内容。实践中可以自定义 request_fingerprint,把业务上无意义的参数过滤掉。 ### 机器加多了为什么速度没有提升? 爬虫吞吐由最慢的一环决定,可能是目标站限流,也可能是代理池、DNS、Redis、数据库或解析 CPU。机器数量增加后,请求会更集中,目标站更容易返回 403、验证码或空页面。取舍是追求速度会牺牲稳定性,尤其是电商、招聘、内容站这类反爬较强的网站。比较稳的做法是先压测单节点瓶颈,再逐步扩容,同时观察失败率而不是只看 QPS。 ### Redis 队列要不要持久化? `SCHEDULER_PERSIST=True` 能让爬虫停止后保留队列和去重集合,适合长周期任务和断点续爬。缺点是调试时容易“幽灵任务”反复出现,明明代码改了,旧请求还在 Redis 里继续跑。边界是一次性任务或测试环境可以关闭持久化,避免污染结果。生产环境则要配合 key 命名、过期策略和清理脚本,不然 Redis 内存会慢慢被历史指纹吃掉。 ### 分布式下数据入库有什么坑? 多个节点同时写同一张表时,最常见问题是重复数据、唯一键冲突和写入压力抖动。去重不能只依赖请求队列,因为同一个商品可能从不同列表页进入,最终还是要在数据库层设计唯一约束。取舍是强一致写入会降低吞吐,批量异步写入速度快但失败重试更复杂。边界做法是 pipeline 里尽量幂等:同一条 item 重复提交,结果也应该是更新或忽略,而不是产生脏数据。
服务端5月31日 01:07
Scrapy Item 和 Item Loader 应该怎么分工使用?Scrapy 里的 Item 更像数据结构声明,Item Loader 更像数据清洗和装配工具。Item 负责告诉项目“我要采集哪些字段”,例如标题、价格、链接、发布时间;Item Loader 负责把页面上脏兮兮的原始文本变成可存储、可复用的数据。两者不是二选一关系,常见做法是先定义 Item,再用 Item Loader 填充字段。 如果页面字段很少,直接返回字典也能跑;如果项目要长期维护,Item 能让字段边界更清楚。Item Loader 的价值主要出现在字段来源复杂、需要去空格、拼接、取第一个值、统一格式时。它的坑也很明显:处理器写得太“聪明”,后面调试时很难判断数据是在 xpath、loader 还是 pipeline 阶段变坏的。 ```python # items.py import scrapy class ProductItem(scrapy.Item): title = scrapy.Field() price = scrapy.Field() url = scrapy.Field() ``` ```python from itemloaders.processors import TakeFirst, MapCompose from scrapy.loader import ItemLoader from .items import ProductItem def strip_text(v): return v.strip() loader = ItemLoader(item=ProductItem(), response=response) loader.default_input_processor = MapCompose(strip_text) loader.default_output_processor = TakeFirst() loader.add_css('title', 'h1::text') loader.add_css('price', '.price::text') loader.add_value('url', response.url) yield loader.load_item() ``` ## 追问 ### 什么时候直接用 dict,什么时候定义 Item? 临时脚本、一次性抓取或者字段只有两三个时,直接 `yield dict` 成本最低,也方便快速验证选择器。项目进入多人维护、字段会被多个 spider 或 pipeline 共享时,Item 更稳,因为字段名集中在一个地方,不容易写成 `titel` 这种隐蔽错误。取舍点在于维护成本:Item 多一层定义,但能换来更清楚的数据契约。边界是 Scrapy 不会强制校验字段类型,Item 不是 Pydantic,真要类型校验还得放到 loader 或 pipeline 里。 ### Item Loader 的输入处理器和输出处理器有什么区别? 输入处理器会作用在每一次 `add_css`、`add_xpath` 或 `add_value` 收进来的原始值上,适合做去空格、去货币符号、日期字符串预处理。输出处理器会在 `load_item()` 时统一收口,适合做 `TakeFirst()`、列表去重、拼接多个值。踩坑最多的是把“取第一个值”放进输入处理器,结果后续再追加字段时数据被提前截断。实践里可以把输入处理器写轻一点,把最终决策留给输出处理器。 ### Item Loader 会不会让代码变复杂? 小页面上它确实可能显得啰嗦,尤其是字段只有标题和链接时,直接写字典更直观。复杂页面就不一样了,同一个字段可能来自 meta、页面文本和接口返回,Loader 能把这些来源集中到一个字段收集流程里。它的边界是不要把业务规则全塞进去,比如“库存为 0 就丢弃商品”更适合 pipeline,而不是 loader。否则 spider、loader、pipeline 的职责会混在一起,排查问题时只能一路打日志。 ### 如何避免 Item Loader 清洗后数据不符合预期? 第一步是让处理器足够小,每个函数只做一件事,例如只去空格、只解析价格、只标准化 URL。第二步是在关键字段上加单元测试,尤其是价格、时间、地区这种容易受页面格式影响的字段。第三步是保留少量原始字段或调试日志,线上数据突然异常时能反查来源。取舍是日志和原始字段会占存储空间,但对长期运行的爬虫来说,这点成本通常比盲查问题低得多。 ### Item、Loader 和 Pipeline 的边界怎么划? Item 定义“有什么字段”,Loader 解决“字段如何从页面变干净”,Pipeline 处理“这条数据能不能入库、入哪儿、是否重复”。这个边界不是绝对的,但越早统一越省事。比如价格文本转数字可以放 Loader,价格小于 0 的异常数据丢弃更适合 Pipeline。常见坑是 Pipeline 里再做大量字符串清洗,导致同一套清洗逻辑无法被多个 spider 复用。
服务端5月31日 01:07
Scrapy 调试和日志怎么做?Shell、parse 命令和 stats 如何配合排查?## 先说结论 Scrapy 调试不要只靠 `print`,更高效的做法是把 shell、parse 命令、日志级别、stats 指标和少量断点组合起来。选择器写不准,用 `scrapy shell`;单个 URL 的回调链路不对,用 `scrapy parse`;线上任务异常,用日志和 stats 定位是请求失败、解析为空,还是 Pipeline 写入出错。日志要能回答“发生了什么、发生在哪个 URL、影响多少数据”,而不是把控制台刷满。 常用命令如下: ```bash scrapy shell "https://example.com/list" scrapy parse "https://example.com/list" --spider=demo -c parse scrapy crawl demo -s LOG_LEVEL=DEBUG scrapy crawl demo -s LOG_FILE=logs/demo.log ``` `DEBUG` 适合本地排查,线上长期跑一般用 `INFO` 或 `WARNING`。如果日志太吵,真正的问题会被淹没;如果日志太少,任务失败时又只能重跑碰运气。 ## 用 shell 调选择器 `scrapy shell` 会拿到和爬虫接近的 response 对象,可以直接测试 CSS、XPath 和正则。比如先确认标题是否存在,再确认列表长度,而不是写完 spider 后才发现字段全是空。 ```python response.css("h1::text").get() response.css(".item").getall()[:2] response.xpath("//a/@href").getall() ``` 边界是 shell 只代表当前 URL 和当前响应。很多站点会按地区、登录态、User-Agent 或反爬策略返回不同内容,本地 shell 成功不等于线上一定成功。遇到这种情况,要把请求头、cookie、代理和中间件一起纳入排查。 ## 用 parse 命令检查回调 `scrapy parse` 能执行指定回调,并展示产生了哪些 item 和 request。它比 shell 更接近 spider 的真实逻辑,适合排查“选择器没问题,但爬虫就是没产出”的情况。常见原因包括回调方法没挂上、规则没匹配、分页链接被过滤、`allowed_domains` 写错。 如果请求被 dupefilter 去重,可以临时给关键请求加 `dont_filter=True` 验证。这个设置不要滥用,线上乱加会让重复页面暴涨。 ## 日志应该记录什么 Scrapy 使用 Python logging,可以在 spider、middleware、pipeline 中按模块打日志。建议记录关键 URL、业务 ID、状态码、解析数量和异常原因,不要把完整 HTML 或大 item 直接打进日志。 ```python import logging logger = logging.getLogger(__name__) logger.info("parsed list", extra={"url": response.url, "count": len(items)}) logger.warning("empty detail", extra={"url": response.url}) ``` 在 `settings.py` 里可以控制输出: ```python LOG_LEVEL = "INFO" LOG_FILE = "logs/spider.log" LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s" ``` ## stats 比单条日志更适合看趋势 日志告诉你某次发生了什么,stats 告诉你整体发生了多少次。比如 `item_scraped_count`、`downloader/exception_count`、`retry/count`、各状态码数量,都能快速判断问题范围。你也可以在代码里自定义计数,例如空列表页、缺字段详情页、入库失败次数。 ```python self.crawler.stats.inc_value("custom/empty_detail") ``` ## 追问 ### shell 能调通,爬虫跑起来却没有数据,怎么排查? 先用 `scrapy parse` 跑同一个 URL,确认 spider 的回调是否真的产出 item。再看分页或详情页 request 有没有被 `allowed_domains`、去重器或中间件拦掉。shell 只验证选择器,不验证调度链路,这是它的边界。实际排查时要把 response、callback 和后续 request 放在一起看。 ### 日志级别应该怎么选? 本地开发用 `DEBUG`,因为你需要看到请求、重试和解析细节。线上长期任务更适合 `INFO` 或 `WARNING`,否则日志量会迅速膨胀,磁盘和检索成本都上来。取舍是日志越详细,定位越快,但噪声也越大。关键是把业务异常打清楚,而不是依赖框架默认日志。 ### 什么时候用 pdb 或 IDE 断点? 当问题发生在复杂条件分支、字段清洗或 Pipeline 写入逻辑里,断点比反复打日志更快。边界是异步框架里随意断住会影响请求调度,本地可以这么做,线上不要这么排查。更稳的方式是用固定样本、关闭高并发,只跑一两个 URL。断点解决“为什么走到这里”,日志和 stats 解决“这种情况发生了多少”。 ### stats 能发现哪些日志不容易发现的问题? stats 很适合发现比例异常,比如 200 状态很多但 item 很少,说明解析可能失效。也能发现重试暴涨、某类异常集中出现、入库失败次数上升。单条日志容易让人盯着个例,stats 能先判断是不是系统性问题。踩坑点是自定义指标命名要稳定,否则不同版本之间没法对比。 ### 日志写文件有什么注意事项? 写文件前先确认目录存在,并考虑日志轮转,否则长跑任务可能把磁盘写满。Scrapy 的 `LOG_FILE` 简单好用,但多爬虫、多进程时要避免都写同一个文件。敏感字段也不要进日志,比如 cookie、token、手机号和完整地址。日志是排障材料,不应该变成新的安全风险。
服务端5月31日 01:07
Scrapy 性能优化该调哪些参数?并发、限速和 Pipeline 怎么取舍?## 先说结论 Scrapy 性能优化不是把并发调到最大,而是在目标站承受能力、网络延迟、本机资源、代理质量和数据写入速度之间找平衡。真正影响吞吐的通常有四块:下载并发、延迟与自动限速、重复请求与缓存、解析和 Pipeline 的耗时。只盯 `CONCURRENT_REQUESTS` 很容易误判,爬虫跑不快可能不是请求少,而是 DNS、代理、数据库写入或解析逻辑卡住了。 可以先用一组保守配置跑基准,再逐步调高: ```python CONCURRENT_REQUESTS = 32 CONCURRENT_REQUESTS_PER_DOMAIN = 8 DOWNLOAD_DELAY = 0.2 RANDOMIZE_DOWNLOAD_DELAY = True AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 0.5 AUTOTHROTTLE_MAX_DELAY = 10 AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 RETRY_TIMES = 2 HTTPCACHE_ENABLED = False ``` 命令上可以用 `scrapy crawl spider -s LOG_LEVEL=INFO` 看整体速度,用 `scrapy bench` 做框架基准,但不要把 bench 结果当成线上能力。真实站点会有限速、验证码、慢接口和异常页面,性能优化必须结合日志和 stats 看。 ## 并发不是越高越好 Scrapy 基于 Twisted 异步网络模型,单进程就能同时处理多个请求。提高并发能减少等待时间,但目标站响应变慢、403 增多、代理超时上升时,再加并发只会让失败更快发生。经验上先看 `downloader/response_status_count/200`、`retry/count`、`download_latency`,确认瓶颈是不是网络请求。 如果一个域名很慢,可以调 `CONCURRENT_REQUESTS_PER_DOMAIN`;如果同时抓多个站,才更需要关注全局 `CONCURRENT_REQUESTS`。边界是本机 CPU、内存、文件句柄和代理池也会被并发放大,尤其是页面解析里用了复杂 XPath、正则或大对象暂存时。 ## AutoThrottle 和下载延迟怎么配 `DOWNLOAD_DELAY` 是固定刹车,简单可靠,适合目标站规则明确的场景。`AutoThrottle` 会根据延迟动态调整速度,更适合响应波动大的站点。取舍是 AutoThrottle 不等于反爬万能药,它主要基于响应延迟判断,不理解验证码、业务封禁和账号风控。 如果站点对请求频率敏感,宁愿慢一点,也不要追求短时间峰值。稳定跑完比十分钟冲高后被封一整天更值钱。 ## Pipeline 和存储经常是隐藏瓶颈 很多爬虫下载很快,最后慢在 Pipeline。每条 item 都同步查库、逐条提交、上传图片或调用外部接口,都会堵住处理链路。更好的方式是批量写入、异步队列、连接池,或者先落 JSON Lines,再由离线任务清洗入库。 这里的边界是可靠性。批量越大,吞吐越好,但失败回滚和重复写入处理越复杂;批量太小,又浪费数据库往返。实际项目里通常会用唯一键去重,再把批量大小控制在数据库能稳定承受的范围内。 ## 缓存、去重和请求优先级 开发阶段打开 HTTP cache 能减少重复请求,调选择器时很省时间。生产采集是否开启要看数据时效性,如果价格、库存、状态变化很快,缓存会让结果过期。请求优先级适合详情页、补采任务和失败重试排序,但不要把调度器当业务队列用,复杂优先级最好在任务生成阶段处理。 ## 追问 ### 并发调高后速度没变,应该先查什么? 先看下载延迟、重试数量和状态码分布,而不是继续加并发。如果 `download_latency` 很高或 429、403 增多,说明目标站或代理已经在限制你。再看 Pipeline 是否耗时,尤其是数据库写入和外部 API 调用。只有确认请求链路仍有空闲,继续提高并发才有意义。 ### AutoThrottle 会不会影响抓取效率? 会,它本来就是用部分速度换稳定性。目标站响应慢时它会主动降速,短期看吞吐下降,长期看可能减少封禁和重试。边界是它只能根据延迟做判断,遇到验证码页返回很快时可能误判为站点很健康。重要任务最好同时监控异常状态码和页面内容。 ### Pipeline 如何优化才不丢数据? 不要一上来就把所有写入都改成异步而不做失败处理。可以先把原始 item 落到 JSON Lines,再批量入库,这样数据库挂了还有恢复来源。批量写入要配唯一键或幂等逻辑,否则重跑任务会产生重复数据。取舍是链路变长了,但排查和恢复能力会强很多。 ### Scrapy 缓存适合线上开启吗? 开发和调试阶段很适合,线上要谨慎。新闻、价格、库存这类强时效数据不适合长期缓存,否则你优化的是速度,损失的是准确性。文档页、分类页、结构稳定的页面可以短期缓存,减少重复下载。边界要写清楚,比如缓存过期时间和哪些请求不允许缓存。 ### 什么时候需要多进程或分布式? 单机 Scrapy 的瓶颈通常先出现在目标站限制、代理质量或存储链路,而不是框架本身。只有单进程资源吃满、任务量大到单机跑不完,或需要多机器协作时,才考虑 scrapyd、队列或分布式调度。分布式会带来去重、状态一致性、任务切分和监控成本。小任务硬上分布式,最后往往是运维复杂度大于性能收益。
服务端5月31日 01:07
Scrapy 数据导出格式怎么选?JSON、CSV 和数据库如何取舍?## 先说结论 Scrapy 内置的数据导出主要覆盖 JSON、JSON Lines、CSV、XML、Pickle、Marshal 等格式,日常项目里最常用的是 JSON Lines、CSV 和数据库写入。选择时不要只看“能不能导出”,更要看数据量、下游使用方式、是否需要断点续跑、字段是否稳定,以及失败后能不能快速恢复。小批量调试用 JSON 很顺手,几十万条以上更推荐 JSON Lines;给运营或分析同学交付表格时用 CSV;需要持续入库、去重和查询时,把导出交给 Item Pipeline 写 MySQL、PostgreSQL、MongoDB 或对象存储会更稳。 Scrapy 的 feed export 可以通过命令行直接完成: ```bash scrapy crawl book -O output.json scrapy crawl book -o output.jl scrapy crawl book -O output.csv ``` `-O` 会覆盖已有文件,适合定时任务每次生成完整结果;`-o` 会追加写入,适合临时补采,但也容易把重复数据混进去。线上任务里我更倾向显式配置 `FEEDS`,因为编码、字段顺序、是否覆盖、存储路径都能写清楚,避免不同同事用不同命令跑出不同文件。 ```python FEEDS = { "exports/%(name)s-%(time)s.jl": { "format": "jsonlines", "encoding": "utf8", "overwrite": False, "fields": ["title", "url", "price"], } } ``` ## JSON 和 JSON Lines 怎么选 JSON 会把全部 item 组织成一个数组,文件看起来完整,适合几百条、几千条的结果检查,也方便直接发给接口调用方。但它的边界是很明显的:文件没写完时不是合法 JSON,爬虫中途挂掉可能留下半截内容,大文件被编辑器打开也容易卡死。 JSON Lines 是一行一个 JSON 对象,天然适合爬虫这种持续流式产出的场景。任务中断时,前面已经写入的行通常还能继续被处理;后续用 `jq`、Spark、Fluent Bit 或日志系统消费也更自然。取舍点在于它不如普通 JSON “一眼看起来规整”,给非技术同事交付时需要提前说明格式。 ## CSV、XML 和二进制格式适合什么场景 CSV 的优势是通用,Excel、BI 工具、数据分析脚本都能打开。坑也在这里:字段里有逗号、换行、引号时必须依赖正确转义;中文还要注意编码,必要时给 Excel 使用方导出 `utf-8-sig`。如果 item 的字段经常变化,CSV 会比 JSON Lines 更难维护,因为列顺序和缺失值会影响下游解析。 XML 现在用得少,但和一些老系统、政企接口或 Java 生态集成时仍然会遇到。Pickle、Marshal 更偏 Python 内部使用,不建议作为跨团队交付格式,因为可读性差,也不适合不可信数据交换。 ## 什么时候不要只靠文件导出 如果爬虫每天运行、数据需要去重、要追踪更新时间,文件导出就不够了。此时可以在 Pipeline 中做校验、清洗、批量写库,并用唯一键控制重复数据。边界是 Pipeline 里的数据库操作不要每条都同步提交,否则吞吐会被数据库拖垮;更稳的做法是批量写入、失败重试,并把原始导出保留一份作为兜底。 ## 追问 ### 为什么大数据量更推荐 JSON Lines? 因为 JSON Lines 是流式追加,Scrapy 每产生一条 item 就能写一行,不必等全部结果结束后再形成完整数组。任务崩溃时,已经写出的行大多还能被后续脚本继续处理,这是普通 JSON 很难做到的。它的代价是文件不是一个单独 JSON 数组,下游如果只接受标准数组,需要再做一次转换。实际项目里,如果采集量超过几万条,我通常优先选 JSON Lines。 ### `-o` 和 `-O` 的区别会带来什么坑? `-o` 是追加写入,`-O` 是覆盖写入。调试时追加很方便,但定时任务里忘了清理旧文件,就会把昨天和今天的数据混在一起。覆盖又有另一个边界:如果你想保留历史结果,必须把文件名带上日期或批次号。生产环境最好把这个选择写进 `FEEDS`,不要依赖人工记命令。 ### CSV 导出时最容易踩哪里? 最常见的是字段里包含换行、逗号或引号,导致下游看起来像“列错位”。另一个坑是中文编码,Excel 在某些环境下打开 UTF-8 CSV 会乱码,需要考虑 `utf-8-sig`。CSV 适合字段稳定的扁平数据,不适合嵌套结构。遇到列表、字典、多个图片链接这类字段,JSON Lines 通常更省心。 ### 什么时候应该写数据库而不是导出文件? 当你需要增量更新、去重、按条件查询或和业务系统联动时,就应该考虑数据库。文件更像交付物或中间结果,数据库才适合长期维护状态。取舍在于数据库会引入连接池、事务、索引和失败重试,复杂度明显更高。我的建议是保留一份原始 JSON Lines,再由 Pipeline 或离线任务入库,这样排查问题时有退路。 ### FEEDS 配置比命令行导出好在哪里? 命令行适合临时跑一次,FEEDS 适合团队协作和线上任务。它能固定格式、编码、字段顺序、覆盖策略和路径模板,减少“我本地能跑、你那边文件不一样”的问题。边界是配置写死后灵活性会差一点,多个环境可能需要不同路径。可以用环境变量或不同 settings 文件拆开,避免把测试路径带到生产。
服务端5月31日 00:57
Scrapy 请求失败后怎么重试?错误处理机制该怎么配?## Scrapy 的重试不是越多越好 Scrapy 自带 RetryMiddleware,能处理连接超时、DNS 错误、部分 HTTP 状态码等失败场景。它的价值不是“保证每个请求都成功”,而是在短暂网络抖动、服务端偶发 500、代理临时不可用时给请求一次恢复机会。真正需要注意的是:重试会消耗队列、带宽和时间,配置不当还会把目标站的压力继续放大。 常见配置如下: ```python RETRY_ENABLED = True RETRY_TIMES = 2 RETRY_HTTP_CODES = [408, 429, 500, 502, 503, 504, 522, 524] DOWNLOAD_TIMEOUT = 15 RETRY_PRIORITY_ADJUST = -1 ``` `RETRY_TIMES=2` 表示失败后最多再试 2 次,不是总共请求 2 次。`RETRY_PRIORITY_ADJUST=-1` 会让重试请求优先级略降低,避免失败请求一直插队。429 通常代表限流,是否重试要看目标站策略;如果没有退避,只是马上重发,可能更快被封。 对业务可预期的失败,最好配合 `errback` 单独处理。比如详情页失败时记录 URL、来源页和错误类型,后续可以补采,而不是只靠日志里一行 traceback。 ```python def start_requests(self): for url in self.urls: yield scrapy.Request(url, callback=self.parse, errback=self.on_error) def on_error(self, failure): request = failure.request self.logger.warning("request failed: %s reason=%r", request.url, failure.value) yield {"url": request.url, "status": "failed"} ``` 如果要对限流做得更稳,可以自定义中间件读取 `Retry-After`,或者在调度层暂停该域名一段时间。不要把所有状态码都塞进 `RETRY_HTTP_CODES`,这会让 404、权限失败和反爬页面反复进入队列。失败原因先分类,再决定是否重试,是 Scrapy 错误处理里最容易被忽略的一步。 还要区分“重试”和“补采”。重试适合马上再试的短暂失败,补采适合任务结束后再单独处理的 URL,比如目标站夜间维护、接口阶段性限流、某批代理质量差。把补采 URL 写入单独队列或表,比在主任务里一直加大 `RETRY_TIMES` 更可控。这样主任务能按时结束,失败样本也不会丢,后续排查还能看到失败发生在哪个批次。 如果项目接入了代理池,还要把代理错误和目标站错误分开统计。代理连接失败、认证失败、目标站返回 5xx,处理方式完全不同,混在一起只会误判。一个简单做法是在代理中间件里给 request.meta 记录代理来源,失败时把来源写进日志或失败 item。这样你能判断是某个代理供应商质量差,还是目标站真的在限流。 ## 追问 ### 哪些错误应该重试,哪些不该重试? 临时网络问题、超时、502、503、504 一般可以重试,因为它们可能只是短暂抖动。404、410 多数表示资源不存在,通常不应该重试,否则只是浪费请求。403 要看情况:如果是登录失效、IP 被拦或权限不足,盲目重试没有意义,应该先修 cookie、代理或请求头。边界最难的是 429,它可能适合延迟后重试,也可能说明当前策略已经触发风控。 ### `errback` 和 RetryMiddleware 有什么区别? RetryMiddleware 是框架层的自动补救,先判断失败是否符合重试条件,符合就重新入队。`errback` 更像业务兜底,当请求最终失败或某些异常冒出来时,你可以记录、补偿或产出失败 item。不要把所有失败都塞进 errback 手动重试,否则容易绕开 Scrapy 的统计和优先级机制。一般做法是让 RetryMiddleware 负责通用重试,errback 负责业务可观测性。 ### 重试次数应该怎么设置? 默认思路是少量重试,通常 1 到 3 次就够了。次数太少会丢掉偶发失败,次数太多会拖慢任务,还可能让失败 URL 长时间占住队列。需要高完整率的采集可以增加补采任务,而不是在主任务里无限重试。生产环境应该看 `retry/count`、`retry/max_reached` 和错误码分布,再决定是否调整。 ### 遇到反爬导致的失败怎么办? 如果错误集中在 403、429、验证码页或异常跳转,问题通常不在重试次数,而在访问策略。可以先降低并发、开启 AutoThrottle、检查 cookie 和 header,再考虑代理或登录态维护。踩坑最多的是把 403 加进重试列表,结果同一个被拦请求重复打过去,封禁更严重。遇到这类情况,应该先暂停放量,确认目标站允许的访问边界。 ### 怎么监控重试是否已经失控? Scrapy stats 会记录重试次数、失败原因和最终放弃数量,这些指标比单看日志可靠。可以在扩展或任务收尾阶段读取 `retry/count`、`downloader/response_status_count/429`、`downloader/exception_type_count/*`,超过阈值就告警。边界是告警不能只看绝对值,大任务天然错误更多,最好同时看比例。比如重试率超过 10% 或 429 持续上升,就该降速或停止任务排查。 ```python def closed(self, reason): retry_count = self.crawler.stats.get_value("retry/count", 0) max_reached = self.crawler.stats.get_value("retry/max_reached", 0) self.logger.info("retry=%s max_reached=%s", retry_count, max_reached) ``` ## 小结 Scrapy 错误处理的核心是分层:通用网络抖动交给 RetryMiddleware,业务失败交给 errback,异常趋势交给 stats 和告警。重试能提高完整率,但它不是反爬、权限和页面不存在的解药。先识别失败类型,再决定是否重试,爬虫才会稳定。
服务端5月31日 00:57
Scrapy 项目怎么写才更稳定?有哪些最佳实践?## Scrapy 最佳实践先从边界开始 Scrapy 项目最怕一开始跑得很快,过两周却没人敢维护。稳定的爬虫不是靠把并发拉满,而是把抓取边界、请求节奏、数据结构和失败处理提前定好。尤其是面向线上站点时,`robots.txt`、下载延迟、并发数和重试策略不是装饰配置,它们决定项目能不能长期运行。 一个比较稳的起步配置可以这样写: ```python ROBOTSTXT_OBEY = True CONCURRENT_REQUESTS = 16 CONCURRENT_REQUESTS_PER_DOMAIN = 4 DOWNLOAD_DELAY = 0.5 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 LOG_LEVEL = "INFO" USER_AGENT = "mycrawler/1.0 (+contact@example.com)" ``` 这里没有万能数字。新闻站、文档站、电商站承压能力完全不同,最佳实践不是照抄参数,而是先用小流量观察响应时间、错误码和封禁情况,再逐步调大。很多“爬虫不稳定”的问题,其实是没有灰度过程。 数据层也要尽早规范。Item 字段最好明确含义,pipeline 负责校验、去重和入库,spider 只做页面解析。把清洗逻辑写满 spider 的短期效率很高,但字段一多,后面改一次规则要翻十几个回调函数。 ```python ITEM_PIPELINES = { "myproject.pipelines.ValidateItemPipeline": 200, "myproject.pipelines.DeduplicatePipeline": 300, "myproject.pipelines.SaveToPostgresPipeline": 500, } ``` 选择器和字段规则也要留测试样本。页面结构一变,CSS/XPath 可能不会报错,只是悄悄返回空列表,这比直接失败更难发现。建议保存少量代表性 HTML,给关键解析函数写单元测试,再用小批量线上 URL 做集成检查。这样后面目标站改版时,你至少知道是选择器坏了、接口变了,还是反爬策略变了。 另一个经常被忽视的实践是把运行参数显式化。比如采集日期、入口 URL、批次号、是否全量,都通过 `-a` 参数或配置传入,不要硬编码在 spider 里。这样同一份代码既能跑日常增量,也能跑一次性补采,日志和数据里还能追溯来源。边界是参数越多越容易误传,关键参数最好在 `from_crawler` 或 `__init__` 里校验,不合法就尽早失败。 ## 追问 ### 并发数和下载延迟怎么取舍? 并发高能提高吞吐,但也会放大目标站压力、错误率和封禁概率。下载延迟能让请求更温和,却会拉长任务时间,数据时效性要求高的业务可能接受不了。我的做法是先固定单域名并发,再用 AutoThrottle 看延迟变化,而不是一上来把 `CONCURRENT_REQUESTS` 调到很大。判断标准不是“跑得快”,而是 2xx 比例、平均响应时间和被限流次数是否稳定。 ### User-Agent 池和代理池是不是必备? 不是。公开文档、开放站点、内部站点通常不需要复杂代理池,反而应该用清晰的 User-Agent 和联系方式。代理池适合目标站有地域限制、频控严格或业务确实需要高吞吐的场景,但它会带来 IP 质量、成本、失败率和合规风险。常见踩坑是代理不可用时没有熔断,Scrapy 反复重试,最后把错误流量放大。先判断是否真的需要代理,再设计代理失败后的降级策略。 ### 为什么建议用 scrapy shell 测选择器? scrapy shell 能快速验证 CSS/XPath,不用每改一次就启动完整任务。它特别适合处理页面结构不稳定的站点,可以马上看出选择器是否拿到了空值、重复节点或脏文本。边界是 shell 只能验证单页,不能代表翻页、登录态、异步接口都没问题。上线前仍然要跑小批量任务,检查 item 完整率和异常日志。 ### 日志应该记录到什么程度? 日志太少,线上问题只能猜;日志太多,磁盘和检索成本都会上来。建议 INFO 记录任务阶段、抓取数量、关键参数,WARNING 记录字段缺失、重试变多、响应异常,ERROR 记录真正影响数据完整性的失败。不要在日志里打印大量 HTML 或敏感 cookie,这类坑排查时很常见。生产环境还要让日志带上 spider 名、批次号和请求 URL,后续定位会快很多。 ### 分布式爬虫什么时候再引入? 只有当单机 Scrapy 的瓶颈已经明确,比如队列太大、任务窗口太短、单机带宽或 CPU 不够,再考虑 scrapy-redis 或自研调度。分布式会带来去重一致性、任务恢复、节点监控和数据幂等问题,不是简单“多开几台机器”。如果业务每天只抓几万页,先把单机调度、pipeline 和监控做好更划算。过早分布式,往往会把一个小问题拆成三台机器上的大问题。 ## 小结 Scrapy 最佳实践不是一组漂亮配置,而是一套工程习惯:尊重目标站、控制节奏、拆清职责、记录关键指标、先小流量验证。只要这些基础稳住,后面无论加代理、分布式还是监控,都不会把项目推向不可维护。
服务端5月31日 00:57
Scrapy 扩展机制怎么用才不和中间件混在一起?## Scrapy 扩展到底负责什么? Scrapy 的扩展机制适合处理“爬虫生命周期级别”的事情,比如启动时加载配置、运行中记录统计、关闭时发通知、把关键指标推到监控系统。它不是用来改每个请求和响应的;如果逻辑要拦截 Request、Response、异常或代理,那通常应该放在 downloader middleware 或 spider middleware。这个边界很重要,很多项目后期变乱,就是因为把告警、埋点、请求改写都塞进一个类里,最后谁也不敢动。 扩展本质上是一个普通 Python 类,Scrapy 通过 `from_crawler` 创建实例,并让它订阅信号。常见信号包括 `spider_opened`、`spider_closed`、`item_scraped`、`request_dropped` 等。你可以把它理解成 Scrapy 项目的“事件监听器”:平时不参与下载链路,等事件发生时再做自己的事。 ```python from scrapy import signals class StatsAlertExtension: @classmethod def from_crawler(cls, crawler): ext = cls(crawler.stats, crawler.settings) crawler.signals.connect(ext.opened, signal=signals.spider_opened) crawler.signals.connect(ext.closed, signal=signals.spider_closed) return ext def __init__(self, stats, settings): self.stats = stats self.threshold = settings.getint("ALERT_ITEM_MIN", 1) def opened(self, spider): spider.logger.info("stats alert extension enabled") def closed(self, spider, reason): count = self.stats.get_value("item_scraped_count", 0) if count < self.threshold: spider.logger.warning("too few items: %s, reason=%s", count, reason) ``` 启用扩展时写进 `EXTENSIONS`,数字是优先级,值越小越早加载。项目里建议把自定义扩展放在独立模块,别直接堆在 spider 文件里,否则复用和测试都很麻烦。 ```python EXTENSIONS = { "myproject.extensions.StatsAlertExtension": 500, } ALERT_ITEM_MIN = 20 ``` 如果扩展要依赖配置,最好在 `from_crawler` 阶段读取并校验。配置缺失时可以抛出 `NotConfigured`,这样 Scrapy 会明确告诉你扩展没有启用,而不是跑到一半才出现空值错误。需要注意的是,扩展越靠近全局治理,越应该保持“可关闭、可降级”。告警接口挂了不应该让采集任务整体失败,除非这就是业务要求。 如果团队里有多个爬虫共享同一套监控规则,可以把阈值、告警开关和通知目标都放进 settings,再由扩展读取。这样开发环境可以只打日志,生产环境再接入真实告警,避免本地调试时频繁打扰别人。另一个实用做法是把扩展输出的统计字段命名固定下来,比如 `business/empty_item_count`、`business/invalid_price_count`,后续接 Grafana 或日志平台时不会因为字段名变化而断图。 ## 追问 ### 扩展和中间件应该怎么取舍? 扩展看全局生命周期,中间件看单次请求链路,这是最稳的判断标准。比如统计最终采集量、爬虫结束发企业微信通知,用扩展更自然;给请求加代理、处理 403、改 User-Agent,就应该用中间件。边界踩错以后,扩展可能变成“万能工具类”,调试时你会发现请求还没发出去,告警逻辑却先影响了调度。我的经验是:只要函数参数里必须拿到 request 或 response,先别急着写扩展。 ### `from_crawler` 里为什么经常连接 signals? 因为扩展实例需要拿到 Scrapy 的运行上下文,包括 settings、stats、signals、engine 等对象。直接在 `__init__` 里 new 一个扩展也能写,但拿不到 crawler,就没法按 Scrapy 的方式订阅事件。这里的坑是信号函数签名要和信号匹配,少写参数可能运行到关闭阶段才报错。建议先从 `spider_opened` 和 `spider_closed` 两个信号开始,确认日志正常后再增加 item 或 request 相关信号。 ### 扩展里能不能做耗时操作? 能做,但要非常克制。Scrapy 基于事件循环运行,扩展里长时间同步请求外部接口,会拖慢爬虫关闭、item 处理甚至整个 reactor。告警、上报监控这类操作最好设置短超时,或者丢给队列、后台服务处理。特别是 `spider_closed`,很多人会在这里上传文件、发报表,一旦接口卡住,任务看起来就像“明明爬完了却不退出”。 ### 自定义统计指标放在哪里更合适? 如果指标和生命周期相关,放扩展里比较清楚,比如启动时间、关闭原因、最终 item 数、错误比例。若指标来自某个 item 清洗步骤,也可以在 pipeline 中 `stats.inc_value()`,扩展最后统一读取并汇总。取舍点在于数据产生的位置:不要为了集中管理,把所有业务 pipeline 都反向依赖扩展。否则扩展一改,数据入库链路也跟着抖。 ### 生产环境使用扩展有什么边界? 扩展适合补齐监控和治理,不适合承载核心业务解析。它可以检查 item 数是否异常、记录关闭原因、输出错误分布,但不应该决定页面怎么解析、数据怎么清洗。另一个常见坑是配置优先级和内置扩展冲突,导致 Telnet、CoreStats 等行为被误关。上线前至少跑一次小流量任务,确认 stats、日志、告警都按预期出现。 ## 小结 Scrapy 扩展的价值在于把生命周期治理从 spider 里拆出来。它越像事件监听器,项目越好维护;它越像业务大杂烩,后期越难排查。用它做统计、告警、监控和收尾动作,用中间件处理请求响应,用 pipeline 处理数据,这个分工通常最省心。
服务端5月31日 00:57
Scrapy CrawlSpider 适合爬哪些网站?## 直接答案 CrawlSpider 是 Scrapy 里用规则自动跟链接的 Spider,适合网站结构清楚、链接规律稳定、需要从列表页一路爬到详情页的场景。它的核心是 `Rule` 和 `LinkExtractor`:前者定义“哪些链接要跟、用哪个回调处理”,后者负责从页面里提取符合条件的链接。普通 Spider 更像你手写路线图,CrawlSpider 更像给爬虫装了导航规则。它能减少重复代码,但也更容易因为规则写得太宽,把不该爬的登录页、搜索页、标签页甚至日历页一起卷进去。 ```python from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class NewsSpider(CrawlSpider): name = "news" allowed_domains = ["example.com"] start_urls = ["https://example.com/news/"] rules = ( Rule(LinkExtractor(allow=(r"/news/page/\d+/",)), follow=True), Rule(LinkExtractor(allow=(r"/news/\d+\.html",)), callback="parse_article", follow=False), ) def parse_article(self, response): yield {"title": response.css("h1::text").get(), "url": response.url} ``` 使用 CrawlSpider 时不要覆盖 `parse()`,因为它被内部规则系统使用。需要解析详情页就写 `parse_article` 这类自定义回调,并在 Rule 里指定 callback。规则越精确,爬虫越可控;规则越宽,开发越省事,但队列失控、重复抓取和误入无效页面的概率也越高。生产里最好配合 `allowed_domains`、深度限制、去重日志和 URL 采样检查一起用。 ## 追问 ### CrawlSpider 和普通 Spider 怎么选? 如果网站路径固定、你只需要列表页翻页和详情页解析,CrawlSpider 会更省代码。若流程依赖复杂参数、登录状态、多接口组合,普通 Spider 通常更清晰。CrawlSpider 的优势是自动发现链接,代价是规则调试成本更高。边界很简单:链接结构能用正则稳定描述,就可以考虑 CrawlSpider;每一步都要业务判断,就别硬套。 ### Rule 里的 `follow` 到底是什么意思? `follow=True` 表示这个规则匹配到的页面下载后,还会继续从响应里提取新链接。`follow=False` 则通常用于详情页,只解析数据,不再从详情页继续扩散。踩坑点是详情页如果误设成 follow=True,页面里的推荐文章、广告链接、标签链接可能继续扩散,队列会变得很脏。取舍上,列表、分类、分页可以 follow,详情、附件、用户页一般不要 follow。 ### LinkExtractor 的 allow 和 deny 应该怎么写? `allow` 用来圈定你想要的 URL 模式,`deny` 用来排除明显不需要的路径。规则不要写得只看“能匹配”,还要看后续维护者能不能看懂。比如 `/news/\d+\.html` 比一个巨长的通配正则更安全,也更容易排查。常见坑是忘了排除 `?reply=`, `#comment`, `/login` 这类链接,结果爬虫在无价值页面里打转。 ### 为什么不应该覆盖 CrawlSpider 的 parse 方法? CrawlSpider 的 `parse` 已经承担了按 Rule 分发响应、提取链接、生成后续请求的职责。你覆盖它之后,规则系统可能直接失效,表现为 start_urls 能访问,但后续链接不再跟进。正确做法是写新的回调函数,比如 `parse_item`、`parse_article`,再在 Rule 里引用。这个坑很常见,因为普通 Spider 里大家习惯写 parse,迁移到 CrawlSpider 时容易顺手复制旧代码。 ### 如何防止 CrawlSpider 爬过界? 先设置 `allowed_domains`,再把 LinkExtractor 的 allow 写窄,不要只依赖域名限制。其次配置深度、下载延迟和并发,避免规则失控时给目标站造成压力。上线前抽样打印命中的 URL,确认分页、详情、排除路径都符合预期。对于大型站点,建议先跑小范围分类页,验证规则后再扩大入口,否则一次错误规则就可能制造几十万条无效请求。 CrawlSpider 还适合做“半自动发现链接”的任务:你知道大概范围,但不想手写每一种分页入口。上线前可以先把 callback 里只打印 URL,不写库也不下载大文件,观察几百个命中的链接是否符合预期。规则稳定后再打开 item 解析和持久化,这一步能省掉很多返工。对内容站来说,CrawlSpider 的收益很明显;对搜索结果页、筛选页特别多的站点,规则必须保守,否则参数组合会把队列撑爆。 如果网站有多语言、多地区或移动端路径,CrawlSpider 的规则最好显式写出允许范围。不要只写 `allow=(r"/article/",)` 就上线,因为相似路径可能包含预览页、打印页、AMP 页和评论页。重复内容多时,Scrapy 的请求去重只能处理 URL 级重复,正文级重复还要靠业务字段或内容哈希。这个边界分清楚,CrawlSpider 才会从“自动乱爬”变成可靠的站内采集工具。 最后,别把 CrawlSpider 当成反爬解决方案。它只是链接发现和请求调度的封装,并不会自动处理登录、验证码、限速或动态渲染。遇到需要登录的站点,仍然要先完成会话管理;遇到 JS 渲染内容,也要决定抓接口还是接浏览器。把这些能力拆开设计,规则负责找路,回调负责解析,中间件负责通用请求问题,项目会更容易维护。
服务端5月31日 00:57
Scrapy 遇到 JavaScript 动态网页怎么办?## 直接答案 Scrapy 本身不会执行 JavaScript,它拿到的是服务器直接返回的 HTML。遇到动态网页时,第一步不是立刻上 Selenium 或 Playwright,而是打开浏览器开发者工具,找页面真正请求的数据接口。如果数据来自 XHR 或 Fetch,请优先用 Scrapy 直接请求接口;只有内容必须经过浏览器渲染、签名依赖运行时环境、或交互流程很重时,才把 Playwright、Selenium、Splash 接进来。这个取舍很重要,因为浏览器渲染的成本通常比普通 HTTP 请求高一个数量级。 ```python # settings.py DOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", } TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor" PLAYWRIGHT_BROWSER_TYPE = "chromium" # spider.py yield scrapy.Request( "https://example.com/list", meta={"playwright": True, "playwright_include_page": False}, callback=self.parse, ) ``` 如果能抓接口,就直接复用接口参数、请求头、分页字段和必要 token。接口方案快、稳定、并发高,也更容易做重试和监控。浏览器方案适合少量复杂页面,但要限制并发、设置超时、关闭页面资源,否则内存会很快涨上去。实际项目里常见的做法是混合:列表接口用 Scrapy 抓,少数详情页或反爬校验页才走 Playwright。 ## 追问 ### Selenium、Playwright 和 Splash 怎么选? 新项目优先考虑 Playwright,因为它支持现代浏览器、异步能力好,和 Scrapy 集成也更顺。Selenium 生态成熟,适合已有浏览器自动化脚本或必须兼容特定驱动的团队。Splash 较轻,适合简单渲染和 Lua 脚本控制,但面对复杂前端应用时调试体验不如真浏览器。取舍边界是交互复杂度和吞吐量:越像真实用户操作,越偏 Playwright;越像批量渲染 HTML,越要控制成本。 ### 为什么页面在浏览器能看到,Scrapy 里却没有? 因为浏览器看到的是 HTML、JS、接口数据和运行时状态合成后的结果,而 Scrapy 默认只拿第一份 HTML。你应该先查看 Network 面板,过滤 XHR/Fetch,看数据是不是来自 JSON 接口。还要检查接口是否依赖 cookie、Referer、签名参数、时间戳或 Authorization。一个常见坑是只复制接口 URL,没有复制必要请求头,导致接口返回空数据或风控页面。 ### 使用 Playwright 会带来哪些坑? 最大坑是资源泄漏,页面、上下文或浏览器实例没有关闭时,爬虫跑一段时间就会内存暴涨。第二个坑是等待条件写得太宽,比如固定 sleep 三秒,既慢又不稳定。更好的方式是等待某个选择器、接口响应或 DOM 状态出现。还要注意并发数不能沿用纯 Scrapy 的配置,浏览器并发通常要小得多,否则机器 CPU 和目标站风控都会先扛不住。 ### 动态网页一定要完整渲染吗? 不一定。很多页面只是首屏用 JS 拉接口,真正数据在 JSON 里,完整渲染反而是绕远路。只有当数据经过前端计算、加密逻辑藏在 JS 里、或必须点击展开后才出现时,渲染才有价值。边界判断可以很朴素:如果 Network 里能稳定复现接口,就抓接口;如果接口参数无法还原,再考虑浏览器。不要为了“像真人”而默认渲染所有页面,那会让爬虫变慢、变贵、也更难排错。 ### 如何让动态页面爬虫更稳定? 先把请求链路拆清楚:入口页、接口、详情页、登录态、反爬校验分别记录日志和状态码。对浏览器渲染请求设置单独超时和重试,不要让一个页面卡住整个调度器。静态资源如图片、字体、视频可以拦截掉,减少带宽和内存压力。配置上建议把浏览器并发、普通请求并发分开看,别用一个 `CONCURRENT_REQUESTS` 解决所有问题。 动态网页还有一个现实问题:你看到的“渲染失败”不一定是 JS 没执行,可能是接口被风控、地区不匹配、账号权限不足或首屏骨架屏还没消失。排查时最好保存三类证据:原始 HTML、关键接口响应、渲染后的截图。只看最终选择器为空,很容易误判方向。对于大型采集任务,可以先用少量 URL 跑 Playwright 验证渲染路径,再把能还原的接口逐步替换成普通 Scrapy 请求。这样浏览器只承担兜底角色,整体成本会可控得多。 配置层面还要把浏览器请求单独标记出来,方便统计成功率和耗时。比如在 `meta` 里加 `rendered=True`,日志里区分普通请求和渲染请求。遇到超时、空页面、验证码时,不要无限重试浏览器请求,可以降级保存现场交给人工分析。浏览器池本身也要定期重启,否则长时间运行后可能出现句柄泄漏、缓存膨胀和页面上下文污染。
服务端5月31日 00:57
Scrapy 请求去重是怎么判断重复的?## 直接答案 Scrapy 的请求去重由调度器调用 dupefilter 完成,默认实现是 `RFPDupeFilter`。它会为请求生成 fingerprint,通常由规范化后的 URL、请求方法、请求体组成;指纹已经出现过,就认为这个请求重复,不再入队。它解决的是“同一个请求不要重复抓”,不是“同一条业务数据不要重复入库”。所以列表页、详情页、翻页链接能靠它减少浪费,但商品 ID、文章 ID、用户 ID 的业务级去重,还应该放在 pipeline 或存储层。 ```python # settings.py DUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter" DUPEFILTER_DEBUG = False JOBDIR = ".job/article_spider" # 需要断点续爬时启用 # spider.py yield scrapy.Request(url, callback=self.parse_detail) yield scrapy.Request(url, callback=self.parse_detail, dont_filter=True) # 只给确实需要重复访问的请求 ``` 默认去重对 URL 会做规范化,比如参数顺序不同但含义相同的 URL,通常会生成相同指纹。请求方法和 body 也参与计算,所以同一个接口的 GET 和 POST 不会被当成同一个请求。`dont_filter=True` 是绕过去重的开关,适合登录、分页入口刷新、重试某个状态页,但不能随手加;一旦在列表链接上滥用,调度队列会膨胀,甚至把站点反复打穿。 ## 追问 ### Scrapy 去重和数据库唯一索引有什么区别? Scrapy 去重发生在请求入队前,目标是少发重复请求,节省带宽和时间。数据库唯一索引发生在数据写入时,目标是避免重复数据污染结果。两者最好同时存在,因为 URL 不重复不代表数据不重复,同一商品可能有 PC、移动端、活动页三个 URL。取舍上,请求去重提升爬取效率,业务去重保证数据质量,不能互相替代。 ### 为什么有些看起来一样的页面没有被去重? 最常见原因是 URL 里有追踪参数、时间戳、随机数或不同排序参数,Scrapy 认为它们是不同请求。另一个原因是 POST body 不同,哪怕接口地址一样,也会生成不同指纹。遇到这种情况,不要先怪 dupefilter,应该先确认哪些参数影响内容,哪些只是噪声。噪声参数可以在生成请求前清理,或者自定义指纹规则,但清理过度会把真正不同的页面合并掉。 ### 什么时候需要自定义去重规则? 当默认 URL 指纹无法表达业务唯一性时才需要自定义。比如搜索接口里 page、keyword 决定内容,而 `_t`、`callback`、`utm_source` 不决定内容,就可以在指纹里忽略后者。分布式爬虫也常把指纹放到 Redis,让多个 worker 共享已抓请求集合。边界是维护成本:规则越业务化,越容易在目标站改版后误杀请求,所以要给命中去重的样本留日志。 ```python from scrapy.dupefilters import RFPDupeFilter from w3lib.url import canonicalize_url class CleanQueryDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): url = canonicalize_url(request.url, keep_blank_values=False) return self.fingerprinter.fingerprint(request.replace(url=url)).hex() ``` ### `dont_filter=True` 应该怎么用才安全? 它适合少量入口型或状态型请求,比如每次启动都要访问首页拿 cookie,或者轮询一个会变化的任务状态页。不要把它放在详情页和翻页请求上,否则同一个链接会被反复入队。一个实用边界是:如果这个请求的返回内容依赖时间、登录态或外部状态,可以考虑跳过去重;如果只依赖 URL,本该让去重生效。踩坑最多的是复制登录请求代码时把 `dont_filter=True` 一起复制到了所有请求。 ### 断点续爬时去重状态会保留吗? 只有配置 `JOBDIR` 后,队列和去重指纹才会持久化到磁盘,爬虫重启后可以继续使用。没有 `JOBDIR` 时,默认去重集合在内存里,进程结束就没了。这个能力适合长任务,但不适合频繁变动的短任务,因为旧指纹可能让你误以为“怎么不爬了”。如果目标站内容更新很快,应该定期清理 jobdir,或把增量策略改成按更新时间、业务 ID 控制。 还要注意,请求去重不是越激进越好。新闻站、论坛和电商列表经常会出现同一个 URL 在不同时间返回不同内容的情况,例如首页、热榜页、库存接口和价格接口。如果这些页面被默认指纹长期挡住,增量爬虫就会漏掉更新。更稳妥的做法是把“稳定详情页”和“会变化的入口页”分开:详情页交给默认去重,入口页按调度周期允许重复访问,再在解析出的业务 ID 上做增量判断。这样既不浪费大量详情请求,也不会因为去重太早而错过新数据。 在分布式场景里,去重还会影响任务分配公平性。多个节点共享 Redis 指纹集合时,一个节点先写入指纹,其他节点就不会再抓同一请求,这能减少重复劳动。但如果指纹规则里混入了节点本地状态,结果就会变得不可预测。建议把指纹生成逻辑做成纯函数,只依赖 URL、方法、body 和明确保留的参数,部署前用一批样例 URL 做回归测试。
服务端5月31日 00:57
Scrapy 如何处理 Cookies 和多会话登录?## 直接答案 Scrapy 默认会通过 CookiesMiddleware 维护 cookie:同一个会话里的响应 Set-Cookie 会被保存,后续请求会自动带上。登录类爬虫通常用 `FormRequest` 先提交账号密码,再把登录后的请求接在回调里;如果要同时爬多个账号、多个店铺或多个地区,就用 `meta['cookiejar']` 隔离会话。真正容易出错的地方不是“能不能带 cookie”,而是 cookie 什么时候该让 Scrapy 管、什么时候该你自己管。手动在 `headers` 里塞 `Cookie` 看起来快,但会绕开 Scrapy 的 cookie 合并逻辑,后续重定向、刷新 token、跨域跳转都可能变乱。 ```python import scrapy class AccountSpider(scrapy.Spider): name = "account" custom_settings = {"COOKIES_ENABLED": True, "COOKIES_DEBUG": False} def start_requests(self): yield scrapy.FormRequest( "https://example.com/login", formdata={"username": "u1", "password": "p1"}, meta={"cookiejar": "u1"}, callback=self.after_login, ) def after_login(self, response): yield response.follow("/user/orders", callback=self.parse_orders, meta={"cookiejar": response.meta["cookiejar"]}) def parse_orders(self, response): yield {"url": response.url, "title": response.css("title::text").get()} ``` 如果站点把登录状态放在 `localStorage` 或 JS 变量里,单靠 cookie 不够,要先在浏览器抓包确认真正校验的是 cookie、Authorization header,还是隐藏接口里的 token。生产环境还要注意 cookie 的生命周期:短期任务放内存即可,跨天任务才需要持久化到 Redis、数据库或加密文件。保存 cookie 时不要把明文账号、验证码结果、敏感 token 写进日志,尤其不要开启 `COOKIES_DEBUG` 后直接把日志上传到公共平台。 ## 追问 ### 什么时候用 Scrapy 自动 cookie,什么时候手动传 cookies? 能让 Scrapy 自动管理时优先让它管,因为它会处理 Set-Cookie、域名、路径、过期时间和重定向后的合并。手动传 `cookies={...}` 适合一次性请求,比如带一个固定地区、语言或实验分组标识。不要把完整 `Cookie` 字符串放进 headers,除非你明确知道不会再依赖响应里的 Set-Cookie。这个取舍的边界是会话是否会变化:会变化就交给中间件,不变化才手动给。 ### 多账号同时爬时 cookie 会不会串号? 默认情况下,同一个 spider 会共用一个 cookie jar,所以多账号并发登录如果不隔离,确实可能出现 A 账号请求带上 B 账号 cookie 的事故。解决办法是为每个账号设置不同的 `meta['cookiejar']`,并在后续所有请求里继续传递这个值。踩坑点是 `response.follow()` 不会替你“记住业务身份”,忘了传 meta 就会回到默认 jar。并发越高,这类串号越隐蔽,最好把账号标识也写入 item 或日志方便排查。 ### 登录后仍然被跳回登录页,应该先查什么? 先看登录响应是否真的成功,不要只看 HTTP 200,很多站点失败时也返回 200 但页面里写着错误提示。其次检查是否缺少 CSRF token、验证码、动态签名或必须的 Referer。再看后续请求是不是丢了 `cookiejar`,以及重定向过程中 cookie 域名是否从 `www.example.com` 变成了 `example.com`。如果这些都正常,再考虑是否存在设备指纹或风控,而不是一上来就换代理。 ### cookie 需要持久化吗? 短任务不建议持久化,内存 cookie 简单、干净,任务结束就释放,也不容易留下敏感信息。长周期采集、登录成本高或验证码昂贵时,可以把 cookie 加密后存储,并记录过期时间和账号状态。边界在于 cookie 失效成本和泄露风险:越敏感的站点越要少存、加密存、按需刷新。一个常见坑是复用过期 cookie 后误判为“选择器失效”,其实页面已经被重定向到登录页。 ### Scrapy 能处理 JWT 或 Authorization 吗? 能处理,但 JWT 不属于 cookie 机制,通常要放在请求头或接口参数里。登录后从响应 JSON、HTML 脚本或浏览器存储中取到 token,再在后续请求里加 `Authorization: Bearer xxx`。如果 token 会刷新,需要在中间件或回调里统一更新,避免部分请求继续使用旧 token。取舍上,纯接口站点用 header 管 token 更清晰;传统 Web 站点则继续让 cookie 中间件处理会话。 还有一个容易被忽略的细节:Scrapy 的 cookie jar 是跟请求链路走的,不是跟账号对象自动绑定的。如果你从一个登录回调里拆出多个分页请求,分页请求都要带同一个 `cookiejar`;如果中途又发起刷新 token 或退出登录请求,也要确认它不会污染同组会话。对于需要定期续期的站点,可以把“检测是否登录失效”写成一个小函数,例如看到登录按钮、特定错误码或跳转地址时重新登录。这样比在每个解析函数里临时判断更稳,也能避免同一个账号被多个并发请求同时刷新,造成服务端把旧 cookie 全部作废。 如果你要把 cookie 持久化,建议同时保存来源域名、账号标识、创建时间和最近验证时间,而不是只保存一串 cookie 值。恢复时先访问一个轻量的个人中心或状态接口确认有效,再进入正式抓取。这样做多了一次请求,但能避免大量业务请求都拿着失效会话去跑。对于涉及个人数据的网站,还要把 cookie 当成密码处理,权限、加密和清理策略都不能省。
服务端5月31日 00:56
Scrapy settings.py 里哪些配置最该优先调整?Scrapy 的 `settings.py` 决定爬虫速度、稳定性、反爬风险和数据质量。新项目最该优先调整并发、延迟、超时、重试、请求头、robots、日志、pipeline、middleware 和环境覆盖方式。不要一开始就追求最快,先让目标站、代理池、数据库和自己机器都扛得住。速度可以逐步加,封禁和脏数据一旦出现,排查成本会高很多。 ## 基础配置先保持清楚 `BOT_NAME`、`SPIDER_MODULES`、`NEWSPIDER_MODULE` 通常由项目生成,但要和部署项目名一致。Scrapyd 日志、任务和发布记录都会反复出现这些名字,命名混乱会让排查变难。`ROBOTSTXT_OBEY` 默认建议开启,是否关闭要看授权、目标站条款和数据用途。 ```python BOT_NAME = "news_crawler" SPIDER_MODULES = ["news_crawler.spiders"] NEWSPIDER_MODULE = "news_crawler.spiders" ROBOTSTXT_OBEY = True ``` ## 并发和延迟决定稳定性 最常见的事故是把 `CONCURRENT_REQUESTS` 开太高,又把 `DOWNLOAD_DELAY` 设为 0。建议先用保守配置跑基线,观察状态码、平均延迟、item 数和失败率,再逐步调高。AutoThrottle 适合响应波动明显的网站,但它不是反爬万能药。 ```python CONCURRENT_REQUESTS = 16 CONCURRENT_REQUESTS_PER_DOMAIN = 4 DOWNLOAD_DELAY = 0.5 RANDOMIZE_DOWNLOAD_DELAY = True DOWNLOAD_TIMEOUT = 20 AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_MAX_DELAY = 10 ``` ## 请求头、Cookie、重试分场景配置 普通静态页面通常设置合理 UA 即可;登录态、地区化或个性化页面才可能需要 Cookie。Cookie 一旦过期,爬虫可能不报错,却一直抓登录页。重试也要控制,网络错误和 5xx 适合重试,403、验证码和参数错误不适合反复撞。 ```python USER_AGENT = "Mozilla/5.0 (compatible; ResearchBot/1.0)" COOKIES_ENABLED = False RETRY_ENABLED = True RETRY_TIMES = 2 RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408] ``` ## Pipeline 和 Middleware 别乱塞 `ITEM_PIPELINES` 适合清洗、校验、去重和入库,`DOWNLOADER_MIDDLEWARES` 适合代理、请求头、限速和异常处理。优先级数字越小越先执行,顺序错了会导致代理没生效或脏数据先入库。Spider 负责页面解析,通用能力尽量沉到 pipeline 和 middleware。 ```python ITEM_PIPELINES = { "news_crawler.pipelines.ValidatePipeline": 200, "news_crawler.pipelines.MongoPipeline": 500, } DOWNLOADER_MIDDLEWARES = { "news_crawler.middlewares.ProxyMiddleware": 350, } ``` 生产环境可以用 `-s` 或环境变量覆盖配置,例如 `scrapy crawl article -s LOG_LEVEL=INFO -s CONCURRENT_REQUESTS=8`。多份 settings 文件能用,但容易漏改;部署系统注入环境变量通常更可靠。 ## 追问 ### CONCURRENT_REQUESTS 越高越好吗? 不是。并发提高会增加吞吐,也会放大限流、代理失败和数据库压力。合理做法是先跑稳定基线,再按错误率和延迟调高。踩坑是只看抓取速度,不看 pipeline 是否堆积。 ### ROBOTSTXT_OBEY 要不要开? 默认建议开启,特别是公开网页和长期采集任务。关闭它只是 Scrapy 不再自动检查 robots,不代表可以随意抓。实际要结合授权、条款、频率和用途判断。内部测试站或明确授权数据源可以按约定关闭。 ### COOKIES_ENABLED 什么时候打开? 页面依赖登录态、地区选择或会话校验时才考虑打开。普通静态页面不建议默认开启,因为请求状态会变复杂。打开后要监控 Cookie 过期和登录页误抓。最常见坑是爬虫运行正常,解析到的却是风控页。 ### Retry 为什么不能无限加? 重试能处理临时网络问题,不能解决封禁、验证码和页面结构变化。无限重试会浪费代理、拖慢队列,还可能让限流更严重。建议区分可重试状态码,并限制次数。遇到 403 或 429,应先查频率、代理和请求行为。 ### settings.py 能放敏感信息吗? 不应该。数据库密码、代理账号、Token 和 Cookie 应来自环境变量或密钥系统。写进 settings.py 只是省事,却容易提交到仓库。边界是本地可以提供示例值,生产值必须外部注入。日志里也不要把这些值打印出来。
服务端5月31日 00:56
Scrapy 爬虫运行中如何监控和定位问题?Scrapy 监控要回答三个问题:爬虫是否还活着,数据产出是否正常,异常卡在哪一步。只看进程状态不够,因为进程可能还在跑,却一直拿到 403、验证码、空页面或登录页。生产环境至少要看请求量、状态码、失败率、item 数、入库数、运行耗时、队列积压和内存占用。指标少一点没关系,但每个指标都要能指导动作。 ## 先用 Stats 建基础盘 Scrapy 自带 Stats Collector,会记录请求、响应、重试、异常和 item 数。最小成本的做法是在爬虫结束时把关键指标写进日志或监控系统。`item_scraped_count` 看产出,`downloader/response_status_count/403` 看封禁,`downloader/exception_type_count/*` 看网络异常。 ```python class StatsPipeline: def close_spider(self, spider): s = spider.crawler.stats.get_stats() spider.logger.info({ "items": s.get("item_scraped_count", 0), "requests": s.get("downloader/request_count", 0), "errors": s.get("log_count/ERROR", 0), }) ``` ## 日志要能还原现场 本地调试可以用 DEBUG,线上默认 INFO,关键异常才打 ERROR。日志里要带 spider、任务参数、URL、状态码、代理标识、解析阶段和 item 主键。不要把 Cookie、Token、手机号直接打到日志里,集中日志平台会放大泄露风险。 ```python LOG_LEVEL = "INFO" LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s" RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408] ``` 日志采集可以用 ELK、Loki,也可以先用本地滚动文件。边界是小项目不用一开始上大平台,但必须设置切割和保留天数,别让日志写满磁盘。 ## 用 Scrapyd API 做运行管理 Scrapyd API 能查看任务、取消任务和读取日志,适合接内部管理后台。Telnet Console 适合临时排查运行中的 crawler 和 stats,但不要暴露公网。排查顺序建议从请求入口开始:先看状态码和响应内容,再看选择器,最后看 pipeline 和入库。 ```bash curl http://127.0.0.1:6800/listjobs.json?project=news_crawler curl http://127.0.0.1:6800/cancel.json -d project=news_crawler -d job=JOB_ID ``` ## 告警围绕业务结果 告警不应只看崩溃。更有价值的是 30 分钟无新增、字段为空率升高、详情页成功率下降、重复率异常、磁盘快满。阈值先按一两周基线设置,别一开始太敏感,否则团队很快会忽略告警。 ## 追问 ### Scrapy 自带 stats 够用吗? 基础监控够用,比如请求数、状态码、重试、异常和 item 数。业务指标不够,比如字段为空率、价格异常和入库失败原因。它们应该在 pipeline 或 middleware 里补。边界是明细不要全塞指标,单 URL 细节更适合日志。 ### item 数突然变 0 怎么查? 先看请求是否成功,再看 403、429、302 是否异常。然后保存一条响应,确认是不是验证码、登录页或页面结构变化。最后再改 XPath 或 CSS 选择器。常见坑是直接改解析规则,真实原因却是代理池失效。 ### 日志和指标有什么区别? 指标适合看趋势和触发告警,比如错误率上升。日志适合还原现场,比如某个 URL 为什么失败。只靠日志会被细节淹没,只靠指标又解释不了原因。取舍上,指标要稳定,日志要可检索。 ### Telnet Console 能在线上开吗? 能开,但必须限制监听地址和访问来源。它对查看 crawler、engine、stats 很方便。风险是权限太大,暴露出去等于把运行进程交给别人。更稳妥的是内网临时开启,排查结束关闭。 ### 告警太多没人看怎么办? 删掉没有行动价值的告警,比如单次超时。保留连续无产出、失败率持续升高、磁盘快满这类会影响结果的问题。告警内容要带 spider、版本、任务参数和日志位置。踩坑是只告警失败不告警恢复,大家不知道问题是否结束。