返回博客
指南
Sorin-Gabriel MaricaLast updated on Apr 30, 20264 min read

使用 PHP 进行网络抓取:库、代码和最佳实践实践指南

使用 PHP 进行网络抓取:库、代码和最佳实践实践指南
简而言之:PHP 是一门非常适合网络爬虫的语言,这得益于其内置的 cURL 和 DOMDocument 等扩展,以及包含 Guzzle、Symfony DomCrawler 和用于无头浏览的 Symfony Panther 等工具的丰富 Composer 生态系统。本指南将带您逐步了解完整的工作流程:获取页面、解析 HTML、将结果存储为 CSV/JSON/MySQL 格式、处理错误以及避免被封锁。

使用 PHP 进行网页抓取,是指通过 PHP 脚本和库,以编程方式获取网页并从其 HTML 中提取结构化数据的过程。如果您日常工作已使用 PHP 编程,那么仅为了从网站提取数据而更换语言是毫无必要的。PHP 默认自带 cURL 绑定和内置 DOM 解析器,而 Composer 则为您提供了经过实战检验的 HTTP 客户端、CSS 选择器引擎,甚至无头浏览器。

本教程面向希望获得实用、以代码为先的逐步指导的中级 PHP 开发者。您将从低级 cURL 调用开始,逐步过渡到 Guzzle 和 Symfony HttpBrowser 等高级库,使用 Symfony Panther 处理 JavaScript 渲染的页面,并最终解决数据存储、错误处理以及避免被列入封锁列表等生产环境中的问题。 本 PHP 网页抓取教程中的每个示例都贯穿于同一个场景(抓取一个公开的图书列表网站),因此您可以完整地跟随整个工作流,而不是在彼此无关的代码片段之间来回跳转。

为何 PHP 是网络爬虫的理想选择

提到爬网时,PHP 或许并非人们首先想到的语言,但它具备多项实际优势。首先,如果您的现有技术栈已基于 PHP 运行,添加爬网功能意味着无需新增任何运行时依赖。您的团队可以继续维护现有代码,部署流程保持不变,同时还能避免因切换到其他语言而产生的认知负担。

其次,PHP 的内置扩展出人意料地非常适合这项任务。 curl 扩展负责处理 HTTP 请求, domlibxml 提供符合标准的 HTML/XML 解析器, mbstring 并解决了字符编码的难题。进行基础抓取时,您无需额外安装任何组件。

第三,Composer 生态系统填补了所有剩余的空白。Guzzle 提供了一个支持中间件的现代化 HTTP 客户端。Symfony DomCrawler 在 DOMDocument 基础上增加了 CSS 选择器查询功能。Symfony Panther 能够驱动真实的 Chrome 或 Firefox 实例,用于处理 JavaScript 密集型页面。这些工具生态成熟且维护活跃。

那么,在爬取方面 PHP 和 Python 孰优孰劣?Python 拥有更庞大的爬取社区以及 Beautiful Soup 和 Scrapy 等库,但这并不意味着 PHP 是个糟糕的选择。如果 PHP 是你最擅长的语言,你编写出可运行的爬虫的速度,会比使用仍在学习的语言更快。最好的爬取语言,就是你在凌晨两点也能调试的那一种。

PHP 爬虫库一览

在编写代码前,了解现有工具及其适用场景会大有裨益。下表针对最关键的三个维度——功能、是否支持 JavaScript 以及学习难度——对主流 PHP 爬虫库进行了对比。

库 / 工具

用途

支持 JavaScript

学习难度

维护状态

cURL (ext-curl)

低级 HTTP 请求

内置,始终可用

Guzzle

带中间件的异步 HTTP 客户端

低–中

积极维护

DOMDocument + DOMXPath

HTML/XML 解析、XPath 查询

中等

内置

Symfony DomCrawler

CSS 选择器和 XPath 查询

积极维护

Goutte(已弃用)

结合 HTTP + DOM 爬取

已弃用,请使用 HttpBrowser

Symfony HttpBrowser

Goutte 的继任者,API 相同

正在积极维护

Symfony Panther

无头浏览器(Chrome/Firefox)

中–高

正在积极维护

API 抓取服务

托管请求 + 解析层

取决于提供商

极低

由外部管理

