网站 Favicon 获取 API 技术实现指南

接口概述

本文档介绍一个通过 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

相关推荐
stevenzqzq2 小时前
Android Navigation 组件页面跳转方法说明
android·compose
Kapaseker2 小时前
Compose 响应式布局要变天—入门 FlexBox
android·kotlin
.豆鲨包2 小时前
【Android】HttpURLConnection解析
android·java
亘元有量-流量变现3 小时前
鸿蒙、安卓、苹果音频设备技术深度解析与开发实践
android·wpf·harmonyos·亘元有量·积分墙
冬奇Lab17 小时前
相机拍照流程:从快门按下到JPEG存储的完整旅程
android·音视频开发·源码阅读
alexhilton17 小时前
在Compose中用Shader实现透明的粘稠元球效果
android·kotlin·android jetpack
jwn99918 小时前
Laravel5.x核心特性全解析
android·php·laravel
studyForMokey19 小时前
【Android面试】RecylerView专题
android·spring·面试
android_cai_niao21 小时前
Android中有什么技术过时了可以直接用新技术的
android·过时技术