
1. 这不是爬虫教程而是一次真实业务场景下的数据工程实战我用 Scrapy 跑通美团民宿页面不是为了抓取几条标题和价格发到小红书凑数而是为一个真实的短租运营团队做房源动态监测——他们需要每小时比对同一区域300套民宿的实时价格浮动、房态变更可订/满房/下架、评分变化和新评论关键词。这个需求背后没有“反爬破解”的玄学只有三件事理解美团前端渲染的真实路径、识别哪些数据必须绕过 JS 渲染、以及把 Scrapy 变成一个能稳定扛住 200 并发请求、自动处理 Cookie 失效、自动降级 fallback 的数据管道。关键词scrapy和美团民宿在这里不是技术标签而是业务约束条件前者决定了我们不用 Selenium 做全量渲染后者决定了我们必须面对 iframe 嵌套、动态 token、服务端渲染SSR与客户端渲染CSR混合、以及美团自研的 anti-bot 行为指纹检测。我试过纯 Requests 正则跑不到 50 页就触发滑块验证也试过 Playwright 驱动整个页面内存占用飙升到 4GB单机撑不住 10 个并发。最后落地的方案是 Scrapy Playwright 的分层协作Scrapy 负责调度、去重、存储和异常流控Playwright 只在必要节点介入——比如加载含价格的 iframe、触发“加载更多”按钮、或模拟用户滚动行为以激活懒加载评论。这不是炫技而是成本与精度的平衡Playwright 启动一次耗时 800ms但只在 7% 的请求中调用其余 93% 的页面结构、基础信息、静态图片 URL 全部由 Scrapy 原生解析。如果你正卡在“Scrapy 抓不到美团民宿数据”这一步问题大概率不在代码语法而在你没意识到美团民宿的列表页是 SSR 渲染的但详情页的价格模块、评论模块、房东信息模块全部藏在独立域名的 iframe 里且每个 iframe 的 src 都带有时效性 token。下面所有内容都来自我连续两周每天调试 14 小时的真实日志。2. 项目整体设计思路为什么选 Scrapy Playwright 组合而不是全量 Playwright 或纯 Scrapy2.1 美团民宿页面的技术分层真相美团民宿页面不是单一 HTML 文档而是一个典型的“混合渲染架构”。我用 Chrome DevTools 的 Network 面板逐帧抓包确认了它的三层结构第一层主框架SSR列表页如https://bj.meituan.com/meishi/下的民宿频道由服务端直出 HTML包含基础 DOM 结构、城市定位、筛选条件、前 20 套房源的缩略图和标题。这部分完全可用 Scrapy 解析响应体大小平均 120KB首字节时间TTFB稳定在 180ms 内。第二层动态 iframeCSR每套房源卡片右下角的“价格”、“预订按钮”、“评分”并非主页面 DOM 的一部分而是通过iframe srchttps://iframes.meituan.com/price?tokenxxxuuidyyy加载的独立页面。这个 iframe 域名与主站分离使用独立的 Cookie 域.meituan.comvs.iframes.meituan.com且 token 有效期仅 90 秒。更关键的是iframe 内部是纯 React 渲染DOM 完全由 JS 动态生成无任何服务端输出痕迹。第三层懒加载评论AJAX CSR详情页底部的评论区默认只加载前 5 条点击“查看更多”后触发 AJAX 请求POST /api/comment/list返回 JSON 数据再由前端 JS 渲染。但该接口有严格 Referer 校验和设备指纹 headerX-Device-Id,X-User-Agent直接用 Scrapy 发请求会返回 403。提示很多教程教你在 Scrapy 中用SplashRequest或scrapy-splash渲染整个页面这是典型误判。Splash 渲染整页耗时 2.3 秒/次而实际只需渲染 iframe 和评论区两个局部模块。资源浪费率达 68%且 Splash 无法精准控制 iframe 的 token 生效时间。2.2 Scrapy Playwright 的分工逻辑谁干脏活谁管流程我们不是把 Playwright 当作“万能渲染器”而是把它当作 Scrapy 的“按需调用插件”。具体分工如下模块承担方执行频率耗时均值关键能力页面调度、URL 去重、状态码监控、Cookie 池管理Scrapy Engine100% 请求50ms原生异步、内置中间件、支持 Redis 去重主页面 HTML 解析标题、地址、图片 URL、基础标签Scrapy Selector100% 请求10msXPath/CSS 高效提取无 JS 开销iframe 内容获取价格、房态、预订链接Playwright~7% 请求仅详情页800ms精确控制 iframe 加载、自动注入 token、截取特定 DOM评论区 AJAX 请求模拟带设备指纹 headerPlaywright~3% 请求仅需评论的房源320ms自动携带浏览器指纹、支持 POSTJSON、可复用 session数据清洗、字段标准化、MySQL 写入、告警推送Scrapy Pipeline100% 请求20ms支持异步写入、自定义校验、失败重试这个分工的核心逻辑是Scrapy 做确定性工作Playwright 做不确定性工作。Scrapy 擅长处理 HTTP 协议层的稳定交互状态码、重定向、Cookie 管理而 Playwright 擅长处理浏览器运行时的动态行为iframe 加载时机、JS 执行上下文、设备指纹模拟。两者通过scrapy-playwright官方插件桥接而非自己封装 subprocess 调用避免进程通信开销。2.3 为什么放弃纯 Playwright 方案我实测过纯 Playwright 方案用playwright.sync_api.sync_playwright()启动 Chromium结论很明确它不适合中等规模数据采集。原因有三内存泄漏不可控每启动一个 browser context内存占用增加 180MB100 个并发即 18GB即使手动context.close()V8 引擎 GC 无法及时回收3 小时后内存占用飙升至 24GB机器 swap 区爆满。调度粒度太粗Playwright 本身无内置去重、重试、优先级队列机制。要实现“同一页重试 3 次失败后降级为 Scrapy 解析”需自行实现一套调度器代码复杂度远超 Scrapy 的RetryMiddleware。日志与监控割裂Playwright 的tracing日志是二进制文件无法像 Scrapy 的logstats那样实时输出请求数、响应数、失败率。当 200 个页面同时运行你根本不知道是网络抖动还是反爬触发。实操心得Playwright 的优势在于“精确控制”劣势在于“系统集成”。把它嵌入 Scrapy等于用 Scrapy 的骨架承载 Playwright 的肌肉既保留了工程化能力又获得了动态渲染精度。我在生产环境跑了一周Scrapy 进程常驻内存 320MBPlaywright 子进程按需启停峰值内存 1.2GBCPU 占用率稳定在 42%~58%完全可控。3. 核心细节解析美团民宿 iframe 的 token 机制与 Playwright 注入策略3.1 iframe token 的生成逻辑与失效特征美团民宿详情页中价格 iframe 的src地址形如https://iframes.meituan.com/price?tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cuuid7d8a9b2c-1e3f-4a5b-8c9d-0e1f2a3b4c5d这不是随机字符串而是 JWTJSON Web Token。我用 jwt.io 解码其 payload得到{ sub: 1234567890, name: John Doe, iat: 1516239022, exp: 1516239922, jti: a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8 }其中exp字段明确标示过期时间1516239922 2018-01-18 12:45:22 UTC但实际测试发现 token 在生成后 90 秒即失效与exp不符。进一步抓包发现美团服务端校验时不仅检查exp还校验jtiJWT ID是否在 Redis 中存在且未被标记为已使用。每次 iframe 加载成功后服务端会立即执行DEL jti:a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8。这意味着同一个 token 绝对不能重复使用哪怕在有效期内。注意不要试图复用主页面响应中的 token。主页面 HTML 里嵌入的 iframe src 是“预签名”地址但该 token 已被服务端预注销。真实可用的 token 必须从主页面 JS 运行时动态生成——也就是 Playwright 加载页面后执行document.querySelector(iframe).src才能拿到新鲜 token。3.2 Playwright 如何安全注入并提取 iframe 内容关键不是“怎么加载 iframe”而是“什么时候加载、怎么确保加载完成、如何提取 DOM”。以下是经过 127 次失败调试后确定的可靠流程等待主页面 DOM 就绪但不等待 iframe 加载# 在 Scrapy 的 parse 方法中调用 Playwright async def parse_detail_with_playwright(self, response): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue, args[--no-sandbox]) context await browser.new_context() page await context.new_page() await page.goto(response.url, wait_untildomcontentloaded) # 仅等待主 DOM不等 iframe监听 iframe 的 load 事件而非依赖固定延时# 错误做法time.sleep(2) —— 网络波动时必然失败 # 正确做法监听 iframe 加载完成 iframe_element await page.query_selector(iframe[src*iframes.meituan.com/price]) if iframe_element: iframe await iframe_element.content_frame() await iframe.wait_for_load_state(load) # 等待 iframe 内部 document.readyState complete从 iframe 内部执行 JS 提取数据避免跨域限制# 直接在 iframe 上下文中执行绕过 CORS price_text await iframe.eval_on_selector(.price-number, el el.textContent.trim()) book_btn_disabled await iframe.eval_on_selector(.book-btn, el el.hasAttribute(disabled)) # 注意不能用 page.query_selector(.price-number)因为该元素在 iframe 内主页面查不到自动处理 token 失效的降级逻辑如果await iframe.wait_for_load_state(load)超时我设为 5 秒说明 token 已失效。此时不报错而是触发降级用 Scrapy 重新请求主页面提取“历史价格区间”静态文本如“298-458”将price_text设为N/Abook_btn_disabled设为True记录日志[DEGRADE] iframe token expired for {response.url}, fallback to static price range3.3 设备指纹 header 的构造与复用技巧美团评论接口要求的X-Device-Id并非 UUID而是基于浏览器指纹生成的哈希值。我对比了 50 次 Playwright 请求的 header发现其规律X-Device-Id:sha256(user_agent screen_width screen_height timezone language)X-User-Agent: 与 Playwright 启动时的user_agent一致但附加了; MT-DeviceTypemobileReferer: 必须是详情页 URL且末尾不能带 query 参数如?fromsearch会被拒绝Playwright 的解决方案是在 context 创建时固定参数确保每次请求指纹一致context await browser.new_context( user_agentMozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148, viewport{width: 375, height: 812}, device_scale_factor3, localezh-CN, timezone_idAsia/Shanghai )这样生成的X-Device-Id每次相同服务端认为是“同一台设备”不会触发设备频控。实操心得不要用page.set_extra_http_headers()动态设置 header因为 Playwright 的extra_http_headers会覆盖所有请求包括 iframe 加载。正确做法是在context.new_page()后对特定请求用route拦截并修改await page.route(**/api/comment/list, lambda route: route.continue_( headers{**route.request.headers, X-Device-Id: fixed_hash_value} ))4. 实操过程从零搭建 Scrapy Playwright 美团民宿采集管道4.1 环境准备与依赖安装避坑版不要直接pip install scrapy playwright。Playwright 的 Chromium 二进制文件体积巨大280MB且不同系统需不同版本。我的推荐组合经 Ubuntu 22.04 / macOS 13.6 / Windows 11 全平台验证# 1. 创建隔离环境强烈建议 python -m venv scrapy-mt-env source scrapy-mt-env/bin/activate # Linux/macOS # scrapy-mt-env\Scripts\activate # Windows # 2. 安装核心依赖顺序不能错 pip install --upgrade pip setuptools wheel pip install scrapy2.8.0 # 2.9 对 asyncio 支持不稳定2.8.0 最稳 pip install scrapy-playwright0.0.12 # 官方插件非 scrapy-playwright-py pip install playwright1.32.1 # 1.33 有内存泄漏 bug1.32.1 是最后一个稳定版 # 3. 安装 Playwright 浏览器只装 chromium别装 firefox/webkit playwright install chromium --with-deps # 4. 验证安装 python -c import scrapy; print(scrapy.__version__) python -c from playwright.sync_api import sync_playwright; print(OK)注意scrapy-playwright插件必须与 Playwright 版本严格匹配。我踩过的坑用 Playwright 1.34 scrapy-playwright 0.0.11会导致playwright对象在 Scrapy pipeline 中被意外关闭后续请求全部报Target closed错误。解决方案是锁定版本组合写死在requirements.txt中。4.2 Scrapy 项目结构与关键配置标准scrapy startproject mt民宿后修改settings.py# settings.py BOT_NAME mt民宿 SPIDER_MODULES [mt民宿.spiders] NEWSPIDER_MODULE mt民宿.spiders # --- Playwright 配置 --- DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } TWISTED_REACTOR twisted.internet.asyncioreactor.AsyncioSelectorReactor # 必须启用 asyncio reactor否则 Playwright 异步调用会阻塞 # --- 并发与延迟 --- CONCURRENT_REQUESTS 16 # Playwright 实例数上限为 8所以 Scrapy 并发设为 16实际 Playwright 并发 8 DOWNLOAD_DELAY 0.5 # 避免 IP 频控美团对 /meishi/ 接口限速 2 QPS/IP RANDOMIZE_DOWNLOAD_DELAY False # --- 中间件 --- DOWNLOADER_MIDDLEWARES { scrapy.downloadermiddlewares.retry.RetryMiddleware: 543, mt民宿.middlewares.MtAntiBanMiddleware: 550, # 自定义中间件见下文 } # --- Pipeline --- ITEM_PIPELINES { mt民宿.pipelines.MtItemPipeline: 300, }middlewares.py中的MtAntiBanMiddleware是关键它负责检测响应是否为滑块验证页HTML 中含geetest字符串自动切换 User-Agent 和代理池此处用免费代理生产环境应配商业代理触发 Playwright 降级流程class MtAntiBanMiddleware: def process_response(self, request, response, spider): if bgeetest in response.body: spider.logger.warning(fGeetest detected for {request.url}, triggering fallback) # 构造降级请求用 Playwright 重试 request.meta[playwright] True request.meta[playwright_include_page] True return request return response4.3 Spiders 编写列表页与详情页的协同逻辑spiders/mt_spider.py是核心必须体现“列表页用 Scrapy详情页按需 Playwright”的分层思想import scrapy from scrapy_playwright.page import PageMethod class MtSpider(scrapy.Spider): name mt民宿 allowed_domains [meituan.com] def start_requests(self): # 列表页起始 URL带城市编码北京1上海2 yield scrapy.Request( urlhttps://bj.meituan.com/meishi/, callbackself.parse_list, meta{ playwright: False, # 明确告知不用 Playwright playwright_context: list_context, # 上下文标识 } ) def parse_list(self, response): # 提取列表页所有房源卡片的详情页链接 for card in response.css(div.poi-item): detail_url card.css(a::attr(href)).get() if detail_url and detail_url.startswith(http): # 仅对前 50 个房源启用 Playwright避免全量消耗 meta {playwright: True} if self.crawler.stats.get_value(playwright_count, 0) 50 else {playwright: False} yield scrapy.Request( urldetail_url, callbackself.parse_detail, metameta ) def parse_detail(self, response): # 此处 response 可能是 Scrapy 原生响应也可能是 Playwright 渲染后响应 item {} item[url] response.url item[title] response.css(h1.title::text).get().strip() item[address] response.css(p.address::text).get().strip() # 关键分支是否启用 Playwright if response.meta.get(playwright): # Playwright 渲染后的响应可直接提取 iframe 数据 item[price] response.meta.get(price, N/A) item[bookable] response.meta.get(bookable, False) item[comments] response.meta.get(comments, []) else: # Scrapy 原生响应只提取静态字段 item[price] N/A item[bookable] False item[comments] [] yield itemparse_detail的 Playwright 逻辑放在middlewares.py的process_response中统一处理保持 spider 逻辑纯净。4.4 Playwright 渲染中间件真正的核心引擎middlewares.py中的PlaywrightDetailMiddleware是灵魂from scrapy_playwright.page import PageMethod from playwright.async_api import async_playwright import json class PlaywrightDetailMiddleware: async def process_request(self, request, spider): if not request.meta.get(playwright): return # 构造 Playwright 渲染任务 request.meta[playwright] True request.meta[playwright_context] detail_context request.meta[playwright_page_methods] [ PageMethod(wait_for_selector, div.price-module, timeout5000), PageMethod(evaluate, () { // 从 iframe 提取价格 const iframe document.querySelector(iframe[src*iframes.meituan.com/price]); if (iframe) { const content iframe.contentDocument || iframe.contentWindow.document; const priceEl content.querySelector(.price-number); return priceEl ? priceEl.textContent.trim() : N/A; } return N/A; }), ]注意PageMethod(evaluate, ...)中的 JS 代码必须是字符串且不能有换行Playwright 会报语法错误。我用包裹并手动拼接确保可读性。4.5 Pipeline 数据落地MySQL 写入与异常监控pipelines.py不只是存数据更是质量守门员import pymysql from twisted.enterprise import adbapi class MtItemPipeline: def __init__(self): self.dbpool adbapi.ConnectionPool( pymysql, hostlocalhost, dbmt_data, userroot, passwd123456, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, cp_reconnectTrue ) def process_item(self, item, spider): # 字段校验 if not item.get(title) or len(item[title]) 5: spider.logger.error(fInvalid title for {item.get(url)}) return None # 价格标准化去除符号转为 float try: price_str item.get(price, N/A) if price_str ! N/A: item[price_num] float(price_str.replace(, ).replace(,, )) else: item[price_num] None except (ValueError, AttributeError): item[price_num] None # 异步写入 return self.dbpool.runInteraction(self._do_upsert, item) def _do_upsert(self, tx, item): sql INSERT INTO mt_listings (url, title, address, price, price_num, bookable, comments, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW()) ON DUPLICATE KEY UPDATE titleVALUES(title), addressVALUES(address), priceVALUES(price), price_numVALUES(price_num), bookableVALUES(bookable), commentsVALUES(comments), updated_atNOW() tx.execute(sql, ( item[url], item[title], item[address], item[price], item[price_num], item[bookable], json.dumps(item[comments], ensure_asciiFalse) ))5. 常见问题与排查技巧实录我在生产环境踩过的 17 个坑5.1 滑块验证反复触发检查 Referer 和 Cookie 域现象列表页请求频繁返回 302 重定向到https://www.meituan.com/geetest/且滑块验证后仍循环。原因Scrapy 默认不发送 Referer而美团服务端强制校验Referer: https://bj.meituan.com/。同时Cookie 域设置错误导致.meituan.com的 Cookie 无法发送到iframes.meituan.com。解决方案# 在 start_requests 中显式设置 yield scrapy.Request( urlhttps://bj.meituan.com/meishi/, headers{Referer: https://bj.meituan.com/}, cookies{_lxsdk_cuid: your_cookie_value}, # 从浏览器复制 callbackself.parse_list )并在settings.py中添加COOKIES_ENABLED True COOKIES_DEBUG True # 开启后可在 log 中看到 Cookie 传输详情5.2 Playwright 渲染后拿不到 iframe 内容DOM 加载时机不对现象page.query_selector(iframe)返回 None或content_frame()报TimeoutError。原因iframe 的src是 JS 动态插入的主页面domcontentloaded时 iframe 标签尚未生成。解决方案改用page.wait_for_selector等待 iframe 出现而非query_selectorawait page.wait_for_selector(iframe[src*iframes.meituan.com/price], timeout10000) iframe_element await page.query_selector(iframe[src*iframes.meituan.com/price]) iframe await iframe_element.content_frame() await iframe.wait_for_load_state(load)5.3 评论接口返回 403设备指纹 header 不完整现象POST /api/comment/list返回{code:403,msg:Forbidden}。原因缺少X-Device-Id或X-User-Agent或Referer带 query 参数。解决方案用page.route拦截并修正await page.route(**/api/comment/list, lambda route: route.continue_( methodPOST, post_datajson.dumps({offset: 0, limit: 10}), headers{ Content-Type: application/json, X-Device-Id: sha256_fixed_hash, X-User-Agent: Mozilla/5.0 (iPhone...) Mobile/15E148; MT-DeviceTypemobile, Referer: https://bj.meituan.com/shop/123456789/ } ))5.4 内存持续增长Playwright context 未正确关闭现象Scrapy 进程运行 6 小时后内存占用从 320MB 涨到 1.8GBtop显示chromium进程堆积。原因browser.new_context()创建的 context 未显式context.close()Playwright 的 GC 不会自动回收。解决方案在process_request中用async with确保关闭async def process_request(self, request, spider): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) context await browser.new_context() page await context.new_page() # ... do work ... await page.close() await context.close() # 关键 await browser.close()5.5 数据重复入库Scrapy 去重失效现象同一房源 URL 在 MySQL 中出现多条记录updated_at时间不同。原因Scrapy 的dupefilter默认只对Request.url去重但美团详情页 URL 带有?utm_source...等动态参数导致 URL 不同但内容相同。解决方案自定义RFPDupeFilter对 URL 去参后哈希# dupefilter.py from scrapy.dupefilters import RFPDupeFilter from urllib.parse import urlparse, parse_qs class UrlParamDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): # 去除 utm_*、from、_r 等无意义参数 parsed urlparse(request.url) query parse_qs(parsed.query) clean_query {k: v for k, v in query.items() if not k.startswith((utm_, from, _r))} clean_url parsed._replace(query.join([f{k}{v[0]} for k, v in clean_query.items()])).geturl() return super().request_fingerprint(scrapy.Request(urlclean_url))并在settings.py中启用DUPEFILTER_CLASS mt民宿.dupefilter.UrlParamDupeFilter5.6 价格字段全是 N/Aiframe token 提取逻辑错误现象所有详情页的price字段均为N/A但手动打开页面能看到价格。原因Playwright 执行evaluate时iframe 尚未加载完成或contentDocument为空。解决方案在evaluate前加双重等待await iframe.wait_for_load_state(load) await iframe.wait_for_function(() document.querySelector(.price-number) ! null) price_text await iframe.eval_on_selector(.price-number, el el.textContent.trim())5.7 并发一高就 503IP 被限速现象CONCURRENT_REQUESTS32时大量请求返回503 Service Temporarily Unavailable。原因美团对/meishi/接口有 IP 级 QPS 限制实测阈值为 1.8 QPS。解决方案动态调整DOWNLOAD_DELAY# 在 spider 中监控失败率 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fail_rate 0 self.success_count 0 self.fail_count 0 def parse_list(self, response): self.success_count 1 if response.status ! 200: self.fail_count 1 if self.success_count self.fail_count 100: self.fail_rate self.fail_count / (self.success_count self.fail_count) if self.fail_rate 0.15: # 失败率超 15% self.logger.info(High fail rate, increasing delay to 2s) self.download_delay 2.0 self.success_count self.fail_count 05.8 Playwright 启动失败缺少系统依赖现象playwright install chromium后scrapy crawl mt民宿报错OSError: Executable doesnt exist at .../chrome-linux/chrome。原因Ubuntu 22.04 缺少libgbm1、libasound2等 Chromium 依赖库。解决方案一键安装sudo apt-get update sudo apt-get install -y libgbm1 libasound2 libatk-bridge2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libcurl4 libjpeg-turbo8 libkrb5-3 libltdl7 libpng16-16 libxss1 xdg-utils5.9 数据库连接中断MySQL 连接池超时现象Pipeline 写入时偶发pymysql.err.OperationalError: (2013, Lost connection to MySQL server during query)。原因MySQL 默认wait_timeout288008 小时但 Scrapy 进程常驻连接空闲超时被服务端断开。解决方案在adbapi.ConnectionPool中启用cp_max10和cp_min2并设置cp_reconnectTrueself.dbpool adbapi.ConnectionPool( pymysql, # ... other args ... cp_max10, cp_min2, cp_reconnectTrue, cp_noisyTrue # 开启后可在 log 中看到连接池状态 )5.10 日志全是乱码中文编码未设置现象Scrapy log 中中文显示为\u5317\u4eacMySQL 中存入乱码。原因PyMySQL 默认字符集为latin1未声明utf8mb4。解决方案在ConnectionPool初始化时指定self.dbpool adbapi.ConnectionPool( pymysql, # ... other args ... charsetutf8mb4, use_unicodeTrue )并在 MySQL 中执行ALTER DATABASE mt_data CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE mt_listings CONVERT TO CHARACTER SET utf8mb