XPath仅在存在时才从HTML中抓取两个节点值

时间:2012-10-15 20:55:51

标签: php xpath web-scraping

我使用Curl,XPath和PHP来从HTML源代码中删除产品名称和价格。这是一个类似于我正在检查的源代码的示例:

<div class="Gamesdb">
  <p class="media-title">
    <a href="/Games/Console/4-/105/Bluetooth-Headset/">Bluetooth Headset</a>
  </p>
  <p class="sub-title"> Console </p>
  <p class="rating star-50">
    <a href="/Games/Console/4-/105/Bluetooth-Headset/ProductReviews.html">(1)</a>
  </p>
  <p class="mt5">
    <span class="price-preffix">
      <a href="/Games/Console/4-/105/Bluetooth-Headset/">1 New</a>
      from 
    </span>
    <a class="wt-link" href="/Games/Console/4-/105/Bluetooth-Headset/">
      <span class="price">
        <em>£34</em>
        .99
      </span>
      <span class="free-delivery"> FREE delivery</span>
    </a>
  </p>
  <p class="mt10">
    <a class="primary button" href="/Games/Console/4-/105/Bluetooth-Headset/">
      Product Details
      <span style="color: rgb(255, 255, 255); margin-left: 6px; font-size: 16px;">»</span>
    </a>
  </p>
</div>

我想提取媒体标题,即:

<p class="media-title">
    <a href="/Games/Console/4-/105/Bluetooth-Headset/">Bluetooth Headset</a>
    </p>

仅当以下价格等级出现时:

<span class="price">
    <em>£34</em>
    .99
    </span>

列出的许多其他产品都不包含它。 我需要提取产品名称和价格,或者根本不提取任何内容,然后转到下一个产品。

以下是我目前正在使用的代码示例,该示例无论其他条件如何都能有效获取所有结果:

$results=file_get_contents('SCRAPEDHTML.txt');

$html = new DOMDocument();
@$html->loadHtml($results);
$xpath = new DOMXPath($html);
$nodelist = $xpath->query('//p[@class="media-title"]|//span[@class="price"]');

foreach ($nodelist as $n){

$results2[]=$n->nodeValue;

}

我相信使用正确的xpath查询是可能的,但到目前为止还无法实现。非常感谢提前。

2 个答案:

答案 0 :(得分:0)

您不能拥有一个同时返回产品名称及其价格的XPath。我的建议是首先获得包含两个信息的所有div个节点:

//div[p[@class='media-title'] and //span[@class='price']]

(所有div个节点,其p子节点具有类media-titlespan后代节点具有类price');然后循环所有返回的节点,并使用另外两个XPath来提取产品名称和价格:

p[@class='media-title']

//span[@class='price']

答案 1 :(得分:0)

我假设每div.Gamesdb只有一个“项目”。如果没有,源html中可能没有足够的结构来单独使用xpath。您可能需要索引产品名称并查找匹配产品名称附近的价格。

您可以使用单个巨型XPath执行此操作,但我建议您使用多个XPath。我将展示两种方式。

首先创建您的DOMXPath并注册帮助以匹配类名。

// This helper is the equivalent to the XPath:
// contains(concat(' ',normalize-space(@attr),' '), ' $token ')
// It's not necessary, but it's a bit easier to read and more
// bulletproof than @ATTR="TOKEN"
function has_token($attr, $token)
{
    $attr = $attr[0];
    $regex = '/(?:^|\s)'.preg_quote($token,'/').'(?:\s|$)/Su';
    return (bool) preg_match($regex, $attr->value);
}

$xp = new DOMXPath($d);
$xp->registerNamespace("php", "http://php.net/xpath");
$xp->registerPHPFunctions("has_token");

然后您可以使用巨型XPath:

$xp_container = '/html/body//div[php:function("has_token", @class, "Gamesdb")]';
$xp_title = 'p[php:function("has_token", @class, "media-title")]';
$xp_price = '//span[php:function("has_token", @class, "price")]';

$xp_titles_prices = "$xp_container[{$xp_title}][{$xp_price}]/{$xp_title} | $xp_container[{$xp_title}][{$xp_price}]{$xp_price}";


$nodes = $xp->query($xp_items);

$items = array();

$i = 0; // enumerator
foreach ($nodes as $node) {
    $key = ($node->nodeName==='p') ? 'title' : 'price';
    $value = '';
    switch ($key) {
        case 'price':
            // remove inner whitespace
            $value = preg_replace('/\s+/Su', '', trim($node->textContent));
            break;
        case 'title':
            $value = preg_replace('/\s+/Su', ' ', trim($node->textContent));
            break;
    }
    $items[(int) floor($i/2)][$key] = $value;
    $i += 1;
}

然而,整体代码很脆弱且不清楚。 XPath联合运算符(|)按文档顺序返回节点,因此我们无法将列表平分。 PHP代码必须遍历节点列表中的每个项目,并使用DOM确定哪个字段对应于此数据。如果您想扩展代码以收集第三项(例如价格),请考虑您必须进行的更改。现在想象一下,从现在起三个月后进行这些更改,这段代码不再是你的想法了。

我建议您使用多个XPath调用,并使用PHP而不是XPath执行“我们是否有价格和标题的数据”:

$xpitems = '/html/body//div[php:function("has_token", @class, "Gamesdb")]';
// below use $xpitems context:
$xptitle = 'normalize-space(p[php:function("has_token", @class, "media-title")])';
$xpprice = 'normalize-space(//span[php:function("has_token", @class, "price")])';

$nodeitems = $xp->query($xpitems);

$items = array();
foreach ($nodeitems as $nodeitem) {
    $item = array(
        'title' => $xp->evaluate($xptitle, $nodeitem),
        'price' => str_replace(' ', '', $xp->evaluate($xpprice, $nodeitem)),
    );
        // Only add this item if we have data for *all* fields:
    if (count(array_filter($item)) === count($item)) {
        $items[] = $item;
    }
}

这更容易阅读和理解,并且将来更容易扩展。