Perl XPath语句带有条件 - 这可行吗?

4
这个问题已经被重新表述了。我正在使用CPAN Perl模块WWW::Mechanize浏览网站,HTML::TreeBuilder-XPath捕获内容,xacobeo在HTML/XML上测试我的XPath代码。目标是从基于PHP的网站调用此Perl脚本,并将抓取的内容上传到数据库中。因此,即使内容“丢失”,仍需要对其进行账务处理。
下面是一个经过测试、简化的样例代码,描述了我的挑战。注意:
  1. 此页面动态生成,包含不同商店输出的各种ITEMS;每个商店存在不同数量的Products*。这些产品列表可能有或没有一个明细表格。
  2. 捕获的数据必须是数组,并且必须保持任何明细列表(如果存在)与产品清单的关联。

以下示例xml根据商店更改(如上所述),但为简洁起见,我只显示了一种“类型”的输出。我意识到所有数据都可以捕获到一个数组中,然后使用正则表达式来解密内容,以便将其上传到数据库中。我正在寻求更好的XPath知识,以帮助简化这个(和未来的)解决方案。

<!DOCTYPE XHTML>
<table id="8jd9c_ITEMS">
<tr><th style="color:red">The Products we have in stock!</th></tr>

<tr><td><span id="Product_NUTS">We have nuts!</span></td></tr>
<tr><td>
    <!--Table may or may not exist  -->
           <table>                                  
      <tr><td style="color:blue;text-indent:10px">Almonds</td></tr>
      <tr><td style="color:blue;text-indent:10px">Cashews</td></tr>
      <tr></tr>
    </table>
</td></tr>

<tr><td><span id="Product_VEGGIES">We have veggies!</span></td></tr>
<tr><td>
    <!--Table may or may not exist -->
    <table>
      <tr><td style="color:blue;text-indent:10px">Carrots</td></tr>
      <tr><td style="color:blue;text-indent:10px">Celery</td></tr>
      <tr></tr>
    </table>
</td></tr>

<tr><td><span id="Product_ALCOHOL">We have booze!</span></td></tr>
    <!--In this case, the table does not exist -->
</table>

一个XPath语句:
'//table[contains(@id, "ITEMS")]/tr[position() >1]/td/span/text()'

将找到:

We have nuts!
we have veggies!
We have booze!

并且XPath语句为:

'//table[contains(@id, "ITEMS")]/tr[position() >1]/td/table/tr/td/text()'

将会找到:

Almonds
Cashews
Carrots
Celery

这两个XPath语句可以合并:

'//table[contains(@id, "ITEMS")]/tr[position() >1]/td/span/text() | //table[contains(@id, "ITEMS")]/tr[position() >1]/table/tr/td/text()'

找到:
We have nuts!
Almonds
Cashews
We have veggies!
Carrots
Celery
We have booze!

以上数组可以使用正则表达式(在真实代码中)进行解密,以获取其产品到列表的关联。 但是,该数组是否可以使用XPath构建,以保持该关联?

例如(伪语言,不起作用):

'//table[contains(@id, "ITEMS")]/tr[position()>1]/td/span/text() | 
if exists('//table[contains(@id, "ITEMS")]/tr[position() >1]/table)) 
then ("NoTable") else ("TableRef") | 
Save this result into @TableRef ('//table[contains(@id, "ITEMS")]/tr[position() >1]/table/tr/td/text()')'

在Perl中,传统意义上无法构建多维数组,详见perldoc perlref。但是希望类似上述解决方案可以创建类似下面的内容:

@ITEMS[0] => We have nuts!
@ITEMS[1] => nutsREF     <-- say, the last word of the span value + REF
@ITEMS[2] => We have veggies!
@ITEMS[3] => veggiesREF  <-- say, the last word of the span value + REF
@ITEMS[4] => We have booze!
@ITEMS[5] => NoTable     <-- value accounts for the missing info

@nutsREF[0] => Almonds
@nutsREF[1] => Cashews

@veggiesREF[0] => Carrots
@veggiesREF[1] => Celery 

在实际代码中,产品已知,因此可以预先定义my @veggiesREFmy @nutsREF以等待XPath输出。
我意识到XPath if/else/then功能在XPath 2.0版本中。我正在使用ubuntu系统并在本地工作,但我仍不清楚我的apache2服务器是否在使用它还是1.0版本。我该如何检查?
最后,如果您能展示如何从PHP表单提交调用Perl脚本以及如何将Perl数组传回调用的PHP函数,那将有助于获得奖金。 :)
谢谢!
最终编辑:
下面这篇文章底部的评论是针对最初的文章过于模糊而发表的。之后重新发布(并悬赏)得到了ikegami的回应,他使用了非常有创意的方法解决了伪问题,但我很难理解并在我的实际应用中重复使用——该应用涉及多个html页面上的多次使用。在我们的对话中的第18条评论左右,我终于发现了他使用的($cat)的含义和用法——这是一种未记录的Perl语法。对于新读者来说,理解这种语法可以理解(和重新格式化)他对问题的聪明解决方案。他的帖子当然符合OP所寻求的基本要求,但没有使用HTML :: TreeBuilder :: XPath来完成它。
jpalecek使用HTML :: TreeBuilder :: XPath,但不将捕获的数据放入数组中以传递给PHP函数并上传到数据库中。
我从两位回答者那里学到了东西,希望这篇文章能帮助像我这样的Perl新手。非常感谢任何最终的贡献。

