你可能正在做一个书签管理工具、链接目录,或者一个会展示第三方网站的面板。你希望每个站点都能显示 favicon,让列表看起来更精致——但对每个 URL 都直接请求 /favicon.ico 其实很脆弱。相当多的站点用的是 SVG 图标、自定义路径,或者通过条件式的 <link> 标签来声明图标,而不是依赖根目录下的那个默认文件。

这篇教程会带你走完「用 PHP 从 URL 获取 favicon」的完整流程:解析 HTML 中所有 <link rel="icon"> 声明,回退到旧的 /favicon.ico 端点,校验响应内容,最后把二进制文件保存到本地。

前置条件

你需要 PHP 7.4 或更高版本,并启用以下两个扩展:

官方 PHP Docker 镜像和绝大多数 Linux 发行版都默认自带这两个扩展。开始写代码前可以先跑一下 php -m 确认它们在列表中。

步骤一:抓取页面 HTML

从一个基础的 cURL 请求开始。User-Agent 请求头比看起来更重要——有些站点对非浏览器客户端会返回精简版的 HTML,结果可能直接省略掉 favicon 的 <link> 标签。

<?php
function fetchHtml(string $url): string|false {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; FaviconFetcher/1.0)',
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $html = curl_exec($ch);
    curl_close($ch);
    return $html;
}
?>

一定要对重定向次数和超时做限制。一个跑 30 秒都收不住的 cURL 请求,很快就会把你的 PHP-FPM worker 池全部占满。

步骤二:解析 HTML 中的 rel="icon" 标签

HTML 规范把 rel="icon" 定义为声明 favicon 的标准方式(可以参考 WHATWG 链接关系文档)。浏览器可以从任意一个匹配的 <link> 中挑选,所以我们扫描每一个出现的位置,优先取第一个 href 可用的。

<?php
function findIconUrl(string $baseUrl, string $html): ?string {
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML($html);
    libxml_clear_errors();

    $xpath = new DOMXPath($dom);
    $links = $xpath->query('//link[@rel="icon" or @rel="shortcut icon"]');

    foreach ($links as $link) {
        $href = $link->getAttribute('href');
        if ($href === '') continue;
        return resolveUrl($baseUrl, $href);
    }
    return null;
}
?>

注意我们也把 shortcut icon 加进来了 —— 那是微软 IE 时代留下的老古董,但出乎意料的是,不少老站点至今还在用这种方式声明 favicon。

步骤 3:处理相对路径

favicon 的 href 经常是相对路径,比如 /static/icon.png 或 icons/favicon.svg。可以用 parse_url 拆解,再写一个小辅助函数把它们转成绝对 URL,然后再下载。

<?php
function resolveUrl(string $base, string $ref): string {
    if (parse_url($ref, PHP_URL_SCHEME) !== null) {
        return $ref; // already absolute
    }
    $parts = parse_url($base);
    $origin = ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? '');
    if (str_starts_with($ref, '/')) {
        return $origin . $ref;
    }
    $path = $parts['path'] ?? '/';
    $dir = substr($path, 0, strrpos($path, '/') + 1);
    return $origin . $dir . $ref;
}
?>

步骤 4:兜底请求 /favicon.ico

如果 HTML 里压根没有 <link rel="icon"> 标签,所有浏览器其实都会自动去请求 /favicon.ico。把这个 endpoint 当作兜底,免得在面对老式站点时返回一片空白图标。

<?php
function fallbackIconUrl(string $pageUrl): string {
    $parts = parse_url($pageUrl);
    return ($parts['scheme'] ?? 'https') . '://' . ($parts['host'] ?? '') . '/favicon.ico';
}
?>

步骤 5:下载并校验二进制

接下来就是下载图标、确认拿到的确实是图片,然后存到本地。务必用 cURL 重新发一次请求,不要复用抓 HTML 那个响应 —— favicon 的 URL 可能挂在另一个域名或者 CDN 上,缓存策略完全不同。

<?php
function downloadFavicon(string $iconUrl, string $savePath): bool {
    $ch = curl_init($iconUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 5,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; FaviconFetcher/1.0)',
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    $data = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?? '';
    curl_close($ch);

    if ($data === false || $code !== 200 || $data === '') {
        return false;
    }
    // Reject obvious non-image responses (HTML error pages, JSON, etc.)
    if (stripos($type, 'image/') !== 0 && stripos($type, 'application/octet-stream') !== 0) {
        return false;
    }
    return file_put_contents($savePath, $data) !== false;
}
?>

把整条 PHP Get Favicon from URL 流程串起来

完整流程是这样:抓取 HTML,解析 link 标签,找不到就回退到根目录的 icon,然后保存:

<?php
function getFaviconFromUrl(string $pageUrl, string $savePath): bool {
    $html = fetchHtml($pageUrl);
    if ($html === false) return false;

    $iconUrl = findIconUrl($pageUrl, $html) ?? fallbackIconUrl($pageUrl);
    return downloadFavicon($iconUrl, $savePath);
}
?>

调用方式如下:

<?php
$ok = getFaviconFromUrl('https://example.com', __DIR__ . '/example.ico');
echo $ok ? 'Saved.' : 'Failed.';
?>

常见坑

何时切换到专用工具

对于几千个 URL 的规模,自己手写一个 PHP 函数完全够用。但一旦你需要多格式输出(ICO、PNG、SVG、Apple Touch)、深色模式适配、maskable manifest 图标,或者要稳定覆盖那些 <link> 结构古怪的站点,cURL 那套流程就会变成沉重的维护负担。像 Mzu favicondl 这类工具会帮你完成解析、回退、归一化和打包,覆盖各种浏览器和设备,你就可以丢掉这些样板代码,把精力放回产品本身。

你也可以把这套手动方案与目前流行的公开 favicon API 端点 做个对比,看看哪种取舍更适合你的技术栈——手动抓取给你完全的控制权,而 API 则用外部依赖换取了开发时间。