面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月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 大概是这样:import scrapyclass 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 一起调。边界是目标站点的承受能力和反爬策略,不是你的机器能开多少连接。比较稳的做法是先小并发跑通,再根据错误率和响应时间逐步增加。# settings.pyCONCURRENT_REQUESTS = 16DOWNLOAD_DELAY = 0.5AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_TARGET_CONCURRENCY = 4.0RETRY_TIMES = 3ROBOTSTXT_OBEY = TrueScrapy 项目上线后最容易踩什么坑?第一类是选择器太脆,页面结构一改就抓不到字段,所以关键字段要做空值校验和告警。第二类是去重规则没想清楚,带时间戳或追踪参数的 URL 会造成重复抓取。第三类是 Pipeline 入库没有幂等,爬虫重跑后数据重复。Scrapy 提供框架能力,但“抓取是否合法、数据是否可靠、失败是否可恢复”仍要靠项目设计兜底。
服务端阅读 05月31日 01:07

Scrapy 中间件有什么作用?适合哪些场景?

Scrapy 中间件可以理解为爬虫流程里的拦截层:请求发出去之前能改,响应回来之后能查,异常发生时也能兜底。它主要分为下载器中间件和爬虫中间件,前者夹在引擎和下载器之间,常用来处理请求头、代理、重试、Cookie、响应状态;后者夹在引擎和 Spider 之间,更适合处理输入给 Spider 的响应和 Spider 产出的请求。中间件的价值是把通用逻辑从 Spider 里抽走,但边界也明显:业务字段解析不要塞进中间件,否则后面排查时会分不清数据到底在哪一步被改掉。判断一个逻辑要不要做成中间件,可以看它是否能被多个 Spider 复用,以及是否只依赖请求、响应、异常这些通用对象。追问下载器中间件和爬虫中间件有什么区别?下载器中间件关注“请求能不能顺利拿到响应”,所以常见场景是加请求头、切代理、处理 403、记录耗时、对异常做重试。爬虫中间件关注“响应和请求如何进出 Spider”,比如过滤某些响应、统一补充 meta、处理 Spider 抛出的异常。取舍上,大多数反爬和网络层问题放下载器中间件更自然,业务解析前后的流程控制才考虑爬虫中间件。踩坑点是把两者职责混在一起,比如在下载器中间件里解析商品价格,短期能跑,长期会让代码很难测试。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 responseprocess_request、process_response、process_exception 分别怎么用?process_request 在请求进入下载器前执行,适合补请求头、代理、Cookie 或直接返回缓存响应。process_response 在响应回到引擎后执行,适合检查状态码、替换响应、对异常页面重新发请求。process_exception 只处理下载阶段抛出的异常,比如超时、连接失败、DNS 错误。边界是返回值会改变流程:返回 Request 会重新调度,返回 Response 会跳过下载,返回 None 才是继续交给下一个中间件;不理解这个规则,很容易写出重复请求或响应丢失的问题。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 已经处理过一次,你又额外重试一次。# settings.pyDOWNLOADER_MIDDLEWARES = { "myproject.middlewares.RandomHeaderMiddleware": 400, "myproject.middlewares.ProxyMiddleware": 410, "myproject.middlewares.StatusRetryMiddleware": 550,}哪些逻辑不适合写进中间件?和具体页面结构强相关的字段解析不适合写进中间件,应该留在 Spider 或 Item Pipeline 里。中间件也不适合保存大量业务状态,例如把所有已抓商品、分类树、价格规则都塞进去,这会让它变成隐藏的业务中心。边界判断可以很简单:如果这个逻辑换一个 Spider 仍然有价值,它适合抽成中间件;如果只服务某个页面字段,就别放进去。实际项目里滥用中间件会让调试很痛苦,因为响应还没到 parse 方法就已经被改过,日志不全时很难还原现场。如何设计一个可维护的代理中间件?代理中间件不要只做随机选择,还应该记录代理的失败次数、最近使用时间、适用域名和是否需要隔离登录态。轻量任务可以从列表里轮询,成本低也容易排查;高并发任务则需要独立代理池服务,负责健康检查和下线坏代理。取舍上,本地简单实现开发快,但多 Spider 共用时容易重复踩同一个坏代理;中心化代理池更稳定,却需要额外维护。常见坑是失败后立刻无限重试同一个请求,最后把调度队列拖慢,应该设置最大重试次数并对状态码做区分。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 的通用流程逻辑,尤其是网络请求、反爬、重试、代理、日志和异常处理。写中间件时最重要的是职责边界和执行顺序:通用逻辑抽出来,业务解析留在业务层,返回值和优先级要明确。这样中间件才是扩展点,而不是另一个难排查的黑盒。生产环境里还要给关键中间件补日志和开关,遇到代理池抖动、目标站改规则或重试风暴时,可以快速关闭某一层,而不是停掉整套爬虫。
服务端阅读 05月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),页面插一个广告位就会错位。更稳的方式是先找业务语义明显的容器,再在容器内取标题、链接和价格,避免整页范围内抓到推荐位或页脚里的相似元素。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、规范日期格式,这些步骤比事后在数据库里修数据可靠得多。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,下次发布就变成另一个值,用它做选择器风险很高。取舍上,选择器写得宽一点能抗改版,但可能误抓广告、推荐位和隐藏模板;写得窄一点更准确,却更容易被结构变动打断。实战里可以先定位稳定容器,再在容器内做相对选择,避免全局搜索抓到重复字段。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 字符串却没有反转义,或者把多个相似字段里的第一个误当成目标字段。import jsonraw = 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 处理复杂关系,正则只提取文本里的明确模式。解析代码一旦进入生产,就要配合默认值、校验和日志,否则页面一次小改版就可能让数据悄悄变脏。
服务端阅读 05月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,但正文其实是风控提示页,解析器照样能跑,却会把错误内容写进数据库。# middlewares.pyimport randomclass 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 的 DOWNLOADDELAY、RANDOMIZEDOWNLOADDELAY 和 AutoThrottle 都能控制速度,但它们解决的问题不一样。固定延迟简单可预测,适合小站点和低并发任务;AutoThrottle 会根据响应延迟动态调整,更适合站点负载变化明显的场景。边界是 AutoThrottle 不是“越开越安全”,如果目标站本身响应很快但限制按分钟计数,它可能仍然跑得太快。实际项目里建议从保守参数开始,看 429、403、超时比例再调,不要一上来把 CONCURRENTREQUESTS 拉满。如果任务有时效要求,可以按域名拆分队列,让高价值页面优先抓取,而不是所有 URL 使用同一套并发策略。# settings.pyCONCURRENT_REQUESTS = 8CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 1.5RANDOMIZE_DOWNLOAD_DELAY = TrueAUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_START_DELAY = 1AUTOTHROTTLE_MAX_DELAY = 10AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0代理池什么时候该用,什么时候不该用?代理池适合 IP 维度限流明显的网站,例如同一 IP 连续访问几十次后开始返回 403 或验证码。它不适合拿来掩盖所有问题,因为代理质量差会带来超时、脏 IP、地区不一致、TLS 指纹异常等新麻烦。取舍上,少量稳定代理加合理限速,通常比大量廉价代理随机切换更可靠。踩坑点是登录态和代理绑定:如果登录请求用 A 代理,后续详情页突然换到 B 代理,服务端很可能判定会话异常。class ProxyMiddleware: def process_request(self, request, spider): proxy = spider.proxy_pool.pick() request.meta["proxy"] = proxy登录、Cookie 和验证码怎么处理?需要登录的网站先用 FormRequest 或接口请求建立会话,让 Scrapy 的 CookieMiddleware 保持后续请求状态。简单验证码可以接第三方识别服务,但这会增加成本和失败率,也可能触碰站点规则边界。更推荐的思路是先确认数据是否有公开接口、RSS、站点地图或授权数据源,不要默认把验证码当成必须绕过的障碍。常见坑是登录成功后没有检查响应内容,只看状态码 200 就继续抓,最后抓到一堆登录页 HTML。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 限制,中间件负责把这些策略做成可复用逻辑。遇到强验证码、设备指纹和复杂加密时,要先评估成本和合规边界,而不是把所有问题都交给代理池。
服务端阅读 05月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。这个循环持续到队列清空、运行被停止或触发关闭条件。Spider -> Engine -> Scheduler -> Engine -> DownloaderDownloader -> Engine -> Spider -> Item PipelineSpider -> new Request -> Scheduler常见配置会影响这条数据流的节奏:CONCURRENT_REQUESTS = 16DOWNLOAD_DELAY = 0.5RETRY_ENABLED = TrueRETRY_TIMES = 2DOWNLOADER_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 数。
服务端阅读 05月31日 01:07

