小站刚上线那阵子,我搞了个新客满减券,想着拉点人气。结果活动开始没十分钟,券就被领光了。我还挺高兴,心想这转化效果不错啊。等发货的时候才发现不对劲——好些订单的收件地址,看着就离谱。

什么“广东省深圳市南山区随便路100号”,还有“北京朝阳区大望路SOHO现代城B座2201”——这个看着正常,但我后来发现同一个地址被十几个不同IP下了单,手机号全是170、171开头的。发货是不可能发的,运费都赔不起。

当时我第一个反应就是查IP。结果更晕了——同一个IP,一会儿显示郑州联通,一会儿跳到深圳电信。查了一圈才明白,大部分人都在NAT后面,一个出口IP几百人共用很正常。

后来跟几个做电商的朋友取经,发现大家都遇到过类似的事。靠单一维度根本防不住,羊毛党的技术也在涨。今天就把我折腾了大半年的经验摊开说说,全是自己踩过的坑。

从垃圾地址里挤出水来

最开始我想得简单——弄个黑名单IP库不就完了?现实马上就给了我一巴掌。移动用户基本都在NAT后面,成百上千人共享一个出口IP。基站切换、跨省漫游的时候,IP归属地会跟着路径飘。

多用户共享IP示意图,展示NAT场景下的复杂性

那我换个思路,把异常地址全列出来总行了吧?比如“XX大学菜鸟驿站”“某某快递柜”,想着统一格式的一刀切。可羊毛党很快就换了套路,改写成看起来像住宅的地址,甚至精确到门牌号。你拿地图去搜,十有八九查无此地。

// 先写个简单的地址清洗函数,把乱七八糟的符号干掉
function cleanAddress(raw) {
  return raw.replace(/[^\u4e00-\u9fa5\d\-]/g, '')
            .replace(/深圳/g, '深圳市')
            .replace(/^(深圳|广州|上海|北京)/, '广东省深圳市');
}

这个函数看着简单,跑起来才发现坑多。同一个用户“张三”,第一单写的是“广东省深圳市南山区科技园南区”,第二单变成“深圳南山区科技南路”,第三单干脆来了个“粤B南山科技园”。系统眼里这三个完全是不同的字符串,但人工一看就知道是同一个地方。

我试了两轮才摸到门道。先把历史订单里两万多条收件地址倒出来,用脚本把所有非中文字符(除了数字和-)过滤掉,什么表情包、乱码符号全干掉。再按省、市、区、街道、门牌的顺序拆开。有的订单只写到区,那就留空街道字段。有的连省都没写,比如“南山区学府路XX号”,就通过区名反推省和市,写一个简单的字典映射。

文本清洗完还差最后一步——把地址变成坐标。我调了高德地图的地理编码API,每一条清洗后的地址传过去,拿回经度、纬度和行政区划代码。这一步踩了个大坑:高德免费版每天调用次数有限,两万多条地址分批跑了三天才跑完。中间还因为网络超时丢了一批数据,又重跑了一次。建议你如果也是小体量,先按用户ID去重,一个用户只取最近三个地址去调接口。

拿到经纬度后,我按0.01度*0.01度(大约一公里乘一公里)划分网格,每个地址归到一个格子ID里。同一片区域的地址,就算写法不完全一样,也能被归到同一个网格里。这样批量比对的时候速度快很多。

经纬度网格划分示例,展示同一格子内的地址如何被归为一组 Address database cleaning and geocoding

IP库选不对,后面全白干

地址数据清洗好了,跑批时又遇到新问题:订单里的IP归属地,有时飘得很离谱。同一个深圳网格,IP显示在郑州,系统拼命报警。后来我才发现,锅主要在IP库。

最早我用的那个免费库,版本停在了两年前。一次活动夜里爆单,大量“北京”IP实际来自机房。翻文档才知,那库根本没更新。痛定思痛,我换了ipip.net的商业版,又备了纯真和MaxMind GeoLite2做对照。三个库一起跑,误判立刻就少了。记得设个季度提醒去更新数据库,运营商调整段、云厂商回收IP,都靠增量文件跟上节奏。

两万条订单,如果一条条去网页复制,半天也搞不定。我写了并发脚本,调用ip-api.com的批量接口,一次塞一百个IP,返回JSON。本地离线库更快,把GeoLite2-City.mmdb读进内存,毫秒级定位。

import requests, json
url = 'http://ip-api.com/batch'
payload = [{'query': row['user_ip']} for row in orders[:100]]
res = requests.post(url, json=payload).json()
for r in res:
    row = next(o for o in orders if o['id']==r['id'])
    row['city'] = r.get('city') or '未知'

脚本跑完,两千单只要三十秒,比人肉查快太多。移动网络和NAT出口最常“漂”,我现在的策略是:只要IP与地址落在同一省份,就不标红,仅提示“待人工复核”。跨省才直接预警。这样误杀率从百分之三十降到百分之八。如果你发现某个IP段反复漂移,可以拉黑它,但别忘了定期解封——运营商也会搬家。

  • 优先选能每日或每周更新的商业或社区库
  • 线上业务务必保留人工复核通道
  • 别迷信精确到街道,多数场景城市级够用

反刷单是一场持久战,工具重要,心态更重要。边看边调才能稳住阵脚。

三步交叉法:从乱杀到精准拦截

库换好了,脚本也能跑了,结果跑出来一堆“不一致”的订单。我盯着屏幕发呆——这些不一致到底哪些是真刷单,哪些是用户出差、搬家、用公司网络?

