如何用PHP爬虫获取网页加载时关联的外部域名?
优化PHP页面关联域名检测爬虫的实现方案
你的现有实现只覆盖了<link>标签里的CSS资源,漏了很多其他类型的外部资源,而且路径拼接、去重逻辑都有问题,下面是更完善的实现思路和代码示例:
现有代码的核心问题
- 仅处理
<link>标签的CSS,忽略了<script>、<img>、<iframe>等标签的外部资源 - 相对路径拼接错误:直接用
$url."/".$attribute会导致重复斜杠(比如https://example.com//css/style.css) - 没有去重机制,会重复爬取相同资源,浪费资源
- 未解析CSS/JS内部引用的外部域名(比如CSS里
background-image: url(https://cdn.example.com/img.jpg))
更优实现思路
- 全面提取资源:遍历所有可能加载外部资源的HTML标签,同时解析内联CSS/JS里的资源引用
- 正确处理URL:用
parse_url拆解原URL,结合路径逻辑生成绝对路径,避免路径错误 - 去重机制:维护已爬取URL集合和已检测域名集合,避免重复操作
- 高效爬取:静态资源先用HEAD请求确认可用性,再发起GET;可考虑多进程/异步请求提升速度
- 深度解析:递归解析CSS里的
url(),以及JS里的动态加载逻辑(比如fetch、import)
改进后的代码示例
<?php // 已爬取URL集合,避免重复爬取 $crawledUrls = []; // 已检测到的域名集合 $detectedDomains = []; // 获取页面内容(支持HEAD/GET) function fetchUrl($url, $method = 'GET') { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 生产环境建议开启验证 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); $response = curl_exec($ch); curl_close($ch); return $response; } // 将相对URL转为绝对URL function resolveAbsoluteUrl($baseUrl, $relativeUrl) { $base = parse_url($baseUrl); $relative = parse_url($relativeUrl); // 如果已经是绝对URL,直接返回 if (!empty($relative['scheme'])) { return $relativeUrl; } // 处理基础路径 $basePath = $base['path'] ?? '/'; if (substr($basePath, -1) !== '/') { $basePath = dirname($basePath) . '/'; } // 拼接相对路径并清理冗余 $absolutePath = $basePath . ($relative['path'] ?? ''); $absolutePath = preg_replace('#(/\.?/)#', '/', $absolutePath); $absolutePath = preg_replace('#/(?!\.\./)[^/]+/\.\./#', '/', $absolutePath); // 构建完整URL return $base['scheme'] . '://' . $base['host'] . $absolutePath . (isset($relative['query']) ? '?' . $relative['query'] : ''); } // 提取HTML中的所有资源URL function extractHtmlResources($html, $baseUrl) { $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html); libxml_clear_errors(); $resources = []; // 处理各类资源标签 $tagAttrMap = [ 'script' => 'src', 'link' => 'href', 'img' => 'src', 'iframe' => 'src', 'video' => 'src', 'audio' => 'src' ]; foreach ($tagAttrMap as $tag => $attr) { $elements = $dom->getElementsByTagName($tag); foreach ($elements as $elem) { $url = $elem->getAttribute($attr); if (!empty($url)) { $resources[] = resolveAbsoluteUrl($baseUrl, $url); } } } // 处理内联CSS中的资源 $styleElements = $dom->getElementsByTagName('style'); foreach ($styleElements as $style) { $cssResources = extractCssResources($style->nodeValue, $baseUrl); $resources = array_merge($resources, $cssResources); } return $resources; } // 提取CSS中的资源URL function extractCssResources($css, $baseUrl) { $resources = []; preg_match_all('/url\(["\']?(.*?)["\']?\)/i', $css, $matches); foreach ($matches[1] as $url) { if (!empty($url) && !str_starts_with($url, 'data:')) { $resources[] = resolveAbsoluteUrl($baseUrl, $url); } } return $resources; } // 提取URL对应的域名 function getDomainFromUrl($url) { $parsed = parse_url($url); return $parsed['host'] ?? ''; } // 递归爬取资源并检测域名 function crawlResource($url) { global $crawledUrls, $detectedDomains; // 跳过已爬取的URL if (in_array($url, $crawledUrls)) { return; } $crawledUrls[] = $url; echo "爬取中: {$url}\n"; $content = fetchUrl($url); if (empty($content)) { return; } // 记录当前URL的域名 $domain = getDomainFromUrl($url); if (!empty($domain) && !in_array($domain, $detectedDomains)) { $detectedDomains[] = $domain; } // 根据内容类型处理后续爬取 $ch = curl_init($url); curl_setopt($ch, CURLOPT_NOBODY, true); curl_exec($ch); $mime = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); if (str_contains($mime, 'text/html')) { // HTML页面,提取所有资源继续爬取 $resources = extractHtmlResources($content, $url); foreach ($resources as $resource) { crawlResource($resource); } } elseif (str_contains($mime, 'text/css')) { // CSS文件,提取内部资源继续爬取 $cssResources = extractCssResources($content, $url); foreach ($cssResources as $resource) { crawlResource($resource); } } // 可扩展:解析JS中的动态资源加载逻辑 } // 主入口函数 function startCrawl($domain) { global $detectedDomains; // 统一处理输入的域名,补全协议 $baseUrl = 'https://' . ltrim($domain, 'https://http://'); crawlResource($baseUrl); echo "\n检测到的关联域名:\n"; foreach ($detectedDomains as $domain) { echo "- {$domain}\n"; } } // 示例调用 startCrawl('example.com'); ?>
额外优化建议
- 增加JS解析:可以借助PHP的V8扩展或第三方库,解析JS中的
fetch、import等动态资源加载逻辑 - 限制爬取深度:设置最大递归深度参数,避免无限爬取
- 缓存机制:对已爬取的资源进行本地缓存,重复检测时直接复用结果
- 并发爬取:使用
curl_multi或多进程实现并发请求,大幅提升爬取速度
内容的提问来源于stack exchange,提问作者Cyberduck
相关产品推荐
相关产品推荐