Scrapy Pipeline 管道如何清洗、去重并入库?

Scrapy Pipeline 是 Item 离开 spider 后进入存储系统前的处理链。它适合做数据清洗、字段校验、去重、补全、入库、发送消息等工作。和 spider 相比,Pipeline 更靠近数据出口,所以它不应该关心页面怎么解析,而应该关心“这条数据是否可信、如何保存、失败后怎么办”。Pipeline 可以配置多个,Scrapy 会按优先级从小到大依次执行。每个 process_item 要么返回 item 交给下一个管道,要么抛出 DropItem 丢弃数据。这里的取舍很实际:把所有逻辑写进一个 Pipeline 简单,但后期很难复用;拆得太细又会增加配置和排查成本。通常可以按职责拆成校验、去重、存储三类。# pipelines.pyfrom itemadapter import ItemAdapterfrom scrapy.exceptions import DropItemclass 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 itemclass 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# settings.pyITEM_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,比如数据库超时、接口偶发失败,这类问题更适合重试或落失败队列。否则数据会“安静地丢失”,日志里看得到,但业务侧很难补回来。
服务端阅读 05月31日 01:07

Scrapy 分布式爬虫如何用 Redis 稳定实现?

Scrapy 本身是单进程内的爬虫框架,要做分布式,关键是把“请求队列”和“去重集合”从本地内存搬到所有节点都能访问的地方。最常见方案是 scrapy-redis:多个 spider 实例共享 Redis 里的 request queue 和 dupefilter set,每个节点从同一个队列取任务,处理完再把新请求推回队列。这套方案的好处是改造成本低,单机 Scrapy 项目不用重写成全新的调度系统。代价是 Redis 变成核心依赖,网络抖动、队列堆积、去重 key 设计不当都会影响全局吞吐。分布式爬虫不是“机器越多越快”,目标站限速、代理质量、数据库写入能力、队列序列化开销都会成为边界。# settings.pySCHEDULER = 'scrapy_redis.scheduler.Scheduler'DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'SCHEDULER_PERSIST = TrueREDIS_URL = 'redis://:password@127.0.0.1:6379/0'ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 300,}from scrapy_redis.spiders import RedisSpiderclass ProductSpider(RedisSpider): name = 'product' redis_key = 'product:start_urls' def parse(self, response): yield {'url': response.url, 'title': response.css('h1::text').get()}启动后向 Redis 写入入口地址即可: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 重复提交,结果也应该是更新或忽略,而不是产生脏数据。
服务端阅读 05月31日 01:07