有几点需要注意。Goutte 曾是多年来的首选“全能”爬取库,但现已弃用。在撰写本文时,推荐的迁移路径是 Symfony HttpBrowser,它基于 Symfony 的 BrowserKit 和 HttpClient 组件,提供了几乎完全相同的 API。如果您正在启动新项目,请完全跳过 Goutte,直接使用 HttpBrowser。

对于大多数静态页面抓取任务,Guzzle(用于获取数据)配合 Symfony DomCrawler(用于解析)是一个稳健且轻量级的组合。请将 Symfony Panther 保留给真正需要执行 JavaScript 的页面,因为启动无头浏览器会显著降低速度并消耗更多资源。

搭建 PHP 爬取环境

让我们先解决一些先决条件。你需要 PHP 8.1 或更高版本(以支持现代库中的 enum 和 fiber 功能)、Composer 以及一些扩展。

检查您的 PHP 版本和已加载的扩展:

php -v
php -m | grep -E 'curl|dom|mbstring|json'

如果这四个扩展中有任何一个缺失,请在您的 php.ini 中启用它们,或通过系统包管理器进行安装(例如, sudo apt install php-curl php-xml php-mbstring 在 Debian/Ubuntu 上)。

接下来,初始化一个项目目录并引入本教程中将用到的库:

mkdir php-scraper && cd php-scraper
composer init --no-interaction
composer require guzzlehttp/guzzle symfony/dom-crawler symfony/css-selector symfony/browser-kit symfony/http-client

这一行 composer require 行代码即可为您提供用于 HTTP 的 Guzzle、用于解析的 DomCrawler,以及用于综合爬取工作流的 Symfony HttpBrowser。当我们需要无头浏览器支持时,稍后会添加 Symfony Panther。

创建一个 scrape.php 文件,并在顶部添加 Composer 自动加载器:

<?php
require __DIR__ . '/vendor/autoload.php';

现在,您可以开始抓取第一个页面了。

使用 cURL 抓取页面

PHP 的 cURL 扩展是您工具箱中底层级的 HTTP 工具。虽然它输出冗长,但能让您完全掌控每个请求的细节,这在需要模拟特定浏览器指纹或调试连接问题时非常有用。

以下是一个基本的 GET 请求,用于获取一个公开图书目录的首页(我们将始终使用 http://books.toscrape.com 作为整个演示的目标):

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'http://books.toscrape.com/',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTPHEADER     => [
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept-Language: en-US,en;q=0.9',
    ],
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_COOKIEJAR      => '/tmp/cookies.txt',
    CURLOPT_COOKIEFILE     => '/tmp/cookies.txt',
]);

$html = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'cURL error: ' . curl_error($ch);
}

curl_close($ch);

有几点值得注意。 CURLOPT_COOKIEJARCURLOPT_COOKIEFILE 可实现跨请求的 Cookie 持久化,这对服务器会追踪会话状态的多步骤爬取流程至关重要。设置一个逼真的 User-Agent 请求头,能让请求看起来像普通浏览器流量,而非单纯的 PHP 脚本。此外, CURLOPT_FOLLOWLOCATION 会自动处理 301/302 重定向,因此您无需手动追踪。

对于 POST 请求(例如提交搜索表单),请替换为 CURLOPT_POST => true 并添加 CURLOPT_POSTFIELDS 替换为表单数据。其余的模板代码保持不变。

cURL 虽然可用,但其底层实现过于简单,最终你仍需编写封装代码来处理头部信息、重试和错误处理。这正是 Guzzle 派上用场的地方。

使用 Guzzle 获取页面

Guzzle 将 PHP 的 cURL(或流)层封装成一个简洁的面向对象 API。若尚未安装,请通过 Composer 安装,然后加载同一页面:

use GuzzleHttp\Client;

$client = new Client([
    'timeout' => 30,
    'headers' => [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        'Accept-Language' => 'en-US,en;q=0.9',
    ],
]);

$response = $client->get('http://books.toscrape.com/');
$html = (string) $response->getBody();

这明显减少了冗余代码。Guzzle 还提供了用于日志记录、重试逻辑和头部注入的中间件钩子,这意味着你可以集中处理横切关注点,而不是在各处 curl_setopt

使用 Guzzle 承诺实现并发请求

