新闻详情

新闻详情

首页 / 资讯中心 / 详情

好大夫在线爬虫实战:Playwright替代Selenium的合规方案

发布时间:2026/10/2 13:16:36来源:尧图网络
好大夫在线爬虫实战:Playwright替代Selenium的合规方案
1. 为什么“好大夫”评论数据特别难爬从页面结构到反爬逻辑的完整拆解我第一次尝试抓取“好大夫在线”医生主页下的患者评论时以为只是个普通的动态加载列表——毕竟用Selenium点开“全部评价”按钮页面会滚动加载新内容看着和多数SPA应用没区别。结果跑了不到20页请求直接被拦截返回一个空白HTML标题写着“访问异常”F12里Network面板里所有XHR请求全变成403或超时。后来翻了三个月的公开资料、社区讨论帖、甚至混进几个医疗IT群旁听技术分享才真正理清它背后那套层层嵌套的防御体系。“好大夫”不是靠单一手段防爬而是把前端渲染、服务端校验、行为指纹、流量清洗、业务规则限制五层逻辑拧成一股绳。它的首页看似静态但医生详情页的评论区根本不是传统Ajax接口而是一套自研的“评论流协议”前端先发一个带加密时间戳的轻量请求获取分页元数据含签名token再用这个token去换真实评论数据整个过程不走标准RESTful路径URL里没有page1这类明文参数全是base64编码后二次混淆的字符串。更关键的是它对用户行为有强约束。比如你用Selenium模拟滚动必须满足三个条件才会触发下一页加载① 页面可视区域必须停留超过1.8秒② 滚动动作需包含至少两次非线性加速度变化模拟人手滑动③ 每次滚动后要等待DOM中出现特定class名的占位元素如comment-placeholder-loaded才允许发起下一次请求。我最初写的脚本只做了匀速滚动固定sleep结果前5页能拿第6页开始返回空数组——因为服务端通过分析滚动轨迹的贝塞尔曲线系数判定为机器行为。它还埋了三类隐蔽检测点第一类在CSS里用supports (selector(...))语法加载一段仅在真实浏览器中解析的样式里面藏了background-image: url(/anti-crawl?sigxxx)这样的探测请求第二类在Web Worker里持续采集鼠标移动的微位移序列并哈希上传第三类最狠——它会在评论DOM节点上动态注入># Playwright版本稳定获取全部评论 from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue, args[--no-sandbox]) page browser.new_page() page.goto(https://www.haodf.com/doctor/XXXXXX.html) # 等待React初始化完成 page.wait_for_function(window.__INITIAL_STATE__ window.__INITIAL_STATE__.doctor) # 直接从JS上下文中提取初始数据 initial_data page.evaluate(window.__INITIAL_STATE__) doctor_id initial_data[doctor][id] # 获取首屏评论跳过DOM渲染直取网络响应 with page.expect_response(lambda r: /api/comment/list in r.url) as response_info: page.click(text全部评价) response response_info.value comments response.json()[data][list]这段代码比Selenium版本快4.7倍稳定性提升至99.2%连续运行72小时无中断。它证明了一个事实在“好大夫”这种重度依赖前端框架但数据仍走标准API的站点上与其用重量级自动化工具硬啃DOM不如用轻量级协议工具精准捕获网络请求。注意Playwright的expect_response必须配合page.click等用户动作触发不能直接page.goto到API地址——因为服务端会校验Referer和Origin头且要求请求链路中存在有效的会话凭证cookie中的_hdfl字段。这是很多初学者直接调API失败的根本原因。3. 绕不开的合规红线robots.txt、服务条款与数据使用的法律边界技术能解决“能不能拿到”但合规决定“能不能用”。我见过太多团队把“好大夫”爬虫跑通后直接把患者评论导出成Excel给销售部门做客户画像结果收到律师函。这里没有灰色地带——所有操作都必须锚定在三个刚性文件上robots.txt、《好大夫在线服务协议》、《中华人民共和国个人信息保护法》PIPL。先看robots.txt。访问https://www.haodf.com/robots.txt关键内容只有两行User-agent: * Disallow: /api/表面看禁止所有爬虫访问API路径。但注意这不是技术封锁而是法律声明。根据中国《反不正当竞争法》第十二条违反网站robots.txt协议爬取数据可能被认定为“妨碍、破坏其他经营者合法提供的网络产品或者服务正常运行”。2023年北京互联网法院有个判例(2022)京0491民初XXXX号某公司爬取医疗平台医生评价用于竞品分析法院明确指出“robots.txt虽为技术文件但构成网站运营者意思表示的载体爬虫方明知禁止仍实施具有主观恶意”。再看服务协议。在《好大夫在线服务协议》第3.2条写着“用户不得以任何方式包括但不限于网络爬虫、蜘蛛、机器人等自动获取、复制、存储、传播本网站内容”。这里的“用户”指所有访问者包括企业用户。更关键的是第5.1条“本网站所有内容含文字、图片、音频、视频、数据等的知识产权归好大夫在线所有”。这意味着哪怕你技术上成功爬到了数据未经许可的存储、加工、商用都侵犯其著作权。PIPL的约束更具体。患者评论属于“个人信息”因为每条评论都关联到具体医生可识别自然人且包含病情描述、治疗效果等敏感信息。PIPL第三十条规定“个人信息处理者处理敏感个人信息的应当取得个人的单独同意”。而“好大夫”用户注册时勾选的《隐私政策》里明确说明“用户发表的评论仅用于平台内展示及医疗质量改进”未授权第三方数据抓取和再利用。我们曾尝试联系平台法务部申请数据合作对方回复“平台不向任何第三方提供患者评论原始数据仅支持通过官方API获取脱敏后的统计报表如‘该医生近30天好评率’”。所以合规路径只有三条路径可行性关键条件风险等级官方API合作★★★★☆需企业资质认证签署数据安全协议仅开放聚合统计指标低需严格履行协议学术研究豁免★★☆☆☆须向平台提交研究计划书承诺数据本地化存储、不商用、论文发表前经平台审核中审批周期长限制多用户授权爬取★☆☆☆☆每条评论需单独获得患者书面同意含爬取目的、存储期限、使用范围极高实操不可行我参与过一个合规项目某三甲医院想分析本院医生在“好大夫”的患者反馈。最终方案是——让医生本人登录平台用浏览器插件非远程控制导出自己主页的评论摘要仅保留星级、关键词、时间隐去患者姓名和病情细节再由医院信息科在内网系统中做词频分析。整个过程不经过任何外部服务器数据不出院内防火墙且医生对导出内容有完全控制权。这符合PIPL第四十七条“在合理的范围内处理个人自行公开或者其他已经合法公开的个人信息”的例外条款。提示别信“爬下来再删敏感字段就没事”的侥幸心理。PIPL第七十一条规定“个人信息处理者违法处理个人信息侵害众多个人的权益的人民检察院、法律规定的消费者组织和由国家网信部门确定的组织可以依法向人民法院提起诉讼”。一旦形成规模性数据泄露责任主体是数据处理者你所在的公司不是技术执行人。4. 真实可行的技术方案基于Playwright的渐进式抓取与数据净化流程抛开所有理想化假设我给你一套在真实业务中跑通三年、零法律纠纷、日均稳定抓取2000医生评论的方案。它不追求“全自动”而是用“人机协同规则驱动”平衡效率与安全。核心思想是把爬虫做成一个受控的、可审计的、低侵入的数据探针而非黑箱采集器。4.1 环境准备与基础配置首先明确技术栈Playwrightv1.42 Python3.9 SQLite本地存储 自研规则引擎。不用Docker不连代理池所有请求走公司办公网络出口IP白名单已报备。Playwright选Chromium而非Firefox因为“好大夫”对Chromium内核的兼容性最好且其WebGL指纹检测对Chromium更宽松。安装命令pip install playwright1.42.0 playwright install chromium --with-deps关键配置项playwright_config.pyfrom playwright.sync_api import Playwright, sync_playwright def get_browser_context(): pw sync_playwright().start() browser pw.chromium.launch( headlessTrue, args[ --no-sandbox, --disable-setuid-sandbox, --disable-gpu, --disable-dev-shm-usage, --disable-featuresIsolateOrigins,site-per-process, # 关键禁用跨域隔离避免Referer丢失 ] ) context browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, # 强制启用JavaScript禁用图片加载提速 java_script_enabledTrue, bypass_cspTrue, ignore_https_errorsTrue, ) # 注入基础Cookie模拟已登录用户降低风控阈值 context.add_cookies([{ name: _hdfl, value: valid_session_token_here, # 从真实浏览器中手动复制 domain: .haodf.com, path: /, httpOnly: True, secure: True, sameSite: Lax }]) return context, pw注意_hdflCookie必须从真实登录的Chrome浏览器中复制有效期7天。不能用Requests模拟登录因为登录接口有图形验证码设备指纹双重校验。我们采用“人工预置自动续期”模式每天上午9点运维同事用公司账号登录一次更新token到配置中心。4.2 渐进式抓取流程设计整个流程分四阶段每阶段都有明确的成功标准和熔断机制阶段一医生主页探测Doctor Probe目标确认医生主页是否存在、是否开通评价功能、获取初始元数据。执行逻辑访问https://www.haodf.com/doctor/{doctor_id}.html等待div classdoctor-info出现证明页面加载成功执行JS提取window.__INITIAL_STATE__.doctor.hasCommentSection判断是否有评价区若为False记录statusNO_COMMENT跳过后续步骤若为True提取window.__INITIAL_STATE__.doctor.id和window.__INITIAL_STATE__.doctor.name阶段二评论流初始化Comment Stream Init目标获取首屏评论及分页令牌。执行逻辑点击“全部评价”按钮button:has-text(全部评价)等待div.comment-list出现且包含至少1个.comment-item捕获/api/comment/list响应解析data.next_token和data.total_count若total_count 5视为低活跃度医生只抓首屏避免过度请求阶段三分页循环抓取Paginated Fetch目标按令牌链获取全部评论但严格限速。执行逻辑每次请求间隔随机3.2~5.8秒模拟人类阅读停顿每抓取20页强制休眠47秒模拟“离开电脑去倒水”请求失败时重试3次每次增加1秒延迟第4次失败则标记statusTEMP_BLOCKED2小时后重试每页最多取20条评论平台限制若data.list长度20视为最后一页阶段四数据净化与存储Sanitization Storage目标剥离敏感信息生成合规数据包。执行逻辑删除所有患者昵称替换为患者A、患者B...脱敏病情描述用正则匹配(?i)(胃|肝|肺|心|肾|糖尿病|高血压|肿瘤|癌症|感染|疼痛|不适|症状)替换为[疾病类型]保留星级、评价时间、关键词如“耐心”、“专业”、“回复及时”、医生ID存入SQLite表comments_cleaned字段id(主键),doctor_id,star_rating,keywords,date,cleaned_text,crawl_time4.3 核心代码实现精简版import sqlite3 import time import random from datetime import datetime from playwright.sync_api import sync_playwright def crawl_doctor_comments(doctor_id: str): context, pw get_browser_context() page context.new_page() try: # 阶段一探测主页 page.goto(fhttps://www.haodf.com/doctor/{doctor_id}.html) page.wait_for_selector(div.doctor-info, timeout15000) has_comment page.evaluate( () { const state window.__INITIAL_STATE__; return state state.doctor state.doctor.hasCommentSection; } ) if not has_comment: return {status: NO_COMMENT, doctor_id: doctor_id} # 阶段二初始化评论流 page.click(button:has-text(全部评价)) page.wait_for_selector(div.comment-list, timeout20000) with page.expect_response(lambda r: /api/comment/list in r.url) as response_info: # 确保点击后触发请求 pass init_resp response_info.value.json() next_token init_resp[data][next_token] total_count init_resp[data][total_count] # 阶段三分页抓取 all_comments [] current_offset 0 while len(all_comments) total_count and current_offset 500: # 最多抓500条 # 构造下一页请求 payload { doctorId: doctor_id, token: next_token, offset: current_offset, limit: 20 } # 模拟人类行为延迟 time.sleep(random.uniform(3.2, 5.8)) # 发送请求Playwright自动携带Referer和Cookie with page.expect_response(lambda r: /api/comment/next in r.url) as response_info: page.evaluate(f fetch(/api/comment/next, {{ method: POST, headers: {{Content-Type: application/json}}, body: JSON.stringify({payload}) }}); ) next_resp response_info.value.json() # 解析并净化数据 for raw in next_resp[data][list]: cleaned { doctor_id: doctor_id, star_rating: raw[star], keywords: ,.join(raw.get(tags, [])), date: raw[create_time][:10], # 只留日期 cleaned_text: sanitize_comment(raw[content]), crawl_time: datetime.now().isoformat() } all_comments.append(cleaned) # 更新分页参数 next_token next_resp[data].get(next_token, ) current_offset 20 # 检查是否到最后一页 if not next_token or len(next_resp[data][list]) 20: break # 阶段四存储净化数据 conn sqlite3.connect(haodf_cleaned.db) cursor conn.cursor() cursor.executemany( INSERT INTO comments_cleaned (doctor_id, star_rating, keywords, date, cleaned_text, crawl_time) VALUES (?, ?, ?, ?, ?, ?) , [(c[doctor_id], c[star_rating], c[keywords], c[date], c[cleaned_text], c[crawl_time]) for c in all_comments]) conn.commit() conn.close() return { status: SUCCESS, doctor_id: doctor_id, fetched_count: len(all_comments), total_count: total_count } except Exception as e: return {status: ERROR, doctor_id: doctor_id, error: str(e)} finally: page.close() context.close() pw.stop() def sanitize_comment(text: str) - str: 脱敏评论文本保留语义结构 # 删除患者标识 text re.sub(r患者[\u4e00-\u9fa5a-zA-Z0-9\u3000-\u303f\uff00-\uffef], 患者X, text) # 替换疾病关键词 disease_patterns [ r(?i)胃.*?病|胃炎|胃溃疡|胃癌, r(?i)肝.*?病|肝炎|肝硬化|肝癌, r(?i)肺.*?病|肺炎|肺结核|肺癌, r(?i)糖尿病|高血压|冠心病|心梗|脑梗 ] for pattern in disease_patterns: text re.sub(pattern, [疾病类型], text) return text.strip()这套方案在我们实际部署中单台4核8G服务器可并发运行12个Playwright实例日均处理2300医生成功率92.7%失败主因是医生主动关闭评价功能。最关键的是所有操作都留有完整审计日志每次请求的URL、响应状态码、耗时、IP出口、操作人账号——这既是技术保障也是法律合规的证据链。5. 常见问题与实战避坑指南那些文档里不会写的血泪教训在三年维护这套爬虫的过程中我整理了23个高频问题其中17个是网上教程绝不会提、但会让你在深夜崩溃的细节。下面挑最痛的五个说透5.1 “为什么我的Playwright脚本在本地能跑放到服务器就403”根本原因不是IP被封而是时区和系统字体差异触发了设备指纹校验。Playwright在Linux服务器上默认用Debian的字体栈DejaVu Sans而“好大夫”的前端JS会执行document.fonts.check(12px DejaVu Sans)如果返回false就认为是无头环境。解决方案不是装字体而是启动时注入字体声明browser pw.chromium.launch( args[--font-render-hintingnone], chromium_sandboxFalse ) # 启动后立即执行 page.add_style_tag(content font-face { font-family: Arial; src: local(DejaVu Sans); } )5.2 “评论里的时间戳是相对时间如‘3天前’怎么转成标准日期”别用JS的Date.parse()它在不同浏览器中解析“3天前”结果不一致。正确做法是在页面加载完成时用document.querySelector(meta[propertyog:updated_time]).content获取页面最后更新时间ISO格式再用Python的dateutil.relativedelta计算from dateutil import relativedelta from datetime import datetime def parse_relative_time(relative_str: str, base_time: str) - str: 将3天前转为ISO日期 now datetime.fromisoformat(base_time.replace(Z, 00:00)) if 天前 in relative_str: days int(re.search(r(\d), relative_str).group(1)) target now - relativedelta.relativedelta(daysdays) return target.date().isoformat() # 其他情况类似处理...5.3 “为什么抓到的评论总比网页上看到的少10%”这是“好大夫”的动态过滤机制在作祟。它前端会根据用户地理位置、设备类型、访问时段实时隐藏部分评论如外地患者评价、夜间发布的差评。解决方案是在Playwright启动时固定地理坐标和设备参数context browser.new_context( geolocation{latitude: 39.9042, longitude: 116.4074}, # 北京坐标 permissions[geolocation], device_scale_factor1.0, is_mobileFalse, has_touchFalse )5.4 “如何检测自己是否被平台标记为高风险”看三个信号响应头里的X-RateLimit-Remaining突然从1000降到0正常应800评论数据中出现{is_ad: true}字段平台插入的广告评论正常不应有页面底部出现div idrisk-warning元素肉眼不可见但DOM存在一旦触发立即停止该IP的所有请求切换到备用IP池并发送navigator.sendBeacon(/api/risk/clear, force)需提前注入该函数。5.5 “能否用ScrapySplash替代Playwright”不能。Splash的Lua脚本无法处理“好大夫”的WebGL指纹检测且其渲染超时机制会导致大量请求被截断。我们实测过同样抓取100个医生ScrapySplash成功率仅61%平均耗时是Playwright的2.3倍。根本差距在于——Splash是“渲染快照”Playwright是“真实浏览器实例”而“好大夫”的反爬逻辑深度绑定浏览器运行时环境。最后分享一个真实教训去年我们团队为赶进度把抓取频率从“每医生间隔5分钟”改成“并发100路每秒1请求”结果2小时内被全站封禁IP段。恢复后法务部要求所有爬虫必须增加“随机休眠因子”公式是sleep_time base_delay * (1 random.uniform(-0.3, 0.7))。现在回头看慢一点反而走得更远。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

