网虫Spider订阅
← 文章列表
工程实践9 min read

让 IP 纯净度的每一分都能被读者自己验算:四条判据、四套权重,五个分数手算全对

同类工具给一个不解释的数字。这篇把评分过程整个摊开:一个 IP 的四条判据、总分怎么来的、四个使用场景怎么从同一批判据得出 58 到 91 的不同结论——全部用线上接口返回的真实数据手算复现,一分不差。

查 IP 纯净度的工具都会给你一个分数。79 分。 然后呢?

79 是因为它在机房网段,还是因为它上过黑名单?这两件事对「拿它跑采集」的影响完全不同,但分数一样。你没法从一个数字里把它们分开,也就没法判断该不该信这个结论。

这篇把评分过程摊开,并且——全部数字你可以自己拿计算器验算

一、先把真实数据拿出来#

curl -s "https://ipure.art/api/lookup?ip=8.8.8.8"

评分部分:

"risk": {
  "purity": 78,
  "riskPoints": 22,
  "level": "neutral",
  "label": "一般",
  "verdict": "基本可用,严格风控的平台可能偶尔弹验证码",
  "confidence": "medium",
  "factors": [
    { "id": "usage",           "category": "hosting",  "points": 25,
      "label": "IP 类型:机房 / 数据中心",
      "detail": "机房 / 数据中心 IP 是自动化流量的主要来源" },
    { "id": "exposure:ports",  "category": "exposure", "points": 6,
      "label": "对外开放 2 个端口",
      "detail": "开放端口:53、443" },
    { "id": "native",          "category": "geo",      "points": -5,
      "label": "原生 IP",
      "detail": "注册地与使用地一致,由当地运营商直接分配" },
    { "id": "sharing",         "category": "sharing",  "points": -4,
      "label": "独享或低共享出口",
      "detail": "同网段仅观测到 6 台设备" }
  ]
}

现在验算:

25 + 6 + (-5) + (-4) = 22        ← riskPoints
100 - 22 = 78                     ← purity

对上了。四条判据,加起来就是分数,没有隐藏项。

报告页把同一件事画出来:

「每一分的来源」:四条判据各一行,横向条形图按分值长短排列,右侧标 −25 / −6 / +5 / +4

左侧扣分、右侧加分,共用同一比例尺

二、为什么是累加风险点,不是加权平均#

scoring/score.ts
/**
 * 内部模型是「从 0 分起累加风险点」而非加权平均:一个 IP 只要命中 Tor 出口,
 * 无论其它指标多干净都必须是高风险;平均模型会把这种决定性证据稀释掉。
 *
 * 对外输出的是纯净度(100 - 风险点数,越高越干净)。两套尺度并存是有意的:
 * 累加风险点让 floor(一票否决)有清晰语义,而纯净度才符合用户直觉。
 */

这是个真实的建模选择。加权平均的问题:一个 Tor 出口如果地理清晰、无黑名单记录、注册信息完整,平均下来可能还是个中等分——而它是 Tor 出口这一件事,本身就该一票否决

累加模型里「一票否决」有清晰语义(给一个足够大的点数,或者设 floor),平均模型里没有。

但对外必须换成纯净度,因为「78 分」符合直觉,「22 风险点」不符合。两套尺度并存不是冗余,是内部建模和对外表达各取所需。

三、结论只说后果,不说原因#

分档表里有条注释值得单独拎出来:

/**
 * verdict 一律只描述**后果**,不说原因。同样是 47 分,可能是纯粹因为
 * 落在机房网段,也可能是因为上了黑名单 —— 按档位写死的结论没法区分,
 * 说"多个数据源判定异常"就会在前一种情况下变成臆断。
 * 原因由 factors 逐条列出,那里才是准确的。
 */

8.8.8.8 的 verdict 是「基本可用,严格风控的平台可能偶尔弹验证码」——它描述的是你会遇到什么,没有说「因为它是机房 IP」。

