Quay lại blog

PHP Web Scraping: Hướng Dẫn Thực Tế

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

11-Aug-2026

TL;DR:

  • PHP cung cấp mọi thứ mà một trình thu thập thông tin cơ bản cần. ext-curl lấy dữ liệu, DOMDocument cộng với DOMXPath phân tích, và không có gói Composer nào liên quan. Một đoạn mã hoạt động khoảng mười lăm dòng.
  • Cài đặt php-cli mặc định là không đủ. Trên một máy Ubuntu sạch sẽ php-cli chỉ mở jsonlibxml; curl, dommbstring mỗi cái phải được cài đặt riêng biệt. Kiểm tra trước khi bạn viết mã, không phải sau.
  • DOMDocument::loadHTML() sẽ khiến đầu ra của bạn ngập trong cảnh báo. Các trang thực là HTML5 và trình phân tích mong đợi HTML4. Bọc việc tải trong libxml_use_internal_errors(true) hoặc tiếng ồn sẽ giống như một lỗi khi không có gì sai cả.
  • API Thu thập Thông tin Toàn cầu Scrapeless trả về một phong bì JSON. Cú pháp nằm trong data, vì vậy bạn giải mã trước và phân tích sau.
  • chrome-php điều khiển một trình duyệt từ xa, nhưng thời gian chờ mặc định của nó được thiết kế cho một cái cục bộ. Đo lường chống lại một điểm cuối CDP trên đám mây, thời gian mặc định 5 giây của thư viện hoàn thành 2 trong 6 lần thử; nâng sendSyncDefaultTimeout đã đưa điều đó lên 5 trong 6.
  • Bắt đầu miễn phí: trang quản lý Scrapeless cung cấp một khóa mà hoạt động với mọi ví dụ dưới đây.

Những gì bạn cần

Mỗi ví dụ ở đây được chạy trên PHP 8.3.6 (cli) chống lại quotes.toscrape.com, một trang web được xuất bản đặc biệt để thực hành thu thập thông tin.

Điều ngạc nhiên đầu tiên trên một máy sạch là cài đặt PHP cơ bản ít bao gồm như thế nào. Một php-cli vừa được cài đặt báo cáo chỉ jsonlibxml từ bộ mà quan trọng ở đây. Ba cái mà bạn thực sự cần được gửi riêng biệt:

bash Copy
# Ubuntu/Debian — php-cli alone is not enough
apt-get install -y php-cli php-curl php-xml php-mbstring

php -m | grep -E '^(curl|dom|libxml|mbstring)$'
# curl
# dom
# libxml
# mbstring

php-xml là gói cung cấp DOMDocument; phần mở rộng được gọi là dom, đó là lý do tại sao việc tìm kiếm xml không tìm thấy gì và khiến mọi người đi lòng vòng.

Lấy một trang với ext-curl

file_get_contents() hoạt động cho các trường hợp trivial, nhưng nó không cung cấp mã trạng thái, không kiểm soát tiêu đề và không có thời gian chờ đáng kể. ext-curl là cơ sở cho bất cứ điều gì thực sự:

php Copy
<?php
$ch = curl_init('https://quotes.toscrape.com/');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; php-guide/1.0)',
]);
$html   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo "http={$status} bytes=" . strlen($html) . "\n";

Điều đó in http=200 bytes=11064. Hai tùy chọn mang nhiều trọng lượng hơn vẻ bề ngoài: CURLOPT_RETURNTRANSFER là điều khiến curl_exec() trả về nội dung thay vì in nó, và không có CURLOPT_USERAGENT nhiều trang trả lời một khách hàng PHP trống rỗng khác đi hoặc hoàn toàn không.

Phân tích bằng DOMDocument và DOMXPath

Mở rộng DOM của PHP là một triển khai đầy đủ của tiêu chuẩn W3C DOM, và DOMXPath mang lại cho bạn sức mạnh truy vấn giống như bất kỳ thư viện thu thập thông tin nào. Cạm bẫy là trình tải.

php Copy
<?php
$doc = new DOMDocument();
libxml_use_internal_errors(true);   // without this, every HTML5 tag warns
$doc->loadHTML($html);
libxml_clear_errors();

$xpath  = new DOMXPath($doc);
$quotes = $xpath->query("//div[@class='quote']");
echo "quotes=" . $quotes->length . "\n";

$first  = $quotes->item(0);
$text   = trim($xpath->query(".//span[@class='text']", $first)->item(0)->textContent);
$author = trim($xpath->query(".//small[@class='author']", $first)->item(0)->textContent);

