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?!
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')
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.
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()
dans une ligne en utilisant lambda
soup.find(lambda tag:tag.name=="a" and "Edit" in tag.text)