beautifulsoup¶
El paquete Beautiful Soup es ampliamente utilizado en técnicas de «scraping» permitiendo «parsear»[2] principalmente código HTML. [1]
$ pip install beautifulsoup4
Haciendo la sopa¶
Para empezar a trabajar con Beautiful Soup es necesario construir un objeto de tipo BeautifulSoup
que reciba el contenido a «parsear»:
>>> from bs4 import BeautifulSoup
>>> contents = """
... <html lang="en">
... <head>
... <title>Just testing</title>
... </head>
... <body>
... <h1>Just testing</h1>
... <div class="block">
... <h2>Some links</h2>
... <p>Hi there!</p>
... <ul id="data">
... <li class="blue"><a href="https://example1.com">Example 1</a></li>
... <li class="red"><a href="https://example2.com">Example 2</a></li>
... <li class="gold"><a href="https://example3.com">Example 3</a></li>
... </ul>
... </div>
... <div class="block">
... <h2>Formulario</h2>
... <form action="" method="post">
... <label for="POST-name">Nombre:</label>
... <input id="POST-name" type="text" name="name">
... <input type="submit" value="Save">
... </form>
... </div>
... <div class="footer">
... This is the footer
... <span class="inline"><p>This is span 1</p></span>
... <span class="inline"><p>This is span 2</p></span>
... <span class="inline"><p>This is span 2</p></span>
... </div>
... </body>
... </html>
... """
>>> soup = BeautifulSoup(contents, features='html.parser')
Atención
Importar el paquete usando bs4
. Suele llevar a equívoco por el nombre original.
A partir de aquí se abre un abanico de posibilidades que iremos desgranando en los próximos epígrafes.
Localizar elementos¶
Una de las tareas más habituales en técnicas de «scraping» y en «parsing» de contenido es la localización de determinadas elementos de interés.
Fórmulas de localización¶
A continuación se muestran, mediante ejemplos, distintas fórmulas para localizar elementos dentro del DOM [3]:
Localizar todos los enlaces:
>>> soup.find_all('a') [<a href="https://example1.com">Example 1</a>, <a href="https://example2.com">Example 2</a>, <a href="https://example3.com">Example 3</a>]
El primer argumento posicional de
find_all()
es el nombre del «tag» que queremos localizar.Localizar todos los elementos con la clase
inline
:>>> soup.find_all(class_='inline') [<span class="inline"><p>This is span 1</p></span>, <span class="inline"><p>This is span 2</p></span>, <span class="inline"><p>This is span 2</p></span>]
Los argumentos nominales de
find_all()
se utilizan para localizar elementos que contengan el atributo referenciado.Truco
Si el atributo a localizar tiene guiones medios (por ejemplo
aria-label
) no podremos usarlo como nombre de argumento (error sintáctico). Pero sí podemos usar un diccionario en su lugar:soup.find_all(attrs={'aria-label': 'box'})
Localizar todos los «divs» con la clase
footer
:>>> soup.find_all('div', class_='footer') # ≈ soup.find_all('div', 'footer') [<div class="footer"> This is the footer <span class="inline"><p>This is span 1</p></span> <span class="inline"><p>This is span 2</p></span> <span class="inline"><p>This is span 2</p></span> </div>]
Localizar todos los elementos cuyo atributo
type
tenga el valortext
:>>> soup.find_all(type='text') [<input id="POST-name" name="name" type="text"/>]
Localizar todos los los
h2
que contengan el textoFormulario
:>>> soup.find_all('h2', string='Formulario') [<h2>Formulario</h2>]
Localizar todos los elementos de título
h1, h2, h3, ...
. Esto lo podemos atacar usando expresiones regulares:>>> soup.find_all(re.compile(r'^h\d+.*')) [<h1>Just testing</h1>, <h2>Some links</h2>, <h2>Formulario</h2>]
Localizar todos los «input» y todos los «span»:
>>> soup.find_all(['input', 'span']) [<input id="POST-name" name="name" type="text"/>, <input type="submit" value="Save"/>, <span class="inline"><p>This is span 1</p></span>, <span class="inline"><p>This is span 2</p></span>, <span class="inline"><p>This is span 2</p></span>]
Localizar todos los párrafos que están dentro del pie de página (usando selectores CSS):
>>> soup.select('.footer p') [<p>This is span 1</p>, <p>This is span 2</p>, <p>This is span 2</p>]
Nota
En este caso se usa el método
select()
.
Localizar único elemento¶
Hasta ahora hemos visto las funciones find_all()
y select()
que localizan un conjunto de elementos. Incluso en el caso de encontrar sólo un elemento, se devuelve una lista con ese único elemento.
Beautiful Soup nos proporciona la función find()
que trata de localizar un único elemento. Hay que tener en cuenta dos circunstancias:
En caso de que el elemento buscado no exista, se devuelve None.
En caso de que existan múltiples elementos, se devuelve el primero.
Veamos algunos ejemplos de esto:
>>> soup.find('form')
<form action="" method="post">
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
</form>
>>> # Elemento que no existe
>>> soup.find('strange-tag')
>>>
>>> # Múltiples "li". Sólo se devuelve el primero
>>> soup.find('li')
<li class="blue"><a href="https://example1.com">Example 1</a></li>
Localizar desde elemento¶
Todas las búsquedas se pueden realizar desde cualquier elemento preexistente, no únicamente desde la raíz del DOM.
Veamos un ejemplo de ello. Si tratamos de localizar todos los títulos «h2» vamos a encontrar dos de ellos:
>>> soup.find_all('h2')
[<h2>Some links</h2>, <h2>Formulario</h2>]
Pero si, previamente, nos ubicamos en el segundo bloque de contenido, sólo vamos a encontrar uno de ellos:
>>> second_block = soup.find_all('div', 'block')[1]
>>> second_block
<div class="block">
<h2>Formulario</h2>
<form action="" method="post">
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
</form>
</div>
>>> second_block.find_all('h2')
[<h2>Formulario</h2>]
Otras funciones de búsqueda¶
Hay definidas una serie de funciones adicionales de búsqueda para cuestiones más particulares:
Localizar los «div» superiores a partir de un elemento concreto:
>>> gold = soup.find('li', 'gold') >>> gold.find_parents('div') [<div class="block"> <h2>Some links</h2> <p>Hi there!</p> <ul id="data"> <li class="blue"><a href="https://example1.com">Example 1</a></li> <li class="red"><a href="https://example2.com">Example 2</a></li> <li class="gold"><a href="https://example3.com">Example 3</a></li> </ul> </div>]
Se podría decir que la función
find_all()
busca en descendientes y que la funciónfind_parents()
busca en ascendientes.También existe la versión de esta función que devuelve un único elemento:
find_parent()
.Localizar los elementos hermanos siguientes a uno dado:
>>> blue_li = soup.find('li', 'blue') >>> blue_li.find_next_siblings() [<li class="red"><a href="https://example2.com">Example 2</a></li>, <li class="gold"><a href="https://example3.com">Example 3</a></li>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento:
find_next_sibling()
.Localizar los elementos hermanos anteriores a uno dado:
>>> gold_li = soup.find('li', 'gold') >>> gold_li.find_previous_siblings() [<li class="red"><a href="https://example2.com">Example 2</a></li>, <li class="blue"><a href="https://example1.com">Example 1</a></li>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento:
find_previous_sibling()
.Localizar todos los elementos a continuación de uno dado:
>>> submit = soup.find('input', type='submit') >>> submit.find_all_next() [<div class="footer"> This is the footer <span class="inline"><p>This is span 1</p></span> <span class="inline"><p>This is span 2</p></span> <span class="inline"><p>This is span 2</p></span> </div>, <span class="inline"><p>This is span 1</p></span>, <p>This is span 1</p>, <span class="inline"><p>This is span 2</p></span>, <p>This is span 2</p>, <span class="inline"><p>This is span 2</p></span>, <p>This is span 2</p>]
Al igual que en las anteriores, es posible aplicar un filtro al usar esta función.
También existe la versión de esta función que devuelve un único elemento:
find_next()
.Localizar todos los elementos previos a uno dado:
>>> ul_data = soup.find('ul', id='data') >>> ul_data.find_all_previous(['h1', 'h2']) [<h2>Some links</h2>, <h1>Just testing</h1>]
También existe la versión de esta función que devuelve un único elemento:
find_previous()
.Si hubiéramos hecho esta búsqueda usando
find_parents()
no habríamos obtenido el mismo resultado ya que los elementos de título no son elementos superiores de «ul»:>>> ul_data.find_parents(['h1', 'h2']) []
Atajo para búsquedas¶
Dado que la función find_all()
es la más utilizada en Beautiful Soup se ha implementado un atajo para llamarla:
>>> soup.find_all('span')
[<span class="inline"><p>This is span 1</p></span>,
<span class="inline"><p>This is span 2</p></span>,
<span class="inline"><p>This is span 2</p></span>]
>>> soup('span')
[<span class="inline"><p>This is span 1</p></span>,
<span class="inline"><p>This is span 2</p></span>,
<span class="inline"><p>This is span 2</p></span>]
Aunque uno de los preceptos del Zen de Python es «Explicit is better than implicit», el uso de estos atajos puede estar justificado en función de muchas circunstancias.
Acceder al contenido¶
Simplificando, podríamos decir que cada elemento de la famosa «sopa» de Beautiful Soup puede ser un bs4.element.Tag
o un «string».
Para el caso de los «tags» existe la posibilidad de acceder a su contenido, al nombre del elemento o a sus atributos.
Nombre de etiqueta¶
Podemos conocer el nombre de la etiqueta de un elemento usando el atributo name
:
>>> soup.name
'[document]'
>>> elem = soup.find('ul', id='data')
>>> elem.name
'ul'
>>> elem = soup.find('h1')
>>> elem.name
'h1'
Truco
Es posible modificar el nombre de una etiqueta con una simple asignación.
Acceso a atributos¶
Los atributos de un elemento están disponibles como claves de un diccionario:
>>> elem = soup.find('input', id='POST-name')
>>> elem
<input id="POST-name" name="name" type="text"/>
>>> elem['id']
'POST-name'
>>> elem['name']
'name'
>>> elem['type']
'text'
Exite una forma de acceder al diccionario completo de atributos:
>>> elem.attrs
{'id': 'POST-name', 'type': 'text', 'name': 'name'}
Truco
Es posible modificar el valor de un atributo con una simple asignación.
Contenido textual¶
Es necesario aclarar las distintas opciones que proporciona Beautiful Soup para acceder al contenido textual de los elementos del DOM.
Atributo |
Devuelve |
---|---|
|
Una cadena de texto con todos los contenidos textuales del elemento incluyendo espacios y saltos de línea |
|
Un generador de todos los contenidos textuales del elemento incluyendo espacios y saltos de línea |
|
Un generador de todos los contenidos textuales del elemento eliminando espacios y saltos de línea |
|
Una cadena de texto con el contenido del elemento, siempre que contenga un único elemento textual |
Ejemplos:
>>> footer = soup.find(class_='footer')
>>> footer.text
'\n This is the footer\n This is span 1\nThis is span 2\nThis is span 2\n'
>>> list(footer.strings)
['\n This is the footer\n ',
'This is span 1',
'\n',
'This is span 2',
'\n',
'This is span 2',
'\n']
>>> list(footer.stripped_strings)
['This is the footer', 'This is span 1', 'This is span 2', 'This is span 2']
>>> footer.string # El "footer" contiene varios elementos
>>> footer.span.string # El "span" sólo contiene un elemento
'This is span 1'
Mostrando elementos¶
Cualquier elemento del DOM que seleccionemos mediante este paquete se representa con el código HTML que contiene. Por ejemplo:
>>> data = soup.find(id='data')
>>> data
<ul id="data">
<li class="blue"><a href="https://example1.com">Example 1</a></li>
<li class="red"><a href="https://example2.com">Example 2</a></li>
<li class="gold"><a href="https://example3.com">Example 3</a></li>
</ul>
Existe la posibilidad de mostrar el código HTML en formato «mejorado» a través de la función prettify()
:
>>> pretty_data = data.prettify()
>>> print(pretty_data)
<ul id="data">
<li class="blue">
<a href="https://example1.com">
Example 1
</a>
</li>
<li class="red">
<a href="https://example2.com">
Example 2
</a>
</li>
<li class="gold">
<a href="https://example3.com">
Example 3
</a>
</li>
</ul>