echo "first_author={$author}\n";
echo "first_text=" . mb_substr($text, 0, 40) . "\n";

Đầu ra:

text Copy
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr

Ba điều đáng lưu ý.

libxml_use_internal_errors(true) không phải là tùy chọn trong thực tế. DOMDocument thực hiện phân tích HTML4, vì vậy mọi phần tử HTML5 và thuộc tính không được trích dẫn trên một trang hiện đại sẽ dấy lên một cảnh báo. Bỏ qua cuộc gọi và một lần thu thập thông tin thành công chôn vùi đầu ra của nó dưới tiếng ồn của trình phân tích — sách hướng dẫn PHP tài liệu hóa công tắc này như là cách hỗ trợ để kiểm soát việc báo cáo đó.

Tham số thứ hai của DOMXPath::query() giới hạn truy vấn đến một nút. Nếu không có nó, .//span[@class='text'] tìm kiếm toàn bộ tài liệu và bạn nhận được văn bản của trích dẫn đầu tiên cho mỗi hàng. Tham số duy nhất đó là sự khác biệt giữa việc lấy thông tin theo hàng và một bộ dữ liệu có sai sót tinh tế.

mb_substr() hơn là substr() quan trọng vì trang sử dụng dấu ngoặc kép kiểu xoắn. substr() cắt trên byte và sẽ phân tách một ký tự nhiều byte thành UTF-8 không hợp lệ, đó chính xác là loại lỗi xảy ra ba bước sau trong cơ sở dữ liệu.

Nơi PHP thuần hết limit

Đoạn mã ở trên hoạt động vì mục tiêu hiển thị nội dung của nó từ phía máy chủ và không quan tâm ai đang yêu cầu. Hai điều chấm dứt điều đó: nội dung chỉ tồn tại sau khi JavaScript thực thi, và các trang quyết định rằng một khách hàng HTTP trống rỗng không phải là một trình duyệt. Không cái nào là một vấn đề của PHP — không có khách hàng HTTP nào giải quyết được cả hai, bằng bất kỳ ngôn ngữ nào.

Điều hướng yêu cầu qua một bể proxy xử lý một phần của trường hợp thứ hai, và nếu bạn đang làm việc trong một khung, hướng dẫn tích hợp proxy Laravel bao quát cấu hình đó một cách sâu sắc hơn bài viết này.

Tại thời điểm đó có hai biện pháp leo thang, và chúng là những công cụ khác nhau hơn là những phiên bản tốt hơn của nhau. Giữ con đường HTTP bình thường nơi nó hoạt động; nó vẫn là lựa chọn nhanh nhất và rẻ nhất với một khoảng cách rộng.

Biện pháp leo thang đầu tiên: API Thu thập Thông tin Toàn cầu

Biện pháp leo thang đầu tiên giữ cho mã của bạn hình dạng như một khách hàng HTTP và chuyển phần khó khăn sang phía máy chủ. Yêu cầu là bình thường ext-curl; phản hồi là nơi sự khác biệt xuất hiện.

php Copy
<?php
$key     = getenv('SCRAPELESS_API_KEY');
$payload = json_encode([
    'actor' => 'unlocker.webunlocker',
    'input' => ['url' => 'https://quotes.toscrape.com/', 'js_render' => false],
]);

$ch = curl_init('https://api.scrapeless.com/api/v2/unlocker/request');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_TIMEOUT        => 90,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json', "x-api-token: {$key}"],
]);
$raw    = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$body = json_decode($raw, true);
echo "http={$status} envelope_keys=" . implode(',', array_keys($body)) . "\n";

$apiHtml = $body['data'];              // the markup lives here, not in $raw
echo "bytes=" . strlen($apiHtml) . "\n";
text Copy
http=200 envelope_keys=code,data
bytes=11064

Bì thư là thứ cần được nội bộ hóa. $raw là JSON, và việc truyền nó đến DOMDocument tạo ra một tài liệu không có các nút khớp và không có lỗi — các bộ chọn đơn giản trả về không có hàng. Giải mã, lấy data, rồi phân tích. Giữ phần unwrap đó trong một trợ giúp thay vì lặp lại json_decode(...)['data'] ở các điểm gọi.

Ngoài số byte, mã phân tích không thay đổi. Cung cấp $apiHtml vào cùng một khối DOMXPath trả về cùng mười câu trích dẫn, đó là điều quan trọng: chỉ việc lấy dữ liệu là khác.

