面试题手册

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

服务端阅读 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、版本、任务参数和日志位置。踩坑是只告警失败不告警恢复,大家不知道问题是否结束。
服务端阅读 05月31日 00:56

Scrapy 项目上线后如何部署和管理爬虫?

Scrapy 上线不要只把代码丢到服务器然后执行 scrapy crawl。更稳的做法是先固定依赖、配置、日志和启动入口,再交给 Scrapyd、systemd、Supervisor 或 Docker 管理。小项目用 Scrapyd 发布和调度很快,团队项目更适合 Docker 加 CI/CD;如果任务很多,还要补上队列、监控、告警和回滚。部署的重点不是工具越多越好,而是出问题时能定位、能停止、能恢复。上线前先固定运行环境先确认项目能用同一套命令复现运行结果。依赖写进 requirements.txt 或 pyproject.toml,生产参数放环境变量,不要在服务器上临时安装和手改配置。发布前至少跑一次小范围任务,检查请求、解析、入库和日志路径。pip install scrapyd scrapyd-clientscrapyd-deploy default -p news_crawlercurl http://127.0.0.1:6800/schedule.json -d project=news_crawler -d spider=articleScrapyd 适合轻量管理Scrapyd 能发布项目、启动爬虫、查看任务、取消任务和读取日志,适合单机或少量机器。它的边界也明显:它不是完整调度平台,不负责复杂依赖、资源隔离和跨机器统一排队。生产环境里建议只放内网,不要把 6800 端口暴露到公网。任务参数可以通过 API 传入,但 Token、Cookie、数据库密码不要明文塞进调度参数。Docker 解决环境漂移如果经常出现“本地能跑,服务器不能跑”,Docker 更合适。镜像里固定 Python、系统库、项目依赖和启动命令,服务器只负责拉镜像并注入环境变量。代价是镜像构建、日志采集和资源限制要额外配置,特别是用 Playwright、Selenium、lxml 时要提前处理系统依赖。FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .CMD ["scrapy", "crawl", "article", "-s", "LOG_LEVEL=INFO"]进程管理和回滚不能省裸机部署可以用 systemd 或 Supervisor。它们能守护进程,但不要无脑自动重启;如果目标站返回大量 403,重启只会制造更多封禁和脏日志。每次发布保留上一个 egg、镜像 tag 或 release 目录,一次只改一个变量,比如只改代码、只改配置或只换代理池。追问Scrapyd 和 Docker 该怎么选?Scrapyd 更像 Scrapy 的轻量运行面板,适合快速发布、启动和停止爬虫。Docker 更强调环境一致,适合多人协作和依赖复杂的项目。两者可以组合:Scrapyd 跑在 Docker 里。坑是 Docker 不会自动解决调度、重试和限速问题。多台机器怎么管理版本?每次发布生成唯一版本号,比如 Git commit、构建编号或镜像 tag。调度记录 spider、参数、版本和机器,异常数据才能追到来源。不要每台机器手工拉代码,时间一长必然版本不一致。小团队可以先脚本同步,但多人发布要接 CI/CD。线上爬虫要自动重启吗?可以,但要限制条件和次数。临时网络失败适合重启,登录态失效、403 激增和代码异常不适合无限重启。告警里要写清失败原因和日志位置。踩坑最多的是把所有异常交给进程管理器,最后越重启封得越快。哪些配置不能写死?代理、数据库密码、Cookie、Token、运行环境和并发阈值都不该写死。它们应该来自环境变量、密钥系统或部署平台配置。非敏感默认值可以留在 settings.py。生产参数一旦写进代码,泄露和回滚都会很麻烦。如何判断部署成功?不要只看进程存在,要看任务被调度、请求成功、Item 入库、错误率正常。发布后可以跑一个测试 URL 或小时间窗口。成功标准最好脚本化,避免每次人工翻日志。Scrapy 常见坑是启动成功但没有数据,这比直接崩溃更隐蔽。
计算机基础阅读 05月31日 00:26

TCP 为什么需要三次握手?两次或四次不行吗?