Scrapy Item 和 Item Loader 应该怎么分工使用?

Scrapy 里的 Item 更像数据结构声明,Item Loader 更像数据清洗和装配工具。Item 负责告诉项目“我要采集哪些字段”,例如标题、价格、链接、发布时间;Item Loader 负责把页面上脏兮兮的原始文本变成可存储、可复用的数据。两者不是二选一关系,常见做法是先定义 Item,再用 Item Loader 填充字段。如果页面字段很少,直接返回字典也能跑;如果项目要长期维护,Item 能让字段边界更清楚。Item Loader 的价值主要出现在字段来源复杂、需要去空格、拼接、取第一个值、统一格式时。它的坑也很明显:处理器写得太“聪明”,后面调试时很难判断数据是在 xpath、loader 还是 pipeline 阶段变坏的。# items.pyimport scrapyclass ProductItem(scrapy.Item): title = scrapy.Field() price = scrapy.Field() url = scrapy.Field()from itemloaders.processors import TakeFirst, MapComposefrom scrapy.loader import ItemLoaderfrom .items import ProductItemdef 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 复用。
服务端阅读 05月31日 01:07

Scrapy 调试和日志怎么做?Shell、parse 命令和 stats 如何配合排查?

先说结论Scrapy 调试不要只靠 print,更高效的做法是把 shell、parse 命令、日志级别、stats 指标和少量断点组合起来。选择器写不准,用 scrapy shell;单个 URL 的回调链路不对,用 scrapy parse;线上任务异常,用日志和 stats 定位是请求失败、解析为空,还是 Pipeline 写入出错。日志要能回答“发生了什么、发生在哪个 URL、影响多少数据”,而不是把控制台刷满。常用命令如下:scrapy shell "https://example.com/list"scrapy parse "https://example.com/list" --spider=demo -c parsescrapy crawl demo -s LOG_LEVEL=DEBUGscrapy crawl demo -s LOG_FILE=logs/demo.logDEBUG 适合本地排查,线上长期跑一般用 INFO 或 WARNING。如果日志太吵,真正的问题会被淹没;如果日志太少,任务失败时又只能重跑碰运气。用 shell 调选择器scrapy shell 会拿到和爬虫接近的 response 对象,可以直接测试 CSS、XPath 和正则。比如先确认标题是否存在,再确认列表长度,而不是写完 spider 后才发现字段全是空。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 直接打进日志。import logginglogger = logging.getLogger(__name__)logger.info("parsed list", extra={"url": response.url, "count": len(items)})logger.warning("empty detail", extra={"url": response.url})在 settings.py 里可以控制输出: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、各状态码数量,都能快速判断问题范围。你也可以在代码里自定义计数,例如空列表页、缺字段详情页、入库失败次数。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、手机号和完整地址。日志是排障材料,不应该变成新的安全风险。
服务端阅读 05月31日 01:07

