首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >十个互联网医院平台横向对比是怎么做的:指标、采样频次与代码骨架

十个互联网医院平台横向对比是怎么做的:指标、采样频次与代码骨架

原创
作者头像
小白学大数据
发布2026-09-08 17:02:32
发布2026-09-08 17:02:32
1170
举报

家里老人去年开始频繁跑医院复查,我在外地帮不上忙,能指望的就是在线问诊。研究一圈下来发现一个尴尬的事实:广告都打得响,能横向对比的数据没人给。每家都说自己医生多、响应快、有三甲背景,"多"是多少,"快"是多快,全凭一张宣传稿。

既然没人给数据,我就自己动手。评估框架分三步:先核查资质,再固定指标和采样频次,最后用脚本把各平台公开的服务信息拉下来结构化对比,跑一段时间看差异。这篇文章先把名单和筛选标准讲清楚,再把脚本骨架拆开,代码可以直接改成自己的版本。

先说名单:十个平台和它们的底细

在线问诊平台分两类底子。一类是实体医院自建的互联网医院,诊疗直接挂在医院体系内;另一类是企业申办的互联网医院,按监管要求同样必须依托实体医疗机构,取得《医疗机构执业许可证》并在诊疗科目中加注"互联网诊疗"。2018 年国家卫健委等部门发布《互联网医院管理办法(试行)》等三个配套文件,2022 年又出台《互联网诊疗监管细则(试行)》,准入门槛一直在抬高。想核实一家平台是否正规,别只看 App 评分,去国家卫健委官网查它的医疗机构执业登记,再查目标医生的医师执业注册信息,比什么都有说服力。

下面十家都是跑了多年、能查到资质的主流平台,按我调研的顺序列,不构成排名:

平台

业务底色

相对突出的点

微医

挂号起家,乌镇互联网医院

三甲号源对接早,预约转诊链条完整

好大夫在线

医生评价体系沉淀深

图文、电话问诊覆盖广,医生审核较严

京东健康

互联网医院叠加药品供应链

电子处方流转和送药履约是强项

阿里健康

背靠支付宝、淘宝流量入口

医鹿 App 集成度高,触达用户基数大

平安健康

前身平安好医生,自有医学团队

有 AI 导诊分流,问诊路径短

丁香医生

医学内容社区出身

循证倾向明显,回答相对克制

春雨医生

老牌轻问诊

起步早,图文问诊价格门槛低

医联

慢病管理导向

专科随访、病程记录做得细

妙手医生

慢病复诊加药品配送

处方药履约链路完整

腾讯健康

微信小程序入口

打开即用,老人上手成本低

排这张表有几个没想到的地方。丁香医生不少人以为只做科普,问诊入口其实藏得深,但医生回答普遍规范;微医的 App 里挂号功能远比问诊醒目,冲着问诊去的新用户容易绕晕。这类体感信息宣传页看不出来,得真用几轮才知道。

测什么:先定指标,再上脚本

脚本只是工具,指标得先定死。我挑的都是能客观量化的公开信息:

  • 目标科室在线医生数,固定时段抽样
  • 图文问诊价格区间
  • 首响时间,即提交问诊到医生首次回复的间隔
  • 药事服务,是否支持电子处方、药品能否配送到家

为什么强调固定时段抽样?医生在线数在早中晚差异很大,单次抓取只是横截面快照,只有按固定频次跑一段时间,攒出时间序列,横向对比才有意义。给老人选慢病随访平台,我盯在线医生密度和首响时间;感冒发烧开药,看价格区间和送药范围。指标跟着需求走,脚本才不会变成自嗨。

接口定位不需要逆向。打开目标页面或网页版,F12 进开发者工具,Network 面板过滤 XHR,刷新后挑返回 JSON 的请求逐个看响应,医生列表、价格、号源状态通常就在里面。医疗平台的风控普遍更严,签名参数、请求头校验都常见,第一次拿到 403 先检查 header 和 cookie,别急着硬刚。

合规边界:先划红线再写代码

这类采集,合规优先级高于功能。医疗数据比一般电商敏感,《个人信息保护法》《数据安全法》对个人信息和重要数据的处理有明确约束。我给自己定的执行边界:

  1. 只采集平台主动公开展示的服务信息:医生职称、价格、在线状态、号源情况。
  2. 不采集、不留存任何患者相关内容,聊天记录、处方明细、健康档案一概不碰。
  3. 控制请求频次,给单平台并发设上限,不留存原始响应全文。
  4. 遵守 robots 与平台服务条款,脚本仅用于个人调研,不对外提供接口或数据。

这条边界直接决定技术选型:不需要分布式采集,单机加可控并发足够;但十家平台同时盯,出口 IP 的限流问题躲不掉,这两点都在下面的代码里处理了。

代码:带限速和代理池的探测骨架

工程上把每家平台的探测逻辑收敛成独立模块,统一入口、统一日志、统一重试。骨架是精简过的,能跑通,解析部分按目标平台接口改即可。

数据模型和日志

用 dataclass 记录一次探测结果,字段带类型注解,采样时间一并记下,方便后面按时间序列分析:

代码语言:javascript
复制
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 下不会因为中文炸掉:

代码语言:javascript
复制
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 是客户端问题,重试多少次都白费。退避用指数加随机抖动,避免重试请求在同一时刻撞在一起。代理池顺手做成一个类,轮询取出出口:

代码语言:javascript
复制
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 是全局并发闸门,等于给整体请求频率设了硬上限,这正是合规边界里那条"控制频次"的落点:

代码语言:javascript
复制
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 打开中文不乱码:

代码语言:javascript
复制
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 占位,替换成你实测拿到的公开接口即可:

代码语言:javascript
复制
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 按行解析喂给脚本,还能按城市、运营商选路,适合周期性监测这种场景。提取代码:

代码语言:javascript
复制
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 即可:

代码语言:javascript
复制
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 删除。

目录
  • 先说名单:十个平台和它们的底细
  • 测什么:先定指标,再上脚本
  • 合规边界:先划红线再写代码
  • 代码:带限速和代理池的探测骨架
    • 数据模型和日志
    • 重试与代理池
    • 单平台探测
    • 落盘和总入口
  • 十家平台轮询,为什么需要代理池
  • 方法学说明与局限
  • 怎么选,我的建议
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档