TCP 三次握手的重点不是“发了三次包”,而是让双方确认三件事:对方能收、对方能发、双方的初始序列号都同步了。客户端先发 SYN,带上自己的 seq=x;服务端回 SYN+ACK,确认 x 并给出 seq=y;客户端再回 ACK,确认 y。到这一步,双方才有足够信息进入 ESTABLISHED,后续字节流才能可靠编号和确认。三次握手的过程怎么走?文字时序图可以这样看:客户端说“我要连,seq=x”;服务端说“收到 x,我这边 seq=y”;客户端再说“收到 y”。第一次后客户端进入 SYNSENT,第二次后服务端进入 SYNRCVD,第三次到达后双方进入 ESTABLISHED。ack=x+1、ack=y+1 不是随便加 1,而是 SYN 本身会占用一个序列号。为什么两次握手不够?两次握手的问题是服务端无法确认客户端是否收到了自己的 SYN+ACK。假设网络里一个过期 SYN 延迟到达服务端,如果两次就建立连接,服务端可能误分配资源,而客户端根本没有这次连接意图。第三次 ACK 能让服务端确认“客户端确实收到了我的序列号”。取舍是多半个往返,但换来更明确的状态确认。为什么不是四次握手?四次也能完成目标,只是没必要。服务端本可以先 ACK 客户端序列号,再单独发 SYN;TCP 把这两步合并成 SYN+ACK,少发一个报文。三次已经完成双方序列号同步和收发能力确认,再拆成四次只会增加成本。第三次 ACK 丢了会怎样?客户端通常已经认为连接建立,可以继续发送数据。服务端还停在 SYN_RCVD,等待 ACK 或重传 SYN+ACK;如果客户端后续数据包带 ACK,服务端也可能借此完成建连。边界是客户端不再发任何数据,服务端会超时重传,最后释放半连接资源。三次握手和 SYN Flood 有什么关系?服务端收到 SYN 后需要保存半连接状态,SYN Flood 就利用了这点。攻击者大量发 SYN 却不完成第三次 ACK,半连接队列可能被占满。防御常见做法包括 SYN Cookies、调大半连接队列、缩短重试、限流和流量清洗;取舍是队列调大会耗内存,SYN Cookies 也可能影响部分 TCP 选项能力。追问三次握手能证明应用层一定可用吗?不能,它只能证明传输层基本收发链路是通的。握手成功后,TLS、鉴权、线程池、数据库都可能继续失败。排查时要分清边界,“端口通”不等于“接口健康”,很多慢请求卡在握手之后。初始序列号为什么要随机?随机初始序列号能降低旧报文混入新连接的概率,也增加伪造 TCP 报文的难度。早期序列号可预测时,攻击者更容易猜中合法窗口。代价是双方必须在握手阶段交换序列号,抓包工具显示相对序列号只是为了方便阅读。服务端收到 SYN 后马上分配资源,会不会有风险?有风险,所以内核会限制半连接队列并设置超时重传。SYN Flood 正是利用这个窗口消耗资源。工程上不能只调 backlog,还要看 somaxconn、tcpmaxsyn_backlog、SYN Cookies 和应用 accept 速度。握手协商的参数会影响后续传输吗?会,MSS、窗口扩大、SACK、时间戳通常都在握手阶段协商。窗口扩大失败可能让高 RTT 链路吞吐上不去,SACK 不可用会降低多包丢失后的恢复效率。排查慢连接时,不能只看握手成功,还要看握手里协商出了什么能力。
计算机基础阅读 05月31日 00:26

TCP 首部有哪些关键字段?它们分别解决什么问题?

