开发者问题收集

如何使用XPath检索字符串而不返回空错误

2020-06-23
154

我正尝试将“Private Equity Group; USA”写入文件。

“Private Equity Group”打印正常,但“USA”部分出现错误

TypeError: null is not an object (evaluating 'style.display')"

HTML 代码:

<div class="cl profile-xsmall">
  <div class="cl profile-small-bold">Private Equity Group</div>
  USA
</div>

“USA”的 XPath 为:

//*[@id="addrDiv-Id"]/div/div[3]/text()

当我打印 XPath 或将其放在 if 语句中时出现错误:

if (internet.has_xpath?('//*[@id="addrDiv-Id"]/div/div[3]/text()')){
    file.puts "#{internet.find(:xpath, '//*[@id="addrDiv-Id"]/div/div[3]/text()')}"
}
3个回答

Capybara 不是通用的 xpath 库 - 它是一个旨在测试的库,因此以元素为中心。使用的 xpath 需要引用元素,而不是文本节点。

if (internet.has_xpath?('//*[@id="addrDiv-Id"]/div/div[3]')){
  file.puts internet.find(:xpath, '//*[@id="addrDiv-Id"]/div/div[3]').text
}

尽管为此使用 XPath 只是一个坏主意。只要可能,就默认使用 CSS,它更容易阅读,浏览器处理速度也更快 - 类似于

if (internet.has_css?('#addrDiv-Id > div > div:nth-of-type(3)')){
  file.puts internet.find('#addrDiv-Id" > div > div:nth-of-type(3)').text
}

或者如果 HTML 允许(我不知道没有看到更多的 HTML)

if (internet.has_css?('#addrDiv-id .cl.profile-xsmall')){
  file.puts internet.find('#addrDiv-id .cl.profile-xsmall').text
}

或者如果它适合您的用例,甚至更干净

file.puts internet.first('#addrDiv-id .cl.profile-xsmall')&.text
Thomas Walpole
2020-06-24

另一种方法:

xml = %{<div class="cl profile-xsmall">
<div class="cl profile-small-bold">Private Equity Group</div>
USA</div>}

require 'rexml/document'
doc = REXML::Document.new xml
print(REXML::XPath.match(doc, 'normalize-space(string(//div[@class="cl profile-xsmall"]))'))

输出:

["Private Equity Group USA"]
E.Wiest
2020-06-24

我想说的是 HTML 格式不正确,使用 span 会更好,但这样可以:

require 'nokogiri'

doc = Nokogiri::HTML(<<EOT)
<div class="cl profile-xsmall">
  <div class="cl profile-small-bold">Private Equity Group</div>
  USA
</div>
EOT

div = doc.at('.profile-small-bold')

[div.text.strip, div.next_sibling.text.strip].join(' ')
# => "Private Equity Group USA"

可以简化为:

[div, div.next_sibling].map { |n| n.text.strip }.join(' ')
# => "Private Equity Group USA"

问题是您有两个嵌套的 div,后面跟着“USA”,因此指向包含所需正文的内部节点很重要。然后“USA”位于以下文本节点中,可以使用 next_sibling 访问:

div.next_sibling.class # => Nokogiri::XML::Text
div.next_sibling # => #<Nokogiri::XML::Text:0x3c "\n  USA\n">

注意,我使用的是 CSS 选择器;它们更易于阅读,Nokogiri 文档也对此表示赞同。我没有证据表明它们更快,而且由于 Nokogiri 使用 libxml 来处理两者,因此可能没有值得担心的真正差异,因此请使用更有意义的方法,如果您好奇的话,可以运行基准测试。

您可能想对 div class="cl profile-xsmall" 节点使用 text ,但不要陷入其中,因为这是一个陷阱:

doc.at('.profile-xsmall').text # => "\n  Private Equity Group\n  USA\n"
doc.at('.profile-xsmall').text.gsub(/\s+/, ' ').strip # => "Private Equity Group USA"

text 将返回连接在一起的文本节点的字符串。在这种特殊的 罕见 情况下,它会产生一个有点可用的结果,但是,通常你会得到这样的结果:

doc = Nokogiri::HTML('<div><p>foo</p><p>bar</p></div>')
doc.at('div').text # => "foobar"
doc.search('p').text # => "foobar"

一旦这些文本节点被连接起来,就 真的 很难再次将它们分开。 Nokogiri 的文档 谈到了这一点:

Note: This joins the text of all Node objects in the NodeSet:

doc = Nokogiri::XML('<xml><a><d>foo</d><d>bar</d></a></xml>')
doc.css('d').text # => "foobar"

Instead, if you want to return the text of all nodes in the NodeSet:

doc.css('d').map(&:text) # => ["foo", "bar"]

The XPath for "USA" is:

//*[@id="addrDiv-Id"]/div/div[3]/text()

嗯,不,根据您给我们的 HTML 不是。但是,让我们假装一下。

使用节点的绝对路径是编写脆弱选择器的好方法。只需对 HTML 进行少量更改即可中断对节点的访问。相反,找到跳过 HTML 的路径点来找到您想要的节点,利用 CSS 和 XPath 向下搜索 DOM。

通常,像您这样的选择器是由浏览器生成的,这不是一个值得信任的好来源。浏览器通常会对格式错误的 HTML 进行修复,这会将其更改为与 Nokogiri 或解析器所看到的内容不同,从而导致目标不存在,或者浏览器在 JavaScript 发生更改后显示 HTML,这可能会移动节点、隐藏节点、添加新节点等。

不要信任浏览器,而是在命令行中使用 curlwgetnokogiri 转储文件并使用文本编辑器查看它。然后,您将看到它就像 Nokogiri 看到的那样,在任何修复或破坏之前。

the Tin Man
2020-06-24