当需要抓取多个页面时,逐个发起请求会极其缓慢。Guzzle 通过其 Pool 类支持基于 Promise 的并发,让你能在控制并发级别的同时并行发送多个请求。

use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

$client = new Client(['timeout' => 30]);

$urls = [
    'http://books.toscrape.com/catalogue/page-1.html',
    'http://books.toscrape.com/catalogue/page-2.html',
    'http://books.toscrape.com/catalogue/page-3.html',
];

$requests = function () use ($urls) {
    foreach ($urls as $url) {
        yield new Request('GET', $url);
    }
};

$pool = new Pool($client, $requests(), [
    'concurrency' => 5,
    'fulfilled'   => function ($response, $index) {
        echo "Page $index fetched: " . $response->getStatusCode() . "\n";
    },
    'rejected'    => function ($reason, $index) {
        echo "Page $index failed: " . $reason->getMessage() . "\n";
    },
]);

$pool->promise()->wait();

当并发级别设为 5 时,Guzzle 会同时发起最多五个请求,而非等待每个请求完成。在抓取 50 个页面的任务中,这能将总运行时间从数分钟缩短至数秒。根据 Guzzle 关于并发请求的文档,Pool API 底层使用了 cURL 的多连接处理机制,因此性能提升是切实的,而不仅仅是语法糖。

解析 HTML:DOMDocument 与 XPath

获取原始 HTML 字符串后,需要从中提取结构化数据。PHP 的内置 DOMDocument 类会将 HTML 加载为树形结构,而 DOMXPath 允许您使用 XPath 表达式查询该树。

libxml_use_internal_errors(true); // suppress malformed-HTML warnings

$doc = new DOMDocument();
$doc->loadHTML($html);

$xpath = new DOMXPath($doc);

// Select every book title on the page
$titles = $xpath->query('//article[@class="product_pod"]//h3/a/@title');

foreach ($titles as $node) {
    echo $node->nodeValue . "\n";
}

libxml_use_internal_errors(true) 调用至关重要。现实中的 HTML 几乎从未是有效的 XML,若不使用该标志,PHP 会针对每个未闭合的标签或属性不匹配的情况抛出警告。抑制这些警告可让你解析杂乱的页面,而不会让日志被淹没。

XPath 在处理复杂查询时非常强大。想要提取所有价格低于 20 英镑的书籍吗?你可以结合轴和谓词:

$products = $xpath->query('//article[@class="product_pod"]');

foreach ($products as $product) {
    $title = $xpath->query('.//h3/a/@title', $product)->item(0)->nodeValue;
    $price = $xpath->query('.//p[@class="price_color"]', $product)->item(0)->textContent;

    $numericPrice = (float) str_replace('£', '', $price);
    if ($numericPrice < 20.00) {
        echo "$title: $price\n";
    }
}

DOMDocument 配合 XPath 能让你完全掌控一切,且无需任何外部依赖。其代价是冗长性:即便是简单的查询也需要几行代码来设置。这正是 Symfony DomCrawler 大显身手之处。

解析 HTML:Symfony DomCrawler 与 CSS 选择器

Symfony DomCrawler 基于 DOMDocument 构建,但提供了更友好的 API。无需手动编写 XPath,您可以使用 CSS 选择器(大多数 Web 开发者已熟悉)并以类似 jQuery 的风格链式调用方法。

use Symfony\Component\DomCrawler\Crawler;

$crawler = new Crawler($html);

$crawler->filter('article.product_pod')->each(function (Crawler $node) {
    $title = $node->filter('h3 a')->attr('title');
    $price = $node->filter('.price_color')->text();
    echo "$title: $price\n";
});

将其与上文的 DOMXPath 版本对比。虽然意图相同,但 DomCrawler 的代码长度减半,且更易于阅读。 filter() 方法接受任何有效的 CSS 选择器, text() 返回文本内容,并 attr() 提取属性值。

在数据抓取中,何时该使用 CSS 选择器,何时又该使用 XPath?CSS 选择器能覆盖 90% 的实际场景,对于编写前端代码的人来说也更直观。当需要向上遍历(根据子节点的文本选择父节点)、在查询中执行字符串操作,或遍历同级节点时,XPath 则更具优势。 一个实用的经验法则:优先使用 CSS 选择器,仅当 CSS 无法表达所需内容时才转用 XPath。

