在 BeautifulSoup 中扩展 CSS 选择器

Posted

技术标签:

【中文标题】在 BeautifulSoup 中扩展 CSS 选择器【英文标题】:Extending CSS selectors in BeautifulSoup 【发布时间】:2016-03-27 02:52:00 【问题描述】:

问题:

BeautifulSoup 为CSS selectors 提供非常有限的支持。例如,唯一受支持的伪类是 nth-of-type,它只能接受数值 - 不允许使用 evenodd 之类的参数。

是否可以扩展 BeautifulSoup CSS 选择器或让它在内部使用 lxml.cssselect 作为底层 CSS 选择机制?


让我们看一个示例问题/用例。在以下 html 中仅定位偶数行:

<table>
    <tr>
        <td>1</td>
    <tr>
        <td>2</td>
    </tr>
    <tr>
        <td>3</td>
    </tr>
    <tr>
        <td>4</td>
    </tr>
</table>

lxml.htmllxml.cssselect,通过:nth-of-type(even)很容易做到:

from lxml.html import fromstring
from lxml.cssselect import CSSSelector

tree = fromstring(data)

sel = CSSSelector('tr:nth-of-type(even)')

print [e.text_content().strip() for e in sel(tree)]

但是,在BeautifulSoup

print(soup.select("tr:nth-of-type(even)"))

会抛出错误:

NotImplementedError:nth-​​of-type 伪类当前仅支持数值。


请注意,我们可以使用 .find_all() 解决它:

print([row.get_text(strip=True) for index, row in enumerate(soup.find_all("tr"), start=1) if index % 2 == 0])

【问题讨论】:

【参考方案1】:

Beautifulsoup 官方并不支持所有的 CSS 选择器。

如果 python 不是唯一的选择,我强烈推荐 JSoup(java 等价物)。它支持所有的 CSS 选择器。

它是开源的(MIT 许可证) 语法简单 支持所有的 css 选择器 也可以跨越多个线程以扩大规模 Java 中丰富的 API 支持以存储在 DB 中。因此,它很容易集成。

如果您仍然想坚持使用 python,另一种替代方法是使用 jython 实现。

http://jsoup.org/

https://github.com/jhy/jsoup/

【讨论】:

【参考方案2】:

查看源代码后,BeautifulSoup 似乎在其接口中没有提供任何方便的点来扩展或猴子修补其现有功能。使用来自lxml 的功能也是不可能的,因为BeautifulSoup 仅在解析期间使用lxml,并使用解析结果从中创建自己的相应对象。 lxml 对象未保留,以后无法访问。

话虽如此,只要有足够的决心以及 Python 的灵活性和自省能力,一切皆有可能。您甚至可以在运行时修改 BeautifulSoup 方法内部:

import inspect
import re
import textwrap

import bs4.element


def replace_code_lines(source, start_token, end_token,
                       replacement, escape_tokens=True):
    """Replace the source code between `start_token` and `end_token`
    in `source` with `replacement`. The `start_token` portion is included
    in the replaced code. If `escape_tokens` is True (default),
    escape the tokens to avoid them being treated as a regular expression."""

    if escape_tokens:
        start_token = re.escape(start_token)
        end_token = re.escape(end_token)

    def replace_with_indent(match):
        indent = match.group(1)
        return textwrap.indent(replacement, indent)

    return re.sub(r"^(\s+)([\s\S]+?)(?=^\1)".format(start_token, end_token),
                  replace_with_indent, source, flags=re.MULTILINE)


# Get the source code of the Tag.select() method
src = textwrap.dedent(inspect.getsource(bs4.element.Tag.select))

# Replace the relevant part of the method
start_token = "if pseudo_type == 'nth-of-type':"
end_token = "else"
replacement = """\
if pseudo_type == 'nth-of-type':
    try:
        if pseudo_value in ("even", "odd"):
            pass
        else:
            pseudo_value = int(pseudo_value)
    except:
        raise NotImplementedError(
            'Only numeric values, "even" and "odd" are currently '
            'supported for the nth-of-type pseudo-class.')
    if isinstance(pseudo_value, int) and pseudo_value < 1:
        raise ValueError(
            'nth-of-type pseudo-class value must be at least 1.')
    class Counter(object):
        def __init__(self, destination):
            self.count = 0
            self.destination = destination

        def nth_child_of_type(self, tag):
            self.count += 1
            if pseudo_value == "even":
                return not bool(self.count % 2)
            elif pseudo_value == "odd":
                return bool(self.count % 2)
            elif self.count == self.destination:
                return True
            elif self.count > self.destination:
                # Stop the generator that's sending us
                # these things.
                raise StopIteration()
            return False
    checker = Counter(pseudo_value).nth_child_of_type
"""
new_src = replace_code_lines(src, start_token, end_token, replacement)

# Compile it and execute it in the target module's namespace
exec(new_src, bs4.element.__dict__)
# Monkey patch the target method
bs4.element.Tag.select = bs4.element.select

This 是正在修改的代码部分。

当然,这不是优雅和可靠的。我不认为这会在任何地方被认真使用。

【讨论】:

感谢强大的I don't envision this being seriously used anywhere, ever.! :)

以上是关于在 BeautifulSoup 中扩展 CSS 选择器的主要内容,如果未能解决你的问题,请参考以下文章

BeautifulSoup 提取节点的 XPATH 或 CSS 路径

BeautifulSoup:从 html 获取 css 类

如何使用 BeautifulSoup 从内联样式中提取 CSS 属性

第三章:爬虫基础知识回顾

为什么BeautifulSoup无法解析页面的所有元素? (答案:BeautifulSoup中的CSS选择器)

解析嵌入的 css beautifulsoup