跳到內容

DomCrawler 組件

編輯此頁面

DomCrawler 組件簡化了 HTML 和 XML 文件中的 DOM 導航。

注意

雖然有可能,但 DomCrawler 組件並非設計用於操作 DOM 或重新轉儲 HTML/XML。

安裝

1
$ composer require symfony/dom-crawler

注意

如果您在 Symfony 應用程式之外安裝此組件,您必須在您的程式碼中引入 vendor/autoload.php 檔案,以啟用 Composer 提供的類別自動載入機制。請閱讀這篇文章以瞭解更多詳細資訊。

用法

另請參閱

本文說明如何在任何 PHP 應用程式中將 DomCrawler 功能作為獨立組件使用。請閱讀Symfony 功能測試文章,以瞭解在建立 Symfony 測試時如何使用它。

Crawler 類別提供了查詢和操作 HTML 和 XML 文件的方法。

Crawler 的實例代表一組 DOMElement 物件,這些物件是可以如下遍歷的節點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\DomCrawler\Crawler;

$html = <<<'HTML'
<!DOCTYPE html>
<html>
    <body>
        <p class="message">Hello World!</p>
        <p>Hello Crawler!</p>
    </body>
</html>
HTML;

$crawler = new Crawler($html);

foreach ($crawler as $domElement) {
    var_dump($domElement->nodeName);
}

專門的 LinkImageForm 類別對於在遍歷 HTML 樹狀結構時與 html 連結、圖片和表單互動非常有用。

注意

DomCrawler 將嘗試自動修正您的 HTML 以符合官方規範。例如,如果您在另一個 <p> 標籤內巢狀一個 <p> 標籤,它將被移動為父標籤的兄弟標籤。這是預期的,並且是 HTML5 規範的一部分。但是,如果您遇到意外的行為,這可能是原因之一。雖然 DomCrawler 並非旨在轉儲內容,但您可以透過轉儲它來查看 HTML 的「修正」版本。

節點過濾

使用 XPath 表達式,您可以選取文件中的特定節點

1
$crawler = $crawler->filterXPath('descendant-or-self::body/p');

提示

DOMXPath::query 在內部用於實際執行 XPath 查詢。

如果您偏好 CSS 選取器而不是 XPath,請安裝CssSelector 組件。它允許您使用類似 jQuery 的選取器

1
$crawler = $crawler->filter('body > p');

匿名函式可用於使用更複雜的條件進行過濾

1
2
3
4
5
6
7
8
9
use Symfony\Component\DomCrawler\Crawler;
// ...

$crawler = $crawler
    ->filter('body > p')
    ->reduce(function (Crawler $node, $i): bool {
        // filters every other node
        return ($i % 2) === 0;
    });

若要移除節點,匿名函式必須傳回 false

注意

所有篩選方法都會傳回具有篩選內容的新 Crawler 實例。若要檢查篩選器是否實際找到任何內容,請在這個新的 crawler 上使用 $crawler->count() > 0

filterXPath()filter() 方法都適用於 XML 命名空間,這些命名空間可以自動探索或明確註冊。

考量以下 XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<entry
    xmlns="http://www.w3.org/2005/Atom"
    xmlns:media="http://search.yahoo.com/mrss/"
    xmlns:yt="http://gdata.youtube.com/schemas/2007"
>
    <id>tag:youtube.com,2008:video:kgZRZmEc9j4</id>
    <yt:accessControl action="comment" permission="allowed"/>
    <yt:accessControl action="videoRespond" permission="moderated"/>
    <media:group>
        <media:title type="plain">Chordates - CrashCourse Biology #24</media:title>
        <yt:aspectRatio>widescreen</yt:aspectRatio>
    </media:group>
</entry>

可以使用 Crawler 進行篩選,而無需使用 filterXPath() 註冊命名空間別名

1
$crawler = $crawler->filterXPath('//default:entry/media:group//yt:aspectRatio');

filter()

1
$crawler = $crawler->filter('default|entry media|group yt|aspectRatio');

注意

