简介:这是一套面向开发者与数据技术学习者的开源网页抓取系统,专为解决网站静态/动态内容批量下载、本地化数据采集等实际需求而设计,适用于数据挖掘、竞品分析、学术爬虫实践等场景。资源包共28个文件,含8个PHP核心逻辑文件(如api.php、wget_site.sh、smtp类等)、3个CSS与2个JS前端交互文件、3张JPG演示图直观呈现操作界面,另有README.md、LICENSE、.gitignore等工程规范文件,整体仅318KB,轻量易部署。已有346人学习下载,体现其在中小型爬虫项目中的实用热度。用户可直接运行wget_site.sh调用本地wget实现免第三方API的稳定扒站,深入理解任务调度、数据解析与反爬适配等模块设计;配套演示图与清晰目录结构(含work、assets、down等功能子目录)大幅降低上手门槛,是学习网络爬虫工程化落地的优质开源范例。
1. 这不是爬虫工具,而是一套可审计、可定制、可部署的网站静态资源采集系统
“扒站”这个词在开发者社区里常被误读为黑灰产行为,但实际在合法合规前提下,它指代的是对公开网页结构、样式、脚本、图片等静态资源的完整镜像采集与本地重建——典型场景包括:前端团队做竞品页面快照归档、UI 设计师提取设计规范素材库、内容运营人员备份活动页以防 CDN 失效、SEO 工程师分析页面渲染路径。2024 年这套系统之所以强调“至白”“非第三方 API”,核心在于彻底剥离对外部服务的依赖:不调用任何云解析服务、不走中间代理节点、不依赖商业爬虫平台,所有逻辑由本地 PHP 脚本驱动,所有网络请求由wget原生命令发起,所有资源路径解析、HTML 重写、链接修正均由开源代码自主完成。它面向的是有 Linux 服务器运维能力、熟悉 PHP 环境配置、需要完全掌控数据流向的中高级开发者或 DevOps 工程师,而非点选式小白工具用户。
2. 用 wget + PHP 构建最小可行采集链:从命令行到可复用函数封装
2.1 wget 是唯一网络层入口,必须禁用递归但保留会话与重定向能力
wget在此系统中不是辅助工具,而是唯一的 HTTP 客户端。它不启用-r(递归)或-p(页面所需全部文件),因为那会导致不可控的深度抓取和域名越界;相反,我们采用“单页精准拉取 + 后置资源补全”策略。关键参数组合如下:
wget --no-cookies \ --no-check-certificate \ --user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \ --header="Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" \ --timeout=30 \ --tries=2 \ --restrict-file-names=windows \ --convert-links \ --page-requisites \ -O "index.html" \ "https://example.com/"提示:
--convert-links是核心开关——它让 wget 自动将远程绝对 URL 替换为相对路径,使页面在本地打开时能正确加载 CSS/JS/图片;--page-requisites仅下载 HTML 中<link>、<script>、<img>标签直接引用的资源(不递归抓取这些资源内部再引用的资源),避免无限嵌套。--restrict-file-names=windows防止 Linux 下生成非法文件名(如:/*),确保 Windows 环境也能解压查看。
2.2 PHP 负责资源调度与 HTML 二次修正,解决 wget 的三大硬伤
wget 虽可靠,但在真实站点中存在三类无法自动处理的问题:
- 动态路径混淆:
<img src="/static/logo.png">→ 实际应为https://example.com/static/logo.png,但 wget 有时会错误解析为file:///static/logo.png; - 内联 JS/CSS 中的 URL:
<script>fetch('/api/data')</script>中的/api/data不会被 wget 捕获,导致 JS 执行失败; - Base 标签干扰:若页面含
<base href="https://cdn.example.com/">,wget 会按此 base 下载资源,但本地打开时却指向错误域名。
PHP 脚本(如rebuild.php)需在 wget 完成后立即执行,其主流程为:
<?php // rebuild.php —— 输入:wget 输出目录路径;输出:修正后的 index.html 及资源树 $rootDir = realpath($argv[1] ?? './site'); $htmlPath = $rootDir . '/index.html'; if (!file_exists($htmlPath)) { die("ERROR: index.html not found in {$rootDir}\n"); } $html = file_get_contents($htmlPath); $dom = new DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); // 1. 移除 <base> 标签,防止后续 relative link 解析错乱 $baseNodes = $dom->getElementsByTagName('base'); for ($i = $baseNodes->length - 1; $i >= 0; $i--) { $baseNodes->item($i)->parentNode->removeChild($baseNodes->item($i)); } // 2. 重写所有 src/href 属性,统一转为相对路径(以 index.html 为基准) $attrs = ['src', 'href', 'data-src', 'data-href']; foreach ($attrs as $attr) { $nodes = $dom->getElementsByTagName('*'); foreach ($nodes as $node) { if ($node->hasAttribute($attr)) { $url = $node->getAttribute($attr); if (filter_var($url, FILTER_VALIDATE_URL)) { // 提取路径部分,忽略协议+域名 $parsed = parse_url($url); $relPath = ltrim($parsed['path'], '/'); $node->setAttribute($attr, $relPath); } } } } // 3. 保存修正后 HTML file_put_contents($htmlPath, $dom->saveHTML()); echo "✓ HTML rebuilt: {$htmlPath}\n"; ?>参数说明:
LIBXML_HTML_NOIMPLIED防止 DOMDocument 自动补全<html><body>结构,避免污染原始结构;parse_url()是安全提取路径的唯一方式,比正则更鲁棒;ltrim($parsed['path'], '/')确保路径以static/logo.png形式存在,而非/static/logo.png(后者在本地文件系统中会被解释为根目录)。
3. 源码级可配置项详解:5 个必调参数决定采集精度与兼容性
3.1config.php中的 5 个核心开关及其真实影响
系统通过config.php统一管理行为策略,以下 5 项参数在 2024 年主流站点(含 Vue/React SSR 页面、CDN 资源分离架构、HTTP/2 优先站点)中必须显式设置:
| 参数名 | 默认值 | 作用说明 | 修改建议 |
|---|---|---|---|
ALLOWED_DOMAINS | ['*'] | 控制 wget 只允许访问哪些域名(防跨域采集) | 生产环境必须设为['example.com', 'cdn.example.com'],禁止通配符 |
RESOURCE_TIMEOUT_MS | 5000 | PHP 解析 HTML 时,对每个外部资源 URL 的 DNS+连接超时阈值(毫秒) | 若目标站使用 Cloudflare,建议调高至8000 |
MAX_RESOURCE_DEPTH | 2 | 允许 PHP 主动补抓的资源层级(如 HTML → CSS → CSS 中 @import 的另一个 CSS) | 静态站设1;含复杂 CSS 架构的设2;设0则仅处理 HTML 直接引用资源 |
REWRITE_CSS_URLS | true | 是否解析并重写 CSS 文件内的url()函数路径 | 必须开启,否则 CSS 中背景图、字体等仍指向线上地址 |
SKIP_JS_EXECUTION | true | 是否跳过执行内联 JS(因 PHP 无法运行 JS,设 false 会导致解析失败) | 始终保持true,JS 渲染逻辑不在本系统职责范围内 |
3.2REWRITE_CSS_URLS = true的实现细节与边界条件
当开启 CSS 重写时,PHP 不会真正加载 CSS 文件,而是用正则提取url(...)内容并替换。关键代码段如下:
// css_rewriter.php function rewriteCssUrls($cssContent, $baseUrl) { return preg_replace_callback( '/url\(\s*[\'"]?([^\'"\)]+)[\'"]?\s*\)/i', function ($matches) use ($baseUrl) { $rawUrl = $matches[1]; if (filter_var($rawUrl, FILTER_VALIDATE_URL)) { // 远程 URL → 提取 path 并转为相对路径 $parsed = parse_url($rawUrl); return 'url("' . ltrim($parsed['path'], '/') . '")'; } elseif (strpos($rawUrl, '//') === 0) { // 协议相对 URL → 视为同协议,按 $baseUrl 解析 $absUrl = parse_url($baseUrl)['scheme'] . ':' . $rawUrl; $parsed = parse_url($absUrl); return 'url("' . ltrim($parsed['path'], '/') . '")'; } else { // 已是相对路径,直接返回 return $matches[0]; } }, $cssContent ); }注意:该正则不匹配
url(data:image/png;base64,...)或url(#some-id)(SVG 引用),这两类无需重写;strpos($rawUrl, '//') === 0专门处理//cdn.example.com/img.png这类协议相对 URL,避免误判为绝对路径。
4. 非第三方 API 接口的设计哲学:为什么拒绝封装 curl 或 Guzzle
4.1 所谓“非第三方 API”是指零外部依赖,连 Composer 都不引入
本系统所有网络交互严格限定在两个原生能力内:
- Shell 层:仅调用
wget(Linux/macOS 原生命令,CentOS/RHEL/Ubuntu 均预装); - PHP 层:仅使用
file_get_contents()、DOMDocument、parse_url()、preg_replace_callback()等内置函数,不 require 任何第三方包。
这意味着:
- 无需
composer install,无vendor/目录; - 无需配置
php.ini开启allow_url_fopen(wget 已承担网络请求); - 无需处理 Guzzle 的 PSR-7、PSR-18 兼容性问题;
- 无需担心
curl版本差异导致的 TLS 1.3 支持问题(wget 由系统维护,更稳定)。
验证方式极其简单:
# 进入源码目录,检查是否含 vendor 或 composer.json ls -la | grep -E "(vendor|composer\.json|package\.json)" # 应无任何输出4.2 “API 接口”在此处指 CLI 命令行接口,而非 Web API
标题中“API 接口”并非指/api/v1/xxx这类 HTTP 接口,而是指系统对外暴露的标准化命令行调用契约。例如:
# 标准采集命令(带参数校验) php run.php --url=https://example.com --output=./mirror --depth=1 # 批量采集(接受 JSON 配置文件) php run.php --config=config/batch.json # 仅重写 HTML(跳过 wget,用于二次加工) php rebuild.php --dir=./mirror/example.comrun.php的参数解析逻辑如下:
// run.php $options = getopt('u:o:d:c:', ['url:', 'output:', 'depth:', 'config:']); if (isset($options['u']) || isset($options['url'])) { $targetUrl = $options['u'] ?? $options['url']; } else { die("ERROR: --url is required\n"); } if (!filter_var($targetUrl, FILTER_VALIDATE_URL)) { die("ERROR: invalid URL format: {$targetUrl}\n"); }逻辑说明:
getopt()是 PHP 内置函数,无需扩展;FILTER_VALIDATE_URL比正则更准确识别合法 URL;错误信息直接输出到 stderr,符合 Unix 工具规范,方便 Shell 脚本捕获if [ $? -ne 0 ]; then ...。
5. 实战排错:3 类高频失败场景与对应日志定位法
5.1 wget 报错 “ERROR 403: Forbidden” 的 3 种真实原因及对策
这不是简单的反爬,而是现代站点常见的主动防御机制,需分层排查:
| 日志线索 | 真实原因 | 解决方案 |
|---|---|---|
Resolving example.com... failed: Name or service not known. | DNS 解析失败,但ping example.com成功 → 说明系统 DNS 缓存污染或/etc/resolv.conf配置错误 | 执行nslookup example.com 8.8.8.8测试公共 DNS,若成功则修改/etc/resolv.conf为nameserver 8.8.8.8 |
| `Connecting to example.com | 192.0.2.1 | :443... failed: Connection refused.` |
ERROR 403: Forbidden且响应头含cf-ray: xxxxx | Cloudflare 人机验证拦截(非 User-Agent 问题) | 添加--header="Cookie: __cf_bm=xxx"(需先手动访问获取有效 cookie)或改用--user-agent="Googlebot/2.1"(部分站对搜索引擎 UA 放行) |
5.2 PHP 重写后页面空白的 2 个隐蔽根源
若rebuild.php执行成功但浏览器打开index.html为空白,90% 情况源于:
DOMDocument 加载失败未报错:
libxml_use_internal_errors(true)屏蔽了 HTML 解析错误,需主动检查:$errors = libxml_get_errors(); if (!empty($errors)) { foreach ($errors as $error) { echo "XML Error: {$error->message} at line {$error->line}\n"; } libxml_clear_errors(); }CSS/JS 路径重写后 404:
wget未下载某些资源(如.woff2字体),但 PHP 仍尝试重写其路径。验证方法:# 查看 wget 日志中缺失资源 grep -i "404" wget-log.txt # 检查对应文件是否存在于 output 目录 find ./mirror -name "font.woff2"若不存在,需在
config.php中将MAX_RESOURCE_DEPTH提高,并确认wget命令中--accept参数包含.woff2,.woff,.ttf。
5.3 使用wget -o /etc/yum.repos.d/centos-base.repo https://mirrors.aliyun.com/repo/ce类命令的警示
该命令常见于 CentOS 系统初始化,但它与本系统完全无关且危险:
wget -o是覆盖写入文件,若误将index.html路径写成/etc/...,将直接破坏系统配置;https://mirrors.aliyun.com/repo/ce是 YUM 源地址,返回的是.repo文本,不是 HTML;- 本系统所有 wget 输出均强制指定
-O filename(单文件)或-P dir(目录),绝不用-o(日志文件)替代输出目标。
务必养成习惯:执行任何wget前,先echo命令确认路径:
echo "wget -O ./mirror/index.html https://example.com/" # 确认无误后再删掉 echo 执行本文还有配套的精品资源,点击获取