Scrapy 性能优化该调哪些参数?并发、限速和 Pipeline 怎么取舍?

先说结论Scrapy 性能优化不是把并发调到最大,而是在目标站承受能力、网络延迟、本机资源、代理质量和数据写入速度之间找平衡。真正影响吞吐的通常有四块:下载并发、延迟与自动限速、重复请求与缓存、解析和 Pipeline 的耗时。只盯 CONCURRENT_REQUESTS 很容易误判,爬虫跑不快可能不是请求少,而是 DNS、代理、数据库写入或解析逻辑卡住了。可以先用一组保守配置跑基准,再逐步调高:CONCURRENT_REQUESTS = 32CONCURRENT_REQUESTS_PER_DOMAIN = 8DOWNLOAD_DELAY = 0.2RANDOMIZE_DOWNLOAD_DELAY = TrueAUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_START_DELAY = 0.5AUTOTHROTTLE_MAX_DELAY = 10AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0RETRY_TIMES = 2HTTPCACHE_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、队列或分布式调度。分布式会带来去重、状态一致性、任务切分和监控成本。小任务硬上分布式,最后往往是运维复杂度大于性能收益。
服务端阅读 05月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 可以通过命令行直接完成:scrapy crawl book -O output.jsonscrapy crawl book -o output.jlscrapy crawl book -O output.csv-O 会覆盖已有文件,适合定时任务每次生成完整结果;-o 会追加写入,适合临时补采,但也容易把重复数据混进去。线上任务里我更倾向显式配置 FEEDS,因为编码、字段顺序、是否覆盖、存储路径都能写清楚,避免不同同事用不同命令跑出不同文件。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 文件拆开,避免把测试路径带到生产。
服务端阅读 05月31日 00:57

Scrapy 请求失败后怎么重试?错误处理机制该怎么配?

Scrapy 的重试不是越多越好Scrapy 自带 RetryMiddleware,能处理连接超时、DNS 错误、部分 HTTP 状态码等失败场景。它的价值不是“保证每个请求都成功”,而是在短暂网络抖动、服务端偶发 500、代理临时不可用时给请求一次恢复机会。真正需要注意的是:重试会消耗队列、带宽和时间,配置不当还会把目标站的压力继续放大。常见配置如下:RETRY_ENABLED = TrueRETRY_TIMES = 2RETRY_HTTP_CODES = [408, 429, 500, 502, 503, 504, 522, 524]DOWNLOAD_TIMEOUT = 15RETRY_PRIORITY_ADJUST = -1RETRY_TIMES=2 表示失败后最多再试 2 次,不是总共请求 2 次。RETRY_PRIORITY_ADJUST=-1 会让重试请求优先级略降低,避免失败请求一直插队。429 通常代表限流,是否重试要看目标站策略;如果没有退避,只是马上重发,可能更快被封。对业务可预期的失败,最好配合 errback 单独处理。比如详情页失败时记录 URL、来源页和错误类型,后续可以补采,而不是只靠日志里一行 traceback。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 持续上升,就该降速或停止任务排查。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 和告警。重试能提高完整率,但它不是反爬、权限和页面不存在的解药。先识别失败类型,再决定是否重试,爬虫才会稳定。
服务端阅读 05月31日 00:57

Scrapy 项目怎么写才更稳定?有哪些最佳实践?

