Scrapy选择器“a :: text”和“a :: text”之间的区别

时间:2018-02-01 06:52:25

标签: python python-3.x css-selectors scrapy pseudo-element

我已经创建了一个刮刀来从网页中获取一些产品名称。它运作顺利。我已经使用CSS选择器来完成这项工作。但是,我唯一无法理解的是选择器a::texta ::text之间的区别(不要忽略后者中a::text之间的空格) 。当我运行我的脚本时,无论选择哪个选择器,我都会得到相同的结果。

import requests
from scrapy import Selector

res = requests.get("https://www.kipling.com/uk-en/sale/type/all-sale/?limit=all#")
sel = Selector(res)
for item in sel.css(".product-list-product-wrapper"):
    title = item.css(".product-name a::text").extract_first().strip()
    title_ano = item.css(".product-name a ::text").extract_first().strip()
    print("Name: {}\nName_ano: {}\n".format(title,title_ano))

如您所见,titletitle_ano都包含相同的选择器,禁止后者的空格。然而,结果总是一样的。

我的问题:两者之间是否存在实质性差异,何时使用前者和后者?

1 个答案:

答案 0 :(得分:8)

有趣的观察!过去几个小时我一直在研究这个问题,事实证明,除此之外还有很多其他内容。

如果你来自CSS,你可能希望写a::text的方式与编写a::first-linea::first-lettera::beforea::after。没有惊喜。

另一方面,标准选择器语法会建议a ::text匹配::text元素的后代a伪元素,使其成为相当于a *::text。但是,.product-list-product-wrapper .product-name a没有任何子元素,因此根据权限,a ::text应该不匹配。它匹配的事实表明Scrapy没有遵循语法。

Scrapy使用Parsel(本身基于cssselect)将选择器转换为XPath,这是::text的来源。考虑到这一点,让我们来看看Parsel如何实现::text

>>> from parsel import css2xpath
>>> css2xpath('a::text')
'descendant-or-self::a/text()'
>>> css2xpath('a ::text')
'descendant-or-self::a/descendant-or-self::text()'

因此,与cssselect一样,后代组合子后面的任何内容都会转换为descendant-or-self轴,但由于文本节点是DOM中元素节点的正确子节点,因此::text被视为独立节点并直接转换为text()descendant-or-self轴与a元素的后代匹配任何文本节点,就像a/text()匹配任何文本节点< a元素的em> child (孩子也是后代)。

即使你向选择器添加一个明确的*,这种情况也会发生:

>>> css2xpath('a *::text')
'descendant-or-self::a/descendant-or-self::text()'

但是,使用descendant-or-self轴意味着a ::text可以匹配a元素中的所有文本节点,包括嵌套在a中的其他元素中的文本节点。在以下示例中,a ::text将匹配两个文本节点:'Link '后跟'text'

<a href="https://example.com">Link <span>text</span></a>

因此,虽然Scrapy对::text的实现是对Selectors语法的严重违反,但它似乎是故意这样做的。

事实上,Scrapy的其他伪元素::attr() 1 表现相似。当以下选择器没有任何后代元素时,它们都匹配属于id元素的div属性节点:

>>> css2xpath('div::attr(id)')
'descendant-or-self::div/@id'
>>> css2xpath('div ::attr(id)')
'descendant-or-self::div/descendant-or-self::*/@id'
>>> css2xpath('div *::attr(id)')
'descendant-or-self::div/descendant-or-self::*/@id'

...但div ::attr(id)div *::attr(id)会匹配id个后代中的所有div个属性节点及其自己的id属性,例如如下例所示:

<div id="parent"><p id="child"></p></div>

当然,这是一个不太合理的用例,因此我们不得不怀疑这是否是::text实施的无意的副作用。

将伪元素选择器与用于替换伪元素的任何简单选择器的伪元素选择器进行比较:

>>> css2xpath('a [href]')
'descendant-or-self::a/descendant-or-self::*/*[@href]'

这正确地将后代组合子转换为descendant-or-self::*/*并附加隐式child轴,确保[@href]谓词永远不会在a元素上进行测试。

如果您是XPath,Selectors甚至是Scrapy的新手,这可能看起来非常令人困惑和压倒一切。所以这里总结了何时使用一个选择器而不是另一个:

  • 如果您的a::text元素仅包含文字,或者您只对此a元素的顶级文本节点感兴趣而不是其嵌套,请使用a元件。

  • 如果您的a ::text元素包含嵌套元素,并且您想要提取此a元素中的所有文本节点,请使用a

    如果可以使用a ::text,如果您的a元素仅包含文字,则其语法会令人困惑,因此为了保持一致性,请使用a::text代替

1 有趣的是,::attr()出现在Non-element Selectors spec中,正如您所期望的那样,它与Selectors语法的行为一致,使其行为在Scrapy中与规范不一致。另一方面,::text明显缺少规范;根据这个答案,我认为你可以合理地猜出原因。