Scrapy 翻译 XPath 的方式是否与 Python 的 lxml 模块不同?
Does Scrapy translate XPath differently than Python's lxml module?
我正在尝试抓取一个网站,但我在 Scrapy 的响应对象上使用的 Xpath 表达式有问题。
根据我对 XPath 的了解,我认为我使用了正确的 XPath 表达式。
所以我使用网络浏览器加载网页,然后下载并保存为 HTML 文件。
然后我尝试了两种不同的 XPath 表达式。
第一种方法是使用 Python 的 lxml.html 模块打开文件并将其作为 HTMLParser 对象加载。
第二种方法是使用 Scrapy 并将其指向保存的 HTML 文件。
在这两种情况下,我都使用了相同的 XPath 表达式。但是我得到了不同的结果。
示例 HTML 代码是这样的(不完全是,但我不想 post 一大堆代码一字不差):
<html>
<body>
<div>
<table type="games">
<tbody>
<tr row="1">
<th data="week_number">1</th>
<td data="date">"9/13/2020"</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
例如,我试图在“TABLE”中的“TR”元素下的“TH”元素中抓取周数。
我使用 Chrome 代替 Firefox 仔细检查了文件内容(Firefox 将“tbody”元素添加到表中,根据此 post:
Parsing HTML with XPath, Python and Scrapy
根据 Chrome 的 Inspect,<tbody>
元素在文件中。
第一种方法是使用 lxml.html 模块打开 HTML 文件:
from lxml import etree, html
if __name__ == '__main__':
filename_04 = "/home/foo.html"
# Try opening the filename
try:
fh_04 = open(filename_04, "r")
except:
print "Error opening %s. Exiting" % filename_04
sys.exit(1)
# Try reading the contents of the HTML file.
# Then close the file
try:
content_04 = fh_04.read().decode('utf-8')
except UnicodeDecodeError:
print "Error trying to read as UTF-8. Exiting."
sys.exit(1)
fh_04.close()
# Define an HTML parser object
parser_04 = html.HTMLParser()
# Create a logical XML tree from the contents of parser_04
tree_04 = html.parse(StringIO(content_04), parser_04)
game_elements_list = list()
# Get all the <TR> elements from the <table type="games">
game_elements_list = tree_04.xpath("//table[@type = 'games']/tbody/tr")
num_games = len(game_elements_list)
# Now loop thru each of the <TR> element objects of game_elements_list
for x in range(num_games):
# Parse the week number using xpath()
# *** NOTE: this expression returns a list
parsed_week_number = game_elements_list[x].xpath(".//th[@data = 'week_number']/text()")
print ":: parsed_week_number: ", str(parsed_week_number)
p_type = type(parsed_week_number)
print ":: p_type: ", str(p_type)
通过 lxml.html 模块使用 XPath 表达式 returns 这个输出:
:: parsed_week_number: ['1']
:: p_type: <type 'list'>
这是我对 XPath 表达式的期望,因此我的 XPath 表达式是正确的。
然而,当我将 Scrapy 蜘蛛指向本地文件时,我得到了不同的结果:
# I'm only posting the callback method, not the
# method that makes the actual request, because
# the request() call works
def parse_schedule_page(self, response):
game_elements_list = list()
# The xpath expression is the same as the one used in the file that
# uses lxml.html module
game_elements_list = response.xpath("//table[@type = 'games']/tbody/tr")
num_game_elements = len(game_elements_list)
for i in range(num_game_elements):
# Again, the XPath expression is the same
# as the one used in the file that
# uses the lxml.html module
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()")
stmt = ":: parsed_week_number: " + str(parsed_week_number)
self.log(stmt)
p_type = type(parsed_week_number)
stmt = "p_type: " + str(p_type)
self.log(stmt)
"""
To get the week number, I have to add the following line:
week_number = parsed_week_number.extract()
"""
但在 Spider 的情况下,输出是不同的:
2020-07-17 21:22:30 [test_schedule] DEBUG: :: parsed_week_number: [<Selector xpath=".//th[@data-stat = 'week_num']/text()" data=u'1'>]
2020-07-17 21:22:30 [test_schedule] DEBUG: p_type: <class 'scrapy.selector.unified.SelectorList'>
相同的 XPath 表达式不 return <th data="week_number">1</th>
的内容
我知道 Scrapy 使用与 lxml 的 HTMLParser 不同的提取器方法。但是,无论 HTML 数据如何存储,即使提取器方法不同,XPath 表达式的工作方式不应该相同吗?
Scrapy 的 response.xpath() 方法计算 XPath 表达式的方式是否与 lxml.html 的 xpath() 方法不同?
回答你的问题 Scrapy 在内部导入 lxml 并且 XML 路径语言是标准化的,尽管有一段时间没有更新。所以你的 XPATH 表达式应该是相同的。
为了进一步帮助您,URL 对您遇到的特定 XPATH 选择器很有帮助。
提示
作为一般规则,如果我在 运行 脚本时无法让 XPATH 选择器工作,我会转到 scrapy shell 并解决它。一般来说,我倾向于在 scrapy shell 中使用我想要的数据列表并尝试其中的 xpath 以确认它会在编写我的 scrapy 蜘蛛之前在脚本中被拾取。
附加信息
有关 XPATH 的更多信息,请参阅 here
如果你对内部结构有这样的疑问,那么值得看看 Scrapy 代码库,即使你认为你不会理解很多。
在 Scrapy 文档中 here 引用了 response.xpath 方法,但如果您只需单击源文本,您也可以访问源代码。
下面是包含导入的 xpath 方法的相关代码库。
response.xpath 进口
"""
XPath selectors based on lxml
"""
import sys
import six
from lxml import etree, html
response.xpath 方法
def xpath(self, query, namespaces=None, **kwargs):
"""
Find nodes matching the xpath ``query`` and return the result as a
:class:`SelectorList` instance with all elements flattened. List
elements implement :class:`Selector` interface too.
``query`` is a string containing the XPATH query to apply.
``namespaces`` is an optional ``prefix: namespace-uri`` mapping (dict)
for additional prefixes to those registered with ``register_namespace(prefix, uri)``.
Contrary to ``register_namespace()``, these prefixes are not
saved for future calls.
Any additional named arguments can be used to pass values for XPath
variables in the XPath expression, e.g.::
selector.xpath('//a[href=$url]', url="http://www.example.com")
"""
try:
xpathev = self.root.xpath
except AttributeError:
return self.selectorlist_cls([])
nsp = dict(self.namespaces)
if namespaces is not None:
nsp.update(namespaces)
try:
result = xpathev(query, namespaces=nsp,
smart_strings=self._lxml_smart_strings,
**kwargs)
except etree.XPathError as exc:
msg = u"XPath error: %s in %s" % (exc, query)
msg = msg if six.PY3 else msg.encode('unicode_escape')
six.reraise(ValueError, ValueError(msg), sys.exc_info()[2])
if type(result) is not list:
result = [result]
result = [self.__class__(root=x, _expr=query,
namespaces=self.namespaces,
type=self.type)
for x in result]
return self.selectorlist_cls(result)
AaronS 的回答非常完整和彻底,但我相信他错过了您代码中的问题。这是一个简单的错误,它被忽视了。
根据您的日志:
2020-07-17 21:22:30 [test_schedule] DEBUG: :: parsed_week_number: [<Selector xpath=".//th[@data-stat = 'week_num']/text()" data=u'1'>]
2020-07-17 21:22:30 [test_schedule] DEBUG: p_type: <class 'scrapy.selector.unified.SelectorList'>
您可以在第一行中看到 parsed_week_number
的值是一个包含一个 Selector 对象的列表,甚至该对象具有值为 1
的数据属性。 所以您的选择器选择了正确的 XPath,但是要提取它选择的数据,您需要使用方法 .get()
或 .getall()
.
.get()
将 return 列表中第一个选择器的数据(在您的情况下列表只有一个)作为字符串,而 .getall()
将 return 列表中所有选择器的数据作为字符串列表。您可以阅读有关这些方法的更多信息 here.
实际上你需要更正这一行:
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()")
为此:
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()").get()
我正在尝试抓取一个网站,但我在 Scrapy 的响应对象上使用的 Xpath 表达式有问题。
根据我对 XPath 的了解,我认为我使用了正确的 XPath 表达式。
所以我使用网络浏览器加载网页,然后下载并保存为 HTML 文件。
然后我尝试了两种不同的 XPath 表达式。
第一种方法是使用 Python 的 lxml.html 模块打开文件并将其作为 HTMLParser 对象加载。
第二种方法是使用 Scrapy 并将其指向保存的 HTML 文件。
在这两种情况下,我都使用了相同的 XPath 表达式。但是我得到了不同的结果。
示例 HTML 代码是这样的(不完全是,但我不想 post 一大堆代码一字不差):
<html>
<body>
<div>
<table type="games">
<tbody>
<tr row="1">
<th data="week_number">1</th>
<td data="date">"9/13/2020"</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
例如,我试图在“TABLE”中的“TR”元素下的“TH”元素中抓取周数。
我使用 Chrome 代替 Firefox 仔细检查了文件内容(Firefox 将“tbody”元素添加到表中,根据此 post: Parsing HTML with XPath, Python and Scrapy
根据 Chrome 的 Inspect,<tbody>
元素在文件中。
第一种方法是使用 lxml.html 模块打开 HTML 文件:
from lxml import etree, html
if __name__ == '__main__':
filename_04 = "/home/foo.html"
# Try opening the filename
try:
fh_04 = open(filename_04, "r")
except:
print "Error opening %s. Exiting" % filename_04
sys.exit(1)
# Try reading the contents of the HTML file.
# Then close the file
try:
content_04 = fh_04.read().decode('utf-8')
except UnicodeDecodeError:
print "Error trying to read as UTF-8. Exiting."
sys.exit(1)
fh_04.close()
# Define an HTML parser object
parser_04 = html.HTMLParser()
# Create a logical XML tree from the contents of parser_04
tree_04 = html.parse(StringIO(content_04), parser_04)
game_elements_list = list()
# Get all the <TR> elements from the <table type="games">
game_elements_list = tree_04.xpath("//table[@type = 'games']/tbody/tr")
num_games = len(game_elements_list)
# Now loop thru each of the <TR> element objects of game_elements_list
for x in range(num_games):
# Parse the week number using xpath()
# *** NOTE: this expression returns a list
parsed_week_number = game_elements_list[x].xpath(".//th[@data = 'week_number']/text()")
print ":: parsed_week_number: ", str(parsed_week_number)
p_type = type(parsed_week_number)
print ":: p_type: ", str(p_type)
通过 lxml.html 模块使用 XPath 表达式 returns 这个输出:
:: parsed_week_number: ['1']
:: p_type: <type 'list'>
这是我对 XPath 表达式的期望,因此我的 XPath 表达式是正确的。
然而,当我将 Scrapy 蜘蛛指向本地文件时,我得到了不同的结果:
# I'm only posting the callback method, not the
# method that makes the actual request, because
# the request() call works
def parse_schedule_page(self, response):
game_elements_list = list()
# The xpath expression is the same as the one used in the file that
# uses lxml.html module
game_elements_list = response.xpath("//table[@type = 'games']/tbody/tr")
num_game_elements = len(game_elements_list)
for i in range(num_game_elements):
# Again, the XPath expression is the same
# as the one used in the file that
# uses the lxml.html module
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()")
stmt = ":: parsed_week_number: " + str(parsed_week_number)
self.log(stmt)
p_type = type(parsed_week_number)
stmt = "p_type: " + str(p_type)
self.log(stmt)
"""
To get the week number, I have to add the following line:
week_number = parsed_week_number.extract()
"""
但在 Spider 的情况下,输出是不同的:
2020-07-17 21:22:30 [test_schedule] DEBUG: :: parsed_week_number: [<Selector xpath=".//th[@data-stat = 'week_num']/text()" data=u'1'>]
2020-07-17 21:22:30 [test_schedule] DEBUG: p_type: <class 'scrapy.selector.unified.SelectorList'>
相同的 XPath 表达式不 return <th data="week_number">1</th>
我知道 Scrapy 使用与 lxml 的 HTMLParser 不同的提取器方法。但是,无论 HTML 数据如何存储,即使提取器方法不同,XPath 表达式的工作方式不应该相同吗?
Scrapy 的 response.xpath() 方法计算 XPath 表达式的方式是否与 lxml.html 的 xpath() 方法不同?
回答你的问题 Scrapy 在内部导入 lxml 并且 XML 路径语言是标准化的,尽管有一段时间没有更新。所以你的 XPATH 表达式应该是相同的。
为了进一步帮助您,URL 对您遇到的特定 XPATH 选择器很有帮助。
提示
作为一般规则,如果我在 运行 脚本时无法让 XPATH 选择器工作,我会转到 scrapy shell 并解决它。一般来说,我倾向于在 scrapy shell 中使用我想要的数据列表并尝试其中的 xpath 以确认它会在编写我的 scrapy 蜘蛛之前在脚本中被拾取。
附加信息
有关 XPATH 的更多信息,请参阅 here
如果你对内部结构有这样的疑问,那么值得看看 Scrapy 代码库,即使你认为你不会理解很多。
在 Scrapy 文档中 here 引用了 response.xpath 方法,但如果您只需单击源文本,您也可以访问源代码。
下面是包含导入的 xpath 方法的相关代码库。
response.xpath 进口
"""
XPath selectors based on lxml
"""
import sys
import six
from lxml import etree, html
response.xpath 方法
def xpath(self, query, namespaces=None, **kwargs):
"""
Find nodes matching the xpath ``query`` and return the result as a
:class:`SelectorList` instance with all elements flattened. List
elements implement :class:`Selector` interface too.
``query`` is a string containing the XPATH query to apply.
``namespaces`` is an optional ``prefix: namespace-uri`` mapping (dict)
for additional prefixes to those registered with ``register_namespace(prefix, uri)``.
Contrary to ``register_namespace()``, these prefixes are not
saved for future calls.
Any additional named arguments can be used to pass values for XPath
variables in the XPath expression, e.g.::
selector.xpath('//a[href=$url]', url="http://www.example.com")
"""
try:
xpathev = self.root.xpath
except AttributeError:
return self.selectorlist_cls([])
nsp = dict(self.namespaces)
if namespaces is not None:
nsp.update(namespaces)
try:
result = xpathev(query, namespaces=nsp,
smart_strings=self._lxml_smart_strings,
**kwargs)
except etree.XPathError as exc:
msg = u"XPath error: %s in %s" % (exc, query)
msg = msg if six.PY3 else msg.encode('unicode_escape')
six.reraise(ValueError, ValueError(msg), sys.exc_info()[2])
if type(result) is not list:
result = [result]
result = [self.__class__(root=x, _expr=query,
namespaces=self.namespaces,
type=self.type)
for x in result]
return self.selectorlist_cls(result)
AaronS 的回答非常完整和彻底,但我相信他错过了您代码中的问题。这是一个简单的错误,它被忽视了。
根据您的日志:
2020-07-17 21:22:30 [test_schedule] DEBUG: :: parsed_week_number: [<Selector xpath=".//th[@data-stat = 'week_num']/text()" data=u'1'>]
2020-07-17 21:22:30 [test_schedule] DEBUG: p_type: <class 'scrapy.selector.unified.SelectorList'>
您可以在第一行中看到 parsed_week_number
的值是一个包含一个 Selector 对象的列表,甚至该对象具有值为 1
的数据属性。 所以您的选择器选择了正确的 XPath,但是要提取它选择的数据,您需要使用方法 .get()
或 .getall()
.
.get()
将 return 列表中第一个选择器的数据(在您的情况下列表只有一个)作为字符串,而 .getall()
将 return 列表中所有选择器的数据作为字符串列表。您可以阅读有关这些方法的更多信息 here.
实际上你需要更正这一行:
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()")
为此:
parsed_week_number = game_elements_list[i].xpath(".//th[@data = 'week_number']/text()").get()