为何正则表达式在 HTML 解析中风险较高

当你只需从页面中提取一个值时,使用正则表达式确实很诱人 preg_match() 。请克制这种冲动。HTML 并非正则表达式所定义的正则语言,一旦标记发生细微变化(例如新增属性、引号样式切换或多余空格),基于正则表达式的提取就会失效。

// Fragile — breaks if class order changes or attributes are added
preg_match('/<h3 class="title">(.+?)<\/h3>/', $html, $match);

DOM 解析器能优雅地处理所有这些变化。请将正则表达式留给真正的纯文本(日志文件、CSV 行),对于任何源自 HTML 文档的内容,请使用 DOMDocument 或 DomCrawler。

使用 Goutte 及其继任者构建完整的爬虫

Goutte 是一套让 PHP 网页抓取变得触手可及的库。它将 Guzzle 的 HTTP 客户端与 Symfony 的 DomCrawler 整合为一个类,让你能够通过一次调用即可完成数据获取和解析。然而,Goutte 已被官方废弃。其维护者建议迁移至 Symfony HttpBrowser,该组件作为 Symfony BrowserKit 的一部分发布,并提供几乎完全相同的 API。

以下是一个使用 Symfony HttpBrowser 构建的完整爬虫,用于抓取跨多页的图书列表:

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\BrowserKit\HttpBrowser;

$browser = new HttpBrowser(HttpClient::create([
    'headers' => [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
    ],
]));

$books = [];
$url = 'http://books.toscrape.com/catalogue/page-1.html';

while ($url) {
    $crawler = $browser->request('GET', $url);

    $crawler->filter('article.product_pod')->each(function ($node) use (&$books) {
        $books[] = [
            'title' => $node->filter('h3 a')->attr('title'),
            'price' => $node->filter('.price_color')->text(),
            'stock' => trim($node->filter('.availability')->text()),
        ];
    });

    // Follow the "next" pagination link, or stop
    $nextLink = $crawler->filter('li.next a');
    $url = $nextLink->count() > 0
        ? 'http://books.toscrape.com/catalogue/' . $nextLink->attr('href')
        : null;
}

echo count($books) . " books collected.\n";

请注意分页逻辑的运作方式。在解析每页数据后,爬虫会检查是否存在“下一页”链接。如果存在,爬虫将跟随该链接并重复上述过程;如果不存在, $urlnull ,循环随即终止。此模式适用于任何分页列表。

从 Goutte 迁移的工作量极小。如果您的现有代码使用 $goutte = new \Goutte\Client(),请将其替换为 $browser = new HttpBrowser(HttpClient::create())request(), filter()selectLink() 方法保持不变。底层的 HTTP 层从 Guzzle 切换到了 Symfony HttpClient,这为您提供了原生的异步支持,并能与 Symfony 生态系统的其他部分更好地集成。

HttpBrowser 的另一个优势在于:它会在不同请求之间自动追踪 Cookie 和会话。当您多次调用 $browser->request() 多次时,客户端的行为就像一个真实的浏览器会话,无需额外配置即可继承 Cookie。

使用 Symfony Panther 抓取 JavaScript 渲染的页面

当您所需的内容在初始页面加载后由 JavaScript 注入时,静态页面爬虫便会失效。单页应用、无限滚动信息流以及懒加载的产品网格都需要真正的浏览器引擎来渲染。Symfony Panther 通过 WebDriver 协议驱动 Chrome 或 Firefox,填补了这一空白。

安装 Panther 和 ChromeDriver 二进制文件:

composer require symfony/panther
# Panther can auto-detect a locally installed ChromeDriver,
# or you can install one explicitly:
composer require dbrekelmans/bdi
vendor/bin/bdi detect drivers

现在,使用 PHP 抓取一个依赖动态内容渲染的页面:

use Symfony\Component\Panther\Client as PantherClient;

$panther = PantherClient::createChromeClient();
$crawler = $panther->request('GET', 'https://example.com/dynamic-page');

// Wait until the data container is visible in the DOM
$panther->waitFor('.results-container', 10);

$crawler->filter('.results-container .item')->each(function ($node) {
    echo $node->filter('.item-title')->text() . "\n";
});