預設命名空間以字首「default」註冊。可以使用 setDefaultNamespacePrefix() 方法變更它。

如果預設命名空間是文件中唯一的命名空間,則會在載入內容時移除它。這樣做是為了簡化 XPath 查詢。

可以使用 registerNamespace() 方法明確註冊命名空間

1
2
$crawler->registerNamespace('m', 'http://search.yahoo.com/mrss/');
$crawler = $crawler->filterXPath('//m:group//yt:aspectRatio');

驗證目前的節點是否符合選取器

1
$crawler->matches('p.lorem');

節點遍歷

依節點在清單中的位置存取節點

1
$crawler->filter('body > p')->eq(0);

取得目前選取範圍的第一個或最後一個節點

1
2
$crawler->filter('body > p')->first();
$crawler->filter('body > p')->last();

取得與目前選取範圍相同層級的節點

1
$crawler->filter('body > p')->siblings();

取得目前選取範圍之後或之前的相同層級節點

1
2
$crawler->filter('body > p')->nextAll();
$crawler->filter('body > p')->previousAll();

取得所有子節點或祖先節點

1
2
$crawler->filter('body')->children();
$crawler->filter('body > p')->ancestors();

取得符合 CSS 選取器的所有直接子節點

1
$crawler->filter('body')->children('p.lorem');

取得元素的第一個父節點(朝文件根目錄方向),該節點符合提供的選取器

1
$crawler->closest('p.lorem');

注意

所有遍歷方法都會傳回新的 Crawler 實例。

存取節點值

存取目前選取範圍的第一個節點的節點名稱(HTML 標籤名稱),例如「p」或「div」

1
2
// returns the node name (HTML tag name) of the first child element under <body>
$tag = $crawler->filterXPath('//body/*')->nodeName();

存取目前選取範圍的第一個節點的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// if the node does not exist, calling to text() will result in an exception
$message = $crawler->filterXPath('//body/p')->text();

// avoid the exception passing an argument that text() returns when node does not exist
$message = $crawler->filterXPath('//body/p')->text('Default text content');

// by default, text() trims whitespace characters, including the internal ones
// (e.g. "  foo\n  bar    baz \n " is returned as "foo bar baz")
// pass FALSE as the second argument to return the original text unchanged
$crawler->filterXPath('//body/p')->text('Default text content', false);

// innerText() is similar to text() but returns only text that is a direct
// descendant of the current node, excluding text from child nodes
$text = $crawler->filterXPath('//body/p')->innerText();
// if content is <p>Foo <span>Bar</span></p> or <p><span>Bar</span> Foo</p>
// innerText() returns 'Foo' in both cases; and text() returns 'Foo Bar' and 'Bar Foo' respectively

// if there are multiple text nodes, between other child nodes, like
// <p>Foo <span>Bar</span> Baz</p>
// innerText() returns only the first text node 'Foo'

// like text(), innerText() also trims whitespace characters by default,
// but you can get the unchanged text by passing FALSE as argument
$text = $crawler->filterXPath('//body/p')->innerText(false);

存取目前選取範圍的第一個節點的屬性值

1
$class = $crawler->filterXPath('//body/p')->attr('class');

提示

您可以使用 attr() 方法的第二個引數來定義在節點或屬性為空時使用的預設值

1
$class = $crawler->filterXPath('//body/p')->attr('class', 'my-default-class');

從節點清單中擷取屬性和/或節點值

1
2
3
4
$attributes = $crawler
    ->filterXpath('//body/p')
    ->extract(['_name', '_text', 'class'])
;

注意

特殊屬性 _text 代表節點值,而 _name 代表元素名稱(HTML 標籤名稱)。

在清單中的每個節點上呼叫匿名函式

1
2
3
4
5
6
use Symfony\Component\DomCrawler\Crawler;
// ...

$nodeValues = $crawler->filter('p')->each(function (Crawler $node, $i): string {
    return $node->text();
});

匿名函式接收節點(作為 Crawler)和位置作為引數。結果是由匿名函式呼叫傳回的值陣列。