TCP 首部不是一串要死记的字段,而是 TCP 可靠传输的控制面板。端口决定数据交给哪个进程,序列号和确认号让字节流不乱,标志位表达连接状态,窗口、校验和、选项分别处理流量控制、差错检测和能力协商。最小首部 20 字节,带选项最多 60 字节;抓包时真正有用的不是背出字段名,而是看这些字段是否在按预期变化。TCP 首部整体长什么样?文字图示可以这样记:源端口和目的端口各 16 位,后面是 32 位序列号、32 位确认号,再往后是数据偏移、保留位、控制标志、窗口大小、校验和、紧急指针,最后才是可变长选项和数据。数据偏移说明首部在哪里结束,因为 MSS、窗口扩大、时间戳、SACK 等选项会改变首部长度。哪些字段负责定位和可靠性?源端口、目的端口配合源 IP、目的 IP,组成一条连接的四元组。IP 只能送到主机,TCP 还要把数据送到具体应用,例如浏览器临时端口连到服务器 443 端口。NAT、代理、容器网络可能改写端口,所以抓包端口不一定等于应用配置端口。序列号表示本段第一个数据字节的位置,确认号表示“下一步希望收到哪个字节”。ack=1001 通常代表 1001 之前都收到了,这是累积确认。SYN 和 FIN 也各占一个序列号,手算握手、挥手时这里最容易错。标志位、窗口和选项怎么影响排查?SYN 用于建立连接,ACK 表示确认号有效,FIN 是正常关闭,RST 更像异常中止。PSH 不等于强制立刻发送,URG 在现代应用里很少依赖。看到连接断开时,先区分 FIN 还是 RST,前者多是正常收尾,后者常见于端口未监听、应用拒绝或中间设备清理状态。窗口大小告诉对端还能接收多少数据,是流量控制的核心。原始窗口只有 16 位,高带宽高延迟链路通常要靠窗口扩大选项。校验和能发现传输损坏,但不是安全机制;MSS、SACK、时间戳这些选项,则会影响分段大小、丢包恢复和 RTT 估算。追问为什么 TCP 首部最小 20 字节、最大 60 字节?数据偏移字段只有 4 位,单位是 32 位字。最小值通常是 5,也就是 20 字节;最大值是 15,也就是 60 字节。边界在于 TCP 选项最多只有 40 字节,MSS、SACK、时间戳、窗口扩大都要在这里取舍。序列号为什么按字节编号,而不是按报文编号?TCP 给应用层的是连续字节流,不保留消息边界。按字节编号后,拆包、合包、乱序到达都能重新拼回正确顺序。代价是应用如果需要消息边界,必须自己加长度字段或分隔符,很多“粘包”问题就踩在这里。窗口大小是不是越大越好?不是,窗口太小会限制吞吐,窗口太大也可能让数据在接收端或链路上堆积。流量控制看接收缓冲区,拥塞控制看网络承载能力,两者不能混为一谈。调优要结合 RTT、带宽、丢包率和应用消费速度,而不是盲目把窗口调大。抓包时先看哪些字段最有用?建连先看 SYN、SYN+ACK、ACK,以及 MSS、SACK、窗口扩大是否协商成功。传输异常看 seq、ack、窗口、重复 ACK、重传和 RST。常见踩坑是只看服务端日志,以为服务慢;抓包才发现接收窗口归零,瓶颈其实在客户端消费太慢。
服务端阅读 05月31日 00:26

MQTT 是什么?它的核心特点和工作原理是什么?

