Расширение селекторов CSS в BeautifulSoup

Вопрос:

BeautifulSoup обеспечивает очень ограниченную поддержку селекторов CSS. Например, единственным поддерживаемым псевдоклассом является nth-of-type, и он может принимать только числовые значения — такие аргументы, как even или odd, не допускаются.

Можно ли расширить 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.html и lxml.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])

person alecxe    schedule 21.12.2015    source источник


Ответы (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

Это часть код модифицируется.

Конечно, это все, что угодно, только не элегантное и надежное. Я не думаю, что это будет серьезно использоваться где-либо и когда-либо.

person Martin Valgur    schedule 30.12.2015
comment
Спасибо за сильное I don't envision this being seriously used anywhere, ever.! :) - person alecxe; 31.12.2015

Официально Beautifulsoup поддерживает не все селекторы CSS.

Если python не единственный выбор, я настоятельно рекомендую JSoup (эквивалент этого java). Он поддерживает все селекторы CSS.

  • Это с открытым исходным кодом (лицензия MIT)
  • Синтаксис прост
  • Поддерживает все селекторы css
  • Также может охватывать несколько потоков для масштабирования.
  • Богатая поддержка API в java для хранения в БД. Таким образом, это легко интегрировать.

Другой альтернативный способ, если вы все еще хотите придерживаться python, сделать его реализацией jython.

http://jsoup.org/

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

person vivek_nk    schedule 24.12.2015