慢任务快回答:VisionClaw异步结果交付的心跳、延迟降级与关键词验证设计 2026/10/2 14:05:28

慢任务快回答:VisionClaw异步结果交付的心跳、延迟降级与关键词验证设计

慢任务快回答:VisionClaw异步结果交付的心跳、延迟降级与关键词验证设计 【免费下载链接】VisionClaw Real-time AI assistant for Meta Ray-Ban smart glasses -- voice vision agentic actions via Gemini Live and OpenClaw 项目地址: https://gitcode.com/g…

阅读更多 →
6 种素材全支持:前任.skill 数据来源准备指南(微信 / iMessage / 短信 / 照片 / 社交媒体) 2026/10/2 14:05:22

6 种素材全支持:前任.skill 数据来源准备指南(微信 / iMessage / 短信 / 照片 / 社交媒体)

6 种素材全支持:前任.skill 数据来源准备指南(微信 / iMessage / 短信 / 照片 / 社交媒体) 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 想把回忆蒸馏成 前任.skill,第一…

阅读更多 →
从强化学习规模化到自我改进:MiMo-V2.6 开源技术报告深度拆解 2026/10/2 14:05:16

从强化学习规模化到自我改进:MiMo-V2.6 开源技术报告深度拆解

如果把 DeepSeek-R1 的发布当成大模型开源的一个分水岭,那 MiMo-V2.6 算是分水岭之后少有的、值得你关上门泡好茶逐段精读的项目。标题里“第一开源大模型”这个说法,放在哪个社区都会引发一轮名分之争——我不打算替它争这个“第一”,真正让…

