web-dev-qa-db-fra.com

BeautifulSoup - recherche par texte dans une balise

Observez le problème suivant:

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

Pour une raison quelconque, BeautifulSoup ne correspondra pas au texte lorsque la balise <i> Est également présente. Trouver la balise et afficher son texte produit

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

Droite. Selon Docs , soup utilise la fonction de correspondance de l'expression régulière, pas la fonction de recherche. J'ai donc besoin de fournir le drapeau 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

Bien. Cela semble bon. Essayons-le avec de la soupe

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

Modifier

Ma solution basée sur geckons répond: J'ai implémenté ces aides:

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]

Maintenant, quand je veux trouver l'élément ci-dessus, je lance juste find_by_text(soup, 'Edit', 'a', href='/customer-menu/1/accounts/1/update')

30
Eldamir

Le problème est que votre balise <a> Avec la balise <i> À l'intérieur n'a pas l'attribut string que vous attendez de lui. Voyons d'abord ce que l'argument text="" Pour find() fait.

NOTE: L'argument text est un ancien nom, puisque BeautifulSoup 4.4.0 s'appelle string.

De la docs :

Bien que chaîne soit utilisée pour rechercher des chaînes, vous pouvez la combiner avec des arguments permettant de rechercher des balises: Beautiful Soup trouvera toutes les balises dont .string correspond à votre valeur pour chaîne. Ce code trouve les tags dont la chaîne est "Elsie":

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

Jetons maintenant un coup d'œil à l'attribut Tag de string (à partir de la docs ):

Si une balise a un seul enfant et que cet enfant est une NavigableString, il est disponible en tant que .string:

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

(...)

Si une balise contient plus d’une chose, il n’est pas clair à quoi .string doit faire référence, alors .string est défini comme étant None:

print(soup.html.string)
# None

Ceci est exactement votre cas. Votre balise <a> Contient un texte et une balise <i>. Par conséquent, la recherche obtient None lors de la recherche d'une chaîne et ne peut donc pas correspondre.

Comment résoudre ce problème?

Peut-être qu'il y a une meilleure solution mais j'irais probablement avec quelque chose comme ceci:

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)

Je pense qu'il n'y a pas trop de liens pointant vers /customer-menu/1/accounts/1/update, Donc ça devrait être assez rapide.

31
geckon

Vous pouvez passer un fonction qui retourne True si a texte contient "Modifier" à .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>

MODIFIER:

Vous pouvez utiliser la méthode .get_text() au lieu de text dans votre fonction qui donne le même résultat:

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

dans une ligne en utilisant lambda

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