原因在 factors 里,逐条带着分值和来源。按档位生成的解释一定会在某些情况下说谎,因为同一个分数可以由完全不同的判据组合得出。

四、同一批判据,四个场景,58 到 91#

这是我认为最有用的一部分。

scoring/scenario.ts
/**
 * 关键在于**不同用途对 IP 的要求差别很大**,把总分复制几份贴上不同标签
 * 是没有信息量的。举两个相反的例子:
 *
 *   - ChatGPT 对机房和代理出口的容忍度极低,却完全不关心这个 IP
 *     有没有上过垃圾邮件黑名单。
 *   - 发信恰恰相反:机房 IP 发信本来就正常,而 Spamhaus 收录是生死线。
 *
 * 所以每个场景对判据类别各有一套权重,同一份情报能得出真正不同的结论。
 */

四套权重(1 = 与总分一致,0 = 完全不关心,未列出的按 1 处理):

场景权重
AI 服务proxy:2 hosting:1.8 vendor:1.5 sharing:1.2 abuse:0.8 geo:0.8 blocklist:0.2
流媒体geo:2 proxy:1.8 sharing:1.6 hosting:1.4 vendor:1.2 exposure:0.8 blocklist:0.2
跨境电商sharing:2 hosting:1.6 proxy:1.5 abuse:1.3 vendor:1.2 blocklist:0.5
邮件发送blocklist:2.5 abuse:1.8 rdns:1.5 vendor:0.8 exposure:0.8 hosting:0.4 geo:0.3

注意 blocklist 这一列:AI 场景 0.2,邮件场景 2.5,差 12.5 倍。这不是调参调出来的,是这两件事本来就不相干——ChatGPT 不查 Spamhaus。

验算#

拿上面那四条判据,套四套权重,和接口返回的分数对账:

08-场景分验算.py
FACTORS = [                      # (id, category, points)
    ("usage",          "hosting",  25),
    ("exposure:ports", "exposure",  6),
    ("native",         "geo",      -5),
    ("sharing",        "sharing",  -4),
]
for sid, w in WEIGHTS.items():
    s = sum(p * w.get(cat, 1) for _, cat, p in FACTORS)
    mine = round(100 - s)
总分:风险点 25 + 6 + -5 + -4 = 22,纯净度 = 100 - 22 = 78
      接口返回 purity=78 riskPoints=22 → 一致
 
场景           加权明细                                          手算     接口
----------------------------------------------------------------------------
ai           25×1.8 + 6×1 + -5×0.8 + -4×1.2                    58     58    一致
streaming    25×1.4 + 6×0.8 + -5×2 + -4×1.6                    77     77    一致
ecommerce    25×1.6 + 6×1 + -5×1 + -4×2                        67     67    一致
email        25×0.4 + 6×0.8 + -5×0.3 + -4×1                    91     91    一致

五个数字,全部手算复现。

页面上就是这四张卡片:

四个场景卡片:AI 服务 58「勉强可用」、流媒体 77「适合」、跨境电商 67「勉强可用」、邮件发送 91「非常适合」

右上角那句「各场景对 IP 的要求不同,用的是各自的判断权重,不是同一个总分」,就是这套设计的一句话说明

同一个 8.8.8.8:拿去发信 91 分(机房 IP 发信本来就正常,八个黑名单全干净),拿去上 ChatGPT 58 分(机房属性被放大 1.8 倍)。

一个「78 分」的总分完全掩盖了这 33 分的差距。

五、置信度不是装饰#

"confidence": "medium",
"sources": [
  { "name": "rdap",           "kind": "free",       "ok": true,  "ms": 232 },
  { "name": "team-cymru",     "kind": "free",       "ok": true,  "ms": 93 },
  { "name": "ipqualityscore", "kind": "commercial", "ok": false, "ms": 0,
    "skipped": "未配置 IPQS_API_KEY" },
  { "name": "abuseipdb",      "kind": "commercial", "ok": false, "ms": 0,
    "skipped": "未配置 ABUSEIPDB_API_KEY" }
]

