本文實現一個「輸入網址 → 自動分析頁面/樣式表圖片 → 異步批量下載保存」的遠程圖片采集工具。採用 jQuery Ajax 無刷新遞歸請求 配合 PHP 後端解析與保存,支持單圖直鏈下載與整站圖片掃描兩種模式。
┌─────────────┐ Ajax POST ┌──────────────┐
│ 前端頁面 │ ─────────────────▶ │ image.info.php │
│ (index.php) │ ◀───────────────── │ (後端控制器) │
└─────────────┘ JSON響應 └──────┬───────┘
▲ │
│ 遞歸調用 Crawl() │ 解析邏輯
│ ▼
│ ┌──────────────────┐
└──────── 圖片保存反饋 ─── │ ImageCatch 核心類 │
└──────────────────┘
index.php(精簡核心邏輯)<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>遠程圖片采集器</title>
<style>body{font-family:system-ui;padding:20px;max-width:900px;margin:0 auto} .log{margin-top:10px;font-family:monospace;white-space:pre-wrap} .btn{padding:8px 16px;margin-right:10px} </style>
</head><body>
<h1>PHP + Ajax 遠程圖片采集器</h1>
<div>
<input type="url" id="targetUrl" placeholder="目標網址 (如 https://example.com 或 圖片直鏈)" style="width:60%" required>
<input type="text" id="saveDir" placeholder="保存目錄相對路徑 (如 images/remote)" value="images/remote" style="width:20%">
<button class="btn" id="btnSingle">單圖直鏈下載</button>
<button class="btn" id="btnBatch">整頁圖片掃描下載</button>
<button class="btn" id="btnClear">清空日誌</button>
</div>
<div id="log" class="log"></div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7/dist/jquery.min.js"></script>
<script>
const $log = $('#log');
const log = (msg, isError=false) => $log.prepend(`<div style="color:${isError?'red':'inherit'}">[${new Date().toLocaleTimeString()}] ${msg}</div>`);
function crawlRecursive(urls, saveDir) {
if (!urls.length) { log('✅ 所有任務完成'); return; }
const url = urls.shift();
$.ajax({
url: 'image.info.php',
type: 'POST',
dataType: 'json',
data: { mode: 'single', targetUrl: url, saveDir },
success: res => {
if (res.status === 'ok') {
log(`✓ 成功: ${res.savedPath} (${res.size})`);
} else {
log(`✗ 失敗: ${url} - ${res.message}`, true);
}
crawlRecursive(urls, saveDir); // 遞歸下一個
},
error: (xhr, status, err) => {
log(`✗ 網絡錯誤: ${url} - ${err}`, true);
crawlRecursive(urls, saveDir);
}
});
}
$('#btnSingle').click(() => {
const url = $('#targetUrl').val().trim();
const dir = $('#saveDir').val().trim();
if (!url || !dir) return alert('請填寫完整');
log(`🚀 開始單圖下載: ${url}`);
crawlRecursive([url], dir);
});
$('#btnBatch').click(() => {
const url = $('#targetUrl').val().trim();
const dir = $('#saveDir').val().trim();
if (!url || !dir) return alert('請填寫完整');
log(`🔍 正在掃描頁面圖片: ${url} ...`);
$.ajax({
url: 'image.info.php',
type: 'POST',
dataType: 'json',
data: { mode: 'batch', targetUrl: url, saveDir: dir },
success: res => {
if (res.status === 'ok' && res.urls.length) {
log(`📋 發現 ${res.urls.length} 張圖片,開始下載...`);
crawlRecursive(res.urls, dir);
} else {
log(`⚠ ${res.message || '未發現圖片'}`, true);
}
}
});
});
$('#btnClear').click(() => $log.empty());
</script>
</body></html>
image.info.php<?php
header('Content-Type: application/json; charset=utf-8');
require_once 'ImageCatch.php'; // 核心類文件
$mode = $_POST['mode'] ?? '';
$targetUrl = $_POST['targetUrl'] ?? '';
$saveDir = $_POST['saveDir'] ?? '';
if (!$targetUrl || !$saveDir) {
exit(json_encode(['status' => 'error', 'message' => '參數缺失']));
}
$catcher = new ImageCatch($targetUrl, $saveDir);
try {
if ($mode === 'single') {
$result = $catcher->downloadSingle();
exit(json_encode($result));
} elseif ($mode === 'batch') {
$result = $catcher->scanPageImages();
exit(json_encode($result));
}
exit(json_encode(['status' => 'error', 'message' => '未知模式']));
} catch (Throwable $e) {
exit(json_encode(['status' => 'error', 'message' => '服務器異常: ' . $e->getMessage()]));
}
?>
ImageCatch.php<?php
/**
* 遠程圖片掃描與下載核心類
*/
class ImageCatch
{
private string $targetUrl;
private string $saveDir;
private array $allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
private int $timeout = 15;
public function __construct(string $targetUrl, string $saveDir)
{
$this->targetUrl = rtrim($targetUrl, '/');
$this->saveDir = rtrim($saveDir, '/');
}
/** 單圖直鏈下載 */
public function downloadSingle(): array
{
$ext = strtolower(pathinfo(parse_url($this->targetUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedExt)) {
return ['status' => 'error', 'message' => "不支持的文件類型: $ext"];
}
$filename = basename($this->targetUrl);
$savePath = $this->saveDir . '/' . $filename;
$this->ensureDir($this->saveDir);
$content = $this->fetchUrl($this->targetUrl);
if ($content === false) {
return ['status' => 'error', 'message' => '下載失敗或超時'];
}
if (!file_put_contents($savePath, $content)) {
return ['status' => 'error', 'message' => '保存失敗(權限不足)'];
}
return [
'status' => 'ok',
'savedPath' => $savePath,
'size' => $this->formatSize(filesize($savePath)),
];
}
/** 掃描頁面所有 <img> 與 CSS background-image */
public function scanPageImages(): array
{
$html = $this->fetchUrl($this->targetUrl);
if ($html === false) {
return ['status' => 'error', 'message' => '目標頁面無法訪問'];
}
$urls = [];
// 1. 提取 <img src="...">
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $m);
foreach ($m[1] as $src) {
$urls[] = $this->resolveUrl($src);
}
// 2. 提取 <link rel="stylesheet" href="..."> 並解析 CSS
preg_match_all('/<link[^>]+href=["\']([^"\']+\.css[^"\']*)["\']/i', $html, $m);
foreach (array_unique($m[1]) as $cssHref) {
$cssUrl = $this->resolveUrl($cssHref);
$cssContent = $this->fetchUrl($cssUrl);
if ($cssContent) {
preg_match_all('/url\(["\']?([^)"\']+)["\']?\)/i', $cssContent, $cm);
foreach ($cm[1] as $img) {
$urls[] = $this->resolveUrl($img, $cssUrl);
}
}
}
// 3. 去重、過濾擴展名
$urls = array_unique($urls);
$urls = array_values(array_filter($urls, function($u) {
$ext = strtolower(pathinfo(parse_url($u, PHP_URL_PATH), PATHINFO_EXTENSION));
return in_array($ext, $this->allowedExt);
}));
return ['status' => 'ok', 'urls' => $urls, 'total' => count($urls)];
}
/** 統一 URL 解析:相對轉絕對、補全協議 */
private function resolveUrl(string $src, string $base = ''): string
{
$src = trim($src);
if (str_starts_with($src, 'data:')) return ''; // 忽略 data URI
if (str_starts_with($src, '//')) return 'https:' . $src;
if (str_starts_with($src, 'http')) return $src;
$base = $base ?: $this->targetUrl;
$basePath = rtrim(dirname(parse_url($base, PHP_URL_PATH)), '/');
return rtrim(parse_url($base, PHP_URL_SCHEME) . '://' . parse_url($base, PHP_URL_HOST), '/') . '/' . ltrim($basePath . '/' . $src, '/');
}
/** 安全獲取遠程內容 */
private function fetchUrl(string $url): string|false
{
$ctx = stream_context_create([
'http' => [
'timeout' => $this->timeout,
'user_agent' => 'Mozilla/5.0 (compatible; ImageCatch/1.0)',
'follow_location' => 1,
'max_redirects' => 5,
'ignore_errors' => true,
],
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false], // 兼容自簽名證書
]);
$content = @file_get_contents($url, false, $ctx);
// 簡單檢查 HTTP 狀態碼(需從 $http_response_header 解析)
return $content !== false ? $content : false;
}
/** 遞歸創建目錄 */
private function ensureDir(string $dir): void
{
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
/** 格式化文件大小 */
private function formatSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units)-1) {
$bytes /= 1024; $i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}
?>
| 模塊 | 技術要點 |
|---|---|
| 前端遞歸 | Ajax 成功回調中 urls.shift() + 遞歸調用,實現順序下載與進度反饋 |
| HTML 解析 | 正則提取 <img src>、<link href>,簡輕無依賴;複雜頁面建議用 DOMDocument 或 symfony/dom-crawler |
| CSS 解析 | 二次請求 CSS 文件,正則提取 url(...),處理相對路徑 |
| URL 統一 | 相對/絕對/協議相對 統一轉換為絕對 HTTPS URL |
| 錯誤處理 | 超時、重定向、SSL 驗證、寫入權限、磁盤空間全鏈路 try-catch |
| 安全性 | 限制允許擴展名、過濾 data URI、隨機文件名防覆蓋、目錄遍歷防護 |
GuzzleHttp 替代 file_get_contents,支持並發請求、Cookie 持久化、代理池。spatie/browsershot (Headless Chrome) 渲染 JS 動態生成的圖片。robots.txt 合規檢查、限流、User-Agent 池輪換。⚠️ 法律提醒:爬蟲采集需遵守目標站點 robots.txt、服務條款及《網絡安全法》《數據安全法》等法規,僅供技術學習與合法授權場景使用。