# 离线版本

# 一. 使用说明

# 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)
Last Updated: 5/9/2024, 6:29:04 PM