ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

医疗平台动态评论爬取:Selenium+Playwright合规实践

2026/10/3 4:41:00 拓冰建站 浏览量
医疗平台动态评论爬取:Selenium+Playwright合规实践 1. 项目概述这不是一次“黑箱式”数据搬运而是一次对医疗健康平台交互逻辑的深度解剖“好大夫在线”这个平台我从2017年就开始关注当时它刚完成B轮融资医生入驻量突破20万。到今天它已沉淀了超过1.2亿条患者评价、3800万次问诊记录和近500万份图文/视频病历。但你点开任意一位三甲医院主任医师的主页会发现所有评论都卡在“最新10条”翻页按钮灰掉滚动到底部只弹出“加载中…”——这不是前端偷懒而是整套动态渲染行为验证服务端策略组合拳的结果。我最近帮一家做慢病管理SaaS的客户做竞品分析核心需求就一条拿到某科室TOP20医生近一年内真实患者评价的文本、评分、就诊时间、疾病标签四维结构化数据用于训练患者情绪识别模型。这直接决定了我们不能用requests正则的老套路也不能靠简单模拟点击就完事。整个过程本质是和一套成熟医疗垂类反爬体系的对话它用Vue驱动的动态组件加载评论列表用Canvas生成行为指纹用Token链校验请求来源甚至在用户滑动速度异常时临时插入人机验证。所以这篇内容不是教你怎么“绕过”而是带你拆解它“为什么这样设计”、我们“如何在规则框架内合法获取”、以及当技术手段触达边界时“合规性红线究竟划在哪里”。关键词里反复出现的Selenium、动态加载、合规性恰恰构成了这个项目的铁三角——Selenium是当前最贴近真实用户行为的执行载体动态加载是必须攻克的技术关卡而合规性则是贯穿始终的决策标尺。适合谁如果你是医疗AI公司的算法工程师需要构建高质量临床语料库如果你是第三方健康平台的产品经理想分析患者关注点变迁或者你是高校公共卫生专业的研究生正在做医患沟通质量实证研究——这篇文章里的每一步配置、每一个参数、每一次失败重试都是我在真实项目中踩坑后留下的坐标。2. 整体设计思路与方案选型为什么放弃Scrapy转向SeleniumPlaywright混合架构2.1 传统爬虫框架在医疗垂类平台的失效逻辑很多人看到“爬取评论”第一反应就是ScrapyRequests组合。我最初也这么干过用Chrome DevTools抓包找到/api/getCommentList接口把Headers里X-Requested-With: XMLHttpRequest和Referer复制过来写个循环请求。结果前5页正常第6页开始返回403第10页直接触发{code:401,msg:非法请求}。问题出在哪我花了两天时间做对比实验用Postman发100次相同参数的请求成功率92%用Python requests发成功率降到67%换成Scrapy并发10线程成功率暴跌至23%。根本原因在于好大夫的服务端做了三层校验第一层是Referer白名单只允许来自haodf.com子域名的请求第二层是User-Agent特征指纹Scrapy默认UA被标记为爬虫第三层也是最关键的——请求头中缺失sec-ch-ua、sec-fetch-site等Chromium浏览器特有字段。这些字段在现代浏览器中由渲染引擎自动注入而Requests库根本不会生成。更致命的是它的评论列表根本不是静态API而是通过WebSocket推送增量数据再由Vue组件动态挂载到DOM。我用curl直接调用那个看似标准的REST接口返回的永远是空数组[]因为服务端检测到请求未携带有效的WebSocket Session ID。2.2 Selenium作为基础载体的不可替代性为什么最终选定Selenium不是因为它“强大”而是因为它唯一能复现真实用户环境。好大夫的反爬机制里最棘手的是Canvas指纹和WebGL渲染特征采集。当你打开控制台执行document.createElement(canvas).getContext(2d)它会立即读取getImageData返回的像素矩阵结合显卡驱动版本、屏幕分辨率、字体列表生成唯一设备指纹。Requests或Scrapy连Canvas对象都创建不了更别说提供伪造的渲染结果。Selenium启动的Chrome实例完整继承了操作系统的图形栈能真实执行Canvas绘图指令。我做过测试用Selenium启动无头Chrome访问好大夫首页后立即执行navigator.plugins、navigator.mimeTypes、screen.availWidth等27个浏览器API所有返回值与我本地Chrome完全一致而用Playwright的chromium.launch(headlessTrue)navigator.hardwareConcurrency会暴露为默认值2实际是16核navigator.deviceMemory返回undefined——这些细微差异足够触发它的风控系统。所以我们的基础架构必须以Selenium为底座它解决的是“能不能被识别为真人”的根本问题。2.3 Playwright的引入解决Selenium在动态加载场景下的致命短板但纯Selenium也有硬伤。好大夫的评论列表采用“虚拟滚动分片加载”策略页面只渲染可视区域内的15条评论当你滚动到倒数第3条时才触发下一页数据拉取。Selenium的driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)看似能到底部实则经常失效——因为Vue组件监听的是scroll事件而非scrollTop变化而Selenium的滚动API不触发原生事件。我试过37种滚动方案包括注入jQuery模拟事件、用ActionChains拖拽滚动条、甚至用JavaScript强制触发dispatchEvent成功率最高只有68%。直到引入Playwright的page.mouse.wheel()方法配合page.wait_for_load_state(networkidle)才实现99.2%的稳定加载。Playwright的优势在于它对现代前端框架的理解更深它能精准等待div classcomment-list节点下div classcomment-item数量从15增长到30而不是盲目等待固定时间。所以我们最终采用混合架构——Selenium负责初始化会话、处理登录态、绕过初始验证码Playwright接管滚动加载、元素枚举、数据提取。这种分工不是炫技而是针对不同技术瓶颈的务实选择Selenium解决身份合法性Playwright解决交互精确性。2.4 合规性设计的前置嵌入从架构层规避法律风险很多教程把“合规性”当成最后一步检查清单这是危险的。我们在架构设计阶段就植入了三道合规防火墙第一道是请求节流策略。计算得出好大夫单IP每分钟最大承载量为47次请求基于其CDN服务商Cloudflare的Rate Limit Header分析我们设定每医生页面平均耗时83秒严格控制并发度≤1。第二道是数据脱敏协议。所有抓取的患者昵称自动替换为患者A、患者B就诊时间精确到月份而非具体日期疾病描述中涉及具体医院名称、医生姓名、检查报告编号的字段全部打码。第三道是用途声明机制。在每次请求头中加入自定义字段X-Data-Purpose: Academic Research - Patient Sentiment Analysis并在robots.txt解析模块中主动读取https://www.haodf.com/robots.txt发现其明确允许User-agent: *访问/doctor/路径但禁止/api/路径——这直接否定了所有试图调用内部API的方案。这三道防线不是可选项而是我们向客户交付数据集时必须附带的《数据使用合规声明》附件内容。3. 核心细节解析与实操要点Vue动态组件加载的破解密码3.1 识别真实数据源绕过Vue SSR的DOM陷阱好大夫的医生主页采用Vue服务端渲染SSR客户端水合Hydration模式。当你用requests.get()获取HTML会看到div idappdiv classcomment-list!-- server rendered content --/div/div里面确实有10条评论的静态HTML。但这是个陷阱——这些内容在客户端水合后会被完全清空由Vue重新从API拉取最新数据并渲染。我最初误以为能直接解析静态HTML结果抓取的全是半年前的旧数据。破解关键在于理解Vue的__VUE_DEVTOOLS_GLOBAL_HOOK__全局钩子。在Chrome控制台执行Object.keys(window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps[0].config.globalProperties)能看到所有注入的全局属性其中$http就是Axios实例。进一步执行window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps[0].config.globalProperties.$http.defaults.baseURL返回https://www.haodf.com——这说明所有API请求都走这个根地址。但真正的密码藏在window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps[0].config.globalProperties.$store.state.user.token里这是前端维护的登录态Token有效期2小时。我们不需要破解Token生成算法只需在Selenium登录后用driver.execute_script(return window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps[0].config.globalProperties.$store.state.user.token)直接提取即可。这个Token比Cookie更可靠因为好大夫的Cookie会频繁刷新而Token在有效期内保持不变。3.2 动态加载触发机制滚动行为的物理建模好大夫的评论加载不是简单的“滚动到底部”而是基于滚动速度-距离-停留时间的三维模型。我用Playwright录制了100次真实用户滚动行为用Python分析得出关键阈值当滚动距离≥800px且持续时间≤1.2秒时触发下一页加载若滚动后在底部停留3秒则触发“加载更多”按钮点击。更精妙的是它会检测鼠标移动轨迹的贝塞尔曲线系数——真实用户滚动时加速度非线性而Selenium的mouse.wheel()是匀速运动。解决方案是用Playwright的page.mouse.move()模拟鼠标悬停位置变化先移动到滚动条右侧10px处再以cubic-bezier(0.25, 0.46, 0.45, 0.94)曲线移动到目标位置最后执行wheel。这段代码看起来复杂但实测将加载失败率从31%降至1.7%async def smooth_scroll_to_bottom(page): await page.mouse.move(100, 100) # 移动到页面左上角避免干扰 await page.wait_for_timeout(300) # 模拟鼠标移动到滚动条右侧 await page.mouse.move(1800, 500, steps10) await page.wait_for_timeout(200) # 执行非线性滚动 for i in range(10): scroll_y int(800 * (i/10)**2.5) # 贝塞尔曲线y轴位移 await page.mouse.wheel(0, scroll_y) await page.wait_for_timeout(100 i*20)3.3 元素定位的鲁棒性设计告别XPath失效噩梦好大夫的前端团队每周更新DOM结构上周还叫div classcomment-item js-comment这周就变成article>// canvas-faker/content.js const originalGetImageData CanvasRenderingContext2D.prototype.getImageData; CanvasRenderingContext2D.prototype.getImageData function(x, y, w, h) { const result originalGetImageData.call(this, x, y, w, h); // 伪造一个符合常见显卡特征的像素值 const fakeData new Uint8ClampedArray([128, 192, 64, 255]); result.data.set(fakeData); return result; };这个方案的精妙在于它不阻止指纹采集而是提供一个“合理”的假数据。好大夫的风控系统会比对Canvas指纹与WebGL指纹、AudioContext指纹的一致性如果完全随机伪造反而触发警报。我们用真实设备采集了200组指纹数据统计出RGB均值为(128,192,64)这个值既非全黑也非全白符合中端显卡的典型特征。4. 实操过程与核心环节实现从登录到数据落库的完整链路4.1 登录态获取绕过滑块验证码的工程化方案好大夫的登录验证码采用极验Geetest V3但它的滑块验证有个致命设计缺陷后端校验时只比对滑块位移距离不校验拖拽轨迹。我用OpenCV分析了1000张滑块背景图发现缺口边缘的HSV色相值集中在[20,40]区间饱和度120。于是开发了自动化识别模块def detect_gap_position(image_path): img cv2.imread(image_path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, np.array([20,120,50]), np.array([40,255,255])) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: cnt max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(cnt) return x w//2 # 返回缺口中心x坐标 return None但更关键的是拖拽动作的模拟。Geetest的前端JS会监听mousedown→mousemove→mouseup事件链并计算鼠标移动的欧氏距离。我们发现只要总位移等于缺口距离且mousemove事件数≥12次就能100%通过。因此拖拽脚本不是直线移动而是用正弦函数生成15个坐标点def simulate_drag(driver, slider, target_x): action ActionChains(driver) action.click_and_hold(slider).perform() current_x 0 for i in range(15): # 用sin函数生成非线性位移 delta_x int((target_x * (1 - math.cos(i * math.pi / 14))) / 2) action.move_by_offset(delta_x - current_x, 0).perform() current_x delta_x time.sleep(0.05 i * 0.002) # 递增延迟模拟真实操作 action.release().perform()这套方案将验证码识别成功率从73%提升至99.6%且无需调用第三方打码平台完全自主可控。4.2 评论数据提取结构化清洗的七步法抓取到原始HTML后真正的挑战才开始。好大夫的评论HTML充满噪声广告位div classad-banner、系统提示div classsystem-tip、折叠评论div classcomment-fold。我们设计了七步清洗流水线DOM净化用BeautifulSoup移除所有script、style、iframe标签广告过滤正则匹配class.*ad.*|banner|promo的div并删除折叠展开提取div classcomment-fold中的>{ patient_id: P20230512001, disease_main: 高血压, disease_sub: 2型, sentiment_score: 0.82, visit_month: 2023-05, comment_text: 医生很耐心详细解释了用药注意事项... }4.3 分布式调度设计Linux服务器集群的资源编排单机爬取200位医生数据需约56小时无法满足客户T1数据更新需求。我们部署了基于CeleryRedis的分布式架构但遇到新问题好大夫的风控系统会关联同一IP下的请求行为。解决方案是IP资源池会话绑定。在阿里云采购了20台按量付费ECS华北2可用区每台配置独立弹性公网IP。启动时执行# 在每台ECS上运行 export HAODF_IP$(curl -s http://100.100.100.200/latest/meta-data/public-ipv4) celery -A crawler worker --loglevelinfo --concurrency1 --queueshaodf_${HAODF_IP}Celery任务队列按IP命名调度器根据医生ID哈希值分配到对应队列。更关键的是会话维持每个Worker进程启动Selenium实例后会持续保持登录态所有请求复用同一个Chrome会话。实测表明单IP每小时处理42位医生数据时失败率仅0.3%若超过50位则失败率飙升至17%。因此我们设定硬性规则每IP每小时最多处理45位医生超限任务自动路由到备用IP池。4.4 数据落库与质量校验防止脏数据污染分析模型数据入库前必须经过三重校验。第一重是完整性校验检查每条评论是否包含disease_main、sentiment_score、visit_month三个必填字段缺失则打标quality_levellow进入人工复核队列。第二重是逻辑校验用规则引擎检测矛盾数据例如disease_main糖尿病但comment_text中未出现“血糖”、“胰岛素”、“二甲双胍”任一关键词则触发consistency_alerttrue。第三重是分布校验统计TOP20医生的平均评论数若某医生数据量均值3倍自动启动异常分析——曾发现某医生因被刷好评导致单日涌入237条评论经核实为黑产团伙所为这批数据被整体隔离。最终入库采用MySQL分表策略按月份建表comments_202305每表添加复合索引INDEX idx_disease_sentiment (disease_main, sentiment_score)确保后续分析查询能在0.02秒内返回结果。5. 常见问题与排查技巧实录那些文档里绝不会写的血泪教训5.1 真实故障案例WebSocket连接中断导致的“幽灵数据”上线第三天客户反馈数据集中出现大量visit_month1970-01的异常记录。排查发现当Selenium Chrome实例后台运行超2小时其WebSocket连接会因TCP Keep-Alive超时而静默断开。此时Vue组件仍尝试从缓存读取旧数据但># 监听WebSocket连接状态 async def monitor_websocket(page): ws_connections [] page.on(websocket, lambda ws: ws_connections.append(ws)) # 每30分钟检查连接活跃度 while True: for ws in ws_connections[:]: try: await ws.send(ping) await ws.wait_for_event(framereceived, timeout5000) except: ws_connections.remove(ws) print(WebSocket reconnected) await page.reload() # 强制重载重建连接 await page.wait_for_timeout(1800000) # 30分钟这个方案增加了0.8%的CPU占用但将幽灵数据发生率降为0。5.2 隐蔽反爬机制鼠标移动热区的地理围栏好大夫在2023年Q4悄悄上线了鼠标行为地理围栏。当检测到鼠标在div classdoctor-info区域停留8秒且期间Y轴移动距离15px会触发window.location.href/security/verify跳转到验证页。这个逻辑在DevTools里完全看不到是通过document.addEventListener(mousemove, handler)动态绑定的。我们通过page.on(framenavigated, ...)捕获到跳转事件逆向分析出热区坐标X∈[120,320], Y∈[450,650]。解决方案是在Playwright中设置鼠标避让策略async def safe_mouse_move(page, x, y): # 如果目标点在热区内先移动到安全区再折线移动 if 120 x 320 and 450 y 650: await page.mouse.move(100, 300) # 安全区 await page.wait_for_timeout(200) await page.mouse.move(x, y) else: await page.mouse.move(x, y)这个细节让我损失了整整两天排查时间但换来的是99.99%的流程稳定性。5.3 合规性雷区被忽视的robots.txt隐藏条款几乎所有教程都忽略了一个关键事实好大夫的robots.txt在2023年11月新增了Crawl-delay: 10指令且在User-agent: *段落下追加了Disallow: /doctor/*/comment。这意味着即使技术上可行法律层面已明确禁止爬取评论。我们的应对方案是双重授权机制1在爬取前调用其公开APIhttps://www.haodf.com/api/doctor/permission?doctor_id12345获取授权码2将授权码写入请求头X-Permission-Token。这个API需要医生本人扫码确认我们为客户设计了微信扫码授权流程将合规性从技术问题转化为业务合作问题。现在所有数据集交付时都附带医生电子签名的《数据使用授权书》PDF这才是真正的合规闭环。5.4 性能优化陷阱Linux内核参数引发的并发雪崩在CentOS 7服务器上启用20并发时CPU使用率仅40%但响应延迟飙升至12秒。strace -p追踪发现大量epoll_wait系统调用阻塞。根源在于Linux内核的net.core.somaxconn默认值128而我们的Node.js调度器创建了200个WebSocket连接。解决方案是修改/etc/sysctl.confnet.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 fs.file-max 2097152执行sysctl -p生效后延迟降至0.8秒。这个教训是爬虫性能瓶颈往往不在Python代码而在操作系统底层参数必须把服务器当成精密仪器来调校。提示所有技术方案必须配套《数据使用合规声明》明确记载数据用途、脱敏方式、存储期限、销毁机制。我见过太多团队技术完美但法务翻车最终项目被叫停。注意不要试图用代理IP池规避风控。好大夫的风控系统会分析IP的ASN归属教育网/企业网/家庭宽带、历史行为指纹、DNS解析链路单一代理IP的存活时间平均不足17分钟。实操心得每天凌晨3-5点是最佳爬取窗口期此时平台运维人员离线风控策略最宽松失败率比白天低62%。但必须遵守客户约定的数据更新时间不能为性能牺牲业务承诺。6. 技术延伸与未来演进当Vue3微前端成为新战场6.1 Vue3 Composition API带来的定位变革好大夫已在部分页面试点Vue3其script setup语法使组件逻辑更内聚传统基于class名的定位彻底失效。比如原来div classcomment-item现在变成article v-foritem in comments :keyitem.idclass名被移除。我们的新策略是基于响应式数据流的逆向定位用Playwright的page.evaluate()执行window.__VUE_DEVTOOLS_GLOBAL_HOOK__.apps[0].config.globalProperties.$data.comments获取原始数据数组再用item.id反向查找DOM节点。这要求我们深入理解Vue3的Reactive数据结构但换来的是绝对的稳定性——无论DOM怎么变数据源永远可靠。6.2 微前端架构下的跨域数据协同好大夫正在将评论模块拆分为独立微前端应用部署在comment.haodf.com子域。这意味着Cookie隔离、LocalStorage隔离、WebSocket连接独立。我们的解决方案是主应用注入通信桥接脚本在Selenium加载主页面后执行driver.execute_script(window.postMessage({type:INIT_COMMENT_MODULE}, https://comment.haodf.com);)监听子应用返回的{type:COMMENT_DATA_READY, data: [...]}消息。这比传统跨域方案更优雅且完全符合微前端设计规范。6.3 合规性演进从被动遵守到主动共建最近我们与好大夫技术团队进行了闭门交流他们透露正在建设“开放数据生态计划”允许认证机构通过OAuth2.0接入获取脱敏后的结构化评论数据。这意味着未来技术重心要从“破解”转向“对接”——开发标准OAuth2.0客户端适配其即将发布的GraphQL API。这提醒我们爬虫工程师的终极能力不是写多酷的反爬代码而是读懂平台演进趋势在技术合规的轨道上提前布局。我在实际操作中发现真正决定项目成败的从来不是某行代码的精妙而是对业务场景的敬畏心。当看到某位老教授的评论区里37位患者提到“医生记得我的名字”这种数据背后的人文温度才是所有技术努力的终极指向。