medium 不是随便标的:两个商业源缺席,而它们正是代理识别的主力。少了它们,「这个 IP 是不是住宅代理」这个问题没有被真正回答过,只是没有证据说是。

报告如实标 medium,而不是用一个漂亮的 high 蒙混过去。

还有两个更细的一致性字段#

"countryAgreement": { "agreed": 3, "total": 3 },
"flagAgreement":    { "isHosting": 1 }
  • 国家判定:三个源都说 US,三比零
  • 机房判定只有一个源说过

这两条摆在一起才有意义。8.8.8.8 是机房 IP 这件事人尽皆知,但从数据上看,这个判定背后只站着一家。而它恰恰是分值最高的那条判据(+25,占全部风险点的 100% 以上)。

六、一票否决要写死,不要靠分数#

有些结论不该由纯净度决定:

/**
 * ChatGPT / Claude / Gemini 三家均不提供服务的地区(ISO 3166-1 alpha-2)。
 *
 * 命中这些地区的 IP,**无论纯净度多高都上不了境外 AI**:中国大陆更是 GFW
 * 直连不通 + 地区不受支持双重封死。所以「AI 服务」结论要直接判为「地区受限」,
 * 而不是按纯净度给「适合 / 不可用」—— 一个 100 分的北京住宅 IP 也用不了 ChatGPT。
 *
 * 用 denylist 而非 allowlist:不在此列表的地区照常走纯净度评分,避免把合法的
 * 境外代理出口(美/日/新 等)误判为受限。
 */

两个设计点:

① 有些约束是硬的,不该被分数表达。 一个北京的住宅 IP 可以拿满分——它确实是干净的原生住宅 IP。但它上不了 ChatGPT,这跟干不干净没关系。硬塞进评分只会让分数失去意义。

② 用 denylist 而不是 allowlist。 allowlist 会把所有没列进去的地区判为受限——包括那些完全正常的美/日/新出口。默认放行、明确列出例外,错误方向朝着「漏判」而不是「误判」,对这个场景是对的。

七、这套东西能抄走什么#

不限于 IP 评分。任何要给出「一个数字 + 一个结论」的系统都适用:

  1. 累加而不是平均,如果你的领域里存在决定性证据(一票否决)
  2. 内部尺度和对外尺度可以不同——建模方便和用户直觉往往不是一回事
  3. 聚合层只说后果,解释留给明细层,否则解释一定会在某些区间里说谎
  4. 每条判据带上分值和来源,让人能自己加一遍
  5. 同一批证据、不同权重,得出真正不同的场景结论,而不是把总分复制几份贴标签
  6. 置信度要有据可依,缺数据就降置信度,别装满
  7. 记录「几个源确认」,单源和多源的证据强度差得远
  8. 硬约束写死,别塞进评分

第 4 条是全部的基础。能被读者自己验算的分数,和一个不解释的数字,是两种不同的东西——哪怕它们碰巧相等。


附:自己验一遍#

curl -s "https://ipure.art/api/lookup?ip=8.8.8.8" | python3 -c "
import json, sys
d = json.load(sys.stdin)['risk']
print('factors:', [(f['id'], f['points']) for f in d['factors']])
print('sum     =', sum(f['points'] for f in d['factors']))
print('report  =', d['riskPoints'], '/ purity', d['purity'])
"

数据源的返回随时在变,你跑出来的判据可能和本文不同。要验的不是这几个数字,是「加起来等不等于报告里的那个数」。


参考#


Minner
Minner

长期做数据采集与逆向分析,同时负责采集系统后端与数据管道。方向集中在自媒体与电商平台的数据获取、接口协议还原,以及在此之上的数据工程与分析。