MQTT 是一种基于 TCP 的轻量级消息协议,最常见于物联网设备、移动推送和实时状态同步。它的核心不是“像 HTTP 一样请求接口”,而是通过 Broker 做发布/订阅:设备把消息发布到主题,其他客户端订阅主题后由 Broker 推送消息。这个模式让设备不必知道彼此地址,也能在弱网、低带宽和大量连接场景下稳定通信。MQTT 为什么适合物联网?第一个特点是轻量。MQTT 固定头部最小只有 2 字节,比 HTTP 一大串 header 更省流量。对电池供电设备来说,少发一点数据、少建立几次连接,都会影响续航。它还通过 Keep Alive 维持长连接,Broker 可以主动把消息推给客户端,不需要客户端频繁轮询。第二个特点是发布/订阅。发布者只把消息发到 topic,例如 factory/line1/motor/temperature,订阅者通过主题过滤器接收自己关心的消息。Broker 负责连接管理、主题匹配、消息分发和 QoS 状态。这个设计天然支持一对多,比如一台设备上报状态后,监控系统、告警系统和数据存储服务都可以同时收到。第三个特点是可靠性交给 QoS 分级处理。QoS 0 最快但可能丢,QoS 1 保证至少到达但可能重复,QoS 2 尽量做到恰好一次但成本最高。实际项目里通常不是全部用最高级别,而是遥测用 QoS 0,告警和状态变更用 QoS 1,极少数关键命令才考虑 QoS 2。MQTT 的工作流程可以概括为四步:客户端 CONNECT 到 Broker,订阅者 SUBSCRIBE 主题,发布者 PUBLISH 消息,Broker 根据订阅关系转发。断线时,Broker 可以通过遗嘱消息通知其他系统;重连时,持久会话可以恢复订阅和未确认消息。它看起来简单,但真正上线时要同时考虑主题设计、认证授权、离线消息上限和消息幂等。它也不是所有实时通信的默认答案。浏览器前端更常用 WebSocket,服务端内部任务分发可能更适合 Kafka 或 RabbitMQ,MQTT 的强项是大量客户端长连接和主题路由。判断是否使用 MQTT,可以先问三个问题:设备是否经常在线保持连接、消息是否需要按主题推送、网络和功耗是否敏感。如果答案都是否定,HTTP 可能更简单。mosquitto_sub -h test.mosquitto.org -t 'levenx/demo'mosquitto_pub -h test.mosquitto.org -t 'levenx/demo' -m 'hello mqtt'初学者可以用公开测试 Broker 验证协议概念,但不要把它当作生产样板。生产 Broker 要考虑账号隔离、TLS 证书、ACL、限流、日志和监控,还要有备份和升级方案。MQTT 的入门门槛低,真正难的是长期稳定运行。越早把这些工程约束放进设计,后面越少返工。追问MQTT 和 HTTP 最大区别是什么?HTTP 主要是请求/响应,客户端问一次,服务器答一次。MQTT 是长连接加发布/订阅,Broker 可以在有消息时主动推给订阅者。取舍很明显:配置查询、文件上传、管理后台接口适合 HTTP;设备状态、实时告警和低带宽上报更适合 MQTT。很多系统会混用,别试图用一个协议解决所有问题。Broker 是不是单点?逻辑上 Broker 是中心节点,所以单机 Broker 确实可能成为单点。生产环境可以用集群、负载均衡和客户端自动重连降低风险。边界在于 MQTT 长连接有会话状态,故障切换不像无状态 HTTP 那么简单。要验证 Broker 高可用,必须实际测试节点宕机、网络抖动和客户端重连后的消息表现。MQTT 基于 TCP,为什么还需要 QoS?TCP 只能保证一条连接上的字节流可靠、有序,不能保证应用消息在断线、重连、Broker 转发和订阅者离线时符合业务预期。MQTT QoS 是应用层的交付语义,用来处理确认、重传和重复问题。踩坑点是以为 TCP 可靠就等于业务可靠,结果设备掉线时消息丢了还不知道。QoS 要和持久会话、离线队列、业务幂等一起看。MQTT 适合传大文件吗?不适合。MQTT 更适合小消息、高频状态和控制指令,大文件会占用 Broker 内存、网络和队列资源。文件上传、固件下载更适合 HTTP、对象存储或专门的 OTA 通道。实际取舍是:MQTT 可以发文件地址、版本号和下载指令,但不要把固件二进制直接塞进 MQTT payload。新手接入 MQTT 最容易忽略什么?最容易忽略主题规范和安全配置。刚开始大家会用 test/#、匿名连接和公网 1883 调试,跑通很快,但上线后权限和排障都很痛苦。另一个坑是没有给消息设计唯一 ID,遇到 QoS 1 重复投递时无法去重。把 Client ID、topic、QoS、ACL 和日志字段提前约定好,比后期补救省很多时间。
服务端阅读 05月31日 00:26

MQTT QoS 0、1、2 有什么区别?实际项目该怎么选?

