ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python爬虫进阶:从Requests到Scrapy分布式架构实战指南

Python爬虫进阶:从Requests到Scrapy分布式架构实战指南 刚接手第一个正式的爬虫项目时我以为写几个requests.get()就能搞定。结果上线没两天日志里就铺满了exceeded retry limit, last status: 429 too many requests那一刻才意识到会写请求和能稳定抓数据中间隔着一条完整的进阶路线。所以这篇内容我从 Requests 讲起一路讲到 Scrapy 分布式架构把你可能踩的坑、需要补的原理、可以直接抄的配置都理一遍。不管是刚入门想系统建立知识体系还是已经用 Requests 写了不少脚本、想往工程化方向走这篇都适合你。我会尽量少说废话多讲真实项目里能落地的细节。1. Requests这一步值得走扎实很多人觉得 Requests 是爬虫里最简单的环节装上库、调get()、拿response.text三步就结束了。但真实线上环境里这三点恰恰是翻车率最高的地方。Requests 用不好后面换什么框架都是白搭因为你连最基本的“发一个能成功的请求”都没做到。1.1 用Session别让每次请求都在重新握手不使用Session的requests.get()每一次调用都会创建一个新的 TCP 连接完成 DNS 解析、TLS 握手、HTTP 请求、释放连接这整套流程。高频请求时大部分时间其实耗在连接建立上服务器那边也很容易把你识别成非人类行为。正确做法是先创建Session对象然后所有请求都通过它发import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) resp session.get(https://example.com/data, timeout(3.05, 10))Session内部默认使用连接池同一个 host 连接可以复用加上持久连接Keep-Alive机制后请求耗时和服务器压力都会明显下降。另一个隐藏好处是Session会自动维护 Cookie。有些站点的数据接口依赖登录态或者前置页种下的 Cookie如果你每次都新建裸请求怎么调都缺参数换成Session后就自然带上。1.2 超时和重试默认配置不是给你上生产用的requests.get()不传timeout程序会一直等。遇到慢响应或者服务器黑洞你的爬虫就挂在那一动不动整个任务被拖死。timeout参数建议传元组同时指定连接超时和读取超时前者是建立连接允许的时间后者是每个读数据块之间的最大间隔。我这里传的(3.05, 10)就是阿里巴巴规范里推荐的三秒连接、十秒读取。连接超时不要设太长现在网络环境三秒足够判断一个 IP 通不通了。重试这块初级写法是手动循环for attempt in range(5): try: resp session.get(url, timeout(3.05, 10)) resp.raise_for_status() break except requests.RequestException as e: if attempt 4: raise time.sleep(2 ** attempt)这种指数退避写法能用但不够优雅。实际上requests底层是urllib3它自带一套完整的重试机制通过HTTPAdapter挂载到 Session 上即可from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry Retry( total5, # 总重试次数 connect3, # 连接失败重试 read3, # 读取失败重试 status5, # 特定状态码重试 backoff_factor1.5, # 退避因子 status_forcelist[429, 500, 502, 503, 504], respect_retry_after_headerTrue, ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) session.mount(http://, adapter)这里有个关键参数status_forcelist[429, ...]。urllib3默认情况下不会对 429 做重试只有在Retry里显式告诉它哪些状态码需要重试它才认。backoff_factor的计算方式是{backoff_factor} * (2 ** (retry_number - 1))所以 1.5 意味着第一次重试等 0.75 秒第二次 1.5 秒第三次 3 秒以此类推。重试间隔太短等于没重试间隔太长任务被拖死1.5 是综合体验下来比较合适的值。1.3 告诉服务器“你是正经客户端”除了 UAHeaders 里还需要带上Accept、Accept-Language、Referer这些字段。真实浏览器发请求时这些字段一个都不会少服务器端做基础反爬时首先就看这些。很多站点如果发现Accept缺失或者语言设置不符合访问者地理位置会直接拒绝响应。至于代理 IP 池、Cookie 自动维护这些我建议先别急着上。一个站点如果连 UA 和基础 Headers 都伪装好了还在拦你再考虑代理池。很多爬虫新手从一开始就在“对抗反爬”这条路上投入大量精力结果自己连正常页面都没抓明白方向就跑偏了。2. 429 Too Many Requests先看懂它在告诉你什么429 可能是整个爬虫生涯里出现频率最高的状态码。它背后的含义其实很简单你请求太快服务器已经不想搭理你了。但真正有意思的是429 的出现往往意味着你的代码已经成功“打脸”了对方的基础防御对方不得不采用更粗暴的手段来限制你。2.1 429是怎么来的HTTP 429 的定义是“用户在给定的时间内发送了太多请求”服务器会在响应头里带一个Retry-After字段告诉你需要等多久HTTP/2 429 retry-after: 30很多开源限流组件比如 Spring Cloud Gateway 的 RequestRateLimiter、Nginx 的 limit_req都会在触发限制时返回这个状态码。如果服务器端对你的 IP 做了频率统计并且在短时间内检测到超过阈值的行为429 就会落在你头上。我见过有人问“我的代码明明设置了延时为什么还 429”原因一般有两种第一种是延时太短设置的 0.1 秒在服务器眼里就是没有延时第二种是并发数太高多线程或者协程同时发出请求虽然单个 QPS 不高但瞬时并发早就打爆了服务端的限制窗口。2.2 一次完整的排查链路我自己遇到过一次典型场景用 Requests 抓一个资讯站的文章列表刚开始很顺利抓了几千条之后突然开始大量报exceeded retry limit, last status: 429 too many requests。我的排查顺序是这样的你可以直接复用先确认问题是不是网络层面单独 curl 一下目标 URL观察是否能正常返回。如果 curl 也 429说明 IP 已经被限制需要等限流窗口过了再试。看响应头里有没有Retry-After如果服务器给出了明确的等待时间说明对方用的是标准的限流方案如果没有就自己按指数退避策略慢慢加大间隔。检查日志里的请求频率把最近一分钟内发出的请求数统计出来和服务器限流阈值做对比。这时候你大概率会发现要么是并发太高要么是延时设置根本没生效。试着一个请求接一个请求地串行抓取把并发全部去掉如果串行后不再 429说明问题出在并发模型上。串行还是 429再考虑 IP 是不是已经被“记名”了此时需要换出口 IP 或者加代理池。排查完整链路走完绝大多数 429 的根因都能定位出来。不要一看到 429 就怀疑是代理不够干净、UA 不够新先从自己的请求节奏找原因。2.3 限速与退避代码层面的应对解决 429 的正道是控制请求速率让服务器觉得你是个“匀速但有点慢”的用户。最简单的方案是固定延时比如每请求一次就 sleep 两秒。这种方式直白有效但缺陷也很明显如果目标服务器响应很快固定 sleep 会浪费大量时间如果响应慢sleep 又会导致整体任务拖得很长。更好的做法是在请求前根据上一轮请求的耗时动态调整import time import random class RateLimiter: def __init__(self, min_interval1.0, max_interval3.0): self.min_interval min_interval self.max_interval max_interval self.last_request_time 0.0 def wait(self): now time.time() elapsed now - self.last_request_time target_interval random.uniform(self.min_interval, self.max_interval) if elapsed target_interval: time.sleep(target_interval - elapsed) self.last_request_time time.time()这里面加入随机性是有讲究的。固定间隔会让服务器很快识别出你的访问规律一旦检测到同 IP 固定频率访问反而更容易触发限制。随机延时在 1 到 3 秒之间浮动更像真人浏览行为。遇到 429 后也不要在原地反复横跳应该立刻把当前请求退避重试。结合前面 Requests 里的Retry配置respect_retry_after_headerTrue这个参数会优先尊重服务器返回的等待时间服务器说让等 30 秒就等 30 秒不让服务器的话指数退避策略兜底。提示429 并不可怕可怕的是你把它当成随机故障继续高强度访问。服务器难得给了你一个明确的“慢下来”信号你不听下一步就是 IP 封锁或者封账号。3. 从Requests到Scrapy这一步到底跨了什么Requests 用熟练之后很多人会困惑我明明能用 Requests 加几个循环完成同样的事为什么要换 Scrapy我当时的感受是Requests 适合“一次性任务”和“小规模抓取”但当你要抓的站点从 1 个变成 10 个页面从 100 变成 100 万逻辑从线性变成可复用组件时Requests 脚本就会变成一团乱麻。3.1 框架不是Requests的替代品而是另一种处理问题的模型Scrapy 的底层也是 Requests不是。Scrapy 基于异步事件循环实现并发 IO底层用 Twisted 处理网络。它天然支持高并发不需要你手动开线程、处理线程安全、管理线程池这些问题框架都帮你兜住了。两者最大的差异在于组织方式。Requests 脚本是“目标导向”你写的是“做什么”Scrapy 是“框架导向”你被要求思考“怎么拆解”。Scrapy 把一个爬虫拆成五个部分Spider定义爬哪些 URL、从页面提取什么数据Engine控制数据流在各组件间流转Scheduler管理待爬取请求队列Downloader负责发送请求并获取响应Item Pipeline清洗、验证、存储数据核心的数据流可以概括为Spider 生成 Request → Scheduler 排队 → Downloader 发出下载 → 下载完成后回调 Spider 的 parse 方法 → 生成 Item 交给 Pipeline 或者继续生成新的 Request。这种“回调驱动”的写法让爬虫天生具备“爬完一页自动发现下一页”的能力不再需要你手写队列和循环。3.2 中间件才是Scrapy的灵魂如果说 Spider 是爬虫的脑子那中间件就是爬虫的手脚。每次请求出去和响应回来都会经过下载器中间件Downloader Middleware。你需要做的自定义工作90% 都在这个环节。最常见的需求是随机 User-Agent。不要在每一个 Request 里手动指定而是通过中间件统一处理# middlewares.py import random class RandomUserAgentMiddleware: def __init__(self, user_agents): self.user_agents user_agents classmethod def from_crawler(cls, crawler): return cls(crawler.settings.get(USER_AGENTS_LIST)) def process_request(self, request, spider): request.headers[User-Agent] random.choice(self.user_agents)# settings.py 对应配置 DOWNLOADER_MIDDLEWARES { myproject.middlewares.RandomUserAgentMiddleware: 400, scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, }中间件的数值越小越靠近引擎请求处理顺序越靠前。把自定义 UA 中间件设为 400同时禁用 Scrapy 自带的 UA 中间件这样自定义逻辑才能生效不会被默认逻辑覆盖。另外一个容易被忽略的坑是 Scrapy 默认的RetryMiddleware不会对 429 做重试。默认的重试状态码列表是[500, 502, 503, 504, 522, 524]429 不在里面。也就是说你遇到 429 时默认只会得到一次响应然后这条请求就被完全丢弃了。需要显式配置RETRY_HTTP_CODES [429, 500, 502, 503, 504, 522, 524]这行配置不加你前面在 Requests 里学的 429 重试经验在 Scrapy 里等于没用。3.3 几个一旦忽略就会吃亏的参数安装 Scrapy 很简单一句pip install scrapy就够。真正的坑都在settings.py。CONCURRENT_REQUESTS默认 16单机通常够用但如果目标网站响应慢且没有配置限速这个并发会发得太快导致 429 或封 IP。抓陌生站点建议先从 4 或 8 开始。DOWNLOAD_DELAY两个请求之间的最小间隔单位秒。设成 1 到 2配合随机化能极大降低被封概率。实测下来DOWNLOAD_DELAY1.5加RANDOMIZE_DOWNLOAD_DELAYTrue默认开启的组合是稳定性和效率比较均衡的配置。ROBOTSTXT_OBEY默认是 True遵守robots.txt。很多新手抓不到数据第一反应是代码出问题其实是这里没关掉或者目标站点的robots.txt拦住了路径。我个人建议是把这项设为 Falserobots.txt更多是网站的流量声明不是法律边界。但要不要关取决于你的场景和合规判断。LOG_LEVEL上线排查问题时改成INFO会让你看清每个请求的状态日常调试用DEBUG会刷屏但不影响功能。FEED_EXPORT_ENCODING导出 JSON 或 CSV 时不设utf-8容易乱码。直接写FEED_EXPORT_ENCODING utf-8最省心。Scrapy 的完整启动命令是scrapy startproject myproject scrapy genspider myspider example.com scrapy crawl myspider -O output.json-O参数在 Scrapy 2.x 中会用新格式导出不会覆盖已存在的文件比老版的-o更安全。4. 动态内容与iframePlaywright治标思路治本很多站点已经从传统的服务端渲染切到了前端框架Vue、React、Angular页面 DOM 完全由 JavaScript 在浏览器里动态生成。打开网页源码一片空白数据都在 XHR 异步请求里。这种时候你用 Requests 或 Scrapy 直接请求 URL拿到的 HTML 里根本不含数据。我见过不少团队在这个问题上走了弯路先是手动翻浏览器 Network 面板把 XHR 接口一个个抠出来模拟请求。有的站点做得简单数据接口能直接调通但一旦数据接口加了签名校验、参数加密这条路就堵死了。动态渲染方案反而是更省力的选择。4.1 什么时候必须上动态渲染判断标准很简单你用 Scrapy 或 Requests 直接请求目标 URL把返回的 HTML 保存下来搜索你想要的数据字段。搜不到而浏览器里能看到说明内容就是 JS 动态渲染的。此时有两个选择一是逆向分析接口请求的加密参数二是用无头浏览器渲染整个页面再抓取。接口逆向的上限高但对逆向能力要求也高而且站点改版后维护成本高。无头浏览器方案稳直接拿渲染完成后的 DOM代价是性能和资源占用更高。我的做法是优先尝试接口如果目标站点只是简单的前后端分离、请求不带加密参数直接模拟接口如果带加密参数再上无头浏览器。两套方案结合起来能覆盖绝大多数动态页面场景。4.2 Scrapy Playwright 的正确姿势Playwright 是目前比较推荐的无头浏览器库相比 Selenium 更快、更稳API 也更现代。Scrapy 有官方社区维护的scrapy-playwright插件安装后可以直接在 Scrapy 请求的meta里面开启浏览器渲染pip install scrapy playwright playwright install chromium# settings.py DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactorSpider 里这样写import scrapy class DynamicSpider(scrapy.Spider): name dynamic def start_requests(self): yield scrapy.Request( urlhttps://example.com/page, meta{playwright: True, playwright_include_page: True}, callbackself.parse, ) async def parse(self, response): page response.meta[playwright_page] # 等待某个关键节点加载完成 await page.wait_for_selector(div.result-content) # 获取完整渲染后的页面文本 content await page.content() await page.close() yield {url: response.url, content: content}注意这几点playwright_include_pageTrue会把 Playwright Page 对象放进meta必须用async def定义 parse 方法来操作。操作完 Page 一定要await page.close()否则浏览器资源没有被正确释放爬虫跑久了会把内存耗尽。await page.wait_for_selector()是渲染逻辑的核心。不要一打开页面就取内容而是等到你需要的 DOM 节点真正出现后再取值。很多动态渲染失败的问题本质是“页面还没加载完就取值”。4.3 iframe和异步加载的处理iframe 嵌套是动态页面里比较难处理的点。页面里的数据实际上在子 frame 里主框架 DOM 里什么都没有。Playwright 有专门定位 frame 的语法frame page.frame_locator(iframe#content) text await frame.locator(div#main-data).inner_text()frame_locator能直接穿透 iframe 定位元素比 Selenium 里反复切换switch_to.frame舒服得多。异步加载的场景除了wait_for_selector还可以等接口响应async with page.expect_response(**/api/get_data**) as response_info: await page.click(button#load-more) response await response_info.value这种写法可以精确抓到接口返回的数据如果后续流程要处理 JSON会比从 DOM 里抠文本效率高。4.4 Selenium还是Playwright我两个都用过最后项目统一换成了 Playwright。原因很实际Selenium 的 WebDriver 需要额外下载对应浏览器版本驱动版本不匹配时启动即失败Playwright 自带浏览器下载和管理首次安装后基本不会有版本兼容的烦恼。Playwright 还原生支持异步 API和 Scrapy 的异步引擎天然契合一个进程里可以并发跑多个浏览器上下文。Selenium 的优势在于生态老、资料多、历史代码兼容性好。如果你是维护老项目且团队没有迁移意愿继续用没问题新项目我从个人角度建议直接上 Playwright。不用纠结动手写几个页面就能感受到差距。注意无头浏览器的 UA、时区、字体等指纹特征和真实浏览器还是有差距。部分站点专门识别无头浏览器。遇到这种情况可以考虑用 Playwright 启动有头模式加上虚拟显示器如 Xvfb来规避但这就属于反爬对抗的更深处了前期一般用不到。5. 分布式架构先想清楚它解决什么问题很多人一听到“分布式爬虫”就觉得高大上还没跑到数据量需求就开始规划十台机器、Redis 集群和任务调度系统。但分布式不是装饰品它解决的是单机无法承担的问题如果你连单机还没压榨明白上分布式只会让排查问题的复杂度翻好几倍。5.1 伪需求与真需求先说伪需求。你的目标站点一天只有几万条数据单机 Scrapy 并发 8、延时 1 秒跑几个小时就能完成完全没有分布式的必要。引入分布式意味着要额外维护 Redis、处理机器间网络通联、统一日志和监控这些成本可能比爬虫本身还高。真正的需求长这样目标站点有上千万 URL即便单机高速跑也要连续几天才能完成或者你有多个数据源每个都需要保持实时更新一台机器带宽和内存已成瓶颈又或者你的去重集合大到单机内存放不下。这些情况下单机的“墙”已经碰头了才需要往分布式走。分布式爬虫要解决的核心问题有三个任务队列共享、去重集合共享、状态同步。把这三件事理清了分布式架构就有了骨架。5.2 Scrapy-Redis的设计逻辑Scrapy-Redis 是原生 Scrapy 的分布式扩展核心思路就是“用 Redis 替换 Scrapy 调度队列和去重集合的内存实现”。原生 Scrapy 的 Scheduler 是进程内队列DupeFilter 是内存集合这台机器爬到一半的数据另一台机器完全不知道。Scrapy-Redis 把这两块都搬到 Redis 里所有机器共享同一个待爬队列和去重集合谁空闲谁取任务天然实现了负载均衡。配置方式非常简洁# settings.py SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter SCHEDULER_PERSIST True REDIS_URL redis://10.0.0.5:6379/0SCHEDULER_PERSIST True表示爬虫结束后队列和去重集合会保留在 Redis 里下次启动可以基于上次的状态继续爬而不是从头来。这个特性在断点续爬场景里极其重要配合scrapy crawl spider启动命令相当于给爬虫加了“存档”。但 Scrapy-Redis 有一个需要留意的问题原生的scrapy-redis项目已经很久没有大版本更新了Python 3.9 和 Scrapy 2.x 下用起来偶尔会有小毛病。社区里有维护得比较活跃的分支比如scrapy-redis-blib带来了布隆过滤器支持如果你爬取的 URL 数量巨大、内存吃紧可以考虑直接用带布隆过滤器的版本。5.3 在分布式里去重指纹是个核心细节Scrapy 默认的去重逻辑是给每个 Request 生成一个指纹fingerprint指纹相同的请求视为重复。Scrapy 生成请求指纹时不是只取 URL而是综合了请求方法、URL、请求体、特定 Headers 等信息fp hashlib.sha1() fp.update(to_bytes(request.method)) fp.update(to_bytes(canonicalize_url(request.url))) fp.update(request.body or b) fp.update(to_bytes(request.callback.__qualname__))这个设计的目的是让不同参数的请求不被误判为重复。比如两个 URL 形式相同但 POST body 不同如果只按 URL 去重就会错误丢弃加上请求体和回调函数的限定指纹才能区分开不同业务场景下的“同 URL 不同请求”。理解了指纹的构成你就明白为什么分布式去重不能简单用一个集合存 URL而是要存完整的请求指纹。当 URL 量级达到千万级别时内存集合会迅速膨胀几十亿指纹根本放不进内存。此时要么用 Redis Set 的天然去重能力数据量大时注意内存要么引入布隆过滤器来压缩存储。布隆过滤器的思路是用少量内存加上多个哈希函数来“近似判断”元素是否存在。代价是有一定误判率可能把未爬取的 URL 误判为已爬取。但对爬虫场景来说少量漏抓完全可以接受换取的内存收益却非常可观。scrapy-redis-blib就是在这个场景下被选中的。5.4 独立实现分布式的思路如果你的需求超出了 Scrapy-Redis 的治理能力比如需要精细化任务优先级、动态分配 URL 给特定节点或者需要把任务下发与爬取逻辑彻底解耦那你可以考虑自己搭一套。一个比较轻量的方案是用 Redis 的List做任务队列redis_cli.lpush(crawler:request_queue, serialized_request_json)爬虫节点侧用阻塞弹出task redis_cli.brpop(crawler:request_queue, timeout0)brpop是阻塞式弹出队列为空时 worker 会一直等待而不会退出这样新增任务后节点能立刻感知。手动实现的好处是逻辑全部可控不受框架限制坏处是需要自己处理队列顺序、失败重投、任务确认这些边界情况。如果你已经在上游消息系统Kafka、RabbitMQ 等也可以把任务投递到 Kafka爬虫节点消费消息生成 Request。这套方案更适合大型平台任务生产、分发、爬取、数据落地完全解耦各环节可以独立扩容。代价是架构复杂度飙升团队没有专职运维或开发支撑的话不建议一开始就走这条路线。6. Pipeline里的SQLAlchemy让爬虫数据真正可查爬虫项目里最容易被低估的环节是存储。很多人抓完数据往 CSV 里一扔任务就算完事等到数据量涨起来、需要做增量更新、需要关联查询时才发现自己存的数据根本没法用。工程化的爬虫系统存储设计应该在项目开始时就想好而不是最后补。6.1 为什么单独说存储Requests 阶段存数据都是随手json.dump()或者csv.writer()Scrapy 阶段内置的Feed Exports也能导出 JSON、CSV、XML但这些格式在面对大规模数据时有两个致命问题一是随机访问能力为零查一条记录要全文件扫描二是增量写入不方便重复抓取时无法按主键去重。而数据库在事务性、索引、并发写入、增量更新这些方面天然有优势当数据量到达一定规模后数据库几乎是唯一合理的选择。SQLAlchemy 是我在 Scrapy Pipeline 里比较常用的存储方案。它是 Python 生态里最成熟的 ORM兼容多种数据库后续如果要从 SQLite 换 MySQL、PostgreSQL基本不用改业务代码。它还能直接执行原生 SQL灵活性也够。6.2 Pipeline中的批量写入最容易犯的错误是在process_item里每来一条数据就写一次数据库。吞吐量低是一方面频繁连接、持锁甚至会拖慢数据库本身而且中间任意一条插入失败可能导致后续数据写入关闭。正确做法是攒一批批量提交from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.orm import sessionmaker, declarative_base Base declarative_base() class Article(Base): __tablename__ articles id Column(Integer, primary_keyTrue, autoincrementTrue) url Column(String, uniqueTrue, indexTrue) title Column(String) content Column(String) class SqlAlchemyPipeline: def __init__(self, db_url, batch_size200): self.engine create_engine(db_url, pool_size5, max_overflow10) Base.metadata.create_all(self.engine) self.Session sessionmaker(bindself.engine) self.buffer [] self.batch_size batch_size classmethod def from_crawler(cls, crawler): return cls(crawler.settings.get(SQLALCHEMY_DATABASE_URL)) def process_item(self, item, spider): self.buffer.append(Article( urlitem.get(url), titleitem.get(title), contentitem.get(content), )) if len(self.buffer) self.batch_size: self.flush() return item def flush(self): session self.Session() try: session.add_all(self.buffer) session.commit() except Exception: session.rollback() raise finally: session.close() self.buffer.clear() def close_spider(self, spider): self.flush()这个写法的好处在于Spider关闭时自动把缓冲的剩余数据落库避免末尾数据丢失批量提交减少数据库 IOuniqueTrue的 URL 字段在数据库层面做了去重配合爬虫层去重形成双保险。6.3 表结构设计的一点经验表结构不需要设计得很复杂但几个关键字段不能少唯一标识、原始 URL、抓取时间、更新时间、数据原始状态。抓取页面里会变化的数据时加上updated_at字段配合ON DUPLICATE KEY UPDATE或 SQLAlchemy 的session.merge()可以优雅地做增量更新。字段类型也别过度设计。内容类字段统一用String或Text别在一开始就把长度卡死不然后面内容长了只能改表结构。URL 加上indexTrue因为你最常见的查询就是按 URL 查重、按时间过滤索引能明显加速。还有一个很实用的经验把Pipeline配置成可切换的。不同爬虫任务可能数据格式不同对应的存储目标也不同通过from_crawler读取配置里的数据库地址就可以实现同一个 Pipeline 在不同项目中复用只需在settings.py中修改SQLALCHEMY_DATABASE_URL即可。最后分享一个我个人的习惯不管用什么框架、单机还是分布式每个爬虫任务都要先确认“抓多久、数据存哪、失败了怎么办”这三个问题再动手写代码。工程化的爬虫本质上就是稳定的数据搬运管道——Requests 和 Scrapy 只是手段可控、可维护、可扩展才是目标。先把单机跑透再做分布式一步步来路会走得比想象中更顺。
返回列表