$panther->quit();

waitFor() 方法会暂停执行,直到指定的 CSS 选择器出现在渲染后的 DOM 中,并设置超时(此处为 10 秒)以防止无限卡死。这对 PHP 的动态内容抓取至关重要,因为您需要的 HTML 可能根本不存在于初始响应中。

Panther 功能强大但资源消耗高。每次请求都会启动一个真实的浏览器进程,从而消耗内存和 CPU。请仅在确实需要 JavaScript 渲染时使用它。对于通过简单的 XHR/API 调用加载数据的页面,通常更快的做法是在浏览器的“网络”标签页中找到该 API 端点,并直接使用 Guzzle 进行请求。

使用爬取 API 实现免维护提取

在某些情况下,维护自有爬虫的工程成本(代理轮换、验证码破解、浏览器指纹识别、重试逻辑)会超过将这些基础设施外包给专业服务的成本。这就是爬取 API 的最佳应用场景。

集成模式非常简单。您只需向 API 端点发送一个 URL,它就会返回包含该页面的 HTML(或结构化 JSON),且所有反机器人处理都在服务器端完成:

$client = new \GuzzleHttp\Client();

$response = $client->get('https://api.webscrapingapi.com/v1', [
    'query' => [
        'api_key' => 'YOUR_API_KEY',
        'url'     => 'http://books.toscrape.com/',
    ],
]);

$html = (string) $response->getBody();
// Parse $html with DomCrawler as usual

在何种情况下,使用爬取 API 比自行开发更合理?当您需要大规模爬取(每天数千个页面)、针对反机器人防御措施严密的网站,或者您的团队没有时间维护代理池和浏览器基础设施时,请考虑使用它。权衡点在于每次请求的成本与工程工时之间的关系。

托管服务在维护负担方面也表现出色。当目标网站更改其反机器人技术栈时,爬取 API 提供商会更新其基础设施。您的代码保持不变。如果您正在评估选项,请寻找仅对成功响应收费的提供商,这样您就无需为失败的请求付费。

爬取数据的存储:CSV、JSON 和 MySQL

收集数据仅是工作的一半。您需要将其以下游流程(分析、机器学习管道、仪表盘)可用的格式进行持久化存储。

CSV是最简单的选项,适用于扁平的表格数据:

$fp = fopen('books.csv', 'w');
fputcsv($fp, ['Title', 'Price', 'Stock']); // header row

foreach ($books as $book) {
    fputcsv($fp, [$book['title'], $book['price'], $book['stock']]);
}

fclose($fp);

JSON 能保留嵌套结构,且更容易导入 API 和 NoSQL 存储:

file_put_contents(
    'books.json',
    json_encode($books, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);

当您需要可查询的关系型存储时,通过 PDO 连接 MySQL 是正确的选择:

$pdo = new PDO('mysql:host=127.0.0.1;dbname=scraper', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$stmt = $pdo->prepare(
    'INSERT INTO books (title, price, stock) VALUES (:title, :price, :stock)'
);

foreach ($books as $book) {
    $stmt->execute([
        ':title' => $book['title'],
        ':price' => $book['price'],
        ':stock' => $book['stock'],
    ]);
}

使用 PDO 时必须采用预编译语句。这能有效防范 SQL 注入攻击——当将用户生成的文本或外部抓取的文本插入数据库时,这是一种切实存在的风险。

对于文档型数据或频繁变更的模式,MongoDB 是另一种可行的选择。 mongodb/mongodb Composer 包提供了一个直观的 insertMany() 方法,可直接接受关联数组的数组。选择关系型存储还是文档型存储,取决于您抓取的数据结构化程度以及后续的消费场景。

错误处理、重试与日志记录

在笔记本电脑上运行的爬虫与在生产环境中稳定运行的爬虫截然不同。当您发起数千次 HTTP 请求时,网络超时、5xx 响应、连接重置和速率限制错误在所难免。从一开始就在爬虫中构建容错机制,可避免数据无声丢失。

请使用带指数退避机制的 try-catch 块包裹每个 HTTP 调用:

function fetchWithRetry(\GuzzleHttp\Client $client, string $url, int $maxRetries = 3): string
{
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        try {
            $response = $client->get($url);
            return (string) $response->getBody();
        } catch (\GuzzleHttp\Exception\GuzzleException $e) {
            if ($attempt === $maxRetries) {
                throw $e;
            }
            $wait = (int) pow(2, $attempt); // 2s, 4s, 8s
            sleep($wait);
        }
    }
}

对于结构化日志记录,Monolog 是 PHP 生态系统中的事实标准。添加一个轮转文件处理器只需两行代码:

use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;

$log = new Logger('scraper');
$log->pushHandler(new RotatingFileHandler('logs/scraper.log', 7, Logger::INFO));

$log->info('Fetching page', ['url' => $url]);
$log->error('Request failed', ['url' => $url, 'error' => $e->getMessage()]);

记录每个请求的 URL、状态码以及任何异常。当抓取任务在 1,000 页中的第 847 页失败时,日志是唯一能告诉你出了什么问题的线索。这种对生产就绪性的关注,正是原型与可靠管道之间的区别所在。

规避封禁:代理、头部信息与速率限制

网站并不欢迎机器人疯狂轰炸其服务器。如果您的爬虫从单一 IP 地址每分钟发送数百个相同请求,很可能会被封禁。对于长期运行的项目而言,礼貌地进行爬取既是一种道德义务,也是实际的必要条件。

轮换 User-Agent 字符串,确保每个请求不会被识别为来自同一客户端:

$userAgents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15',
    'Mozilla/5.0 (X11; Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0',
];

