Saltar a contenido

Beautiful Soup

Soup (1)

  1. Ella Olsson Unsplash

Beautiful Soup es un paquete ampliamente utilizado en técnicas de «scraping»1 sobre contenido HTML y similares.

Instalación

pip install beautifulsoup4

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:

>>> import 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)!

  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 clase footer:
>>> 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 valor text:
>>> soup.select('[type="text"]')
[<input id="POST-name" name="name" type="text"/>]
  • Localizar todos los input y todos los span:
>>> 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:
>>> soup.select('.footer p')
[<p>This is span 1</p>, <p>This is span 2</p>, <p>This is span 2</p>]

Un único elemento

Existe la opción de localizar un único elemento a través del método select_one():

>>> soup.select_one('.footer p')
<p>This is span 1</p>

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 texto Formulario:
>>> soup.find_all('h2', string='Formulario')
[<h2>Formulario</h2>]
  • 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>]

  1. 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():

>>> soup.find('h2', string='Formulario')
<h2>Formulario</h2>

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"/>]

  1. Devuelve una lista con dos «divs». Nos quedamos con el segundo.
  2. Estos objetos son de tipo Tag.

Hay definidas una serie de funciones adicionales que permiten hacer búsquedas (localizar elementos) de manera relativa al actual:

  • Localizar todos los div superiores al li con clase blue:

>>> 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>]

  1. 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 clase blue:

>>> 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>]

  1. 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 clase gold:

>>> 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>]

  1. 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 tipo submit:

>>> 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>]

  1. También existe la versión de esta función para obtener un único elemento find_next().
  • Localizar todos los elementos h1 y h2 previos al ul con id data:

>>> soup.select_one('ul#data').find_all_previous(['h1', 'h2'])#(1)!
[<h2>Some links</h2>, <h1>Just testing</h1>]

  1. 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.

>>> footer = soup.select_one('.footer')

>>> footer.text
'\n      This is the footer\n      This is span 1\nThis is span 2\nThis is span 2\n'

Devuelve un generador de todos los contenidos textuales del elemento incluyendo espacios y saltos de línea.

>>> footer = soup.select_one('.footer')

>>> list(footer.strings)
['\n      This is the footer\n      ',
 'This is span 1',
 '\n',
 'This is span 2',
 '\n',
 'This is span 2',
 '\n']

Devuelve un generador de todos los contenidos textuales del elemento eliminando espacios y saltos de línea.

>>> footer = soup.select_one('.footer')

>>> list(footer.stripped_strings)
['This is the footer', 'This is span 1', 'This is span 2', 'This is span 2']

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'

  1. El «footer» contiene varios elementos.
  2. 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']

  1. 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.

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 clase blue:
>>> 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 clase blue:
>>> 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.

Solución


  1. 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.