没有回应。:( 我的问题太啰嗦了吗?有些人喜欢额外的信息。这是一个不寻常的请求吗? - Ricalsin
2
td[3 and 4 and 6] 不会返回第三、第四和第六个成员。括号中的表达式返回 true,因此所有的 td 都会被返回。请使用 td[position()=3 or position()=4 or position()=6] - choroba
@choroba 感谢您简短的问题1回答。关于问题2,您是否曾经需要检查XPath中节点的存在(if / else)并相应地更改代码?我倾向于使用XPath表达式检查存在性,然后更改运行它的Perl代码。 - Ricalsin
@Ricalsin:XPath 2.0确实提供了if表达式。在XPath 1.0中,没有if表达式,但在某些情况下,单个XPath表达式可能足以实现条件选择。这就是为什么知道确切情况很重要的原因。 - Dimitre Novatchev
@choroba 请给出一个高层次的答案(并获得绿色勾号! :))。我很想了解您对XSH2和方法的使用。如果能提供有关此主题的最喜爱文档的链接,将不胜感激。感谢您的帮助性回复! - Ricalsin
显示剩余4条评论
2个回答

5
如果我猜测的没错,你的问题是:“如何从提供的输入中获取以下内容?”
my $categorized_items = {
   'We have nuts!'    => [ 'Almonds', 'Cashwes' ],
   'We have veggies!' => [ 'Carrots', 'Celery' ],
   'We have booze!'   => [ ],
};

如果需要的话,我会这样做:

如果需要,请参照以下步骤:

use Data::Dumper qw( Dumper );
use XML::LibXML  qw( );

my $root = XML::LibXML->load_xml(IO=>\*DATA)->documentElement;

my %cat_items;
for my $cat_tr ($root->findnodes('//table[contains(@id, "ITEMS")]/tr[td/span]')) {
   my ($cat) = map $_->textContent(),
      $cat_tr->findnodes('td/span');

   my @items = map $_->textContent(),
      $cat_tr->findnodes('following-sibling::tr[position()=1]/td/table/tr/td');

   $cat_items{$cat} = \@items;
}

print(Dumper(\%cat_items));

__DATA__
...xml...

PS - 你所写的不是合法的HTML。

  1. TABLE元素不能直接放在TR元素内。缺少TD元素。
  2. TR元素不能为空,必须至少有一个TH或TD元素。