$headers = ['User-Agent' => $userAgents[array_rand($userAgents)]];

在请求之间添加随机延迟,以避免可预测的时间间隔模式:

function politeDelay(int $minMs = 1000, int $maxMs = 3000): void
{
    usleep(random_int($minMs, $maxMs) * 1000);
}

通过 robots.txt 。在抓取某个域名之前,先获取其 robots.txt 并检查目标路径是否被禁止访问。你可以手动解析,或使用 spatie/robots-txt:

// Pseudocode — check before scraping
$robots = file_get_contents('http://example.com/robots.txt');
if (str_contains($robots, 'Disallow: /private/')) {
    echo "Skipping disallowed path.\n";
}

代理轮换是防范基于 IP 封禁的最有效手段。若您进行的是规模较大的抓取,通过住宅代理池路由请求可使您的流量几乎无法与自然用户区分。您只需一个选项即可配置 Guzzle 使用代理:

$client = new \GuzzleHttp\Client([
    'proxy' => 'http://user:pass@proxy-host:port',
]);

结合所有这些技术(多样化的请求头、礼貌延迟、遵守 robots.txt 以及代理轮换),将使您在避免被标记的同时,获得最可靠的抓取效果。

法律与道德考量

网络爬虫处于法律灰色地带,具体情况因司法管辖区而异。但有几个原则具有普遍适用性。

robots.txt 是一项自愿性标准,而非法律合同,但若无视该文件,一旦遭遇质疑,您提出的任何善意辩解都将失去说服力。请将其视为必须始终遵守的基本准则。

目标网站的服务条款(ToS)可能明确禁止自动化访问。违反服务条款可能使您面临违约索赔,特别是在美国,自 hiQ Labs 诉 LinkedIn 案以来,该案虽澄清了抓取公开数据并不必然违反《计算机欺诈与滥用法案》,但并未涉及服务条款的执行问题。

