首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP + Ajax 遠程圖片批量采集器:從原理到完整實現

     編輯:PHP 相關知識

本文實現一個「輸入網址 → 自動分析頁面/樣式表圖片 → 異步批量下載保存」的遠程圖片采集工具。採用 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、隨機文件名防覆蓋、目錄遍歷防護

生產環境擴展建議

  • 引入 Redis 隊列 + Worker 進程 異步處理,避免 PHP-FPM 進程阻塞。
  • 使用 GuzzleHttp 替代 file_get_contents,支持並發請求、Cookie 持久化、代理池。
  • 集成 spatie/browsershot (Headless Chrome) 渲染 JS 動態生成的圖片。
  • 添加 robots.txt 合規檢查、限流、User-Agent 池輪換。
  • 存儲對接 S3/OSS/MinIO,支持分佈式部署。
⚠️ 法律提醒:爬蟲采集需遵守目標站點 robots.txt、服務條款及《網絡安全法》《數據安全法》等法規,僅供技術學習與合法授權場景使用。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved