# 离线版本
# 一. 使用说明
# 1.1 数据说明
# 1.1 文件格式
文件类型 | 包格式 |
---|---|
每日全量包 | .tar.gz |
每日更新包 | .tar.gz |
分钟级更新包 | .zip |
# 1.1.2 压缩包解压内容
初始全量包
初始全量包共10个文件:t_phoneno_000 ~ t_phoneno_009 ,为当前活跃全量数据。所有风险手机号按号码最后一位归类,分别存放在t_phoneno_000 ~ t_phoneno_009文件中。
├── t_phoneno_000
├── t_phoneno_001
├── t_phoneno_002
├── t_phoneno_003
├── t_phoneno_004
├── t_phoneno_005
├── t_phoneno_006
├── t_phoneno_007
├── t_phoneno_008
└── t_phoneno_009
每日全量包
全量包共十个文件,所有风险手机号按号码最后一位归类,分别存放在t_phoneno_000 ~ t_phoneno_009文件中。
├── t_phoneno_000
├── t_phoneno_001
├── t_phoneno_002
├── t_phoneno_003
├── t_phoneno_004
├── t_phoneno_005
├── t_phoneno_006
├── t_phoneno_007
├── t_phoneno_008
└── t_phoneno_009
每日更新包与分钟级更新包:
分钟级更新包解压内容与每日更新包一致,更新包含两种类型文件,存放待删除数据的文件和存放待更新数据的文件,类型说明如下:
待删除数据文件
此类型文件存放着需要从数据库中删除的手机号,按手机号最后一位分别存放在d_phoneno_000 ~ d_phoneno_009文件中,格式:一行一个手机号。
待更新数据文件
此类型文件存放着需要进行添加/更新的手机号以及此手机号相关信息,详请见1.1.4,按手机号最后一位分别存放在t_phoneno_000 ~ t_phoneno_009文件中。
├── d_phoneno_000
├── d_phoneno_001
├── d_phoneno_002
├── d_phoneno_003
├── d_phoneno_004
├── d_phoneno_005
├── d_phoneno_006
├── d_phoneno_007
├── d_phoneno_008
├── d_phoneno_009
├── t_phoneno_000
├── t_phoneno_001
├── t_phoneno_002
├── t_phoneno_003
├── t_phoneno_004
├── t_phoneno_005
├── t_phoneno_006
├── t_phoneno_007
├── t_phoneno_008
└── t_phoneno_009
# 1.1.3 文件分隔符
文件分隔符采用 制表符(\t)。
# 1.1.4手机号码格式说明
区域 | 国家码前缀 | 示例 |
---|---|---|
大陆 | 无 | 139xxxx9527 |
非大陆 | 有 | +852527xxx81(+852即为国家码) |
# 1.1.5 数据内容详情
参数 | 参数名称 | 参数类型 | 参数说明 |
---|---|---|---|
phoneno | 手机号 | string | 手机号码 示例 “138xxxx1234” |
update_time | 最近活跃时间 | datetime | 该手机号码最近被监控到的活跃时间 示例“2018-07-10 08:23:14” |
risk | 风险分数 | integer | 基于捕获号码来源渠道、号码状态检测等方式,对手机号的风险程度进行打分,分数范围为【0-9】,分数越大,手机号风险值越高 |
location | 归属地 | string | 大陆精确到市 国外卡只展示归属国家 示例 “南京 联通, 美国 ” |
attribute | 卡属性 | integer | 0 基础运营商(移动、联通和电信) 1 虚拟运营商 -1 海外以及港澳台号码 |
card_type | 卡类型 | integer | 0 普通卡 4 物联网卡 |
p_name_price | 最近黑卡项目/价格 | string | 该手机号码用于注册xxx平台,验证码价格为0.1元/个 |
ctime | 发现时间 | datetime | 该号码首次被发现的时间 |
risk_tag | 风险标签 | integer | 0【无风险】未发现风险 1【猫池卡】 从接码平台或发卡平台直接捕获到黑产持有并使用的手机号 2【沉默卡】通过对手机号的属性,行为,活跃度等多个角度进行综合策略评判,符合黑卡不活跃特征号码 3【账号卡】从发卡平台中捕获黑产出售的业务注册账号对应的手机号 4【拦截卡】黑产通过病毒木马劫持手机短信收发权限,从而控制的手机号码,号码主人为正常用户 5【隐私小号】手机号码租赁平台出售的手机号,容易被黑产购买使用 6【历史风险卡】历史从接码平台或发卡平台直接捕获到黑产持有并使用的手机号,但超过90天未再次捕获,且号码状态发现变化,可能流入正常用户手中 7【网络电话】海外使用基于IP完成语音传输的网络电话号码,如Google Voice 8【疑似猫池卡】手机号码特征符合猫池卡特征的手机号码 9【疑似新号】 疑似新入网的手机号 10【疑似真人作弊】 手机号关联的设备上安装了多款真人众包软件,疑似真人众包兼职群体使用的手机号 |
注: risk与risk_tag后期会有新增,使用上请做好冗余。
risk与risk_tag对应说明
risk | 对应的risk_tag |
---|---|
risk9 | risk_tag=1(猫池卡)、 risk_tag=8(疑似猫池卡) |
risk8 | |
risk7 | risk_tag=4(拦截卡) |
risk6 | risk_tag=2(沉默卡) |
risk5 | risk_tag=6(历史风险卡) |
risk4 | risk_tag=10(疑似真人作弊) |
risk3 | risk_tag=9(疑似新号) |
risk2 | risk_tag=3(账号卡)、 risk_tag=5(隐私小号)、 risk_tag=7(网络电话) |
risk1 | |
risk0 | risk_tag=0(无风险) |
# 1.2 部署及使用
# 1.2.1 服务器资源需求
名称 | 配置要求 | 版本 | 数量 | 备注 |
---|---|---|---|---|
手机号更新服务 | 4核cpu8G内存100G磁盘 | CentOS 7.0+ | 1 | 必选,离线数据更新服务器 |
手机号接口服务 | 4核cpu8G内存100G磁盘 | CentOS 7.0+ | ≥1 | 可选,离线手机号应用服务器 |
Redis Mongodb | ≥40G内存 (主节点内存要求) ≥4核cpu8G内存200G磁盘 | 4.0.2+ | 1(套) | 必选,二选一 |
# 1.2.2 部署
手机号画像库建议存储在redis或mongodb中。
# 1.3 数据下载及更新
# 1.3.1 接口加密
为了确保数据传输的安全,对接口中的data字段进行加密,加密规则如下:
内容 | 说明 |
---|---|
数据格式 | 数据分为两部分,头128位存放解密用的IV,剩余位数存放加密后的数据。 |
算法 | AES 256bit/CFB,IV:放在数据的头128位,KEY:用户的snkey |
数据存储 | 数据加密后以BASE64形式存储 |
# 1.3.2 数据更新
黑卡服务端支持两种更新方式,分别为日更新和分钟级更新。(新版的离线包默认明文交付,如需提供加密库请及时与我们联系)
# 1.3.2.1 日更新
接口介绍
手机号画像服务端会在每天01:30分开始生成前一日的全量及增量数据包,此过程需要对更新数据进行导出,格式化,计算,打包,建议客户在每天03:30以后向服务端发起更新请求。若有更新,则返回更新包下载链接;若无更新,则无需操作。用户在下载完数据更新包后,根据数据包提供的数据对本地数据进行更新。此外,服务端会对下载IP进行限制,因而在下载前需要将客户端的IP添加入IP白名单(控制台-配置 (opens new window))之中。
接口说明
名称 | 内容 |
---|---|
功能描述 | 向服务端请求数据更新信息 |
请求地址 | https://api.yazx.com/check_upgrade/phoneno_plain |
类型 | POST |
请求格式 | JSON |
请求参数 | snuser |
请求参数 | data(密文) ,通过AES加密,加密详情见demo。请求参数(data)详细说明如下表。 |
响应格式 | JSON |
响应内容 | snuser |
响应内容 | 响应状态,各状态说明如下 200:请求成功 201:无需更新 431:用户名或密码错误 433:服务器出错 status: 434:IP地址无效(没有加入白名单) 435:服务不存在 436:服务已过期 437:请求参数错误 404:请求路径不正确 |
响应内容 | errmsg: 相应结果说明 |
响应内容 | target:下一个更新版本的版本号 data(密文) link :数据包下载链接有效期为24小时内过期需重新请求 cksum:更新包的md5校验 continue:是否需要再次请求升级标志位“yes”:是“no”:否 |
请求参数(data)详细说明:
参数名 | 类型 | 说明 |
---|---|---|
curver | string | 版本号,如需要2023年3月28日更新包,请传入curver:20230327 |
type | int | 11(全量包) 12(日更增量包) |
参数示例:
Request
{
"snuser": "test",
"data": "sR52hH3Z\nOHKDLovIfw=="
}
Response:
{
"snuser" : "test",
"status" : 200,
"errmsg": "OK",
"data": "bNEsY6ztdPYtdqHFJF8gS2MLvuImFxL"
}
Request中data明文示例如下:
Request.data
{
"curver": "20230101",
"type": 11
}
同理,Response中data解密后数据如下
Response.data
{
"target": "20230102",
"link": "xxxxxx",
"cksum": "52d54587919cad48bcd7fca71",
"continue": "no"
}
完整测试代码请参考:
#!/bin/python
#-*-coding=utf-8-*-
"""
python3.7+
# install requirement
pip install requests
pip install pycryptodome
"""
import requests
import json
import base64
from Crypto.Cipher import AES
from Crypto import Random
CHECK_UPGRADE_URL = "https://api.yazx.com/check_upgrade/phoneno_plain"
SNUSER = "test"
SNKEY = 'test'
def encrypt(encrypt_str: str, cecret: str):
"""
aes加密数据后,再进行base54编码后返回
:param encrypt_str:
:param cecret:
:return:
"""
remainder = len(encrypt_str) % AES.block_size
if remainder:
padded_value = encrypt_str + '\0' * (AES.block_size - remainder)
else:
padded_value = encrypt_str
# a random 16 byte key
iv = Random.new().read(AES.block_size)
# CFB mode
cipher = AES.new(bytes(cecret, encoding="utf-8"), AES.MODE_CFB, iv, segment_size=128)
# drop the padded value(phone number length is short the 16bytes)
value = cipher.encrypt(bytes(padded_value, encoding="utf8")[:len(encrypt_str)])
ciphertext = iv + value
return str(base64.encodebytes(ciphertext).strip(), encoding="utf8")
def decrypt(encrypt_str: str, cecret: str):
"""
base64解码后,再进行aes解密
:param encrypt_str:
:param cecret:
:return:
"""
data = base64.decodebytes(bytes(encrypt_str, encoding="utf8"))
cihpertxt = data[AES.block_size:]
remainder = len(cihpertxt) % AES.block_size
if remainder:
padded_value = cihpertxt + b'\0' * (AES.block_size - remainder)
else:
padded_value = cihpertxt
cryptor = AES.new(bytes(cecret, encoding="utf-8"), AES.MODE_CFB, data[0:AES.block_size], segment_size=128)
plain_text = cryptor.decrypt(padded_value)
return str(plain_text[0:len(cihpertxt)], encoding="utf8")
def AesEncryptSeg(snkey, phoneno):
remainder = len(phoneno) % 16
if remainder:
padded_value = phoneno + '\0' * (16 - remainder)
else:
padded_value = phoneno
# a random 16 byte key
iv = Random.new().read(AES.block_size)
# CFB mode
cipher = AES.new(snkey.encode(), AES.MODE_CFB, iv, segment_size=128)
# drop the padded value(phone number length is short the 16bytes)
value = cipher.encrypt(padded_value.encode())[:len(phoneno)]
ciphertext = iv + value
return base64.encodestring(ciphertext).strip()
def AesDecryptSeg(snkey, phoneno):
data = base64.decodestring(phoneno)
cihpertxt = data[AES.block_size:]
remainder = len(cihpertxt) % 16
if remainder:
padded_value = cihpertxt + '\0' * (16 - remainder)
else:
padded_value = cihpertxt
cryptor = AES.new(snkey, AES.MODE_CFB, data[0:AES.block_size], segment_size=128)
plain_text = cryptor.decrypt(padded_value)
return plain_text[0:len(cihpertxt)]
def send_check_upgrade_request(snuser, snkey, data):
# encrypt the origin text
encrypt_data = encrypt(json.dumps(data), SNKEY)
payload = {
"snuser": snuser,
"data": encrypt_data,
}
print(payload)
headers = {"Content-Type": "application/json"}
r = requests.post(CHECK_UPGRADE_URL, data=json.dumps(payload), verify=False, headers=headers)
print(r.text)
rjson = json.loads(r.text)
print(rjson)
if rjson["status"] == 200:
print(decrypt(rjson["data"], snkey))
else:
print("The status is not 200")
if __name__ == "__main__":
data = {
'curver': "20230213",
'type': 11
}
send_check_upgrade_request(snuser=SNUSER, snkey=SNKEY, data=data)
# 1.3.2.2 分钟级更新
分钟级更新包格式
包文件格式 | .zip |
---|
接口介绍
手机号画像服务端每分钟会提供一个更新包,并提供接口获取下载链接。客户可每分钟向服务端请求一个下载链接,进行数据下载。
* 如果在linux上使用wget下载,需重命名压缩包的名字,下载链接需带引号。例如:wget -O(大写字母O) filename.zip “url” 此外,服务端会对下载IP进行限制,因而在下载前需要将客户端的IP添加入IP白名单之中。鉴于打包存在延迟影响可能会导致定时请求失败,建议客户使用while循环请求调用,详情请参考测试用例。
接口说明
名称 | 内容 |
---|---|
功能描述 | 向服务端请求数据更新信息 |
请求地址 | https://api.yazx.com/phone_stream/ |
类型 | POST |
请求格式 | JSON |
请求参数 | snuser |
请求参数 | data(密文) ,通过AES加密,加密详情见demo。请求参数(data)详细说明如下表。 |
响应格式 | JSON |
响应内容 | snuser |
响应内容 | 响应状态,各状态说明如下 200:请求成功 201:无需更新 431:用户名或密码错误 433:服务器出错 status 434:IP地址无效(没有加入白名单) 435:服务不存在 436:服务已过期 437:请求参数错误 404:请求路径不正确 |
响应内容 | errmsg: 相应结果说明 |
响应内容 | target:下一个更新版本的版本号 data(密文) link :数据包下载链接有效期为24小时内过期需重新请求 continue:是否需要再次请求升级标志位“yes”:是“no”:否 |
请求参数data详细说明如下:
参数名 | 类型 | 说明 |
---|---|---|
curver | string | 当前版本号,首次接入,如当前全量包版本为20230328时,curver 建议输入 202303280000 |
type | int | 4(分钟级增量包) |
完整代码请求示例:
#!/bin/python
#-*-coding=utf-8-*-
"""
python3.7+
# install requirement
pip install requests
pip install pycryptodome
"""
import requests
import json
import base64
from Crypto.Cipher import AES
from Crypto import Random
CHECK_UPGRADE_URL = "https://api.yazx.com/phone_stream/"
SNUSER = "test"
SNKEY = 'test'
def encrypt(encrypt_str: str, cecret: str):
"""
aes加密数据后,再进行base54编码后返回
:param encrypt_str:
:param cecret:
:return:
"""
remainder = len(encrypt_str) % AES.block_size
if remainder:
padded_value = encrypt_str + '\0' * (AES.block_size - remainder)
else:
padded_value = encrypt_str
# a random 16 byte key
iv = Random.new().read(AES.block_size)
# CFB mode
cipher = AES.new(bytes(cecret, encoding="utf-8"), AES.MODE_CFB, iv, segment_size=128)
# drop the padded value(phone number length is short the 16bytes)
value = cipher.encrypt(bytes(padded_value, encoding="utf8")[:len(encrypt_str)])
ciphertext = iv + value
return str(base64.encodebytes(ciphertext).strip(), encoding="utf8")
def decrypt(encrypt_str: str, cecret: str):
"""
base64解码后,再进行aes解密
:param encrypt_str:
:param cecret:
:return:
"""
data = base64.decodebytes(bytes(encrypt_str, encoding="utf8"))
cihpertxt = data[AES.block_size:]
remainder = len(cihpertxt) % AES.block_size
if remainder:
padded_value = cihpertxt + b'\0' * (AES.block_size - remainder)
else:
padded_value = cihpertxt
cryptor = AES.new(bytes(cecret, encoding="utf-8"), AES.MODE_CFB, data[0:AES.block_size], segment_size=128)
plain_text = cryptor.decrypt(padded_value)
return str(plain_text[0:len(cihpertxt)], encoding="utf8")
def AesEncryptSeg(snkey, phoneno):
remainder = len(phoneno) % 16
if remainder:
padded_value = phoneno + '\0' * (16 - remainder)
else:
padded_value = phoneno
# a random 16 byte key
iv = Random.new().read(AES.block_size)
# CFB mode
cipher = AES.new(snkey.encode(), AES.MODE_CFB, iv, segment_size=128)
# drop the padded value(phone number length is short the 16bytes)
value = cipher.encrypt(padded_value.encode())[:len(phoneno)]
ciphertext = iv + value
return base64.encodestring(ciphertext).strip()
def AesDecryptSeg(snkey, phoneno):
data = base64.decodestring(phoneno)
cihpertxt = data[AES.block_size:]
remainder = len(cihpertxt) % 16
if remainder:
padded_value = cihpertxt + '\0' * (16 - remainder)
else:
padded_value = cihpertxt
cryptor = AES.new(snkey, AES.MODE_CFB, data[0:AES.block_size], segment_size=128)
plain_text = cryptor.decrypt(padded_value)
return plain_text[0:len(cihpertxt)]
def send_check_upgrade_request(snuser, snkey, data):
# encrypt the origin text
encrypt_data = encrypt(json.dumps(data), SNKEY)
payload = {
"snuser": snuser,
"data": encrypt_data,
}
print(payload)
headers = {"Content-Type": "application/json"}
r = requests.post(CHECK_UPGRADE_URL, data=json.dumps(payload), verify=False, headers=headers)
print(r.text)
rjson = json.loads(r.text)
print(rjson)
if rjson["status"] == 200:
print(decrypt(rjson["data"], snkey))
else:
print("The status is not 200")
if __name__ == "__main__":
data = {
'curver': "202302130000",
'type': 4
}
send_check_upgrade_request(snuser=SNUSER, snkey=SNKEY, data=data)
← SaaS版本回调API 产品说明书 →