webentwicklung-frage-antwort-db.com.de

BeautifulSoup - Suche nach Text innerhalb eines Tags

Beachten Sie das folgende Problem:

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.*")
)

Aus irgendeinem Grund stimmt BeautifulSoup nicht mit dem Text überein, wenn auch das Tag <i> Vorhanden ist. Das Finden des Tags und das Anzeigen seines Texts führt zu

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

Recht. Gemäß Docs verwendet soup die Match-Funktion des regulären Ausdrucks, nicht die Suchfunktion. Also muss ich das DOTALL-Flag bereitstellen:

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

In Ordung. Sieht gut aus. Lass es uns mit Suppe versuchen

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?!

Bearbeiten

Meine Lösung basiert auf der Antwort von Geckons: Ich habe diese Helfer implementiert:

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]

Wenn ich das Element oben finden möchte, führe ich einfach find_by_text(soup, 'Edit', 'a', href='/customer-menu/1/accounts/1/update') aus.

30
Eldamir

Das Problem ist, dass Ihr <a> - Tag mit dem darin enthaltenen <i> - Tag nicht das erwartete string - Attribut enthält. Schauen wir uns zunächst an, was das Argument text="" Für find() bewirkt.

HINWEIS: Das Argument text ist ein alter Name, seit BeautifulSoup 4.4.0 heißt es string.

Aus dem docs :

Obwohl string zum Suchen von Zeichenfolgen dient, können Sie es mit Argumenten kombinieren, die nach Tags suchen: Beautiful Soup findet alle Tags, deren .string mit Ihrem Wert für string übereinstimmt. Dieser Code findet die Tags, deren .string "Elsie" ist:

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

Schauen wir uns nun das Tag-Attribut von string an (erneut aus docs ):

Wenn ein Tag nur ein Kind hat und dieses Kind ein NavigableString ist, wird das Kind als .string verfügbar gemacht:

title_tag.string
# u'The Dormouse's story'

(...)

Wenn ein Tag mehrere Elemente enthält, ist nicht klar, auf welche .string-Datei verwiesen werden soll. Daher ist .string als None definiert:

print(soup.html.string)
# None

Das ist genau Ihr Fall. Ihr <a> - Tag enthält einen Text und <i> - Tag. Daher wird beim Versuch, nach einer Zeichenfolge zu suchen, für die Suche None angezeigt, sodass sie nicht übereinstimmt.

Wie löst man das?

Vielleicht gibt es eine bessere Lösung, aber ich würde wahrscheinlich mit so etwas gehen:

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)

Ich denke, dass es nicht zu viele Links gibt, die auf /customer-menu/1/accounts/1/update Verweisen, also sollte es schnell genug sein.

31
geckon

Sie können ein function übergeben, das True zurückgibt, wenn a text "Edit" enthält. zu .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>

BEARBEITEN:

Sie können die .get_text() -Methode anstelle der text in Ihrer Funktion verwenden gibt das gleiche Ergebnis:

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

in einer Zeile mit Lambda

soup.find(lambda tag:tag.name=="a" and "Edit" in tag.text)
10
Amr