使用 CSS 选择特定的 table 单元格

Selecting a specific table cell using CSS

我从 atpworldtour.com 中抓取了排名 table,我正在尝试访问玩家姓名。

table 中一行的示例如下所示:

<tr>
  <td class="rank-cell">1</td>
  <td class="move-cell">
    <div class="move-none"></div>
    <div class="move-text">
    </div>
  </td>
  <td class="country-cell">
    <div class="country-inner">
      <div class="country-item">
        <img src="/~/media/images/flags/srb.png" alt="SRB" onerror="this.remove()">
      </div>
    </div>
  </td>
  <td class="player-cell">
    <a href="/en/players/novak-djokovic/d643/overview" data-ga-label="Novak Djokovic">Novak Djokovic</a>
  </td>
  <td class="age-cell">28</td>
  <td class="points-cell">
    <a href="/en/players/novak-djokovic/d643/rankings-breakdown?team=singles" data-ga-label="rankings-breakdown">15,785</a>
  </td>
  <td class="tourn-cell">
    <a href="/en/players/novak-djokovic/d643/player-activity?matchType=singles" data-ga-label="player-activity">17</a>
  </td>
  <td class="pts-cell">1,500</td>
  <td class="next-cell">0</td>
</tr>

我尝试了几种不同的方法来提取这些信息。到目前为止,我取得的最大成功是:

url = "http://www.atpworldtour.com/en/rankings/singles"
doc = Nokogiri::HTML(open(url))

doc.css("tr").each do |row|
  puts row.css("td a")
end

问题是,玩家名字后每行还有另外两个 link,所以我把它们都放在一起了。玩家的名字是 table 中的第四个单元格,所以我尝试先拉出第四个单元格,然后访问 link:

doc.css("tr").each do |row|
  cell = row.css("td")[3]
  puts cell.css("a").text
end

但是 returns 错误 undefined method 'css' for nil:NilClass.

经过进一步调查,cell 似乎存储了所有带有玩家名称的单元格,而不仅仅是 row 当前迭代的单元格,但是当我随后尝试遍历 cell 我遇到了同样的 undefined method 错误。

我也尝试过使用 XPath 解决这个问题:

doc.xpath("//tr").each do |row|
  puts row.xpath("/td[3]/a").text
end

但输出是一大片空白区域 space,其中应列出名称。

  1. 关于我做错了什么有什么提示吗?
  2. 任何人都可以向我指出有关在 Nokogiri 中使用 CSS/XPath 选择器的详细文档,我将不胜感激。

到目前为止,我发现的所有内容都只涵盖了非常基础的内容,而且我很难找到有关如何执行更复杂操作的信息。


我实际上使用了它:

doc.xpath("//tr").each do |row|
  puts row.at_css("a").text
end

但是任何帮助找到正确的 documentation/tutorials 以使用 XPath 和 CSS 选择器与 Nokogiri 的任何帮助仍然很棒。

包含玩家姓名的 table 单元格有一个 class player-cell:

<td class="player-cell">
  <a href="/en/players/novak-djokovic/d643/overview" data-ga-label="Novak Djokovic">Novak Djokovic</a>
</td>

您可以使用此 class 来获取元素:

doc.css('.player-cell a').map(&:text)
#=> ["Novak Djokovic", "Roger Federer", "Andy Murray", ...]

即使没有明确的 class,您也可以通过以下方式获取第 4 列:

doc.css('td:nth-child(4) a').map(&:text)
#=> ["Novak Djokovic", "Roger Federer", "Andy Murray", ...]

或使用 XPath:

doc.xpath('//td[4]/a').map(&:text)
#=> ["Novak Djokovic", "Roger Federer", "Andy Murray", ...]

也许这将有助于阐明正在发生的事情:

require 'nokogiri'
doc = Nokogiri::HTML('<table><tr><td>foo</td><td>bar</td></tr></table>')

at return是第一个匹配的节点。在本例中是 <tr>。使用 text returns 将其中的所有文本连接在一起:

doc.at('tr').to_html # => "<tr>\n<td>foo</td>\n<td>bar</td>\n</tr>"
doc.at('tr').text # => "foobar"

使用search returns 一个NodeSet,它最容易被认为是一个数组。在这种情况下,它将 return 两个元素,每个元素对应 <tr><td> 对:

doc.search('tr td').size # => 2

text 将 return 节点集中所有节点的文本,再次连接字符串:

doc.search('tr td').to_html # => "<td>foo</td>\n<td>bar</td>"
doc.search('tr td').text # => "foobar"

但是,通过遍历 NodeSet 中的每个节点,我们可以看到单独的文本:

doc.search('tr td').map(&:text) # => ["foo", "bar"]

另一种但稍慢的方法是先找到 <tr> 节点,然后在其中搜索单个 <td> 节点:

doc.at('tr').search('td').size # => 2
doc.at('tr').search('td').to_html # => "<td>foo</td>\n<td>bar</td>"
doc.at('tr').search('td').text # => "foobar"

同样,使用 map 我们可以遍历它们并获得没有连接的文本:

doc.at('tr').search('td').map(&:text) # => ["foo", "bar"]

这是使用单个与单独 selector 下降和 select <td> 节点的速度差异:

require 'fruity'
require 'nokogiri'

doc = Nokogiri::HTML('<table><tr><td>foo</td><td>bar</td></tr></table>')

compare do
  single_selector { doc.search('tr td').map(&:text) }
  separate_selectors { doc.at('tr').search('td').map(&:text) }
end
# >> Running each test 32 times. Test will take about 1 second.
# >> single_selector is faster than separate_selectors by 2x ± 0.1

差异是由于 tr td 对 libXML2 的单次往返调用与 doc.at('tr').search('td') 的两次调用造成的。

不幸的是,有时如果我们需要使用条件逻辑或按顺序访问多个不同类型的子节点,我们不得不使用更长、更慢的形式重新出现在标记中。