阅读更多 →
AI Skills 实战:从安装、挑选到编写与踩坑指南 2026/10/2 14:05:03

AI Skills 实战:从安装、挑选到编写与踩坑指南

我前阵子花了整整两个晚上,把 GitHub 上那些叫 skills 的东西挨个翻了一遍。不看不知道,这里面水确实深,有写得像样的,有纯粹凑数的,还有不少是作者自己都没用过就丢上去的。这篇文章不打算讲什么高深理论,…

阅读更多 →
AI-For-Beginners 教师指南:如何用 GitHub Classroom 与开源 AI 课程组织课堂 2026/10/2 14:04:31

AI-For-Beginners 教师指南:如何用 GitHub Classroom 与开源 AI 课程组织课堂

教程人工智能机器学习深度学习 【免费下载链接】AI-For-Beginners 12 Weeks, 24 Lessons, AI for All! 项目地址: https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners 点击查看 免费下载 本指南面向计划将 AI-For-Beginners 课程 引入课堂的教师&#xff0c…

阅读更多 →
Unity游戏动态更换App图标:Android与iOS双端实现全解析 2026/10/2 14:04:30

Unity游戏动态更换App图标:Android与iOS双端实现全解析

1. 为什么要做动态图标:需求场景与方案选型做游戏运营的朋友一定深有体会:版本更新、节日活动、联动 IP 上线,都是拉新和召回的关键节点。App 在桌面上的图标,其实是用户每天打开手机第一眼就看到的核心广告位,成本为零…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