Beautiful Soup¶
(1)
Beautiful Soup es un paquete ampliamente utilizado en técnicas de «scraping»1 sobre contenido HTML y similares.
Instalación¶
Modo de uso¶
Es importante reseñar que la importación de este módulo es algo «particular» ya que se utiliza el nombre bs4
:
Preparar la sopa¶
Para ilustrar todos los ejemplos vamos a partir del siguiente código HTML:
<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>
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, 'html.parser')#(1)!
- Lo más habitual es usar el «parser» HTML, pero existen otros.
Localizar elementos¶
En esta sección veremos distintas formas de localizar elementos en base a la naturaleza de la consulta.
Selectores CSS¶
A continuación se muestran, mediante ejemplos, distintas fórmulas para localizar elementos dentro del DOM utilizando para ello selectores CSS mediante el método select()
:
- Localizar todos los enlaces:
>>> soup.select('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>]
- Localizar todos los elementos con la clase
inline
:
>>> soup.select('.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>]
- Localizar todos los
div
con la clasefooter
:
>>> soup.select('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
:
- Localizar todos los
input
y todos losspan
:
>>> soup.select('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:
Un único elemento
Existe la opción de localizar un único elemento a través del método select_one()
:
Reglas avanzadas¶
A continuación se muestran, mediante ejemplos, distintas fórmulas para localizar elementos dentro del DOM con reglas avanzadas mediante el método find_all()
:
- Localizar todos los
h2
que contengan el textoFormulario
:
- Localizar todos los elementos de título
h1
,h2
,h3
, ...:
>>> import re
>>> soup.find_all(re.compile(r'^h\d+.*'))#(1)!
[<h1>Just testing</h1>, <h2>Some links</h2>, <h2>Formulario</h2>]
- Utilizamos expresiones regulares para resolver este problema.
Se podría decir que la función find_all()
es un superconjunto de select()
ya que permite hacer lo mismo (también se pueden utilizar selectores CSS) pero abarca reglas avanzadas.
Un único elemento
Existe la opción de localizar un único elemento a través del método find()
:
Cambiando el origen¶
Todas las búsquedas se pueden realizar desde cualquier elemento preexistente, no únicamente desde la raíz del DOM.
- Localizar todos los «input» que cuelgan del segundo «div» con clase
block
:
>>> _, div2 = soup.select('div.block')#(1)!
>>> type(div2)#(2)!
bs4.element.Tag
>>> div2.select('input')
[<input id="POST-name" name="name" type="text"/>,
<input type="submit" value="Save"/>]
- Devuelve una lista con dos «divs». Nos quedamos con el segundo.
- Estos objetos son de tipo
Tag
.
Búsqueda relativa¶
Hay definidas una serie de funciones adicionales que permiten hacer búsquedas (localizar elementos) de manera relativa al actual:
- Localizar todos los
div
superiores alli
con claseblue
:
>>> soup.select_one('li.gold').find_parents('div')#(1)!
[<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>]
- También existe la versión de esta función para obtener un único elemento
find_parent()
.
- Localizar todos los elementos «hermanos» siguientes al
li
con claseblue
:
>>> soup.select_one('li.blue').find_next_siblings()#(1)!
[<li class="red"><a href="https://example2.com">Example 2</a></li>,
<li class="gold"><a href="https://example3.com">Example 3</a></li>]
- También existe la versión de esta función para obtener un único elemento
find_next_sibling()
.
- Localizar todos los elementos «hermanos» anteriores al
li
con clasegold
:
>>> soup.select_one('li.gold').find_previous_siblings()#(1)!
[<li class="red"><a href="https://example2.com">Example 2</a></li>,
<li class="blue"><a href="https://example1.com">Example 1</a></li>]
- También existe la versión de esta función para obtener un único elemento
find_previous_sibling()
.
- Localizar todos los elementos siguientes al
input
que tiene tiposubmit
:
>>> soup.select_one('input[type="submit"]').find_all_next()#(1)!
[<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>]
- También existe la versión de esta función para obtener un único elemento
find_next()
.
- Localizar todos los elementos
h1
yh2
previos alul
con iddata
:
>>> soup.select_one('ul#data').find_all_previous(['h1', 'h2'])#(1)!
[<h2>Some links</h2>, <h1>Just testing</h1>]
- También existe la versión de esta función para obtener un único elemento
find_previous()
.
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».
En 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.select_one('ul#data')
>>> elem.name
'ul'
>>> elem = soup.select_one('h1')
>>> elem.name
'h1'
Acceso a atributos¶
Los atributos de un elemento están disponibles como claves de un diccionario:
>>> elem = soup.select_one('input#POST-name')
>>> elem
<input id="POST-name" name="name" type="text"/>
>>> elem['id']
'POST-name'
>>> elem['name']
'name'
>>> elem['type']
'text'
>>> elem.attrs
{'id': 'POST-name', 'type': 'text', 'name': 'name'}
Contenido textual¶
Es importante aclarar las distintas opciones que proporciona Beautiful Soup para acceder al contenido textual de los elementos del DOM:
Devuelve una cadena de texto con todos los contenidos textuales del elemento incluyendo espacios y saltos de línea.
Devuelve un generador de todos los contenidos textuales del elemento incluyendo espacios y saltos de línea.
Devuelve un generador de todos los contenidos textuales del elemento eliminando espacios y saltos de línea.
Devuelve una cadena de texto con el contenido dele elemento, siempre que contenga un único elemento textual.
>>> footer = soup.select_one('.footer')
>>> footer.string#(1)!
>>> footer.span.string#(2)!
'This is span 1'
- El «footer» contiene varios elementos.
- El «span» sólo contiene un elemento.
Mostrando elementos¶
Cualquier elemento del DOM que seleccionemos mediante este paquete se representa con el código HTML que contiene:
>>> elem = soup.select_one('#data')
>>> elem
<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
:
>>> print(elem.prettify())
<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>
Navegar por el DOM¶
Además de localizar elementos, este paquete permite moverse por los elementos del DOM de manera muy sencilla.
Descendientes¶
Para ir profundizando (descendiendo) en el DOM podemos utilizar los nombres de los «tags» como atributos del objeto, teniendo en cuenta que si existen múltiples elementos sólo se accederá al primero de ellos:
>>> soup.div.p
<p>Hi there!</p>
>>> soup.form.label
<label for="POST-name">Nombre:</label>
>>> type(soup.span)
bs4.element.Tag
Existe la opción de obtener el contenido (como lista) de un determinado elemento:
>>> soup.form.contents#(1)!
['\n',
<label for="POST-name">Nombre:</label>,
'\n',
<input id="POST-name" name="name" type="text"/>,
'\n',
<input type="submit" value="Save"/>,
'\n']
- En esta lista hay una mezcla de «strings» y objetos
bs4.element.Tag
.
Si no se quiere explicitar el contenido de un elemento como lista, también es posible usar un generador para acceder al mismo de forma secuencial:
- Localizar todos los elementos hijos del formulario:
>>> soup.form.children
<generator object Tag.children.<locals>.<genexpr> at 0x10740a2c0>
>>> for elem in soup.form.children:
... if isinstance(elem, bs4.element.Tag):#(1)!
... print(repr(elem))
...
<label for="POST-name">Nombre:</label>
<input id="POST-name" name="name" type="text"/>
<input type="submit" value="Save"/>
-
- Entre los elementos hijos también se encuentran los saltos de línea
'\n'
. - Es por ello que planteamos esta condición para quederanos únicamente con objetos de tipo
Tag
.
- Entre los elementos hijos también se encuentran los saltos de línea
Descendientes
Existe la propiedad .descendants()
que itera sobre todos los elementos hijos mediante búsqueda en anchura.
Ascendientes¶
Para acceder al elemento superior de otro dado, podemos usar el atributo parent:
- Localizar el elemento superior al
li
con claseblue
:
>>> soup.select_one('li.blue').parent
<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>
- Localizar todos los elementos superiores (ascendientes) al
li
con claseblue
:
>>> asc = soup.select_one('li.blue').parents
>>> for elem in asc:
... print(elem.name)
...
ul
div
body
html
[document]
Ejercicio
Escribe un programa en Python que obtenga de https://pypi.org datos estructurados de los «Trending projects» y los muestre por pantalla utilizando el siguiente formato:
<nombre-del-paquete>,<versión>,<descripción>,<url>
Se recomienda usar el paquete requests
para obtener el código fuente de la página. Hay que tener en cuenta que el listado de paquetes cambia cada pocos segundos, a efectos de comprobación.
-
El «scraping» HTML es un proceso automatizado para extraer información de sitios web, utilizando el código HTML como base para identificar y extraer los datos relevantes. ↩