Guía de Uso de Beautiful Soup para Web Scraping en Python

Instalación y elección del parser

Para instalar el paquete principal se utiliza pip:

pip install beautifulsoup4

Beautiful Soup puede valerse de distintos parsers. El más rápido y tolerante a fallos es lxml, aunque también se puede optar por html5lib (100% Python) o por el parser estándar de la biblioteca estándar. La instalación de las alternativas se realiza con:

# lxml
pip install lxml

# html5lib
pip install html5lib

La siguiente tabla resume las características prnicipales de cada parser:

Parser Invocación Ventajas Desventajas
Python estándar BeautifulSoup(markup, "html.parser") - Incluido en Python - Velocidad aceptable - Buena tolerancia a errores - En versiones anteriores a Python 2.7.3 o 3.2.2 la tolerancia es escasa
lxml HTML BeautifulSoup(markup, "lxml") - Muy rápido - Excelente tolerancia a errores - Requiere bibliotecas C externas
lxml XML BeautifulSoup(markup, ["lxml", "xml"]) o BeautifulSoup(markup, "xml") - Muy rápido - Único parser con soporte pleno para XML - Requiere bibliotecas C externas
html5lib BeautifulSoup(markup, "html5lib") - Máxima tolerancia - Interpreta el documento como un navegador - Salida en formato HTML5 - Más lento - No depende de extansiones nativas

Primeros pasos y estructura básica

El siguiente fragmento HTML ejemplifica la capacidad de Beautiful Soup para analizar código mal formado y presentarlo de forma indentada con prettify().

from bs4 import BeautifulSoup

html_crudo = '''
<html><head><title>Biblioteca</title></head>
<body>
<h1 class="titulo">Libros disponibles</h1>
<ul class="catalogo">
<li class="libro"><a href="libro1.html">El Quijote</a></li>
<li class="libro"><a href="libro2.html">1984</a></li>
<li class="libro"><a href="libro3.html">Cien años de soledad</a></li>
</ul>
</body></html>
'''

documento = BeautifulSoup(html_crudo, 'lxml')
print(documento.prettify())

Navegación directa por el árbol

La forma más rápida de acceder a un elemento es utilizando su nombre de etiqueta como atributo. Si hay repeticiones, se devuelve sólo la primera coincidnecia. A continuación se muestran algunos ejemplos de navegación, extracción de nombre, atributos y contenido.

# Usando el HTML anterior
# Obtener la primera etiqueta 'li'
primer_li = documento.li
print(primer_li)

# Nombre de la etiqueta
print(primer_li.name)

# Atributos
print(primer_li.attrs)

# Contenido textual
print(primer_li.string)  # None cuando hay múltiples nodos
print(primer_li.text)    # Todo el texto interior
# Iterar líneas sin espacios vacíos
for cadena in primer_li.stripped_strings:
   print(cadena)

# Anidación
print(documento.head.title.string)

# Hijos directos e iteración
print(documento.ul.contents)
for idx, hijo in enumerate(documento.ul.children):
   print(idx, hijo)

# Descendientes (recursivo)
for idx, descendiente in enumerate(documento.ul.descendants):
   print(idx, descendiente)

# Padres y ancestros
enlace = documento.find('a')
print(enlace.parent)
for ancestro in enlace.parents:
   print(ancestro.name)

# Hermanos
segundo_li = documento.find_all('li')[1]
print('Siguiente hermano:', segundo_li.next_sibling)
print('Anterior hermano:', segundo_li.previous_sibling)
print('Todos los siguientes:', list(segundo_li.next_siblings))

Métodos de búsqueda y filtros

Beautiful Soup ofrece find() y find_all() como herramientas principales. Se puede refinar la búsqueda mediante cinco tipos de filtros.

Tipos de filtro

import re
# Filtro por cadena: nombre exacto de etiqueta
print(documento.find_all('li'))

# Filtro por expresión regular
print(documento.find_all(re.compile('^h')))  # etiquetas que empiezan con 'h'

# Filtro por lista: varios tipos
print(documento.find_all(['a', 'li']))

# Filtro booleano True: cualquier etiqueta
print(documento.find_all(True))

# Filtro mediante función personalizada
def tiene_clase_y_sin_id(tag):
   return tag.has_attr('class') and not tag.has_attr('id')
print(documento.find_all(tiene_clase_y_sin_id))

Parámetros de find_all()

El método admite name, attrs, recursive, text y argumentos por palabra clave.

# Búsqueda por nombre con expresión regular
print(documento.find_all(name=re.compile('^l')))

# Uso de atributos con filtros
print(documento.find_all(class_='libro'))   # clase exacta
print(documento.find_all(href=re.compile('libro'), limit=2))

# Atributo data- personalizado (se pasa mediante attrs)
html_especial = BeautifulSoup('<span data-info="oculto">secreto</span>', 'lxml')
print(html_especial.find_all(attrs={'data-info': 'oculto'}))

# Búsqueda por texto
print(documento.find_all(text=re.compile('Quijote')))

# Limitar resultados
print(documento.find_all('li', limit=2))

# Búsqueda solo en hijos directos (recursive=False)
print(documento.html.find_all('li', recursive=False))

El método find() es equivalente a find_all() con limit=1, pero devuelve un único elemento o None. También se puede invocar la búsqueda usando el objeto como función: documento('a') equivale a documento.find_all('a').

Uso de selectores CSS

El método select() permite emplear selectores CSS estándar para localizar elementos, y los resultados soportan extracción de atributos y texto.

# Documento con estructura anidada para ejemplificar CSS
html_complejo = '''
<div class="panel">
 <ul class="items" id="lista-a">
   <li class="elemento"><span>Manzana</span></li>
   <li class="elemento destacado">Pera</li>
   <li class="elemento">Uva</li>
 </ul>
 <ul class="items chicos" id="lista-b">
   <li class="elemento"><em>A</em></li>
   <li class="elemento especial">B</li>
 </ul>
</div>'''

sopa = BeautifulSoup(html_complejo, 'lxml')

# Selectores variados
print(sopa.select('.items .elemento'))
print(sopa.select('#lista-a .destacado'))
print(sopa.select('li.elemento.especial'))

# Anidamiento de select
bloque = sopa.select('#lista-b')[0]
print(bloque.select('.elemento'))

# Obtener atributos
print(sopa.select('#lista-a li span')[0].attrs)
# Obtener texto
print(sopa.select('#lista-a li span')[0].get_text())

Etiquetas: Python BeautifulSoup web-scraping lxml html-parsing

Publicado el 10-25 12:11