Leo Thang Hai: Một Trình Duyệt Thực Sự Từ PHP

Khi nội dung thực sự cần JavaScript, bạn cần một trình duyệt. chrome-php/chrome giao tiếp với Giao thức DevTools của Chrome và có thể khởi động một trình duyệt Chrome cục bộ hoặc kết nối với một cái từ xa qua WebSocket.

bash Copy
composer require chrome-php/chrome
# Using version ^1.16 for chrome-php/chrome  → v1.16.1
php Copy
<?php
require __DIR__ . '/vendor/autoload.php';

use HeadlessChromium\BrowserFactory;

$key = getenv('SCRAPELESS_API_KEY');
$uri = "wss://browser.scrapeless.com/api/v2/browser?token={$key}";

$browser = BrowserFactory::connectToBrowser($uri, [
    'sendSyncDefaultTimeout' => 60000,   // see below — the default is 5000
]);

$page = $browser->createPage();
$page->navigate('https://quotes.toscrape.com/')->waitForNavigation();

echo "title: "          . $page->evaluate('document.title')->getReturnValue() . "\n";
echo "quotes on page: " . $page->evaluate('document.querySelectorAll(".quote").length')->getReturnValue() . "\n";

$browser->close();

Output:

text Copy
title: Quotes to Scrape
quotes on page: 10

Thời gian chờ Mặc định Có Kích thước Cho Một Trình Duyệt Cục Bộ

Dòng sendSyncDefaultTimeout là phần đáng giá của bài viết. chrome-php mặc định là 5000 ms, điều này là rộng rãi cho một Chrome chạy trên cùng một máy và hạn chế cho một cái qua kết nối TLS. Hai tập hợp khớp của sáu lần chạy, cùng một kịch bản, cùng một mục tiêu, chỉ thay đổi tùy chọn đó:

cấu hình kết quả chế độ thất bại
mặc định của thư viện (5000 ms) 2 thành công, 4 thất bại mọi thất bại OperationTimedOut: Operation timed out after 5s
sendSyncDefaultTimeout => 60000 5 thành công, 1 thất bại thất bại duy nhất là một lỗi khác, tại thời điểm kết nối

Có hai kết luận, và kết luận thứ hai là điều mà mọi người thường bỏ lỡ.

Tăng thời gian chờ là cần thiết. Để mặc định, phần lớn các thử nghiệm đã chết trên cùng một thông điệp, và đó là một thông điệp gây hiểu lầm — nó thông báo một thời gian chờ, vì vậy nó đọc như thể trang hoặc mạng đang chậm, khi cái thực sự hết hạn là thời gian chờ của thư viện trên một vòng chuyền giao thức.

Tăng thời gian chờ cũng không đủ. Thất bại duy nhất mà sống sót là Cannot connect to the browser, make sure it was not closed, được kích hoạt khoảng năm giây, trong khi kết nối vẫn đang được thiết lập thay vì trong quá trình ra lệnh. Một thời gian chờ lệnh lớn hơn không ảnh hưởng đến điều đó. Hãy coi việc có được trình duyệt như là một bước có thể thất bại riêng trong thiết kế của công việc, khác biệt với công việc mà bạn làm khi bạn đã có một cái, và giữ $browser->close() trong một finally để một thất bại giữa công việc không bao giờ làm gián đoạn phiên.

Một Chẩn Đoán Đã Sai

Lý thuyết đầu tiên cho những thời gian chờ đó là chuỗi truy vấn của URI không bao giờ tới được bắt tay WebSocket, mang theo ?token= cùng nó. Có bằng chứng thực sự cho điều đó: Protocol::validateSocketUri() chỉ trả về [$scheme, $host, $port] và từ chối đường dẫn và truy vấn ngay lập tức.

Nó vẫn sai. Bắt tay được xây dựng ở nơi khác, bởi Protocol::getRequestHandshake(), cái gọi một validateUri() riêng biệt trả về năm phần tử và rõ ràng tái bổ sung truy vấn trước dòng yêu cầu. Mã thông báo vẫn đến. Thất bại là độ trễ, và hai chức năng phân tích cùng một URI đến độ sâu khác nhau là một sự tr có tình cờ trông giống như một lỗi.

Đó là một lời nhắc nhở hữu ích rằng trong một thư viện với nhiều bộ phân tích URI, không phải tìm thấy một cái rơi dữ liệu của bạn có nghĩa là nó là cái trên con đường mà bạn quan tâm.