若您抓取属于欧盟居民的个人数据(姓名、电子邮箱、个人资料详情),则需遵守《通用数据保护条例》(GDPR。根据GDPR,网页抓取可能构成数据处理,这意味着您需要具备合法依据(通常为正当利益),并必须按照GDPR的要求处理数据:包括目的限制、存储最小化,以及尊重数据主体的访问请求。如有疑问,请咨询法律专业人士,特别是当您的抓取目标涉及用户生成内容时。

基本道德准则很明确:不要以影响目标网站性能的速度进行抓取,不要收集无正当用途的数据,并在可能的情况下透明地说明您的意图。

关键要点

  • 根据页面类型选择合适的工具。对于静态 HTML,使用 Guzzle 配合 DomCrawler;对于 JavaScript 渲染的内容,使用 Symfony Panther;当反机器人基础设施的防护能力超过您的自建方案时,请使用专业爬取 API。
  • Goutte 已弃用。新项目请使用 Symfony HttpBrowser,它基于持续维护的 Symfony 组件,提供相同的爬取工作流。
  • 从项目初期就构建容错机制。指数退避重试、结构化日志记录和输入验证在生产环境中的爬虫中绝非可有可无。
  • 按下游用户所需的格式存储数据。CSV 用于快速分析,JSON 用于 API 和文档存储,MySQL/PDO 用于关系型查询。
  • 礼貌且合法地进行抓取。轮换请求头和代理,遵守 robots.txt、在请求间添加延迟,并了解收集个人数据对 GDPR 的影响。

常见问题

对于网页抓取项目,PHP 和 Python 哪个更好?

客观而言两者并无优劣之分。Python拥有更庞大的抓取生态系统(Beautiful Soup、Scrapy、Selenium绑定),这意味着有更多的教程和社区解答。PHP内置了强大的HTTP和DOM扩展,且Composer库如Guzzle和DomCrawler已达到生产级水平。请选择团队最熟悉的语言。一个编写精良的PHP抓取程序,其性能永远优于一个维护不善的Python抓取程序。

PHP 能否抓取大量使用 JavaScript 的单页应用程序?

可以,但需要使用无头浏览器。Symfony Panther 通过 WebDriver 协议控制 Chrome 或 Firefox,能够渲染完全动态的页面。对于页面仅从 XHR 端点获取数据的简单场景,您可以完全跳过浏览器,直接使用 HTTP 客户端调用该 API 端点,这样速度更快且资源消耗更少。

网络爬取是否合法?GDPR如何适用?

合法性取决于管辖区域、目标网站的服务条款以及收集的数据类型。在许多司法管辖区,抓取公开可访问的非个人数据通常是被允许的。当处理欧盟居民的个人数据时,GDPR 即适用,这要求具备合法依据(如正当利益)。在进行大规模个人数据抓取前,请务必审查目标网站的服务条款并咨询法律顾问。

如何在使用 PHP 进行抓取时避免 IP 被封禁?

结合使用多种技术:轮换 User-Agent 字符串、在请求间添加随机延迟(1 到 3 秒是合理的范围)、遵守 robots.txt 指令,并通过轮换代理池路由流量。避免从单一 IP 地址发送突发请求。若进行大规模抓取,托管代理或抓取 API 服务可为您自动处理轮换和反检测事宜。

使用 PHP 抓取时,如何处理受登录保护的页面?

通过 POST 请求提交凭据(或使用 Symfony HttpBrowser 通过表单提交),并在后续请求中保持生成的会话 Cookie。使用 HttpBrowser 时,会话 Cookie 会自动保留。使用原生 cURL 时,请设置 CURLOPT_COOKIEJARCURLOPT_COOKIEFILE 设置为相同的路径。务必确认登录操作未触发验证码或双因素验证,并注意:在登录页面后进行抓取可能因网站服务条款而面临更严格的法律风险。

结论

一旦掌握了合适的库,使用 PHP 进行网页抓取便成为一种实用且支持完善的工作流。建议从 cURL 或 Guzzle 开始进行数据获取,叠加 DomCrawler 或 DOMXPath 进行解析,仅在无法避免 JavaScript 渲染时才升级至 Symfony Panther。请以用户期望的格式保存数据,为所有操作添加重试逻辑和日志记录,并始终保持礼貌地进行抓取。

本教程中的示例涵盖了完整的生命周期:从原始 HTTP 请求,到分页处理、并发抓取、数据存储以及防封策略。每项技术都对应着实际生产环境中的具体需求,而不仅仅是简单的演示。

如果您发现自己花在应对反机器人防御上的时间,比编写解析逻辑还要多,不妨考虑将请求基础设施交由 WebScrapingAPI 的 Scraper API 等服务来处理。该服务负责代理轮换、验证码识别和重试操作,让您能够专注于真正重要的数据提取代码。

关于作者
Sorin-Gabriel Marica, 全栈开发工程师 @ WebScrapingAPI
Sorin-Gabriel Marica全栈开发工程师

索林·马里卡(Sorin Marica)是 WebScrapingAPI 的全栈及 DevOps 工程师,负责开发产品功能并维护确保平台平稳运行的基础设施。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。