Подтвердить что ты не робот

BeautifulSoup - поиск по тексту внутри тега

Обратите внимание на следующую проблему:

import re
from bs4 import BeautifulSoup as BS

soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
    Edit
</a>
""")

# This returns the <a> element
soup.find(
    'a',
    href="/customer-menu/1/accounts/1/update",
    text=re.compile(".*Edit.*")
)

soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
    <i class="fa fa-edit"></i> Edit
</a>
""")

# This returns None
soup.find(
    'a',
    href="/customer-menu/1/accounts/1/update",
    text=re.compile(".*Edit.*")
)

По какой-то причине BeautifulSoup не будет соответствовать тексту, если тег <i> также существует. Поиск тега и отображение его текста дает

>>> a2 = soup.find(
        'a',
        href="/customer-menu/1/accounts/1/update"
    )
>>> print(repr(a2.text))
'\n Edit\n'

Right. Согласно Docs, суп использует функцию соответствия регулярного выражения, а не функцию поиска. Поэтому мне нужно предоставить флаг DOTALL:

pattern = re.compile('.*Edit.*')
pattern.match('\n Edit\n')  # Returns None

pattern = re.compile('.*Edit.*', flags=re.DOTALL)
pattern.match('\n Edit\n')  # Returns MatchObject

Хорошо. Выглядит неплохо. Пусть попробует его с супом

soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
    <i class="fa fa-edit"></i> Edit
</a>
""")

soup.find(
    'a',
    href="/customer-menu/1/accounts/1/update",
    text=re.compile(".*Edit.*", flags=re.DOTALL)
)  # Still return None... Why?!

Edit

Мое решение, основанное на ответах geckons: Я реализовал эти помощники:

import re

MATCH_ALL = r'.*'


def like(string):
    """
    Return a compiled regular expression that matches the given
    string with any prefix and postfix, e.g. if string = "hello",
    the returned regex matches r".*hello.*"
    """
    string_ = string
    if not isinstance(string_, str):
        string_ = str(string_)
    regex = MATCH_ALL + re.escape(string_) + MATCH_ALL
    return re.compile(regex, flags=re.DOTALL)


def find_by_text(soup, text, tag, **kwargs):
    """
    Find the tag in soup that matches all provided kwargs, and contains the
    text.

    If no match is found, return None.
    If more than one match is found, raise ValueError.
    """
    elements = soup.find_all(tag, **kwargs)
    matches = []
    for element in elements:
        if element.find(text=like(text)):
            matches.append(element)
    if len(matches) > 1:
        raise ValueError("Too many matches:\n" + "\n".join(matches))
    elif len(matches) == 0:
        return None
    else:
        return matches[0]

Теперь, когда я хочу найти элемент выше, я просто запускаю find_by_text(soup, 'Edit', 'a', href='/customer-menu/1/accounts/1/update')

4b9b3361

Ответ 1

Проблема заключается в том, что тэг <a> с тегом <i> внутри, не имеет атрибута string, который вы ожидаете от него. Сначала рассмотрим, что делает аргумент text="" для find().

ПРИМЕЧАНИЕ. Аргумент text - это старое имя, так как BeautifulSoup 4.4.0 называется string.

Из docs:

Хотя строка предназначена для поиска строк, вы можете комбинировать ее с аргументы, которые обнаруживают теги: Beautiful Soup найдет все теги, чьи .string соответствует вашему значению для строки. Этот код находит теги чья .string - "Elsie":

soup.find_all("a", string="Elsie")
# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

Теперь посмотрим, что атрибут Tag string (снова из docs):

Если тег имеет только одно дочернее устройство, а этот ребенок является навигационной строкой, Ребенок доступен как .string:

title_tag.string
# u'The Dormouse story'

(...)

Если тег содержит несколько вещей, то неясно, что .string должно ссылаться, поэтому .string определяется как None:

print(soup.html.string)
# None

Это именно ваш случай. Тег <a> содержит тег и <i>. Таким образом, find пытается получить None при попытке поиска строки и, следовательно, не может совпадать.

Как это решить?

Может быть, есть лучшее решение, но я бы, вероятно, пошел с чем-то вроде этого:

import re
from bs4 import BeautifulSoup as BS

soup = BS("""
<a href="/customer-menu/1/accounts/1/update">
    <i class="fa fa-edit"></i> Edit
</a>
""")

links = soup.find_all('a', href="/customer-menu/1/accounts/1/update")

for link in links:
    if link.find(text=re.compile("Edit")):
        thelink = link
        break

print(thelink)

Я думаю, что не слишком много ссылок, указывающих на /customer-menu/1/accounts/1/update, поэтому он должен быть достаточно быстрым.

Ответ 2

Вы можете передать function, которые возвращают True, если текст a содержит "Изменить" на .find

In [51]: def Edit_in_text(tag):
   ....:     return tag.name == 'a' and 'Edit' in tag.text
   ....: 

In [52]: soup.find(Edit_in_text, href="/customer-menu/1/accounts/1/update")
Out[52]: 
<a href="/customer-menu/1/accounts/1/update">
<i class="fa fa-edit"></i> Edit
</a>

EDIT:

Вы можете использовать .get_text() вместо text в своей функции, который дает то же самое результат:

def Edit_in_text(tag):
    return tag.name == 'a' and 'Edit' in tag.get_text()