python - examples - BeautifulSoup: busca por texto dentro de una etiqueta
install beautifulsoup python 3 (3)
Observe el siguiente problema:
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.*")
)
Por alguna razón, BeautifulSoup no coincidirá con el texto, cuando la etiqueta
<i>
esté allí.
Encontrar la etiqueta y mostrar su texto produce
>>> a2 = soup.find(
''a'',
href="/customer-menu/1/accounts/1/update"
)
>>> print(repr(a2.text))
''/n Edit/n''
Derecha. Según los Docs , la sopa utiliza la función de coincidencia de la expresión regular, no la función de búsqueda. Entonces necesito proporcionar la bandera 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. Se ve bien. Probémoslo con sopa
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?!
Editar
Mi solución basada en geckons responde: implementé estos ayudantes:
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]
Ahora, cuando quiero encontrar el elemento anterior, solo ejecuto
find_by_text(soup, ''Edit'', ''a'', href=''/customer-menu/1/accounts/1/update'')
El problema es que su etiqueta
<a>
con la etiqueta
<i>
dentro, no tiene el atributo de
string
que espera que tenga.
Primero echemos un vistazo a lo que hace el argumento
text=""
para
find()
.
NOTA: El argumento de
text
es un nombre antiguo, ya que BeautifulSoup 4.4.0 se llama
string
.
De los docs :
Aunque la cadena es para encontrar cadenas, puede combinarla con argumentos que busquen etiquetas: Beautiful Soup encontrará todas las etiquetas cuya cadena coincida con su valor para la cadena. Este código encuentra las etiquetas cuyo .string es "Elsie":
soup.find_all("a", string="Elsie") # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]
Ahora echemos un vistazo a cuál es el atributo de
string
Tag
(de los
docs
nuevamente):
Si una etiqueta tiene solo un elemento secundario y ese elemento secundario es NavigableString, el elemento secundario estará disponible como .string:
title_tag.string # u''The Dormouse''s story''
(...)
Si una etiqueta contiene más de una cosa, entonces no está claro a qué debe referirse .string, por lo que .string se define como Ninguno:
print(soup.html.string) # None
Este es exactamente tu caso.
Su etiqueta
<a>
contiene un texto
y
una etiqueta
<i>
.
Por lo tanto, la búsqueda obtiene
None
cuando intenta buscar una cadena y, por lo tanto, no puede coincidir.
¿Cómo resolver esto?
Quizás haya una mejor solución, pero probablemente elegiría algo como esto:
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)
Creo que no hay demasiados enlaces que apuntan a
/customer-menu/1/accounts/1/update
por lo que debería ser lo suficientemente rápido.
Puede pasar una
function
que devuelva
True
si
a
texto
contiene "Editar" para
.find
def Edit_in_text(tag):
return tag.name == ''a'' and ''Edit'' in tag.get_text()
EDITAR:
Puede usar el método
.get_text()
lugar del
text
en su función que da el mismo resultado:
soup.find(lambda tag:tag.name=="a" and "Edit" in tag.text)
en una línea usando lambda
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>