
家里老人去年开始频繁跑医院复查,我在外地帮不上忙,能指望的就是在线问诊。研究一圈下来发现一个尴尬的事实:广告都打得响,能横向对比的数据没人给。每家都说自己医生多、响应快、有三甲背景,"多"是多少,"快"是多快,全凭一张宣传稿。
既然没人给数据,我就自己动手。评估框架分三步:先核查资质,再固定指标和采样频次,最后用脚本把各平台公开的服务信息拉下来结构化对比,跑一段时间看差异。这篇文章先把名单和筛选标准讲清楚,再把脚本骨架拆开,代码可以直接改成自己的版本。
在线问诊平台分两类底子。一类是实体医院自建的互联网医院,诊疗直接挂在医院体系内;另一类是企业申办的互联网医院,按监管要求同样必须依托实体医疗机构,取得《医疗机构执业许可证》并在诊疗科目中加注"互联网诊疗"。2018 年国家卫健委等部门发布《互联网医院管理办法(试行)》等三个配套文件,2022 年又出台《互联网诊疗监管细则(试行)》,准入门槛一直在抬高。想核实一家平台是否正规,别只看 App 评分,去国家卫健委官网查它的医疗机构执业登记,再查目标医生的医师执业注册信息,比什么都有说服力。
下面十家都是跑了多年、能查到资质的主流平台,按我调研的顺序列,不构成排名:
平台 | 业务底色 | 相对突出的点 |
|---|---|---|
微医 | 挂号起家,乌镇互联网医院 | 三甲号源对接早,预约转诊链条完整 |
好大夫在线 | 医生评价体系沉淀深 | 图文、电话问诊覆盖广,医生审核较严 |
京东健康 | 互联网医院叠加药品供应链 | 电子处方流转和送药履约是强项 |
阿里健康 | 背靠支付宝、淘宝流量入口 | 医鹿 App 集成度高,触达用户基数大 |
平安健康 | 前身平安好医生,自有医学团队 | 有 AI 导诊分流,问诊路径短 |
丁香医生 | 医学内容社区出身 | 循证倾向明显,回答相对克制 |
春雨医生 | 老牌轻问诊 | 起步早,图文问诊价格门槛低 |
医联 | 慢病管理导向 | 专科随访、病程记录做得细 |
妙手医生 | 慢病复诊加药品配送 | 处方药履约链路完整 |
腾讯健康 | 微信小程序入口 | 打开即用,老人上手成本低 |
排这张表有几个没想到的地方。丁香医生不少人以为只做科普,问诊入口其实藏得深,但医生回答普遍规范;微医的 App 里挂号功能远比问诊醒目,冲着问诊去的新用户容易绕晕。这类体感信息宣传页看不出来,得真用几轮才知道。
脚本只是工具,指标得先定死。我挑的都是能客观量化的公开信息:
为什么强调固定时段抽样?医生在线数在早中晚差异很大,单次抓取只是横截面快照,只有按固定频次跑一段时间,攒出时间序列,横向对比才有意义。给老人选慢病随访平台,我盯在线医生密度和首响时间;感冒发烧开药,看价格区间和送药范围。指标跟着需求走,脚本才不会变成自嗨。
接口定位不需要逆向。打开目标页面或网页版,F12 进开发者工具,Network 面板过滤 XHR,刷新后挑返回 JSON 的请求逐个看响应,医生列表、价格、号源状态通常就在里面。医疗平台的风控普遍更严,签名参数、请求头校验都常见,第一次拿到 403 先检查 header 和 cookie,别急着硬刚。
这类采集,合规优先级高于功能。医疗数据比一般电商敏感,《个人信息保护法》《数据安全法》对个人信息和重要数据的处理有明确约束。我给自己定的执行边界:
这条边界直接决定技术选型:不需要分布式采集,单机加可控并发足够;但十家平台同时盯,出口 IP 的限流问题躲不掉,这两点都在下面的代码里处理了。
工程上把每家平台的探测逻辑收敛成独立模块,统一入口、统一日志、统一重试。骨架是精简过的,能跑通,解析部分按目标平台接口改即可。
用 dataclass 记录一次探测结果,字段带类型注解,采样时间一并记下,方便后面按时间序列分析:
from __future__ import annotations
import asyncio
import logging
import random
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable
import httpx
logger = logging.getLogger("probe")
@dataclass(slots=True)
class ProbeResult:
platform: str
endpoint: str
ok: bool
latency_ms: float
attempts: int
detail: str = ""
sampled_at: str = field(
default_factory=lambda: datetime.now().isoformat(timespec="seconds")
)日志终端和文件双写,编码用 utf-8,Windows 下不会因为中文炸掉:
def setup_logging() -> None:
fmt = "%(asctime)s %(levelname)s %(name)s: %(message)s"
logging.basicConfig(
level=logging.INFO,
format=fmt,
handlers=[
logging.StreamHandler(),
logging.FileHandler("probe.log", encoding="utf-8"),
],
)网络请求没有不失败的,重试要有章法:429 和 5xx 值得重试,4xx 是客户端问题,重试多少次都白费。退避用指数加随机抖动,避免重试请求在同一时刻撞在一起。代理池顺手做成一个类,轮询取出出口:
RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
class ProxyPool:
"""轮询代理列表,每次请求取一个出口 IP。"""
def __init__(self, proxies: list[str]) -> None:
self._proxies = proxies
self._cursor = 0
def next(self) -> str | None:
if not self._proxies:
return None
proxy = self._proxies[self._cursor % len(self._proxies)]
self._cursor += 1
return proxy
async def get_with_retry(
client: httpx.AsyncClient,
url: str,
*,
pool: ProxyPool | None = None,
max_attempts: int = 3,
base_delay: float = 0.5,
) -> httpx.Response:
last_error: httpx.HTTPError | None = None
for attempt in range(1, max_attempts + 1):
proxy = pool.next() if pool else None
try:
resp = await client.get(url, timeout=httpx.Timeout(8.0), proxy=proxy)
if resp.status_code in RETRYABLE_STATUS:
raise httpx.HTTPStatusError(
f"HTTP {resp.status_code}", request=resp.request, response=resp
)
return resp
except httpx.HTTPError as exc:
last_error = exc
logger.warning("第 %d 次请求 %s 失败(代理 %s):%s", attempt, url, proxy, exc)
if attempt == max_attempts:
break
delay = base_delay * (2 ** (attempt - 1)) + random.uniform(0, 0.3)
await asyncio.sleep(delay)
if last_error is not None:
raise last_error
raise RuntimeError("unreachable")按请求传 proxy= 需要 httpx 0.26 及以上版本,老版本只能在构造 AsyncClient 时统一传 proxies,代价是切换粒度变粗。
每家平台返回结构不同,用回调注入解析逻辑,主流程保持统一。asyncio.Semaphore 是全局并发闸门,等于给整体请求频率设了硬上限,这正是合规边界里那条"控制频次"的落点:
def parse_online_count(data: dict) -> str:
total = data.get("data", {}).get("total", 0)
return f"当前在线医生 {total} 人"
async def probe_platform(
client: httpx.AsyncClient,
name: str,
url: str,
parse: Callable[[dict], str],
sem: asyncio.Semaphore,
) -> ProbeResult:
async with sem:
started = asyncio.get_running_loop().time()
try:
resp = await get_with_retry(client, url)
detail = parse(resp.json())
elapsed = (asyncio.get_running_loop().time() - started) * 1000
return ProbeResult(name, url, True, elapsed, 1, detail)
except (httpx.HTTPError, ValueError, KeyError) as exc:
return ProbeResult(name, url, False, 0.0, 3, str(exc))ProbeResult 是 dataclass,转 CSV 不用手写拼接。编码用 utf-8-sig,Excel 打开中文不乱码:
import csv
from dataclasses import asdict, fields
def save_csv(results: list[ProbeResult], path: str) -> None:
if not results:
return
with open(path, "w", newline="", encoding="utf-8-sig") as fh:
writer = csv.DictWriter(fh, fieldnames=[f.name for f in fields(ProbeResult)])
writer.writeheader()
for r in results:
writer.writerow(asdict(r))入口把各平台公开接口地址收在一起,并发跑完统一落盘。接口地址每个平台都不同,用 api.example.com 占位,替换成你实测拿到的公开接口即可:
async def main() -> None:
setup_logging()
configs = [
("微医", "https://api.example.com/v1/online_doctors", parse_online_count),
("好大夫在线", "https://api.example.com/v1/online_doctors", parse_online_count),
]
sem = asyncio.Semaphore(2) # 同一时刻最多 2 个在途请求
async with httpx.AsyncClient(
headers={"User-Agent": "personal-research/0.1"},
follow_redirects=True,
timeout=httpx.Timeout(8.0),
) as client:
tasks = [
probe_platform(client, name, url, parse, sem)
for name, url, parse in configs
]
results = await asyncio.gather(*tasks)
save_csv(results, "probe_results.csv")
for r in results:
state = "正常" if r.ok else "失败"
logger.info("%s:%s,耗时 %.0f ms,%s", r.platform, state, r.latency_ms, r.detail)
if __name__ == "__main__":
asyncio.run(main())十家平台并发盯,即便单家 QPS 压得很低,累计请求量也会让本地出口 IP 触达平台的风控阈值,表现是请求集中 429,或连接被直接重置。单机单 IP 没有解,工程常规做法是代理池:请求前取一个出口,失败换下一个,把限流分摊到一批 IP 上。
代理可以直接走服务商提供的提取 API。我自己接的亿牛云支持按需提取短效代理,返回的 IP 按行解析喂给脚本,还能按城市、运营商选路,适合周期性监测这种场景。提取代码:
def load_proxies(extract_url: str, *, timeout: float = 10.0) -> list[str]:
resp = httpx.get(extract_url, timeout=timeout)
resp.raise_for_status()
proxies = [line.strip() for line in resp.text.splitlines() if line.strip()]
logger.info("提取代理 %d 个", len(proxies))
return proxies接入时把池子传给 get_with_retry 即可:
EXTRACT_URL = "https://api.example.com/extract?num=20" # 换成服务商给的提取地址
async def demo(client: httpx.AsyncClient, url: str) -> httpx.Response:
pool = ProxyPool(load_proxies(EXTRACT_URL))
return await get_with_retry(client, url, pool=pool)有两句话必须说透。其一,代理只解决 IP 维度的限流,签名参数、时间戳、TLS 指纹那套风控属于另一层逆向工程,遇到明显在防爬的接口,建议换调研方式,比如用官方公布的服务数据,别把精力耗在跟风控对抗上。其二,代理池不是放开频率的理由,Semaphore 限速和合规边界照旧,它只是让限流来得更晚,不是免责牌。
这套对比有几个先天局限,说清楚免得误导。在线医生数是时点快照,早晚高峰差异大;价格和活动随时在变,采到的只是当时值;各平台科室覆盖不一样,跨平台对比必须锁在同一科室才有意义。所以结论看趋势、看结构,别当精确榜单。另外,文中平台信息基于公开资料整理,使用体验部分来自实际使用,均不构成医疗建议,身体不适请优先线下就诊。
公开资料逐项过完,加上家里人实际用下来的感受,我的筛选标准可以压成四条:平台有可查的互联网医院资质;目标医生能查到执业注册信息;从问诊到取药的流程完整;投诉和售后渠道真实存在。四条过滤完,剩下的差异就是场景偏好。
给老人小孩做决策,我倾向好大夫在线和微医,医生基数大,能挂到具体科室慢慢挑。人在外地需要送药上门,京东健康和阿里健康的履约链路最顺。慢病长期随访,医联把病程管理做进了产品里。头疼脑热想快速问一句,春雨和丁香的轻问诊门槛低。腾讯健康赢在入口,微信直接打开,老人不用另装 App。没有平台能在所有维度都赢,把场景想清楚,比记十个名字管用。
脚本我先挂着跑,攒够跨时段的样本,再把各平台首响时间和价格的实测序列整理出来,另写一篇。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。