BeautifulSoup webscraping find_all(): поиск точного соответствия

Я использую Python и BeautifulSoup для веб-очистки.

Скажем, у меня есть следующий код html для очистки:

<body>
    <div class="product">Product 1</div>
    <div class="product">Product 2</div>
    <div class="product special">Product 3</div>
    <div class="product special">Product 4</div>
</body>

Используя BeautifulSoup, я хочу найти ТОЛЬКО продукты с атрибутом class= "product" (только продукт 1 и 2), а не "специальные" продукты

Если я делаю следующее:

result = soup.find_all('div', {'class': 'product'})

результат включает ВСЕ продукты (1,2,3 и 4).

Что мне делать, чтобы найти продукты, класс которых ТОЧНО соответствует "продукту"?


Код, которым я управлял:

from bs4 import BeautifulSoup
import re

text = """
<body>
    <div class="product">Product 1</div>
    <div class="product">Product 2</div>
    <div class="product special">Product 3</div>
    <div class="product special">Product 4</div>
</body>"""

soup = BeautifulSoup(text)
result = soup.findAll(attrs={'class': re.compile(r"^product$")})
print result

Вывод:

[<div class="product">Product 1</div>, <div class="product">Product 2</div>, <div class="product special">Product 3</div>, <div class="product special">Product 4</div>]

Ответ 1

В BeautifulSoup 4 атрибут class (и несколько других атрибутов, таких как accesskey и атрибут headers на элементах ячейки таблицы) рассматривается как набор; вы сопоставляетесь с отдельными элементами, указанными в атрибуте. Это следует за стандартом HTML.

Таким образом, вы не можете ограничить поиск только одним классом.

Вам нужно будет использовать пользовательскую функцию здесь, чтобы вместо этого сопоставить класс:

result = soup.find_all(lambda tag: tag.name == 'div' and 
                                   tag.get('class') == ['product'])

Я использовал lambda для создания анонимной функции; каждый тег сопоставляется по имени (должен быть 'div'), а атрибут класса должен быть точно равен списку ['product']; например имеют только одно значение.

Демо:

>>> from bs4 import BeautifulSoup
>>> text = """
... <body>
...     <div class="product">Product 1</div>
...     <div class="product">Product 2</div>
...     <div class="product special">Product 3</div>
...     <div class="product special">Product 4</div>
... </body>"""
>>> soup = BeautifulSoup(text)
>>> soup.find_all(lambda tag: tag.name == 'div' and tag.get('class') == ['product'])
[<div class="product">Product 1</div>, <div class="product">Product 2</div>]

Для полноты, вот все такие атрибуты набора из исходного кода BeautifulSoup:

# The HTML standard defines these attributes as containing a
# space-separated list of values, not a single value. That is,
# class="foo bar" means that the 'class' attribute has two values,
# 'foo' and 'bar', not the single value 'foo bar'.  When we
# encounter one of these attributes, we will parse its value into
# a list of values if possible. Upon output, the list will be
# converted back into a string.
cdata_list_attributes = {
    "*" : ['class', 'accesskey', 'dropzone'],
    "a" : ['rel', 'rev'],
    "link" :  ['rel', 'rev'],
    "td" : ["headers"],
    "th" : ["headers"],
    "td" : ["headers"],
    "form" : ["accept-charset"],
    "object" : ["archive"],

    # These are HTML5 specific, as are *.accesskey and *.dropzone above.
    "area" : ["rel"],
    "icon" : ["sizes"],
    "iframe" : ["sandbox"],
    "output" : ["for"],
    }

Ответ 2

Вы можете использовать селектор CSS так:

result = soup.select('div.product.special')

css-selectors

Ответ 3

soup.findAll(attrs={'class': re.compile(r"^product$")})

Этот код соответствует всему, что не имеет product в конце своего класса.

Ответ 4

менять

result = soup.findAll(attrs={'class': re.compile(r"^product$")})

в

result = soup.find_all(attrs={'class': 'product})

и результатом является список и доступ через индекс