接口概述
本文档介绍一个通过 URL 参数获取任意网站 Favicon 图标的技术方案,包括接口设计、调用方法、错误处理及调试技巧。
接口规范
基础信息
| 项目 | 说明 |
|---|---|
| 接口地址 | https://api.afmax.cn/so/ico/index.php |
| 请求方式 | GET |
| 响应格式 | 图片二进制数据(ICO/PNG/JPG/GIF/SVG) |
| 字符编码 | UTF-8 |
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
r |
string | 是 | 目标网站 URL,支持带/不带协议头 |
调用方式详解
方式一:标准 Query 参数
http
GET /so/ico/index.php?r={target_url} HTTP/1.1
Host: api.afmax.cn
示例:
bash
# 带协议头
curl "https://api.afmax.cn/so/ico/index.php?r=https://www.taobao.com/"
# 不带协议头(自动补全)
curl "https://api.afmax.cn/so/ico/index.php?r=www.taobao.com"
方式二:强制 PNG 后缀兼容
部分前端框架或 CMS 系统要求图片 URL 必须以 .png 结尾,可通过添加 ?.png 或 /?.png 实现兼容:
bash
# 方式 A
curl "https://api.afmax.cn/so/ico/index.php?r=https://www.taobao.com?.png"
# 方式 B
curl "https://api.afmax.cn/so/ico/index.php?r=www.taobao.com/?.png"
技术说明 :
?后的内容作为查询字符串被忽略,不影响实际请求。
方式三:RESTful 路径风格
bash
curl "https://api.afmax.cn/so/ico/index.php/7k7k.com/ico.png"
方式四:Base64 编码(防采集场景)
适用于需要隐藏真实目标域名的场景:
bash
# 域名 so.com 的 base64 编码为 c28uY29t
curl "https://api.afmax.cn/so/ico/base64.php/c28uY29t"
编码示例(Python):
python
import base64
domain = "example.com"
encoded = base64.b64encode(domain.encode()).decode()
print(f"https://api.afmax.cn/so/ico/base64.php/{encoded}")
图标抓取逻辑分析
抓取优先级
1. 首先请求 /favicon.ico
↓ 不存在或无法访问
2. 解析 HTML 中的 <link rel="icon"> 或 <link rel="shortcut icon">
↓ 未找到
3. 查找 PWA manifest.json 中的 icons 配置
↓ 未找到
4. 返回 404
支持的图标格式
| 格式 | MIME 类型 | 说明 |
|---|---|---|
| ICO | image/x-icon | 传统 Windows 图标格式 |
| PNG | image/png | 推荐格式,支持透明 |
| JPG/JPEG | image/jpeg | 无透明通道 |
| GIF | image/gif | 支持动画 |
| SVG | image/svg+xml | 矢量格式,2026.4.7 优化支持动态 |
HTTP 状态码与错误处理
状态码详解
| 状态码 | 场景 | 排查建议 |
|---|---|---|
200 OK |
成功获取图标 | 正常响应,可直接使用 |
404 Not Found |
目标网站无可用图标 | 检查目标 URL 是否正确;确认网站是否设置了 favicon |
400 Bad Request |
请求参数错误 | 检查 r 参数是否为空或格式错误 |
444 |
触发流量防护 | 请求频率过高,24 小时后重试;检查是否开启懒加载 |
500 Internal Server Error |
服务器繁忙 | 未缓存资源排队中,2 小时后重试;或先进行预热 |
错误处理代码示例
JavaScript (Fetch)
javascript
async function getFavicon(url) {
const apiUrl = `https://api.afmax.cn/so/ico/index.php?r=${encodeURIComponent(url)}`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
switch (response.status) {
case 404:
console.warn(`[Favicon API] 未找到 ${url} 的图标`);
return '/default-favicon.png'; // 使用默认图标
case 444:
console.error('[Favicon API] 触发流量限制,请降低请求频率');
return null;
case 500:
console.warn('[Favicon API] 服务器繁忙,稍后重试');
// 可加入重试逻辑
return null;
default:
throw new Error(`HTTP ${response.status}`);
}
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.error('[Favicon API] 请求失败:', error);
return '/default-favicon.png';
}
}
Python (Requests)
python
import requests
from urllib.parse import quote
def get_favicon(url, timeout=10):
"""
获取网站 favicon
Args:
url: 目标网站 URL
timeout: 请求超时时间(秒)
Returns:
bytes: 图标二进制数据,失败返回 None
"""
api_url = f"https://api.afmax.cn/so/ico/index.php?r={quote(url, safe='')}?"
try:
response = requests.get(api_url, timeout=timeout)
if response.status_code == 200:
return response.content
elif response.status_code == 404:
print(f"[WARN] 未找到 {url} 的图标")
elif response.status_code == 444:
print(f"[ERROR] 触发流量限制,请降低请求频率")
elif response.status_code == 500:
print(f"[WARN] 服务器繁忙,建议 2 小时后重试")
else:
print(f"[ERROR] 未知错误: HTTP {response.status_code}")
except requests.Timeout:
print(f"[ERROR] 请求超时 ({timeout}s)")
except requests.RequestException as e:
print(f"[ERROR] 请求异常: {e}")
return None
# 使用示例
favicon_data = get_favicon("https://github.com")
if favicon_data:
with open("github.ico", "wb") as f:
f.write(favicon_data)
PHP (cURL)
php
<?php
/**
* 获取网站 Favicon
*
* @param string $url 目标网站 URL
* @param string $savePath 保存路径(可选)
* @return string|bool 成功返回文件路径或二进制数据,失败返回 false
*/
function getFavicon($url, $savePath = null) {
$apiUrl = 'https://api.afmax.cn/so/ico/index.php?r=' . urlencode($url);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
// 必须传递的 Header
CURLOPT_HTTPHEADER => [
'User-Agent: Mozilla/5.0 (compatible; FaviconFetcher/1.0)',
'X-Forwarded-For: ' . $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
'Referer: ' . ($_SERVER['HTTP_HOST'] ?? 'localhost')
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("[Favicon API] cURL 错误: $error");
return false;
}
// 状态码处理
switch ($httpCode) {
case 200:
if ($savePath) {
file_put_contents($savePath, $response);
return $savePath;
}
return $response;
case 404:
error_log("[Favicon API] 未找到 $url 的图标");
return false;
case 444:
error_log("[Favicon API] 触发流量限制");
return false;
case 500:
error_log("[Favicon API] 服务器繁忙 (HTTP 500)");
return false;
default:
error_log("[Favicon API] 未知错误: HTTP $httpCode");
return false;
}
}
?>
调试技巧
1. 使用浏览器开发者工具
打开 Network 面板,观察请求:
请求 URL: https://api.afmax.cn/so/ico/index.php?r=https://example.com
请求方法: GET
状态码: 200 OK
远程地址: [CDN IP]
响应头:
Content-Type: image/x-icon 或 image/png
X-Cache: HIT / MISS (缓存状态)
2. 命令行调试
bash
# 查看完整响应头
curl -I "https://api.afmax.cn/so/ico/index.php?r=github.com"
# 查看请求耗时(排查慢请求)
curl -w "@curl-format.txt" -o /dev/null -s "https://api.afmax.cn/so/ico/index.php?r=github.com"
# curl-format.txt 内容:
# time_namelookup: %{time_namelookup}\n
# time_connect: %{time_connect}\n
# time_appconnect: %{time_appconnect}\n
# time_pretransfer: %{time_pretransfer}\n
# time_redirect: %{time_redirect}\n
# time_starttransfer: %{time_starttransfer}\n
# time_total: %{time_total}\n
3. 缓存状态检测
bash
# 检测服务状态和缓存命中率
curl "https://api.afmax.cn/so/ico/look.php"
4. 常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 图标显示为破损图 | URL 编码问题 | 使用 encodeURIComponent() 编码目标 URL |
| 请求超时 | 目标网站响应慢 | 增加超时时间;使用已缓存的域名测试 |
| 返回 404 但实际有图标 | 非标准图标路径 | 检查网站是否使用 <link rel="icon"> 自定义路径 |
| 图标模糊 | 原图质量低 | 无解决方案,取决于源网站提供的图标 |
| SVG 图标无法显示 | 浏览器兼容性或 API 处理 | 2026.4.7 后已优化,检查 API 版本 |
性能优化建议
1. 客户端缓存
html
<!-- 使用浏览器缓存 -->
<img src="https://api.afmax.cn/so/ico/index.php?r=example.com"
loading="lazy"
decoding="async">
2. 懒加载实现
javascript
// Intersection Observer 实现懒加载
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imgObserver.observe(img);
});
3. 服务端缓存(PHP 示例)
php
<?php
/**
* 带本地缓存的 Favicon 获取
*/
function getFaviconWithCache($url, $cacheDir = './favicon_cache/', $cacheTime = 86400) {
$cacheKey = md5($url);
$cacheFile = $cacheDir . $cacheKey . '.ico';
// 检查本地缓存
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
return file_get_contents($cacheFile);
}
// 从 API 获取
$favicon = getFavicon($url, $cacheFile);
return $favicon;
}
?>
安全注意事项
1. 输入验证
javascript
// 验证 URL 格式
function isValidUrl(string) {
try {
new URL(string.startsWith('http') ? string : 'https://' + string);
return true;
} catch (_) {
return false;
}
}
// 使用
const targetUrl = userInput;
if (!isValidUrl(targetUrl)) {
console.error('无效的 URL');
return;
}
2. 防止 SSRF
如果服务端代理请求,需限制目标地址:
python
import ipaddress
def is_private_ip(hostname):
"""检查是否为内网 IP"""
try:
ip = ipaddress.ip_address(hostname)
return ip.is_private
except ValueError:
return False
# 禁止访问内网地址
if is_private_ip(parsed_url.hostname):
raise ValueError("禁止访问内网地址")
3. 请求头要求(镜像/代理场景)
如果通过服务端代理调用 API,必须传递以下 Header:
User-Agent: {你的应用标识}
X-Forwarded-For: {真实用户 IP}
Referer: {来源页面}
技术实现原理
图标抓取流程
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 接收请求 │────▶│ 解析目标URL │────▶│ 检查缓存 │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌────────────────────────┘
│ 命中
▼
┌─────────────┐
│ 返回缓存 │
└─────────────┘
│ 未命中
▼
┌─────────────┐
│ 请求favicon │────▶ 成功 ──▶ 缓存并返回
│ .ico │
└──────┬──────┘
│ 失败
▼
┌─────────────┐
│ 解析HTML │────▶ 找到 icon 标签 ──▶ 抓取
│ 查找link │ │
│ 标签 │◀──────────────────────────┘
└──────┬──────┘
│ 未找到
▼
┌─────────────┐
│ 检查PWA │────▶ 找到 manifest ──▶ 抓取
│ manifest │ │
└──────┬──────┘◀──────────────────────────┘
│ 未找到
▼
┌─────────────┐
│ 返回404 │
└─────────────┘
参考资源
文档版本: 1.0 | 最后更新: 2026-04-07