當使用巢狀 crawler 時,請注意 filterXPath() 是在 crawler 的上下文中評估的

1
2
3
4
5
6
7
8
$crawler->filterXPath('parent')->each(function (Crawler $parentCrawler, $i): void {
    // DON'T DO THIS: direct child can not be found
    $subCrawler = $parentCrawler->filterXPath('sub-tag/sub-child-tag');

    // DO THIS: specify the parent tag too
    $subCrawler = $parentCrawler->filterXPath('parent/sub-tag/sub-child-tag');
    $subCrawler = $parentCrawler->filterXPath('node()/sub-tag/sub-child-tag');
});

新增內容

crawler 支援多種新增內容的方式,但它們是互斥的,因此您只能使用其中一種方式來新增內容(例如,如果您將內容傳遞給 Crawler 建構子,則稍後無法呼叫 addContent()

1
2
3
4
5
6
7
8
9
10
$crawler = new Crawler('<html><body/></html>');

$crawler->addHtmlContent('<html><body/></html>');
$crawler->addXmlContent('<root><node/></root>');

$crawler->addContent('<html><body/></html>');
$crawler->addContent('<root><node/></root>', 'text/xml');

$crawler->add('<html><body/></html>');
$crawler->add('<root><node/></root>');

注意

addHtmlContent()addXmlContent() 方法預設為 UTF-8 編碼,但您可以使用它們的第二個選用引數來變更此行為。

addContent() 方法會根據給定的內容猜測最佳字元集,如果無法猜測字元集,則預設為 ISO-8859-1

由於 Crawler 的實作是基於 DOM 擴充功能,因此它也能夠與原生 DOMDocumentDOMNodeListDOMNode 物件互動

1
2
3
4
5
6
7
8
9
10
$domDocument = new \DOMDocument();
$domDocument->loadXml('<root><node/><node/></root>');
$nodeList = $domDocument->getElementsByTagName('node');
$node = $domDocument->getElementsByTagName('node')->item(0);

$crawler->addDocument($domDocument);
$crawler->addNodeList($nodeList);
$crawler->addNodes([$node]);
$crawler->addNode($node);
$crawler->add($domDocument);

這些 Crawler 上的方法旨在最初填入您的 Crawler,而非用於進一步操作 DOM(儘管這是可能的)。但是,由於 Crawler 是一組 DOMElement 物件,因此您可以使用 DOMElementDOMNodeDOMDocument 上可用的任何方法或屬性。例如,您可以使用類似以下內容取得 Crawler 的 HTML

1
2
3
4
5
$html = '';

foreach ($crawler as $domElement) {
    $html .= $domElement->ownerDocument->saveHTML($domElement);
}

或者您可以使用 html() 取得第一個節點的 HTML

1
2
3
4
5
// if the node does not exist, calling to html() will result in an exception
$html = $crawler->html();

// avoid the exception passing an argument that html() returns when node does not exist
$html = $crawler->html('Default <strong>HTML</strong> content');

或者您可以使用 outerHtml() 取得第一個節點的外部 HTML

1
$html = $crawler->outerHtml();

表達式評估

evaluate() 方法會評估給定的 XPath 表達式。傳回值取決於 XPath 表達式。如果表達式評估為純量值(例如 HTML 屬性),則會傳回結果陣列。如果表達式評估為 DOM 文件,則會傳回新的 Crawler 實例。

此行為最好透過範例來說明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use Symfony\Component\DomCrawler\Crawler;

$html = '<html>
<body>
    <span id="article-100" class="article">Article 1</span>
    <span id="article-101" class="article">Article 2</span>
    <span id="article-102" class="article">Article 3</span>
</body>
</html>';

$crawler = new Crawler();
$crawler->addHtmlContent($html);

$crawler->filterXPath('//span[contains(@id, "article-")]')->evaluate('substring-after(@id, "-")');
/* Result:
[
    0 => '100',
    1 => '101',
    2 => '102',
];
*/

$crawler->evaluate('substring-after(//span[contains(@id, "article-")]/@id, "-")');
/* Result:
[
    0 => '100',
]
*/

$crawler->filterXPath('//span[@class="article"]')->evaluate('count(@id)');
/* Result:
[
    0 => 1.0,
    1 => 1.0,
    2 => 1.0,
]
*/

$crawler->evaluate('count(//span[@class="article"])');
/* Result:
[
    0 => 3.0,
]
*/

$crawler->evaluate('//span[1]');
// A Symfony\Component\DomCrawler\Crawler instance

使用 filter() 方法依其 idclass 屬性尋找連結,並使用 selectLink() 方法依其內容尋找連結(它也會在其 alt 屬性中尋找具有該內容的可點擊圖片)。

這兩種方法都會傳回僅包含選取連結的 Crawler 實例。使用 link() 方法取得代表連結的 Link 物件

1
2
3
4
5
6
7
8
9
10
11
12
// first, select the link by id, class or content...
$linkCrawler = $crawler->filter('#sign-up');
$linkCrawler = $crawler->filter('.user-profile');
$linkCrawler = $crawler->selectLink('Log in');

// ...then, get the Link object:
$link = $linkCrawler->link();

// or do all this at once:
$link = $crawler->filter('#sign-up')->link();
$link = $crawler->filter('.user-profile')->link();
$link = $crawler->selectLink('Log in')->link();

Link 物件有幾個有用的方法可以取得有關選取連結本身的更多資訊

1
2
// returns the proper URI that can be used to make another request
$uri = $link->getUri();

注意

getUri() 特別有用,因為它可以清除 href 值並將其轉換為真正應該處理的方式。例如,對於具有 href="#foo" 的連結,這將傳回目前頁面的完整 URI,並附加 #foo。從 getUri() 傳回的始終是您可以對其執行的完整 URI。

圖片

若要依其 alt 屬性尋找圖片,請在現有的 crawler 上使用 selectImage 方法。這會傳回僅包含選取圖片的 Crawler 實例。呼叫 image() 會為您提供特殊的 Image 物件

1
2
3
4
5
$imagesCrawler = $crawler->selectImage('Kitten');
$image = $imagesCrawler->image();

// or do this all at once
$image = $crawler->selectImage('Kitten')->image();

Image 物件具有與 Link 相同的 getUri() 方法。

表單

表單也獲得特殊處理。selectButton() 方法在 Crawler 上可用,它會傳回另一個 Crawler,該 Crawler 符合 <button><input type="submit"><input type="button"> 元素(或它們內部的 <img> 元素)。在 idaltnamevalue 屬性以及這些元素的文字內容中尋找作為引數給定的字串。

此方法特別有用,因為您可以使用它來傳回代表按鈕所在表單的 Form 物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// button example: <button id="my-super-button" type="submit">My super button</button>

// you can get button by its label
$form = $crawler->selectButton('My super button')->form();

// or by button id (#my-super-button) if the button doesn't have a label
$form = $crawler->selectButton('my-super-button')->form();

// or you can filter the whole form, for example a form has a class attribute: <form class="form-vertical" method="POST">
$crawler->filter('.form-vertical')->form();

// or "fill" the form fields with data
$form = $crawler->selectButton('my-super-button')->form([
    'name' => 'Ryan',
]);

Form 物件具有許多非常有用的方法,可用於處理表單

1
2
3
$uri = $form->getUri();
$method = $form->getMethod();
$name = $form->getName();

getUri() 方法不僅僅傳回表單的 action 屬性。如果表單方法是 GET,則它會模仿瀏覽器的行為,並傳回 action 屬性,後跟包含所有表單值的查詢字串。

注意

支援選用的 formactionformmethod 按鈕屬性。getUri()getMethod() 方法會考量這些屬性,以始終傳回正確的動作和方法,具體取決於用於取得表單的按鈕。

您可以在表單上虛擬設定和取得值

1
2
3
4
5
6
7
8
9
10
11
12
// sets values on the form internally
$form->setValues([
    'registration[username]' => 'symfonyfan',
    'registration[terms]'    => 1,
]);

// gets back an array of values - in the "flat" array like above
$values = $form->getValues();

// returns the values like PHP would see them,
// where "registration" is its own array
$values = $form->getPhpValues();

若要處理多維欄位

1
2
3
4
5
6
7
8
<form>
    <input name="multi[]">
    <input name="multi[]">
    <input name="multi[dimensional]">
    <input name="multi[dimensional][]" value="1">
    <input name="multi[dimensional][]" value="2">
    <input name="multi[dimensional][]" value="3">
</form>

傳遞值陣列

1
2
3
4
5
6
7
8
9
10
11
12
13
// sets a single field
$form->setValues(['multi' => ['value']]);

// sets multiple fields at once
$form->setValues(['multi' => [
    1             => 'value',
    'dimensional' => 'an other value',
]]);

// tick multiple checkboxes at once
$form->setValues(['multi' => [
    'dimensional' => [1, 3] // it uses the input value to determine which checkbox to tick
]]);

這很棒,但會變得更好!Form 物件允許您像瀏覽器一樣與表單互動,選取單選值、勾選核取方塊和上傳檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$form['registration[username]']->setValue('symfonyfan');

// checks or unchecks a checkbox
$form['registration[terms]']->tick();
$form['registration[terms]']->untick();

// selects an option
$form['registration[birthday][year]']->select(1984);

// selects many options from a "multiple" select
$form['registration[interests]']->select(['symfony', 'cookies']);

// fakes a file upload
$form['registration[photo]']->upload('/path/to/lucas.jpg');

使用表單資料

執行所有這些操作的重點是什麼?如果您在內部進行測試,您可以擷取表單上的資訊,就像剛透過使用 PHP 值提交一樣

1
2
$values = $form->getPhpValues();
$files = $form->getPhpFiles();

如果您使用外部 HTTP 用戶端,您可以使用表單來擷取建立表單 POST 請求所需的所有資訊

1
2
3
4
5
6
$uri = $form->getUri();
$method = $form->getMethod();
$values = $form->getValues();
$files = $form->getFiles();

// now use some HTTP client and post using this information

使用所有這些整合系統的一個很好的範例是 HttpBrowser,它由 BrowserKit 組件提供。它瞭解 Symfony Crawler 物件,並且可以使用它來直接提交表單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;

// makes a real request to an external site
$browser = new HttpBrowser(HttpClient::create());
$crawler = $browser->request('GET', 'https://github.com/login');

// select the form and fill in some values
$form = $crawler->selectButton('Sign in')->form();
$form['login'] = 'symfonyfan';
$form['password'] = 'anypass';

// submits the given form
$crawler = $browser->submit($form);

選取無效的選項值

依預設,選項欄位(選取、單選)已啟用內部驗證,以防止您設定無效值。如果您想要能夠設定無效值,您可以在整個表單或特定欄位上使用 disableValidation() 方法

1
2
3
4
5
6
// disables validation for a specific field
$form['country']->disableValidation()->select('Invalid value');

// disables validation for the whole form
$form->disableValidation();
$form['country']->select('Invalid value');

解析 URI

UriResolver 類別會採用 URI(相對、絕對、片段等),並將其轉換為相對於另一個給定基本 URI 的絕對 URI

1
2
3
4
5
use Symfony\Component\DomCrawler\UriResolver;

UriResolver::resolve('/foo', 'https://127.0.0.1/bar/foo/'); // https://127.0.0.1/foo
UriResolver::resolve('?a=b', 'https://127.0.0.1/bar#foo'); // https://127.0.0.1/bar?a=b
UriResolver::resolve('../../', 'https://127.0.0.1/'); // https://127.0.0.1/

使用 HTML5 解析器

如果您需要 Crawler 使用 HTML5 解析器,請將其 useHtml5Parser 建構子引數設定為 true

1
2
3
use Symfony\Component\DomCrawler\Crawler;

$crawler = new Crawler(null, $uri, useHtml5Parser: true);

這樣做,crawler 將使用 masterminds/html5 程式庫提供的 HTML5 解析器來解析文件。

本作品,包括程式碼範例,均依Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本