Scrapy 最佳实践先从边界开始Scrapy 项目最怕一开始跑得很快,过两周却没人敢维护。稳定的爬虫不是靠把并发拉满,而是把抓取边界、请求节奏、数据结构和失败处理提前定好。尤其是面向线上站点时,robots.txt、下载延迟、并发数和重试策略不是装饰配置,它们决定项目能不能长期运行。一个比较稳的起步配置可以这样写:ROBOTSTXT_OBEY = TrueCONCURRENT_REQUESTS = 16CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 0.5AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_TARGET_CONCURRENCY = 2.0LOG_LEVEL = "INFO"USER_AGENT = "mycrawler/1.0 (+contact@example.com)"这里没有万能数字。新闻站、文档站、电商站承压能力完全不同,最佳实践不是照抄参数,而是先用小流量观察响应时间、错误码和封禁情况,再逐步调大。很多“爬虫不稳定”的问题,其实是没有灰度过程。数据层也要尽早规范。Item 字段最好明确含义,pipeline 负责校验、去重和入库,spider 只做页面解析。把清洗逻辑写满 spider 的短期效率很高,但字段一多,后面改一次规则要翻十几个回调函数。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 最佳实践不是一组漂亮配置,而是一套工程习惯:尊重目标站、控制节奏、拆清职责、记录关键指标、先小流量验证。只要这些基础稳住,后面无论加代理、分布式还是监控,都不会把项目推向不可维护。
服务端阅读 05月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 项目的“事件监听器”:平时不参与下载链路,等事件发生时再做自己的事。from scrapy import signalsclass 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 文件里,否则复用和测试都很麻烦。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 处理数据,这个分工通常最省心。
服务端阅读 05月31日 00:57

Scrapy CrawlSpider 适合爬哪些网站?

直接答案CrawlSpider 是 Scrapy 里用规则自动跟链接的 Spider,适合网站结构清楚、链接规律稳定、需要从列表页一路爬到详情页的场景。它的核心是 Rule 和 LinkExtractor:前者定义“哪些链接要跟、用哪个回调处理”,后者负责从页面里提取符合条件的链接。普通 Spider 更像你手写路线图,CrawlSpider 更像给爬虫装了导航规则。它能减少重复代码,但也更容易因为规则写得太宽,把不该爬的登录页、搜索页、标签页甚至日历页一起卷进去。from scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Ruleclass 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 渲染内容,也要决定抓接口还是接浏览器。把这些能力拆开设计,规则负责找路,回调负责解析,中间件负责通用请求问题,项目会更容易维护。
服务端阅读 05月31日 00:57

Scrapy 遇到 JavaScript 动态网页怎么办?

直接答案Scrapy 本身不会执行 JavaScript,它拿到的是服务器直接返回的 HTML。遇到动态网页时,第一步不是立刻上 Selenium 或 Playwright,而是打开浏览器开发者工具,找页面真正请求的数据接口。如果数据来自 XHR 或 Fetch,请优先用 Scrapy 直接请求接口;只有内容必须经过浏览器渲染、签名依赖运行时环境、或交互流程很重时,才把 Playwright、Selenium、Splash 接进来。这个取舍很重要,因为浏览器渲染的成本通常比普通 HTTP 请求高一个数量级。# settings.pyDOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",}TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"PLAYWRIGHT_BROWSER_TYPE = "chromium"# spider.pyyield 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,日志里区分普通请求和渲染请求。遇到超时、空页面、验证码时,不要无限重试浏览器请求,可以降级保存现场交给人工分析。浏览器池本身也要定期重启,否则长时间运行后可能出现句柄泄漏、缓存膨胀和页面上下文污染。
服务端阅读 05月31日 00:57

Scrapy 请求去重是怎么判断重复的?

直接答案Scrapy 的请求去重由调度器调用 dupefilter 完成,默认实现是 RFPDupeFilter。它会为请求生成 fingerprint,通常由规范化后的 URL、请求方法、请求体组成;指纹已经出现过,就认为这个请求重复,不再入队。它解决的是“同一个请求不要重复抓”,不是“同一条业务数据不要重复入库”。所以列表页、详情页、翻页链接能靠它减少浪费,但商品 ID、文章 ID、用户 ID 的业务级去重,还应该放在 pipeline 或存储层。# settings.pyDUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter"DUPEFILTER_DEBUG = FalseJOBDIR = ".job/article_spider" # 需要断点续爬时启用# spider.pyyield 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 共享已抓请求集合。边界是维护成本:规则越业务化,越容易在目标站改版后误杀请求,所以要给命中去重的样本留日志。from scrapy.dupefilters import RFPDupeFilterfrom w3lib.url import canonicalize_urlclass 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 做回归测试。
服务端阅读 05月31日 00:57

Scrapy 如何处理 Cookies 和多会话登录?

