使用 PHP 从网站中提取特定数据
Using PHP to extract specific data from websites
我是 PHP 的新人,我想从不同的网站提取库存数量和尺寸等数据。对我将如何去做这件事感到有点困惑。 Domdocument
是要走的路吗?
不确定这是否是最好的方法。
我尝试从 here 的第 164-174 行开始。
非常感谢任何帮助!
编辑 - 这是我更新的代码。不过,不要真的认为这是最有效的做事方式。
<html>
<?php
$url = 'https://kithnyc.com/collections/adidas/products/kith-x-adidas- consortium-response-trail-boost?variant=35276776455';
$html = file_get_contents($url);
//preg_match('~itemprop="image"\scontent="(\w+.\w+.\w+.\w+.\w+.\w+)~', $html, $image);
//$image = $image[1];
preg_match('~,"title":"(\w+.\w+.\w+.\w+.\w+.\w+)~', $html, $title);
$title = $title[1];
preg_match_all('~{"id":(\d+)~', $html, $id);
$id = $id[1];
preg_match_all('~","public_title":"(\d+..)~', $html, $size);
$size = $size[1];
preg_match_all('~inventory_quantity":(\d+)~', $html, $quantity);
$quantity = $quantity[1];
function plain_url_to_link($url) {
return preg_replace(
'%(https?|ftp)://([-A-Z0-9./_*?&;=#]+)%i',
'<a target="blank" rel="nofollow" href="[=11=]" target="_blank">[=11=]</a>', $url);
}
$i = 0;
$j = 2;
echo "$title<br />";
echo "<br />";
//echo $image;
echo plain_url_to_link($url);
echo "<br />";
echo "<br />";
for($i = 0; $i < 18; $i++) {
print "Size: $size[$i] --- Quantity: $quantity[$i] --- ID: $id[$j]";
$j++;
echo "<br />";
}
echo "<br />";
//print_r($quantity);
?>
</body>
</html>
根据一般经验,您必须避免使用正则表达式解析 HTML/XML 内容。原因如下:
Entire HTML parsing is not possible with regular expressions, since it depends on matching the opening and the closing tag which is not possible with regexps.
Regular expressions can only match regular languages but HTML is a context-free language. The only thing you can do with regexps on HTML is heuristics but that will not work on every condition. It should be possible to present a HTML file that will be matched wrongly by any regular expression.
—
使用一个DOM parser instead which is specifically designed for the purpose of parsing HTML/XML documents。这是一个例子:
# Installing Symfony's dom parser using Composer
composer require symfony/dom-crawler symfony/css-selector
<?php
require 'vendor/autoload.php';
use Symfony\Component\DomCrawler\Crawler;
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
$crawler = new Crawler($html);
$price = $crawler->filter('.product-header-title[itemprop="price"]')->text();
// UPDATE: Does not work! as the page updates the button text
// later with javascript. Read more for another solution.
$in_stock = $crawler->filter('#AddToCartText')->text();
if ($in_stock == 'Sold Out') {
$in_stock = 0; // or `false`, if you will
}
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: Buy Now
// We'll fix "Availability" later...
使用此类解析器,您还可以使用 XPath 提取元素。
但是如果你想解析那个页面中包含的 javascript 代码,你最好使用像 Selenium 这样的浏览器模拟器。然后您可以编程访问该页面中所有全局可用的 javascript vars/functions。
更新
获取价格
所以你得到这个错误 运行 上面的代码:
PHP Fatal error:
Uncaught Symfony\Component\CssSelector\Exception\SyntaxErrorException: Expected identifier, but found.
那是因为目标页面使用了无效的 class 名称作为价格元素 (.-price
) 并且此 Symfony 的 CSS 选择器组件无法正确解析它,因此出现异常。这是元素:
<span id="ProductPrice" class="product-header-title -price" itemprop="price" content="220">0.00</span>
为了解决这个问题,让我们改用 itemprop
属性。这是可以匹配它的选择器:
.product-header-title[itemprop="price"]
我相应地更新了上面的代码以反映它。我测试了它,它对价格部分有效。
获取库存状态
现在我实际测试了代码,我看到产品的库存状态是稍后使用 javascript 设置的。当您使用 file_get_contents()
获取页面时,它不存在。你可以自己看看,刷新页面,按钮显示为Buy Now
,然后一秒钟后变为Sold Out
。
但幸运的是,产品变体的数量深埋在页面的某处。这是 Shopify 用来呈现产品页面的巨大对象的 pretty printed copy。
所以现在的问题是用 PHP 解析 javascript 代码。有一些一般方法可以解决这个问题:
请随意跳过这些方法,因为它们并不特定于您的问题。如果您只想找到问题的答案,请直接跳至第 6 个。
最可靠和常见的方法是从此类网站(严重依赖 javascript)抓取数据是使用像 Selenium 这样的浏览器模拟器,它能够执行 javascript 代码。看看 Facebook 的 PHP WebDriver 包,它是可用的 Selenium WebDriver 最复杂的 PHP 绑定。它为您提供了一个 API 来远程控制网络浏览器并对它们执行 javascript。
另外,请参阅 Behat 的 Mink,其中包含适用于无头浏览器和功能齐全的浏览器控制器的各种驱动程序。这些驱动程序包括 Goutte、BrowserKit、Selenium1/2、Zombie.js、Sahi 和 WUnit。
参见V8js,扩展PHP;它将 V8 javascript 引擎嵌入到 PHP 中。它允许您直接从 PHP 脚本评估 javascript 代码。但是,如果您没有大量使用该功能,那么安装 PHP 扩展有点过分了。但是,如果您想使用 DOM 解析器提取相关脚本:
$script = $crawler->filterXPath('//head/following-sibling::script[2]')->text();
使用HtmlUnit to parse the page and then feed the final HTML to PHP. You gonna need a small Java wrapper。对,对你的情况有点矫枉过正。
提取 javascript 代码并使用 JS parser/tokenizer 库解析它,例如 hiltonjanfield/js4php5 or squizlabs/PHP_CodeSniffer which has a JS tokenizer.
如果应用程序正在调用 ajax 来操作 DOM。为了您自己的应用程序,您可能能够重新分派这些请求并解析响应。一个示例是 ajax 页面调用 cart.js
以检索与购物车项目相关的数据。但是这里读取产品变型数量不是这样的。
您可能还记得我告诉过您,使用正则表达式来解析 整个 HTML/XML 文档是个坏主意。但是当其他方法更难时,可以部分使用它们从 HTML/XML 文档中提取字符串。如果您对何时使用它有任何困惑,请阅读我在此 post 顶部引用的 SO 答案。
这种方法是通过 运行 一个简单的正则表达式来匹配产品变体的 inventory_quantity
与整个页面源代码(或者您只能针对脚本标签执行它以获得更好的性能) :
<?php
require 'vendor/autoload.php';
use Symfony\Component\DomCrawler\Crawler;
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
$crawler = new Crawler($html);
$price = trim($crawler->filter('.product-header-title[itemprop="price"]')->text());
preg_match('/35276776455,.+?inventory_quantity":(\d)/', $html, $in_stock);
$in_stock = $in_stock[1];
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: 0
此正则表达式需要变体 ID(在本例中为 35276776455
)才能工作,因为每个产品的数量都带有变体。您可以从 URL 的查询字符串中提取它:?variant=35276776455
。
现在我们已经完成了库存状态并且我们已经用正则表达式完成了它,您可能想对价格做同样的事情并删除 DOM 解析器依赖项:
<?php
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
// You need to check if it's matched before assigning
// $price[1]. Anyway, this is just an example.
preg_match('/itemprop="price".+?>\s*$(.+?)\s*<\/span>/s', $html, $price);
$price = $price[1];
preg_match('/35276776455,.+?inventory_quantity":(\d)/', $html, $in_stock);
$in_stock = $in_stock[1];
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: 0
结论
尽管我仍然认为用正则表达式解析 HTML/XML 文档是个坏主意,但我必须承认可用的 DOM 解析器无法解析嵌入的 javascript 代码(而且可能永远不会),这就是你的情况。我们可以部分地利用正则表达式从 HTML/XML 中提取字符串;使用 DOM 解析器无法解析的部分。所以,总而言之:
- 使用 DOM 解析器 parse/scrape 最初存在于页面中的 HTML 代码。
- 拦截 ajax 可能包含您需要的信息的电话。在单独的 http 请求中重新调用它们以获取数据。
- 对使用 ajax 调用等填充其页面的 parsing/scraping 大量使用 JS 的网站使用浏览器模拟器。
- 部分使用正则表达式提取无法使用 DOM 解析器提取的内容。
如果您只需要这两个字段,则可以使用正则表达式。否则,请考虑其他方法。
我是 PHP 的新人,我想从不同的网站提取库存数量和尺寸等数据。对我将如何去做这件事感到有点困惑。 Domdocument
是要走的路吗?
不确定这是否是最好的方法。
我尝试从 here 的第 164-174 行开始。
非常感谢任何帮助!
编辑 - 这是我更新的代码。不过,不要真的认为这是最有效的做事方式。
<html>
<?php
$url = 'https://kithnyc.com/collections/adidas/products/kith-x-adidas- consortium-response-trail-boost?variant=35276776455';
$html = file_get_contents($url);
//preg_match('~itemprop="image"\scontent="(\w+.\w+.\w+.\w+.\w+.\w+)~', $html, $image);
//$image = $image[1];
preg_match('~,"title":"(\w+.\w+.\w+.\w+.\w+.\w+)~', $html, $title);
$title = $title[1];
preg_match_all('~{"id":(\d+)~', $html, $id);
$id = $id[1];
preg_match_all('~","public_title":"(\d+..)~', $html, $size);
$size = $size[1];
preg_match_all('~inventory_quantity":(\d+)~', $html, $quantity);
$quantity = $quantity[1];
function plain_url_to_link($url) {
return preg_replace(
'%(https?|ftp)://([-A-Z0-9./_*?&;=#]+)%i',
'<a target="blank" rel="nofollow" href="[=11=]" target="_blank">[=11=]</a>', $url);
}
$i = 0;
$j = 2;
echo "$title<br />";
echo "<br />";
//echo $image;
echo plain_url_to_link($url);
echo "<br />";
echo "<br />";
for($i = 0; $i < 18; $i++) {
print "Size: $size[$i] --- Quantity: $quantity[$i] --- ID: $id[$j]";
$j++;
echo "<br />";
}
echo "<br />";
//print_r($quantity);
?>
</body>
</html>
根据一般经验,您必须避免使用正则表达式解析 HTML/XML 内容。原因如下:
Entire HTML parsing is not possible with regular expressions, since it depends on matching the opening and the closing tag which is not possible with regexps.
Regular expressions can only match regular languages but HTML is a context-free language. The only thing you can do with regexps on HTML is heuristics but that will not work on every condition. It should be possible to present a HTML file that will be matched wrongly by any regular expression.
—
使用一个DOM parser instead which is specifically designed for the purpose of parsing HTML/XML documents。这是一个例子:
# Installing Symfony's dom parser using Composer
composer require symfony/dom-crawler symfony/css-selector
<?php
require 'vendor/autoload.php';
use Symfony\Component\DomCrawler\Crawler;
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
$crawler = new Crawler($html);
$price = $crawler->filter('.product-header-title[itemprop="price"]')->text();
// UPDATE: Does not work! as the page updates the button text
// later with javascript. Read more for another solution.
$in_stock = $crawler->filter('#AddToCartText')->text();
if ($in_stock == 'Sold Out') {
$in_stock = 0; // or `false`, if you will
}
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: Buy Now
// We'll fix "Availability" later...
使用此类解析器,您还可以使用 XPath 提取元素。
但是如果你想解析那个页面中包含的 javascript 代码,你最好使用像 Selenium 这样的浏览器模拟器。然后您可以编程访问该页面中所有全局可用的 javascript vars/functions。
更新
获取价格
所以你得到这个错误 运行 上面的代码:
PHP Fatal error:
Uncaught Symfony\Component\CssSelector\Exception\SyntaxErrorException: Expected identifier, but found.
那是因为目标页面使用了无效的 class 名称作为价格元素 (.-price
) 并且此 Symfony 的 CSS 选择器组件无法正确解析它,因此出现异常。这是元素:
<span id="ProductPrice" class="product-header-title -price" itemprop="price" content="220">0.00</span>
为了解决这个问题,让我们改用 itemprop
属性。这是可以匹配它的选择器:
.product-header-title[itemprop="price"]
我相应地更新了上面的代码以反映它。我测试了它,它对价格部分有效。
获取库存状态
现在我实际测试了代码,我看到产品的库存状态是稍后使用 javascript 设置的。当您使用 file_get_contents()
获取页面时,它不存在。你可以自己看看,刷新页面,按钮显示为Buy Now
,然后一秒钟后变为Sold Out
。
但幸运的是,产品变体的数量深埋在页面的某处。这是 Shopify 用来呈现产品页面的巨大对象的 pretty printed copy。
所以现在的问题是用 PHP 解析 javascript 代码。有一些一般方法可以解决这个问题:
请随意跳过这些方法,因为它们并不特定于您的问题。如果您只想找到问题的答案,请直接跳至第 6 个。
最可靠和常见的方法是从此类网站(严重依赖 javascript)抓取数据是使用像 Selenium 这样的浏览器模拟器,它能够执行 javascript 代码。看看 Facebook 的 PHP WebDriver 包,它是可用的 Selenium WebDriver 最复杂的 PHP 绑定。它为您提供了一个 API 来远程控制网络浏览器并对它们执行 javascript。
另外,请参阅 Behat 的 Mink,其中包含适用于无头浏览器和功能齐全的浏览器控制器的各种驱动程序。这些驱动程序包括 Goutte、BrowserKit、Selenium1/2、Zombie.js、Sahi 和 WUnit。
参见V8js,扩展PHP;它将 V8 javascript 引擎嵌入到 PHP 中。它允许您直接从 PHP 脚本评估 javascript 代码。但是,如果您没有大量使用该功能,那么安装 PHP 扩展有点过分了。但是,如果您想使用 DOM 解析器提取相关脚本:
$script = $crawler->filterXPath('//head/following-sibling::script[2]')->text();
使用HtmlUnit to parse the page and then feed the final HTML to PHP. You gonna need a small Java wrapper。对,对你的情况有点矫枉过正。
提取 javascript 代码并使用 JS parser/tokenizer 库解析它,例如 hiltonjanfield/js4php5 or squizlabs/PHP_CodeSniffer which has a JS tokenizer.
如果应用程序正在调用 ajax 来操作 DOM。为了您自己的应用程序,您可能能够重新分派这些请求并解析响应。一个示例是 ajax 页面调用
cart.js
以检索与购物车项目相关的数据。但是这里读取产品变型数量不是这样的。您可能还记得我告诉过您,使用正则表达式来解析 整个 HTML/XML 文档是个坏主意。但是当其他方法更难时,可以部分使用它们从 HTML/XML 文档中提取字符串。如果您对何时使用它有任何困惑,请阅读我在此 post 顶部引用的 SO 答案。
这种方法是通过 运行 一个简单的正则表达式来匹配产品变体的 inventory_quantity
与整个页面源代码(或者您只能针对脚本标签执行它以获得更好的性能) :
<?php
require 'vendor/autoload.php';
use Symfony\Component\DomCrawler\Crawler;
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
$crawler = new Crawler($html);
$price = trim($crawler->filter('.product-header-title[itemprop="price"]')->text());
preg_match('/35276776455,.+?inventory_quantity":(\d)/', $html, $in_stock);
$in_stock = $in_stock[1];
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: 0
此正则表达式需要变体 ID(在本例中为 35276776455
)才能工作,因为每个产品的数量都带有变体。您可以从 URL 的查询字符串中提取它:?variant=35276776455
。
现在我们已经完成了库存状态并且我们已经用正则表达式完成了它,您可能想对价格做同样的事情并删除 DOM 解析器依赖项:
<?php
$html = file_get_contents('https://kithnyc.com/collections/footwear/products/kith-x-adidas-consortium-response-trail-boost?variant=35276776455');
// You need to check if it's matched before assigning
// $price[1]. Anyway, this is just an example.
preg_match('/itemprop="price".+?>\s*$(.+?)\s*<\/span>/s', $html, $price);
$price = $price[1];
preg_match('/35276776455,.+?inventory_quantity":(\d)/', $html, $in_stock);
$in_stock = $in_stock[1];
echo "Price: $price - Availability: $in_stock";
// Outputs:
// Price: 0.00 - Availability: 0
结论
尽管我仍然认为用正则表达式解析 HTML/XML 文档是个坏主意,但我必须承认可用的 DOM 解析器无法解析嵌入的 javascript 代码(而且可能永远不会),这就是你的情况。我们可以部分地利用正则表达式从 HTML/XML 中提取字符串;使用 DOM 解析器无法解析的部分。所以,总而言之:
- 使用 DOM 解析器 parse/scrape 最初存在于页面中的 HTML 代码。
- 拦截 ajax 可能包含您需要的信息的电话。在单独的 http 请求中重新调用它们以获取数据。
- 对使用 ajax 调用等填充其页面的 parsing/scraping 大量使用 JS 的网站使用浏览器模拟器。
- 部分使用正则表达式提取无法使用 DOM 解析器提取的内容。
如果您只需要这两个字段,则可以使用正则表达式。否则,请考虑其他方法。