Quake相似网站图标(Favicon)聚合统计
接口说明
以一个网站图标(Favicon)为种子,先通过相似图标计算服务扩展出一批视觉相似的图标哈希,再在 quake_service 索引中按这些哈希做聚合统计,返回每个相似图标对应的资产数量分布及图标图像(base64)。 用于"以图搜资产"——即使目标站点更换了图标文件、导致哈希不同,也能通过图像相似度把同一套系统/同一厂商的资产聚到一起。
请求地址
https://api.f.360.cn/api/v3/query/similar_icon/aggregation
请求方法
POST
请求参数
| 名称 | 类型 | 位置 | 是否必填 | 示例值 | 说明 |
|---|---|---|---|---|---|
| favicon_hash | string | body | 是 | - | favicon的MD5值 |
| similar | float | body | 是 | 0.9 | 相似度,范围0-1 |
| size | int | body | 否 | - | 返回数量,最多支持50条数据返回 |
| ignore_cache | bool | body | 否 | - | 是否忽略缓存 |
| start_time | string | body | 否 | - | 查询起始时间,接受2020-10-14 00:00:00格式的数据,时区为UTC |
| end_time | string | body | 否 | - | 查询截止时间,接受2020-10-14 00:00:00格式的数据,时区为UTC |
响应参数
data 字段说明
| 名称 | 类型 | 示例值 | 描述 |
|---|---|---|---|
| code | integer | - | 状态码,0 成功,非 0 异常 |
| message | string | - | 提示信息 |
| data | Array(object) | - | 相似图标聚合结果列表,每个元素为一个图标分组 |
| data.key | string | - | 图标 Favicon 的 MD5 哈希 |
| data.doc_count | integer | - | 该图标哈希对应命中的资产数量 |
| data.data | string | - | 图标图像的 base64 编码(优先取命中数据中的图像,缺失时从图标存储桶回源获取) |
请求示例
支持cUrl、Python、Java、PHP、Go语言的请求,示例如下
#!/usr/bin/env bash
set -euo pipefail
APPID="" # apikey
SECRET="" # salt
URL="https://api.f.360.cn/api/v3/query/similar_icon/aggregation"
BODY='{"favicon_hash":"827fd6c561d4b1f932f75e0f9a17f766","similar":0.9}'
NONCE=$(printf '%08d' $((RANDOM % 100000000)))
TIMESTAMP=$(date +%s)
BODY_MD5=$(printf '%s' "$BODY" | openssl dgst -md5 -hex | awk '{print $NF}')
SIGNATURE=$(printf '%s' "${BODY_MD5}${APPID}${NONCE}${TIMESTAMP}${SECRET}" | openssl dgst -md5 -hex | awk '{print $NF}')
SIGNATURE=${SIGNATURE:16}
curl --location --request POST "$URL" \
--header "Content-Type: application/json" \
--header "X-360-Key: ${APPID}" \
--header "X-360-Nonce: ${NONCE}" \
--header "X-360-Timestamp: ${TIMESTAMP}" \
--header "X-360-Signature: ${SIGNATURE}" \
--data "${BODY}"# -*- coding:utf-8 -*—
import hashlib
import json
import random
import time
import requests
APPID = "" # apikey
SECRET = "" # salt
def make_headers(appid, secret, body=""):
headers = {}
headers["X-360-Key"] = appid
headers["X-360-Nonce"] = str(random.randint(0, 99999999)).zfill(8)
headers["X-360-Timestamp"] = str(int(time.time()))
s = (
hashlib.md5(body.encode("utf8")).hexdigest()
+ headers["X-360-Key"]
+ headers["X-360-Nonce"]
+ headers["X-360-Timestamp"]
+ secret
)
md5 = hashlib.md5(s.encode("utf8")).hexdigest()
headers["X-360-Signature"] = md5[16:]
headers["Content-Type"] = "application/json"
return headers
def do_post(url, body):
res = requests.post(
url,
data=json.dumps(body), # noqa: F821
headers=make_headers(APPID, SECRET, json.dumps(body)),
)
print(res.text)
if __name__ == "__main__":
# 请求地址
url = "https://api.f.360.cn/api/v3/query/similar_icon/aggregation"
# 查询参数
params = {"favicon_hash": "827fd6c561d4b1f932f75e0f9a17f766", "similar": 0.9}
do_post(url, params)import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.concurrent.ThreadLocalRandom;
public class Demo {
static String md5(String input) throws Exception {
byte[] digest = MessageDigest.getInstance("MD5")
.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
String appid = ""; // apikey
String secret = ""; // salt
String url = "https://api.f.360.cn/api/v3/query/similar_icon/aggregation";
String body = "{\"favicon_hash\":\"827fd6c561d4b1f932f75e0f9a17f766\",\"similar\":0.9}";
String nonce = String.format("%08d", ThreadLocalRandom.current().nextInt(100000000));
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signature = md5(md5(body) + appid + nonce + timestamp + secret).substring(16);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.header("X-360-Key", appid)
.header("X-360-Nonce", nonce)
.header("X-360-Timestamp", timestamp)
.header("X-360-Signature", signature)
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println(response.body());
}
}<?php
// apikey
$appid = "";
// salt
$secret = "";
$url = 'https://api.f.360.cn/api/v3/query/similar_icon/aggregation';
$body = json_encode(['favicon_hash' => '827fd6c561d4b1f932f75e0f9a17f766', 'similar' => 0.9], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$nonce = str_pad((string) random_int(0, 99999999), 8, "0", STR_PAD_LEFT);
$timestamp = (string) time();
$signature = substr(md5(md5($body) . $appid . $nonce . $timestamp . $secret), 16);
$ch = curl_init($url);
$options = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-360-Key: {$appid}",
"X-360-Nonce: {$nonce}",
"X-360-Timestamp: {$timestamp}",
"X-360-Signature: {$signature}",
],
];
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = $body;
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
if ($response === false) {
echo curl_error($ch), PHP_EOL;
} else {
echo $response, PHP_EOL;
}
curl_close($ch);package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"math/rand"
"net/http"
"time"
)
func md5Hex(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}
func main() {
appid := "" // apikey
secret := "" // salt
url := "https://api.f.360.cn/api/v3/query/similar_icon/aggregation"
body := []byte(`{"favicon_hash":"827fd6c561d4b1f932f75e0f9a17f766","similar":0.9}`)
nonce := fmt.Sprintf("%08d", rand.Intn(100000000))
timestamp := fmt.Sprintf("%d", time.Now().Unix())
signature := md5Hex(md5Hex(string(body)) + appid + nonce + timestamp + secret)[16:]
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-360-Key", appid)
req.Header.Set("X-360-Nonce", nonce)
req.Header.Set("X-360-Timestamp", timestamp)
req.Header.Set("X-360-Signature", signature)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(respBody))
}响应示例
业务正常响应
{
"code": 0,
"message": "Successful.",
"data": [
{
"key": "827fd6c561d4b1f932f75e0f9a17f766",
"doc_count": 84622,
"data": "AAABAAEAICAAAAEAIADSAgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAAAplJREFUWIXt1j2IHGUYB/DfOzdnjIKFkECIVWIKvUFsIkRExa9KJCLaWAgWJx4DilZWgpDDiI0wiViIoGATP1CCEDYHSeCwUBBkgiiKURQJFiLo4d0eOxYzC8nsO9m9XcXC+8MW+3z+9/l6l2383xH+iSBpElyTdoda26xsDqp/h0CVZ3vwKm7tMBngAs7h7eRYebG6hMtMBHbMBX89vfARHprQ5U8cwdFQlIOZCVR5di1+w/wWXT/EY6EoN5NZCODuKZLDwzgSMCuBe2fwfX6QZwtpWzqfBBtLC3txF/ZhxKbBGx0EfsTJS77vwmGjlZrD4mUzUOXZjVjGI65cnTXchB8iupdDUb7QinsQZ7GzZftdQj2JVZ49iC/w6JjksIo7OnS9tiA5Vn6GtyK2+1MY5NkhfGDygVrBAxH5WkPuMjR7/3UsUFLl2Q68s4XkA3ws3v9zoSjX28Kr5wL1xrTxa6ou+f6OZGvqPg9v1wZeaUjcELE/DVfNhWFSvy/enOIZ9eq1sTokEMNLWI79oirP8g6fXpVnh7GEvY1sV/OJ4f0UhyKKk6EoX4x5pEkgXv6L6OM99YqNw/c4kXSwG5nkIfpLCynuiahW1GWeJHkfT4aiXO9atz1XcD6I6yLyHu6bIPk6Hg9FeYZ63y9EjBarPDvQ8VJ1nd9V3D4m+RncForyxFCQ4hSeahlej88Hefauurdwaufr5z/F/ZHAX6nL+mZE18e36IWiHLkFocqzW9QXcNz1+wUHxJ/f10JRPjvGP4pk/vj5L3F8AtufdD+/p6dJDknzX+05fDLGtife/766t9MRgFCUffWTudwE3AqBlVCUf0xLYGTQqzzbhydwJ3Y34g318J1tmX+DPBTlz9MS2MY2/nP8DTGaqeTDf30rAAAAAElFTkSuQmCC"
}
],
"meta": {}
}业务异常响应
{
"code": "q3005",
"message": "调用API过于频繁",
"data": {},
"meta": {}
}{
"code": "GC00105",
"src": "apiholder",
"msg": "appid over field"
}