直接答案Scrapy 默认会通过 CookiesMiddleware 维护 cookie:同一个会话里的响应 Set-Cookie 会被保存,后续请求会自动带上。登录类爬虫通常用 FormRequest 先提交账号密码,再把登录后的请求接在回调里;如果要同时爬多个账号、多个店铺或多个地区,就用 meta['cookiejar'] 隔离会话。真正容易出错的地方不是“能不能带 cookie”,而是 cookie 什么时候该让 Scrapy 管、什么时候该你自己管。手动在 headers 里塞 Cookie 看起来快,但会绕开 Scrapy 的 cookie 合并逻辑,后续重定向、刷新 token、跨域跳转都可能变乱。import scrapyclass 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 当成密码处理,权限、加密和清理策略都不能省。
服务端阅读 05月31日 00:56

Scrapy settings.py 里哪些配置最该优先调整?

Scrapy 的 settings.py 决定爬虫速度、稳定性、反爬风险和数据质量。新项目最该优先调整并发、延迟、超时、重试、请求头、robots、日志、pipeline、middleware 和环境覆盖方式。不要一开始就追求最快,先让目标站、代理池、数据库和自己机器都扛得住。速度可以逐步加,封禁和脏数据一旦出现,排查成本会高很多。基础配置先保持清楚BOT_NAME、SPIDER_MODULES、NEWSPIDER_MODULE 通常由项目生成,但要和部署项目名一致。Scrapyd 日志、任务和发布记录都会反复出现这些名字,命名混乱会让排查变难。ROBOTSTXT_OBEY 默认建议开启,是否关闭要看授权、目标站条款和数据用途。BOT_NAME = "news_crawler"SPIDER_MODULES = ["news_crawler.spiders"]NEWSPIDER_MODULE = "news_crawler.spiders"ROBOTSTXT_OBEY = True并发和延迟决定稳定性最常见的事故是把 CONCURRENT_REQUESTS 开太高,又把 DOWNLOAD_DELAY 设为 0。建议先用保守配置跑基线,观察状态码、平均延迟、item 数和失败率,再逐步调高。AutoThrottle 适合响应波动明显的网站,但它不是反爬万能药。CONCURRENT_REQUESTS = 16CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 0.5RANDOMIZE_DOWNLOAD_DELAY = TrueDOWNLOAD_TIMEOUT = 20AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_MAX_DELAY = 10请求头、Cookie、重试分场景配置普通静态页面通常设置合理 UA 即可;登录态、地区化或个性化页面才可能需要 Cookie。Cookie 一旦过期,爬虫可能不报错,却一直抓登录页。重试也要控制,网络错误和 5xx 适合重试,403、验证码和参数错误不适合反复撞。USER_AGENT = "Mozilla/5.0 (compatible; ResearchBot/1.0)"COOKIES_ENABLED = FalseRETRY_ENABLED = TrueRETRY_TIMES = 2RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408]Pipeline 和 Middleware 别乱塞ITEM_PIPELINES 适合清洗、校验、去重和入库,DOWNLOADER_MIDDLEWARES 适合代理、请求头、限速和异常处理。优先级数字越小越先执行,顺序错了会导致代理没生效或脏数据先入库。Spider 负责页面解析,通用能力尽量沉到 pipeline 和 middleware。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 只是省事,却容易提交到仓库。边界是本地可以提供示例值,生产值必须外部注入。日志里也不要把这些值打印出来。
服务端阅读 05月31日 00:56

Scrapy 爬虫运行中如何监控和定位问题?

Scrapy 监控要回答三个问题:爬虫是否还活着,数据产出是否正常,异常卡在哪一步。只看进程状态不够,因为进程可能还在跑,却一直拿到 403、验证码、空页面或登录页。生产环境至少要看请求量、状态码、失败率、item 数、入库数、运行耗时、队列积压和内存占用。指标少一点没关系,但每个指标都要能指导动作。先用 Stats 建基础盘Scrapy 自带 Stats Collector,会记录请求、响应、重试、异常和 item 数。最小成本的做法是在爬虫结束时把关键指标写进日志或监控系统。item_scraped_count 看产出,downloader/response_status_count/403 看封禁,downloader/exception_type_count/* 看网络异常。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、手机号直接打到日志里,集中日志平台会放大泄露风险。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 和入库。curl http://127.0.0.1:6800/listjobs.json?project=news_crawlercurl 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、版本、任务参数和日志位置。踩坑是只告警失败不告警恢复,大家不知道问题是否结束。