一开始我图省事,只要IP城市跟收件城市不一样,直接标红。结果第一周就误杀了三十多个正常订单。有个做电商运营的朋友打电话骂我:“你搞什么?我同事出差在酒店下单,你给封了?”

什么叫明显对不上?IP显示在黑龙江哈尔滨,收件地址写的是海南三亚。这种不用犹豫。但麻烦的是那些暧昧的——比如IP在北京海淀区,收件地址填的也是北京,但仔细一看,一个是北京房山,一个是北京朝阳。同城你管不管?

我的做法是:先把所有订单跑一遍,提取两个字段——IP归属城市和收件地址城市。名字一样直接放行,不一样就丢进待复核池。这一步要快,用脚本批量比对,几百毫秒扫完几千单。

Excel截图展示IP城市与收件城市的对比,不一致的行用颜色标出

进了待复核池的订单不能一刀切。我一开始把“石家庄IP加保定收件”也标红了,后来查了下两个城市才隔一百三十公里,完全可能是正常用户。所以加了一步计算IP归属地坐标与收件地址坐标之间的直线距离。

调了个地图API。我用的百度地图的坐标转换加距离计算接口,传入两个地址的经纬度返回公里数。阈值试了好几版。一开始设一百公里误杀太多,调到两百公里好很多,最后定的是同省超两百公里标黄,跨省直接标红。比如IP在江苏南京,收件地址是安徽滁州,两地直线距离才六十公里,跨省但近,标黄。IP在辽宁沈阳,收件地址在海南海口,距离两千七百公里,直接红。

地图截图标注两个红点,中间画条直线,显示2700公里距离

单笔订单标红还不够。刷单团伙最怕什么?怕被看出同一个IP覆盖多个城市,或者同一个地址被多个异地IP下单。我写了个聚合脚本,按IP分组统计每个IP关联了多少个不同的收件城市。如果同一个IP出现了五六个城市的订单,而且这些城市之间距离都很远——基本就是刷单团伙在用代理池。再按收件地址分组,统计每个地址被多少个不同城市的IP访问过。一个地址如果被十几个省份的IP下过单,正常用户做不到。

有一次跑完脚本发现一个IP同时下了八十单,收件地址分布在全国十二个城市。顺着查下去,这些地址全是空置厂房和废弃店铺,直接整批拉黑。

柱状图展示一个IP对应多个城市的订单数量分布 Cross-referencing algorithm for fake orders

618实战:跑通全流程

接着聊今年618的事。我把近三个月的历史订单导出来,一共不到两百万条,先用脚本做了一轮同省不同城的快速筛选。这个操作相当于给所有订单打个标签,别小看这一步。

热力图展示IP城市与收件城市不一致的订单密集区域

一开始我照搬之前的两百公里阈值,结果误杀率高达百分之十五。后来改成跨省一律标红,同省超过三百公里再标黄,情况好了很多。这个数值不是拍脑袋定的,而是对比了正常用户的配送距离分布后才确定的。有一批订单全是浙江杭州IP加新疆乌鲁木齐收件,直线距离超过三千公里,果断整批冻结。事后人工复核,百分之九十五都是刷单。

折线图显示订单数量在3000公里距离区间出现明显高峰

单个订单再奇怪也可能是个例,但如果同一个IP在短时间内出现在五个省份下单,那就不正常了。我写了个聚合脚本,按IP和收件电话分组,统计跨省订单数。只要超过三次就进入高危名单。这批数据里有一个IP对应了四十七个不同的收件电话,分布在三个大区。后台一查,这些账号全是新注册的,手机号段集中在170和171虚拟运营商。

表格截图高亮显示同一个IP对应多个收件电话的记录

三条规则一叠加,系统自动卡掉了差不多六成的可疑订单。运营那边算了一笔账,说这一百多万的补贴差点就被羊毛党薅走了。那天夜里我盯着后台数据,觉得之前熬夜调参数总算没白费力气。

后台风控面板截图显示已拦截虚假订单的具体数量

工具说到底就是帮你把脏活累活自动化。我搭了一套半自动流水线,用了三个库。先用requests调IP归属地API,免费的接口有但每天有限制。接着用pandas做筛选,一句df[df.ip_city不等于df.address_city]就搞定。最后用geopy算距离,给两个经纬度坐标就能算出实际距离。

后来换上了MaxMind的离线库,每月一更,本地解析,免费也无限量。GitHub上一搜,还真有人把脚本封好了。我下载解压,里头一个主程序挺直接——把自己那份订单CSV丢进去,改下文件名就能跑起来。它干的活儿无非三件:拿IP猜城市、拿收件地址猜城市、比对两个城市算距离标异常,顺带还帮我理了理那些乱七八糟的格式头一回跑就炸了,报了个“geolite2数据库找不到”。查两天才明白,是MaxMind偷偷改了下载链路。踩完坑我撸了个批处理:每周五凌晨自动拽新数据库。之后再没操心过。

这套脚本我已经丢到 GitHub 上了,代码注释全写的白话,你下载下来改个数据库路径就能直接跑。真正值钱的东西不在代码里——是你愿不愿意花个下午,把那些标红的异常数据一条条翻过去。别指望一步登天,先让脚本跑起来,等看到红黄标记的订单再慢慢调参数,调多了自然有感觉。等你练到十分钟内能拉完一万条订单的异常池,那时候才算真的入了门。