MQTT QoS 解决的是“消息交付可靠性和成本怎么平衡”的问题。QoS 0 是最多一次,速度最快但可能丢;QoS 1 是至少一次,能保证到达但可能重复;QoS 2 是恰好一次,流程最完整但开销也最大。实际项目里不是 QoS 越高越好,而是要看消息丢失、重复和延迟哪个代价更高。三种 QoS 的核心区别QoS 0 只有一条 PUBLISH,没有确认报文。发布者把消息发出去就算完成,网络抖动、客户端断开、Broker 繁忙都可能导致消息丢失。它适合高频遥测,比如温度、湿度、定位点,因为下一条数据很快会覆盖上一条。用 QoS 0 的好处是吞吐高、延迟低、设备耗电少。QoS 1 会多一个 PUBACK。发布者发送 PUBLISH 后等待确认,如果没收到 PUBACK,就会重发。这样能提高送达概率,但接收方可能收到重复消息,所以业务处理必须幂等。比如告警消息、状态变更、日志上报一般可以用 QoS 1,但要给消息带上 messageId 或事件编号。QoS 2 使用 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步握手。它的目标是避免重复交付,适合不能重复执行的关键指令。问题是每条消息要更多报文和状态,延迟、内存和磁盘成本都会增加。很多系统口头说“必须恰好一次”,最后真正需要 QoS 2 的消息其实很少。还要注意一个边界:MQTT 的 QoS 是客户端和 Broker 之间的交付保证,不等于你的业务端到端一定成功。Broker 收到了消息,不代表后端数据库写入成功;订阅者收到了消息,不代表业务处理完成。真正关键的业务还要在 payload 里设计业务流水号、状态机和补偿机制。QoS 还会影响设备功耗和 Broker 资源。移动网络或卫星链路下,QoS 1 的重传可能让设备反复唤醒无线模块,电池消耗会明显上升。Broker 侧也要保存未确认消息,连接越多、离线越久,堆积风险越大。所以 QoS 选择应该按主题分级,而不是全局一刀切。import paho.mqtt.client as mqttclient = mqtt.Client(client_id='device-001')client.connect('broker.example.com', 1883, 60)client.publish('device/001/event', '{"id":"evt-1001"}', qos=1)client.loop(timeout=1.0)client.disconnect()一个比较稳妥的策略是按业务主题设置默认 QoS。状态心跳、实时位置用 QoS 0,告警、配置结果用 QoS 1,涉及扣费、开锁、停机这类强约束命令再评估 QoS 2。即便使用 QoS 2,也不要省掉业务回执,因为设备收到命令和执行成功是两件事。把这两层分清,问题定位会容易很多。追问QoS 1 为什么会重复?QoS 1 的确认依赖 PUBACK,如果发布者发出了消息但没有收到确认,它只能假设消息没成功,然后重发。问题是接收方可能已经收到并处理了第一条,只是确认包在路上丢了。这个重复不是协议 bug,而是“至少一次”语义的必然代价。实际项目要用业务 ID 做去重,不能假设 QoS 1 永远只到一次。QoS 2 能不能保证业务绝对不重复?不能把协议层的恰好一次理解成业务层绝对不重复。QoS 2 主要保证 MQTT 报文交付过程不重复,但业务服务处理消息后可能重启、数据库提交可能超时、下游接口也可能重试。边界在于它管的是客户端到 Broker、Broker 到订阅者这段链路。关键业务仍然需要幂等写入和状态校验。为什么很多物联网数据用 QoS 0?传感器数据通常是连续上报的,单点丢失不会影响整体趋势。比如温度每 5 秒上报一次,丢一条比排队重传更能接受,因为旧数据很快失去价值。QoS 0 的优势是低延迟、低带宽、低功耗,适合电池设备。取舍是你要接受偶发丢包,并在服务端用时间窗口判断设备是否异常。控制命令应该用 QoS 2 吗?不一定。开灯、重启设备、下发配置这类命令更常用 QoS 1 加业务幂等,因为重复执行可以通过 commandId 防住。QoS 2 更适合重复执行会造成严重后果、且设备和 Broker 都能承受额外状态的场景。踩坑点是只提高 QoS,却没有处理命令超时、设备离线和执行回执。命令可靠性通常要靠 QoS、业务 ACK、超时重试一起完成。订阅 QoS 和发布 QoS 不一样会怎样?最终投递给订阅者的 QoS 通常取发布 QoS 和订阅 QoS 中较低的那个。比如发布者用 QoS 1,订阅者只订阅 QoS 0,Broker 会按 QoS 0 投递给它。这个规则容易被忽略,导致发布端以为消息可靠,消费端实际没有确认。排查时要同时看发布代码、订阅代码和 Broker 日志,不能只看一端配置。
服务端阅读 05月31日 00:26

MQTT 发布订阅是怎么工作的?主题、通配符和 Broker 怎么配合?