Chọn Giữa Ba Cách

phương pháp sử dụng khi nào chi phí
ext-curl + DOMXPath HTML được máy chủ_rendered, mục tiêu cho phép thấp nhất; không phụ thuộc
Universal Scraping API bị chặn hoặc thách thức, không cần JS một cuộc gọi HTTP, bì thư để tháo ra
chrome-php + Trình Duyệt Scraping nội dung cần thực thi JavaScript cao nhất; một phiên trình duyệt cho mỗi công việc

Làm việc từ dưới lên, không phải từ trên xuống. Hầu hết các trang trông như thể cần một trình duyệt lại hóa ra nhúng dữ liệu của chúng trong một thẻ <script>, và một truy vấn DOMXPath cộng với json_decode() đánh bại một phiên trình duyệt trên mọi trục.

Kết Luận

PHP là một ngôn ngữ scraping hợp lý, và các phần mà mọi người mong đợi bị thiếu nằm trong thư viện chuẩn. ext-curlDOMXPath hoàn toàn bao phủ các trang được máy chủ_rendered, với libxml_use_internal_errors(true) và một đối số thứ hai hạn chế cho query() như hai chi tiết tách biệt mã làm việc với mã không đúng một cách âm thầm.

Khi một mục tiêu ngừng hợp tác, hãy leo thang một cách có chủ đích. Đường dẫn API giữ cho mã của bạn là một khách hàng HTTP và chỉ yêu cầu bạn tháo một bì thư. Đường dẫn trình duyệt tốn một phiên và, nếu trình duyệt đó là từ xa, một mảnh cấu hình cụ thể: mặc định năm giây của chrome-php là giả định Chrome cục bộ, và để nó ở lại đã khiến bốn trong sáu kết nối khó khăn ở đây.

Sẵn Sàng Để Scrape Từ PHP?

Tạo một khóa trên Scrapeless dashboard và chạy ví dụ ext-curl chống lại một trang mà bạn đã thu thập. Nếu nó trả về những gì bạn mong đợi, bạn đã xong — không phụ thuộc, không trình duyệt. Universal Scraping API có sẵn cho các trang mà nó không hoạt động, và tỷ lệ hiện tại có trên trang giá cả.

Câu hỏi thường gặp

H: Tôi có cần Composer để thu thập dữ liệu bằng PHP không?

Không. Lấy dữ liệu bằng ext-curl và phân tích bằng DOMDocumentDOMXPath không cần gì ngoài các phần mở rộng trong một cài đặt tiêu chuẩn. Composer chỉ xuất hiện khi cần một trình điều khiển trình duyệt như chrome-php/chrome.

H: Tại sao DOMDocument in ra cảnh báo trên mọi trang?

Bởi vì nó thực hiện phân tích HTML4 và các trang hiện đại sử dụng HTML5. Các cảnh báo chỉ mang tính thông tin thay vì là lỗi. Gọi libxml_use_internal_errors(true) trước loadHTML()libxml_clear_errors() sau, và kiểm tra libxml_get_errors() khi bạn thực sự muốn xem chúng.

H: Tại sao phân tích của tôi không trả về gì khi yêu cầu API thành công?

Bạn gần như chắc chắn đang phân tích phong bì. Universal Scraping API trả về JSON với mã trong data, vì vậy DOMDocument nhận một chuỗi JSON và không tìm thấy các nút phù hợp mà không sinh ra lỗi. Giải mã phản hồi và phân tích data.

H: Tôi sử dụng XPath nào cho thuộc tính lớp?

//div[@class='quote'] chỉ khớp với một giá trị thuộc tính chính xác. Đối với một phần tử mang nhiều lớp, hãy sử dụng //div[contains(concat(' ', normalize-space(@class), ' '), ' quote ')], điều này tránh việc khớp quote-footer theo cách mà một contains() không có sẽ khớp.

H: Tôi có nên sử dụng PHP cho một dự án thu thập dữ liệu lớn không?

Đối với công việc lấy và phân tích ở quy mô lớn, có, curl_multi_* cho bạn tính đồng thời thực sự và phần mở rộng DOM nhanh. Nơi mà PHP yếu hơn là tự động hóa trình duyệt lâu dài, nơi mà công cụ xung quanh Playwright và Puppeteer đã phát triển hơn. Một chia sẻ phổ biến là PHP cho đường dẫn HTTP và một dịch vụ trình duyệt cho các trang thực sự cần đến nó.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục