Skip to content

网课平台怎么防挂机刷课?用 IP 归属地检测异地代刷(Python 实操) ​

去搜索引擎搜「刷课」,第一屏几乎全是代刷平台的广告:智慧树、学习通、职教云,明码标价,甚至支持「下单后十几分钟刷完」。这门生意能做大,恰恰说明各网课平台的反制手段还停留在「行为检测」层面——脚本模拟点击、随机暂停,躲开行为模型并不难。但有一类信号脚本很难伪造:IP 归属地。

刷课的本质是「一个真人(或一台云机)批量操作大量账号」。把账号、设备、IP 三条线交叉起来看,异常就会现形。这篇讲平台侧怎么用 IP 归属地 + 简单规则,在 Python 里搭一套刷课检测原型,把可疑账号筛出来交给人工复核。

一、刷课行为在 IP 上留了哪些破绽 ​

代刷服务有几类,各自的 IP 特征不一样:

  • 真人代刷(兼职学生用自己账号帮刷):同一批账号的登录 IP 高度集中——要么同一个宿舍 Wi-Fi,要么同一个城市的家庭宽带,几十个账号挤在几组 IP 上;
  • 云机/云手机托管:账号固定挂在某几个 IDC 机房 IP 上,白天黑夜不间断,运营商字段直接暴露机房特征;
  • 脚本挂机:用公用代理池轮换,IP 属地频繁跳变,今天山东明天福建,与账号实名信息对不上。

共同点是违反直觉:正常学生一天最多一两段学习,登录 IP 稳定;刷课账号要么「一 IP 拖 N 账号」,要么「一地跳三地」。这两条分别是横向聚集和纵向突变,用归属地数据一查一个准。

二、方案设计:事件流水 -> 特征提取 -> 规则打分 ​

不搞机器学习,先用可解释的规则引擎。数据源是现成的:网课平台本来就会记录每次学习会话(账号、IP、时间戳、时长),只要把这些日志过一遍归属地查询,再套四条规则:

  1. 同 IP 多账号:一个 IP 在窗口期内关联超过 N 个账号(共享 NAT 场景单独放行,见第四节);
  2. 跨省突变:同一账号两次会话的省市不一致,且间隔小于阈值;
  3. 机房特征:IP 运营商是 IDC/数据中心,或 VIP 字段 ip_type 为 IDC(家庭宽带和机房混在一起时这是最硬的特征);
  4. 深夜批量:凌晨 0-6 点出现大量会话且时长整齐。

每条规则命中记 1 分,按分数三档处置:0-1 分放行、2 分进人工复核名单、3 分以上直接冻结并触发验证码。分数规则可调,先保证低误伤。

三、Python 实现:会话日志 -> 可疑名单 ​

原型就做一件事:读一份学习会话 JSON,逐个 IP 查归属地(IP9 免费接口,60 次/分钟,批量场景循环调用 + 内存缓存),然后跑上面的规则输出打分表。

python
import json
import time
import requests
from collections import defaultdict

# IP9 免费版 60 次/分钟/IP,加缓存避免重复查询打满额度
CACHE = {}
LAST_CALL = 0.0

def query_geo(ip):
    global LAST_CALL
    if ip in CACHE:
        return CACHE[ip]
    try:
        now = time.time()
        gap = now - LAST_CALL
        if gap < 1.1:
            time.sleep(1.1 - gap)   # 简单限速
        r = requests.get(f"https://ip9.com.cn/get?ip={ip}", timeout=5)
        d = r.json()
        LAST_CALL = time.time()
        if d.get("ret") != 200:
            return None
        CACHE[ip] = d["data"]
        return d["data"]
    except Exception:
        return None

def load_logs(path):
    # 结构: [{"user":"u1","ip":"1.2.3.4","ts":"2026-09-25 23:10:00","dur":45}, ...]
    with open(path, encoding="utf-8") as f:
        return json.load(f)

def classify(logs, same_ip_limit=3, jump_hours=6, night_start=0, night_end=6):
    by_user = defaultdict(list)
    for e in logs:
        by_user[e["user"]].append(e)

    ip_users = defaultdict(set)
    for e in logs:
        ip_users[e["ip"]].add(e["user"])

    result = []
    for user, items in by_user.items():
        items.sort(key=lambda x: x["ts"])
        score = 0
        reasons = []

        # 规则1: 同 IP 多账号
        for ip, users in ip_users.items():
            if user in users and len(users) >= same_ip_limit:
                score += 1
                reasons.append(f"同IP多账号({ip} 关联{len(users)}个账号)")
                break

        # 规则2: 短时间跨省突变
        for i in range(1, len(items)):
            prev_ip, cur_ip = items[i-1]["ip"], items[i]["ip"]
            if prev_ip == cur_ip:
                continue
            prev_geo = query_geo(prev_ip)
            cur_geo = query_geo(cur_ip)
            if prev_geo and cur_geo:
                prev_city = f"{prev_geo.get('prov','')}{prev_geo.get('city','')}"
                cur_city = f"{cur_geo.get('prov','')}{cur_geo.get('city','')}"
                hours = (time.mktime(time.strptime(items[i]['ts'], '%Y-%m-%d %H:%M:%S')) -
                         time.mktime(time.strptime(items[i-1]['ts'], '%Y-%m-%d %H:%M:%S'))) / 3600
                if prev_city != cur_city and 0 <= hours <= jump_hours:
                    score += 1
                    reasons.append(f"跨省突变({prev_city}->{cur_city} 间隔{hours:.1f}h)")
                    break

        # 规则3: 机房特征(家庭宽带正常,IDC 高风险)
        for e in items:
            geo = query_geo(e["ip"])
            if geo:
                isp = geo.get("isp", "")
                ip_type = geo.get("ip_type", "")  # VIP 字段
                if "IDC" in isp or "数据中心" in isp or ip_type == "IDC":
                    score += 1
                    reasons.append(f"机房IP({e['ip']} {isp})")
                    break

        # 规则4: 深夜批量
        night = [e for e in items
                 if night_start <= int(e["ts"][11:13]) < night_end]
        if len(night) >= 2:
            score += 1
            reasons.append(f"深夜批量({len(night)}段)")

        result.append({"user": user, "score": score,
                       "level": "冻结" if score >= 3 else ("复核" if score == 2 else "放行"),
                       "reasons": reasons})
    return result

if __name__ == "__main__":
    logs = load_logs("study_logs.json")
    for row in classify(logs):
        print(f"{row['user']:12s}  {row['level']:4s}  {'; '.join(row['reasons']) or '-'}")

逻辑里值得说一句的是规则 2 的写法:跨省判断不是「IP 变了就报警」,而是先查新旧 IP 的省市对比,同一个市内的 IP 变化直接忽略——学生宿舍换 Wi-Fi、出门开流量,归属地都还在本市,不会误伤。规则 3 用到了 ip_type,这是 IP9 VIP 版字段,免费版用运营商字符串里带不带「IDC/数据中心」也能顶一部分。

跑法:把平台导出的学习会话存成 study_logs.json,python3 anti_shuake.py,几秒出名单。数据量大时注意免费接口 60 次/分钟的额度,脚本里做了缓存和限速;真要全量跑,可以换成 VIP 或者把查询结果落库缓存。

四、落地注意事项 ​

  • 共享 NAT 是最大误伤源:一个学校出口、一个公司出口,几十上百人共享同一组公网 IP,规则 1 会误报。处理办法:白名单常用校园/企业出口 IP,或把「同 IP 多账号」的阈值提高,并叠加设备指纹(同 IP + 同设备型号批量出现才计分)。
  • 阈值要从真实数据里调:先跑一周「只记录不处置」,看看正常用户的得分分布,再定 2 分复核、3 分冻结,别拍脑袋。
  • 处置要分级、可申诉:直接冻结容易引发投诉,冻结前先发验证码;申诉渠道留好,人工复核过的账号进白名单。
  • 合规:IP 归属地属于个人信息范畴,日志保留要有期限说明,处理目的写清楚是「账号安全与反舞弊」,别拿去做与学习无关的分析。

总结 ​

刷课产业拼的是批量,平台反制拼的是「批量账号里找异常聚集」。IP 归属地是成本最低、最不好伪造的信号之一:横向聚集(一 IP 拖 N 号)、纵向突变(跨省跳变)、机房特征(IDC 托管)三条线,配一个可解释的规则引擎就能把大多数代刷账号筛出来。原型里的查询用的 https://ip9.com.cn/get?ip=xxx 免费接口(60 次/分钟),批量场景循环调用即可,想拿到 ip_type(ISP/BUS/IDC)判断机房特征再看官网 https://www.ip9.com.cn 的 VIP 说明。把上文脚本接上你们的学习事件表,先跑一周评分,再逐步放松或收紧阈值——这套东西的重点从来不是一次命中,而是持续迭代。