MQTT 的发布/订阅模式可以理解成“发消息的人不找具体接收者,只把消息交给主题;想要消息的人订阅主题”。发布者只负责把 payload 发到 topic,订阅者只声明自己关心哪些 topic,中间的 Broker 负责匹配和分发。这个设计把生产者和消费者解耦了,所以很适合设备多、上下线频繁、消息一对多的物联网场景。一条消息是怎么走的?流程并不复杂。订阅者先连接 Broker,然后发送 SUBSCRIBE,例如订阅 home/+/temperature。传感器作为发布者把温度发到 home/livingroom/temperature,Broker 发现这个主题匹配订阅规则,就把消息推给订阅者。发布者并不知道谁收到了消息,订阅者也不需要知道消息来自哪台设备,双方只通过 topic 间接关联。Topic 是 MQTT 路由的核心。它是用斜杠分隔的层级字符串,比如 tenant/a/device/001/status。主题区分大小写,Home 和 home 是两个主题。设计主题时不要只考虑今天的功能,还要考虑权限、统计、扩展和排障,否则后面 ACL 和数据分析都会很难做。通配符让订阅变得灵活。+ 匹配单层,例如 home/+/temperature 可以匹配客厅和卧室温度。# 匹配多层,但只能放在末尾,例如 home/# 能收到 home 下所有消息。通配符很方便,也很危险,生产环境要避免业务客户端随便订阅大范围主题。发布订阅不是消息队列的简单替代品。MQTT 更强调实时推送和连接管理,消息是否离线保存取决于会话、QoS 和 Broker 配置。多个订阅者订阅同一主题时,默认每个订阅者都会收到一份消息;如果想做负载均衡,需要使用共享订阅,例如 MQTT 5 常见的 $share/group/sensor/#。真实项目里还要区分“状态”和“事件”。状态可以用 retained message 保留最后一条,比如设备在线状态;事件则应该进入后端存储,比如告警流水和操作记录。把两者混在一起会出问题:新客户端上线后拿到一条 retained 告警,可能误以为刚刚发生。主题命名和 payload 里最好明确消息类型。mosquitto_sub -h localhost -t 'home/+/temperature'mosquitto_pub -h localhost -t 'home/livingroom/temperature' -m '25.6'mosquitto_pub -h localhost -t 'home/kitchen/humidity' -m '60%'发布订阅还有一个好处是便于旁路扩展。原来只有监控服务订阅设备状态,后来新增告警、数据清洗或调试工具,只要再订阅同一类主题即可,不需要改发布端代码。不过这也带来治理问题:谁订阅了什么、是否还在消费、是否造成重复处理,都需要有可观测性。Broker 侧的订阅列表和消费延迟应纳入日常排查。追问发布者和订阅者真的完全不知道彼此吗?协议层面是解耦的,发布者不需要保存订阅者列表,订阅者也不直接连接发布者。业务层面通常还是会约定 payload 格式、主题命名和设备身份,否则收到消息也不知道怎么处理。取舍在于灵活性和治理成本:解耦让扩展容易,但主题规范一旦缺失,系统会变成没人敢改的消息网。主题应该怎么设计才不容易后悔?建议把租户、产品、设备和方向放进主题,例如 tenant/{tid}/product/{pid}/device/{id}/up。这样 ACL 可以按路径限制,日志也容易按设备定位。不要把大量业务字段塞进 topic,比如温度值、时间戳应该放 payload,不该放主题。边界是 topic 适合做路由维度,不适合承载所有数据维度。+ 和 # 通配符有什么坑?+ 只匹配一层,# 匹配多层并且只能出现在末尾,这两个规则经常被写错。订阅 home/# 会收到 home 下几乎所有消息,调试时方便,生产里可能造成流量暴涨。还有一个坑是 ACL 放开了通配符订阅,普通设备就可能读到别人的数据。通配符应该更多给平台服务用,终端设备尽量订阅精确主题。发布订阅和点对点消息有什么区别?点对点模式通常知道明确接收者,消息只交给一个目标。MQTT 发布订阅默认是一对多,任何匹配订阅的客户端都能收到消息。它适合状态广播、设备上报、告警通知,不适合需要严格单消费者处理的任务队列。需要负载均衡消费时,可以用共享订阅,但仍要处理重复投递和消费幂等。为什么订阅后才收到消息?以前的消息去哪了?普通订阅只接收订阅建立之后的新消息,之前发布的消息不会自动补发。想让新客户端上线就拿到最近状态,可以用 Retained Message;想让离线客户端恢复后收到消息,要使用持久会话和合适的 QoS。踩坑点是把 retained 当历史消息,它只保留每个主题最后一条。真正的历史查询应该从数据库查,而不是指望 Broker 保存全部消息。