【问题标题】:Does Scrapy translate XPath differently than Python's lxml module?Scrapy 翻译 XPath 是否与 Python 的 lxml 模块不同?
【发布时间】:2020-11-07 21:52:54
【问题描述】:

我正在尝试抓取一个网站,但我在 Scrapy 的响应对象上使用的 Xpath 表达式有问题。

根据我对 XPath 的了解,我认为我使用了正确的 XPath 表达式。

所以我使用网络浏览器加载网页,然后下载并保存为 HTML 文件。

然后我尝试了两种不同的 XPath 表达式。

第一种方法是使用 Python 的 lxml.html 模块打开文件并将其作为 HTMLParser 对象加载。

第二种方法是使用 Scrapy 并将其指向保存的 HTML 文件。

在这两种情况下,我都使用了相同的 XPath 表达式。但我得到了不同的结果。

示例 HTML 代码是这样的(不完全是,但我不想逐字发布大量代码):

<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”元素添加到表中,根据这篇文章: Parsing HTML with XPath, Python and Scrapy

根据 Chrome 的检查,&lt;tbody&gt; 元素在文件中。

第一种方法是使用 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 表达式会返回以下输出:

:: 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 表达式不返回 &lt;th data="week_number"&gt;1&lt;/th&gt; 的内容

我知道 Scrapy 使用与 lxml 的 HTMLParser 不同的提取器方法。但无论 HTML 数据如何存储,即使提取器方法不同,XPath 表达式的工作方式也不应该相同吗?

Scrapy 的 response.xpath() 方法对 XPath 表达式的评估与 lxml.html 的 xpath() 方法不同吗?

【问题讨论】:

    标签: python xpath scrapy lxml


    【解决方案1】:

    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() 将返回列表中第一个选择器的数据(在您的情况下,列表只有一个)作为字符串,而 .getall() 将返回列表中所有选择器的数据作为列表的字符串。你可以阅读更多关于这些方法的信息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()
    

    【讨论】:

    • 我知道如何解决这个问题。我可以通过发出以下行来获得周数:``` week_number=parsed_week_number.extract()[0] ```我不得不通过将 response.body 转换为来处理 extract() 或 get()一个 html 对象。但是,我的问题的要点是为什么 XPath 表达式从 Scrapy 响应返回的结果与从 html 对象返回的结果不同。我知道 Scrapy 使用了不同类型的选择器,但我认为 XPath 解决了所有这些问题。
    • 很高兴我的回答被撤销并且 renatodvc 被接受。我想当我发帖时,我认为有一个链接来确保 XPATH 选择器确实是正确的会很有帮助。 Renatodvc 提供了很好的建议,我最近似乎在很多帖子中给出了建议。知道 URL 是什么以查看 HTML 和响应 scrapy 给出的差异仍然很有用。有时这是由于 scrapy 可以实际解析的内容。面向 JavaScript 的网站将不会被完全准确地解析。
    • 你很好@AaronS,但没有必要。我认为您的回答无疑对那些将来会遇到这个问题的人有更大的贡献!我总是很高兴在 SO 中看到您周到而完整的答案。 :)
    【解决方案2】:

    回答您的问题 Scrapy 在内部导入 lxml 并且 XML 路径语言是标准化的,尽管有一段时间没有更新。所以你的 XPATH 表达式应该是一样的。

    为了进一步帮助您,URL 将适合您正在努力使用的特定 XPATH 选择器。

    提示

    作为一般规则,如果我在运行脚本时无法让 XPATH 选择器工作,我会转到 scrapy shell 并解决它。一般来说,我倾向于在 scrapy shell 中使用我想要的数据列表并尝试其中的 xpath 以确认它会在编写我的 scrapy 蜘蛛之前在脚本中被拾取。

    附加信息

    有关 XPATH 的更多信息,请参阅here

    如果您对内部结构有此类疑问,则值得查看 Scrapy 代码库,即使您认为自己不会了解很多。

    在 Scrapy Docs 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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-24
      • 1970-01-01
      • 1970-01-01
      • 2017-02-04
      • 1970-01-01
      • 2016-04-14
      相关资源
      最近更新 更多