@Ricalsin,解析器返回一个文档。您需要调用它(不幸命名为)documentElement方法来获取根节点。我已经更新了我的节点以适应您数据的更改,并添加了测试代码。 - ikegami
谢谢@ikegami,我已经搞定了。对我来说,这是高级代码。我正在学习它。:) map $_-> textContent()my ($cat) = map部分让我感到困惑。你能告诉我可以阅读哪些perldocs章节来自我教育吗?(似乎你是唯一愿意为50分玩的人,但我知道你的时间和知识价值更多 - 所以再次“感谢”)。 - Ricalsin
你好 @ikegami,我已经阅读并理解了 Map。但是我找不到关于 LISTEXPR 或者 $variable 的 man 手册。你的描述让我觉得它像是一种倒置列表 - 我在哪里可以找到相关信息呢?顺便说一下,XML::LibXML 没有列出 documentElement 函数。我是不是错过了 Perl 核心手册上关于这些内容的讨论,或者使用 CPAN 上的 Perl 模块总是这么困难/具有欺骗性?(感谢您的回复。) - Ricalsin
1
顺便说一句,“我不想认为...”也可以说成“不可能是...”。我当然不认为自己比许多人在数十年的存在中付出的辛勤努力更懂。我试图让您理解我的智力来源以及我对Perl理解的局限性,因此请求一些“启示”或者链接。我认为您是一位优秀的程序员(正如您对我帖子的回复所证明的那样)。不幸的是,像我这样不那么熟练的人需要像您这样有耐心的人来学习。再次感谢@ikegami提供的帮助。 - Ricalsin
1
然后你接着说:“@Ricalsin,你以前没写过子程序吗?sub foo { my ($cat) = @_; ... }” @ikegami,你体现了程序员所遭受的刻板印象:他们在编码方面越熟练,在人际关系方面就越无能。正如我所说,我认为你对计算机、代码和Perl特别了解。但是,认为这就是成功的全部是错误的。最终,架构和概念化解决方案和市场才是主导 - 而不是一些暂时提升自我价值的混淆语法。感谢你让我学到了很多。 - Ricalsin
显示剩余27条评论

2
  1. How to ascertain that something exists before running query. Eg. if //p[@class='red'] exists, then return //table:

    /.[//p[@class='red']]//table
    
  2. x[3 and 4 and 5]: 3 and 4 and 5 is a boolean expression that yields true. Therefore it will get you all xs. For 3rd, 4th and 5th you want

    x[position() >= 3 and position() <= 5]
    

编辑后问题的答案:

为什么不使用带有多个查询的XML::XPathEngine

my $xp = XML::XPathEngine->new;
my $tree = HTML::TreeBuilder::XPath->new;
$tree->parse (something);

然后,您可以查询:
my $shops = $xp->findnodes('//table[contains(@id, "ITEMS")]/tr[position() >1]/td[@span]', $tree);
for($shops->get_nodelist) {
  print "Name of shop is ".$xp->findvalue('span/text()', $_)."\n"; # <- query relative to $_
  print "The shop sells:\n". join("\n", $xp->findvalues('parent::*/following-sibling::tr[1][not(span)]/td/table/tr/td', $_));
}

这与@ikegami的答案相同(HTML::TreeBuilder::XPath使用了XML::XPathEngine)。顺便说一下,如果商店后面可以有更多产品行,这个应该更新。

你好@jpalecek,感谢您的回答。我发布了一个更详细的问题,几乎是问同样的事情,并在上面放了50点赏金。您能再看一下吗? - Ricalsin
嗨 @jpalecek,我正在尝试运行您的代码。对我来说,使用CPAN模块仍然很困难。就像ikegami一样,加载它的最终技巧是my $root = XML::LibXML->load_xml(IO=>\*DATA)->documentElement;你所提到的HTML::TreeBuilder::XPath甚至还没有解析方法的描述,而HTML::TreeBuilder有。但是,输入$tree->parse(\*DATA);似乎没有给我任何结果。我对此非常无知,感到抱歉。在Perl中,我似乎花费了很多时间去做别人似乎已经掌握的东西-我应该阅读哪些资料以帮助我完成这些明显简单的步骤呢? - Ricalsin
顺便说一下,如果商店可以在产品后面添加更多的行,那么应该进行更新。你为什么这么说? - Ricalsin
嗨 @jpalecek,我发现要将__DATA__的内容传递给HTML::TreeBuilder::XPath,我需要使用$tree->parse_file(\*DATA);。然后,对你的XPath代码进行了一点改动,即tr[position() >1]/td[span](去掉了@符号),这样就可以工作了。你的代码使用了我之前提到的HTML::TreeBuilder::XPath模块。它没有将结果放入哈希或数组中以便上传到数据库,这也是我之前提到的。我无法理解ikegami对($cat)的使用。你能展示一种优雅的保存这些结果的方法吗?另外,你使用$_是否可以避免多次遍历DOM树? - Ricalsin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接