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);
}
專門的 Link、Image 和 Form 類別對於在遍歷 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 擴充功能,因此它也能夠與原生 DOMDocument、DOMNodeList 和 DOMNode 物件互動
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);
表達式評估
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()
方法依其 id
或 class
屬性尋找連結,並使用 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();
表單
表單也獲得特殊處理。selectButton()
方法在 Crawler 上可用,它會傳回另一個 Crawler,該 Crawler 符合 <button>
或 <input type="submit">
或 <input type="button">
元素(或它們內部的 <img>
元素)。在 id
、alt
、name
和 value
屬性以及這些元素的文字內容中尋找作為引數給定的字串。
此方法特別有用,因為您可以使用它來傳回代表按鈕所在表單的 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
屬性,後跟包含所有表單值的查詢字串。
注意
支援選用的 formaction
和 formmethod
按鈕屬性。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 解析器來解析文件。