# Documentación de Beautiful Soup

Beautiful Soup es una librería de Python para extraer datos de archivos en formato HTML y XML. Funciona con tu analizador favorito para ofrecer maneras bien definidas de navegar, buscar y modificar el árbol analizado. Puede llegar a ahorrar horas o días de trabajo a los programadores.

Este manual ilustra con ejemplos la funcionalidades más importantes de Beautiful Soup 4. Te muestro las cosas para las que la librería es buena, cómo funciona, cómo usarla, cómo hacer lo que quieres y qué hacer cuando no se cumplen tus expectativas.

Este documento cubre Beautiful Soup versión 4.13.4. Los ejemplos en este documento fueron escritos para Python 3.8.

Podrías estar buscando la documentación de Beautiful Soup 3. Si es así, debes saber que Beautiful Soup 3 ya no se desarrolla y su soporte fue abandonado el 31 de diciembre de 2020. Si quieres conocer la diferencias entre Beautiful Soup 3 y Beautiful Soup 4, mira Actualizar el código a BS4.

Esta documentación ha sido traducida a otras lenguas por los usuarios de Beautiful Soup:

- 这篇文档当然还有中文版.

- このページは日本語で利用できます(外部リンク)

- 이 문서는 한국어 번역도 가능합니다.

- Este documento também está disponível em Português do Brasil.

- Эта документация доступна на русском языке.

- This document is also available in English.

## Cómo conseguir ayuda

Si tienes alguna pregunta sobre BeautifulSoup, o si tienes problemas, envía un correo electrónico al grupo de discusión. Si tienes algún problema relacionado con el análisis de un documento HTML, asegúrate de mencionar lo que la función diagnose() dice sobre dicho documento.

Cuando informes de algún error en esta documentación, por favor, indica la traducción que estás leyendo.

### Documentación de la API

Este documento está escrito como un manual de instrucciones, pero también puedes leer la tradicional documentación de la API generada a partir del código fuente de Beautiful Soup. Si quieres conocer detalles acerca de la parte interna de Beautiful Soup, o una funcionalidad no incluida en este documento, prueba en la documentación de la API.

# Inicio rápido

Este es un documento HTML que usaré como ejemplo a lo largo de este documento. Es parte de una historia de Alicia en el país de las maravillas:

    html_doc = """<html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>

    <p class="story">Once upon a time there were three little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>

    <p class="story">...</p>
    """

Al procesar el documento de «Las tres hermanas» en Beautiful Soup, se nos devuelve un objeto `BeautifulSoup`, que representa el documento como una estructura de datos anidada:

    from bs4 import BeautifulSoup
    soup = BeautifulSoup(html_doc, 'html.parser')

    print(soup.prettify())
    # <html>
    #  <head>
    #   <title>
    #    The Dormouse's story
    #   </title>
    #  </head>
    #  <body>
    #   <p class="title">
    #    <b>
    #     The Dormouse's story
    #    </b>
    #   </p>
    #   <p class="story">
    #    Once upon a time there were three little sisters; and their names were
    #    <a class="sister" href="http://example.com/elsie" id="link1">
    #     Elsie
    #    </a>
    #    ,
    #    <a class="sister" href="http://example.com/lacie" id="link2">
    #     Lacie
    #    </a>
    #    and
    #    <a class="sister" href="http://example.com/tillie" id="link3">
    #     Tillie
    #    </a>
    #    ; and they lived at the bottom of a well.
    #   </p>
    #   <p class="story">
    #    ...
    #   </p>
    #  </body>
    # </html>

Estas son algunas de las maneras sencillas para navegar por la estructura de datos:

    soup.title
    # <title>The Dormouse's story</title>

    soup.title.name
    # u'title'

    soup.title.string
    # u'The Dormouse's story'

    soup.title.parent.name
    # u'head'

    soup.p
    # <p class="title"><b>The Dormouse's story</b></p>

    soup.p['class']
    # u'title'

    soup.a
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

    soup.find_all('a')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.find(id="link3")
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

Una tarea frecuente es extraer todas las URL encontradas en las etiquetas <a> de una página:

    for link in soup.find_all('a'):
        print(link.get('href'))
    # http://example.com/elsie
    # http://example.com/lacie
    # http://example.com/tillie

Otra tarea habitual es extraer todo el texto de una página:

    print(soup.get_text())
    # The Dormouse's story
    #
    # The Dormouse's story
    #
    # Once upon a time there were three little sisters; and their names were
    # Elsie,
    # Lacie and
    # Tillie;
    # and they lived at the bottom of a well.
    #
    # ...

¿Esto se parece a lo que necesitas? Si es así, sigue leyendo.

# Instalar Beautiful Soup

Si usas una versión reciente de Debian o Ubuntu Linux, puedes instalar Beautiful Soup con el gestor de paquetes del sistema:

`$` `apt`-`get` `install` `python3`-`bs4`

Beautiful Soup 4 está publicado en Pypi, así que si no puedes instalarlo con el gestor de paquetes, puedes instalarlo con `easy_install` o `pip`. El nombre del paquete es `beautifulsoup4`. Asegúrate de que usas la versión correcta de `pip` o `easy_install` para tu versión de Python (podrían llamarse `pip3` y `easy_install3`, respectivamente):

`$` `easy_install` `beautifulsoup4`

`$` `pip` `install` `beautifulsoup4`

(El paquete `BeautifulSoup` no es el que quieres. Ese es el lanzamiento anterior `Beautiful Soup 3`_. Muchos software utilizan BS3, así que aún está disponible, pero si estás escribiendo nuevo código, deberías instalar `beautifulsoup4`).

Si no tienes `easy_install` o `pip` instalados, puedes descargar el código de Beautiful Soup 4 comprimido en un tarball e instalarlo con `setup.py`:

`$` `python` `setup.py` `install`

Si aún así todo falla, la licencia de Beautiful Soup te permite empaquetar la librería completa con tu aplicación. Puedes descargar el tarball, copiar su directorio `bs4` en tu base de código y usar Beautiful Soup sin instalarlo en absoluto.

Yo empleo Python 3.10 para desarrollar Beautiful Soup, aunque debería funcionar con otras versiones recientes.

## Instalar un analizador

Beautiful Soup soporta el analizador de HTML incluido en la librería estándar de Python, aunque también soporta varios analizadores de Python de terceros. Uno de ellos es el analizador de lxml. Dependiendo de tu instalación, puedes instalar lxml con uno de los siguientes comandos:

`$` `apt`-`get` `install` `python`-`lxml`

`$` `easy_install` `lxml`

`$` `pip` `install` `lxml`

Otra alternativa es usar el analizador de Python de html5lib, el cual analiza HTML de la misma manera en la que lo haría un navegador web. Dependiendo de tu instalación, puedes instalar html5lib con uno de los siguientes comandos:

`$` `apt`-`get` `install` `python`-`html5lib`

`$` `easy_install` `html5lib`

`$` `pip` `install` `html5lib`

Esta tabla resume las ventajas e inconvenientes de cada librería de los analizadores:

Analizador | Uso típico | Ventajas | Desventajas

html.parser de Python

`BeautifulSoup(markup, "html.parser")`

- Ya incluido

- Rapidez decente

- Tolerante (en Python 3.2)

- No tan rápido como lxml, menos tolerante que html5lib.

Analizador HTML de lxml

`BeautifulSoup(markup, "lxml")`

- Muy rápido

- Tolerante

- Dependencia externa de C

Analizador XML de lxml

`BeautifulSoup(markup, "lxml-xml")` `BeautifulSoup(markup, "xml")`

- Muy rápido

- El único analizador XML actualmente soportado

- Dependencia externa de C

html5lib

`BeautifulSoup(markup, "html5lib")`

- Extremadamente tolerante

- Analiza las páginas de la misma manera que un navegador web

- Crea HTML5 válido

- Muy lento

- Dependencia externa de Python

Si puedes, te recomiendo que instales y uses lxml para mayor velocidad.

Ten en cuenta que si un documento es inválido, analizadores diferentes generarán árboles de Beautiful Soup diferentes para él. Mira Diferencias entre analizadores para más detalle.

# Haciendo la sopa

Para analizar un documento pásalo al constructor de `BeautifulSoup`. Puedes pasar una cadena de caracteres o abrir un gestor de archivos:

    from bs4 import BeautifulSoup

    with open("index.html") as fp:
        soup = BeautifulSoup(fp, 'html.parser')

    soup = BeautifulSoup("<html>a web page</html>", 'html.parser')

Primero, el documento se convierte a Unicode, y las entidades HTML se convierten a caracteres Unicode:

    print(BeautifulSoup("<html><head></head><body>Sacr&eacute; bleu!</body></html>", "html.parser"))
    # <html><head></head><body>Sacré bleu!</body></html>

Entonces Beautiful Soup analiza el documento usando el mejor analizador disponible. Usará un analizador HTML a no ser que se especifique que se use un analizador XML (ver Analizar XML).

# Tipos de objetos

Beautiful Soup transforma un complejo documento HTML en un complejo árbol de objetos de Python. Pero tan solo tendrás que lidiar con cuatro tipos de objetos: `Tag`, `NavigableString`, `BeautifulSoup` y `Comment`.

class Tag

  Un objeto `Tag` corresponde a una etiqueta XML o HTML en el documento original.

      soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
      tag = soup.b
      type(tag)
      # <class 'bs4.element.Tag'>

  Las etiquetas tienen muchos atributos y métodos, y cubriré la mayoría de ellos en Navegar por el árbol y Buscar en el árbol. Por ahora, las características más importantes de una etiqueta son su nombre y sus atributos.

  name

    Toda etiqueta tiene un nombre:

        tag.name
        # 'b'

    Si cambias el nombre de una etiqueta, el cambio se verá reflejado en cualquier especificación generada por Beautiful Soup a partir de entonces:

        tag.name = "blockquote"
        tag
        # <blockquote class="boldest">Extremely bold</blockquote>

  attrs

    Una etiqueta HTML o XML puede tener cualquier cantidad de atributos. La etiqueta `<b id="boldest">` tiene un atributo «id» cuyo valor es «boldest». Puedes acceder a los atributos de una etiqueta usándola como un diccionario:

        tag = BeautifulSoup('<b id="boldest">bold</b>', 'html.parser').b
        tag['id']
        # 'boldest'

    Puedes acceder a los atributos del diccionario directamente con `.attrs`:

        tag.attrs
        # {'id': 'boldest'}

    Puedes añadir, quitar y modificar los atributos de una etiqueta. De nuevo, esto se realiza usando la etiqueta como un diccionario:

        tag['id'] = 'verybold'
        tag['another-attribute'] = 1
        tag
        # <b another-attribute="1" id="verybold"></b>

        del tag['id']
        del tag['another-attribute']
        tag
        # <b>bold</b>

        tag['id']
        # KeyError: 'id'
        tag.get('id')
        # None

    ## Atributos multivaluados

    HTML 4 define algunos atributos que pueden tomar múltiples valores. HTML 5 elimina un par de ellos, pero define unos cuantos más. El atributo multivaluado más común es `class` (esto es, una etiqueta puede tener más de una clase de CSS). Otros incluyen `rel`, `rev`, `accept-charset`, `headers` y `accesskey`. Por defecto, Beautiful Soup transforma los valores de un atributo multivaluado en una lista:

        css_soup = BeautifulSoup('<p class="body"></p>', 'html.parser')
        css_soup.p['class']
        # ['body']

        css_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser')
        css_soup.p['class']
        # ['body', 'strikeout']

    Si un atributo parece que tiene más de un valor, pero no es un atributo multivaluado definido como tal por ninguna versión del estándar de HTML, Beautiful Soup no modificará el atributo:

        id_soup = BeautifulSoup('<p id="my id"></p>', 'html.parser')
        id_soup.p['id']
        # 'my id'

    Cuando transformas una etiqueta en una cadena de caracteres, muchos atributos se combinan:

        rel_soup = BeautifulSoup('<p>Back to the <a rel="index first">homepage</a></p>', 'html.parser')
        rel_soup.a['rel']
        # ['index', 'first']
        rel_soup.a['rel'] = ['index', 'contents']
        print(rel_soup.p)
        # <p>Back to the <a rel="index contents">homepage</a></p>

    Puedes forzar que todos los atributos sean analizados como cadenas de caracteres pasando `multi_valued_attributes=None` como argumento clave en el constructor de `BeautifulSoup`:

        no_list_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser', multi_valued_attributes=None)
        no_list_soup.p['class']
        # 'body strikeout'

    Puedes usar `get_attribute_list` para obtener un valor que siempre sea una lista, sin importar si es un atributo multivaluado:

        id_soup.p.get_attribute_list('id')
        # ["my id"]

    Si analizas un documento como XML, no hay atributos multivaluados:

        xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
        xml_soup.p['class']
        # 'body strikeout'

    Una vez más, puedes configurar esto usando el argumento `multi_valued_attributes`

        class_is_multi= { '*' : 'class'}
        xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml', multi_valued_attributes=class_is_multi)
        xml_soup.p['class']
        # ['body', 'strikeout']

    Probablemente no tengas que hacer esto, pero si lo necesitas, usa los parámetros por defecto como guía. Implementan las reglas descritas en la especificación de HTML:

        from bs4.builder import builder_registry
        builder_registry.lookup('html').DEFAULT_CDATA_LIST_ATTRIBUTES

class NavigableString

Un string corresponde a un trozo de texto en una etiqueta. Beautiful Soup usa la clase `NavigableString` para contener estos trozos de texto:

    soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
    tag = soup.b
    tag.string
    # 'Extremely bold'
    type(tag.string)
    # <class 'bs4.element.NavigableString'>

Un `NavigableString` es como una cadena de caracteres de Python Unicode, exceptuando que también soporta algunas de las características descritas en Navegar por el árbol y Buscar en el árbol. Puedes convertir un objeto `NavigableString` a una cadena de caracteres Unicode usando `str`:

    unicode_string = str(tag.string)
    unicode_string
    # 'Extremely bold'
    type(unicode_string)
    # <type 'str'>

No puedes editar dicha cadena, pero puedes reemplazar una cadena por otra, usando replace_with():

    tag.string.replace_with("No longer bold")
    tag
    # <b class="boldest">No longer bold</b>

`NavigableString` soporta la mayoría de las características descritas en Navegar por el árbol y Buscar en el árbol, pero no todas. En particular, como una cadena no puede contener nada (la manera en la que una etiqueta contiene una cadena de caracteres u otra etiqueta), strings no admiten los atributos `.contents` o `.string`, o el método `find()`.

Si quieres usar un `NavigableString` fuera de Beautiful Soup, deberías llamar `unicode()` sobre él para convertirlo en una cadena de caracteres de Python Unicode. Si no, tu cadena arrastrará una referencia a todo el árbol analizado de Beautiful Soup, incluso cuando hayas acabado de utilizar Beautiful Soup. Esto es un gran malgasto de memoria.

El objeto `BeautifulSoup` representa el documento analizado en su conjunto. Para la mayoría de propósitos, puedes usarlo como un objeto `Tag`. Esto significa que soporta la mayoría de métodos descritos en Navegar por el árbol and Buscar en el árbol.

Puedes también pasar un objeto `BeautifulSoup` en cualquiera de los métodos definidos en Modificar el árbol, como si fuese un `Tag`. Esto te permite hacer cosas como combinar dos documentos analizados:

    doc = BeautifulSoup("<document><content/>INSERT FOOTER HERE</document", "xml")
    footer = BeautifulSoup("<footer>Here's the footer</footer>", "xml")
    doc.find(text="INSERT FOOTER HERE").replace_with(footer)
    # 'INSERT FOOTER HERE'
    print(doc)
    # <?xml version="1.0" encoding="utf-8"?>
    # <document><content/><footer>Here's the footer</footer></document>

Como un objeto `BeautifulSoup` no corresponde realmente con una etiqueta HTML o XML, no tiene nombre ni atributos. Aún así, es útil comprobar su `.name`, así que se le ha dado el `.name` especial «[document]»:

    soup.name
    # '[document]'

## Cadenas especiales

`Tag`, `NavigableString` y `BeautifulSoup` cubren la mayoría de todo lo que verás en un archivo HTML o XML, aunque aún quedan algunos remanentes. El principal que probablemente encuentres es el `Comment`.

class Comment

    markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
    soup = BeautifulSoup(markup, 'html.parser')
    comment = soup.b.string
    type(comment)
    # <class 'bs4.element.Comment'>

El objeto `Comment` es solo un tipo especial de `NavigableString`:

    comment
    # 'Hey, buddy. Want to buy a used parser'

Pero cuando aparece como parte de un documento HTML, un `Comment` se muestra con un formato especial:

    print(soup.b.prettify())
    # <b>
    #  <!--Hey, buddy. Want to buy a used parser?-->
    # </b>

### Para documentos HTML

Beautiful Soup define algunas subclases de `NavigableString` para contener cadenas de caracteres encontradas dentro de etiquetas HTML específicas. Esto hace más fácil tomar el cuerpo principal de la página, ignorando cadenas que probablemente representen directivas de programación encontradas dentro de la página. (Estas clases son nuevas en Beautiful Soup 4.9.0, y el analizador html5lib no las usa).

class Stylesheet

Una subclase de `NavigableString` que representa hojas de estilo CSS embebidas; esto es, cualquier cadena en una etiqueta `<style>` durante el análisis del documento.

class Script

Una subclase de `NavigableString` que representa JavaScript embebido; esto es, cualquier cadena en una etiqueta `<script>` durante el análisis del documento.

class Template

Una subclase de `NavigableString` que representa plantillas HTML embebidas; esto es, cualquier cadena en una etiqueta `<template>` durante el análisis del documento.

### Para documentos XML

Beautiful Soup define algunas clases `NavigableString` para contener tipos especiales de cadenas de caracteres que pueden ser encontradas en documentos XML. Como `Comment`, estas clases son subclases de `NavigableString` que añaden algo extra a la cadena de caracteres en la salida.

class Declaration

Una subclase de `NavigableString` que representa la declaración al principio de un documento XML.

class Doctype

Una subclase de `NavigableString` que representa la declaración del tipo de documento que puede encontrarse cerca del comienzo de un documento XML.

class CData

Una subclase de `NavigableString` que representa una sección CData.

class ProcessingInstruction

Una subclase de `NavigableString` que representa el contenido de una instrucción de procesamiento XML.

# Navegar por el árbol

Aquí está el documento HTML de las «Tres hermanas» de nuevo:

    html_doc = """
    <html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>

    <p class="story">Once upon a time there were three little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>

    <p class="story">...</p>
    """

    from bs4 import BeautifulSoup
    soup = BeautifulSoup(html_doc, 'html.parser')

Usaré este como ejemplo para enseñarte cómo mover una parte de un documento a otra.

## Bajar

Las etiquetas pueden contener cadenas u otras etiquetas. Estos elementos son los hijos (children) de la etiqueta. Beautiful Soup ofrece muchos atributos para navegar e iterar por los hijos de una etiqueta.

Debe notarse que las cadenas de Beautiful Soup no soportan ninguno de estos atributos, porque una cadena no puede tener hijos.

### Navegar usando nombres de etiquetas

La manera más simple de navegar por el árbol analizado es indicar el nombre de la etiqueta que quieres. Si quieres la etiqueta <head>, tan solo indica `soup.head`:

    soup.head
    # <head><title>The Dormouse's story</title></head>

    soup.title
    # <title>The Dormouse's story</title>

Puedes usar este truco una y otra vez para acercarte a una parte concreta del árbol analizado. Este código obtiene la primera etiqueta <b> dentro de la etiqueta <body>:

    soup.body.b
    # <b>The Dormouse's story</b>

Usar el nombre de la etiqueta como atributo te dará solo la primera etiqueta con ese nombre:

    soup.a
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

Si necesitas obtener todas las etiquetas <a>, o cualquier cosa más complicada que la primera etiqueta con cierto nombre, tendrás que usar uno de los métodos descritos en Buscar en el árbol, como `find_all()`:

    soup.find_all('a')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

### `.contents` y `.children`

Los hijos de una etiqueta están disponibles en una lista llamada `.contents`:

    head_tag = soup.head
    head_tag
    # <head><title>The Dormouse's story</title></head>

    head_tag.contents
    # [<title>The Dormouse's story</title>]

    title_tag = head_tag.contents[0]
    title_tag
    # <title>The Dormouse's story</title>
    title_tag.contents
    # ['The Dormouse's story']

El objeto `BeautifulSoup` por sí solo ya tiene hijos. En este caso, la etiqueta <html> is hija del objeto `BeautifulSoup`.:

    len(soup.contents)
    # 1
    soup.contents[0].name
    # 'html'

Una cadena no tiene `.contents`, porque no puede contener nada:

    text = title_tag.contents[0]
    text.contents
    # AttributeError: 'NavigableString' object has no attribute 'contents'

En lugar de obtenerlos como una lista, puedes iterar sobre los hijos de una etiqueta usando el generador `.children`:

    for child in title_tag.children:
        print(child)
    # The Dormouse's story

Si quieres modificar los hijos de una etiqueta, emplea los métodos descritos en Modificar el árbol. No modifiques la lista `.contents` directamente: eso podría ocasionar problemas que pueden ser sutiles y difíciles de detectar.

### `.descendants`

Los atributos `.contents` y `.children` tan solo consideran los hijos directos de una etiqueta. Por ejemplo, la etiqueta <head> tiene un único hijo directo—la etiqueta <title>:

    head_tag.contents
    # [<title>The Dormouse's story</title>]

Pero la etiqueta <title> tiene un hijo: la cadena «The Dormouse’s story». Puede dar la sensación de que esa cadena es también hija de la etiqueta <head>. El atributo `.descendants` te permite iterar sobre todos los hijos de una etiqueta recursivamente: sus hijos, hijos de sus hijos directos, y así sucesivamente:

    for child in head_tag.descendants:
        print(child)
    # <title>The Dormouse's story</title>
    # The Dormouse's story

La etiqueta <head> tiene un solo hijo, pero tiene dos descendientes: la etiqueta <title> y el hijo de la etiqueta <title>. El objeto `BeautifulSoup` tiene un hijo directo (la etiqueta <html>), pero tiene otros muchos descendientes:

    len(list(soup.children))
    # 1
    len(list(soup.descendants))
    # 26

### `.string`

Si una etiqueta tiene solo un hijo, y dicho hijo es un `NavigableString`, el hijo se obtiene mediante `.string`:

    title_tag.string
    # 'The Dormouse's story'

Si el único hijo de una etiqueta es otra etiqueta, y esa etiqueta tiene un `.string`, entonces se considera que la etiqueta madre tiene el mismo `.string` que su hijo:

    head_tag.contents
    # [<title>The Dormouse's story</title>]

    head_tag.string
    # 'The Dormouse's story'

Si una etiqueta contiene más una cadena, entonces no está claro a qué se debería referir `.string`, así que `.string` pasa a valer `None`:

    print(soup.html.string)
    # None

### `.strings` y `stripped_strings`

Si hay más de una cosa dentro de una etiqueta, puedes seguir obteniendo las cadenas. Usa el generador `.string`:

    for string in soup.strings:
        print(repr(string))
        '\n'
    # "The Dormouse's story"
    # '\n'
    # '\n'
    # "The Dormouse's story"
    # '\n'
    # 'Once upon a time there were three little sisters; and their names were\n'
    # 'Elsie'
    # ',\n'
    # 'Lacie'
    # ' and\n'
    # 'Tillie'
    # ';\nand they lived at the bottom of a well.'
    # '\n'
    # '...'
    # '\n'

Estas cadenas tienden a tener muchos espacios en blanco extra, los cuales puedes quitar usando el generador `.stripped_strings`:

    for string in soup.stripped_strings:
        print(repr(string))
    # "The Dormouse's story"
    # "The Dormouse's story"
    # 'Once upon a time there were three little sisters; and their names were'
    # 'Elsie'
    # ','
    # 'Lacie'
    # 'and'
    # 'Tillie'
    # ';\n and they lived at the bottom of a well.'
    # '...'

Aquí, las cadenas que consisten completamente en espacios en blanco se ignoran, y espacios en blanco al principio y final de las cadenas se eliminan.

## Subir

Continuando con la analogía del árbol genealógico, toda etiqueta tiene una madre: la etiqueta que la contiene.

### `.parent`

Puedes acceder a la madre de una etiqueta con el atributo `.parent`. En el ejemplo de «Las tres hermanas», la etiqueta <head> es la madre de la etiqueta <title>:

    title_tag = soup.title
    title_tag
    # <title>The Dormouse's story</title>
    title_tag.parent
    # <head><title>The Dormouse's story</title></head>

El texto de título tiene una madre: la etiqueta <title> que lo contiene:

    title_tag.string.parent
    # <title>The Dormouse's story</title>

La madre de una etiqueta de alto nivel como <html> es el objeto `BeautifulSoup` mismo:

    html_tag = soup.html
    type(html_tag.parent)
    # <class 'bs4.BeautifulSoup'>

Y el `.parent` de un objeto `BeautifulSoup` se define como `None`:

    print(soup.parent)
    # None

### `.parents`

Puedes iterar sobre todas las madres de los elementos con `.parents`. Este ejemplo usa `.parent` para moverse” de una etiqueta <a> en medio del documento a lo más alto del documento:

    link = soup.a
    link
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
    for parent in link.parents:
        print(parent.name)
    # p
    # body
    # html
    # [document]

### `.self_and_parents`

El generador `.self_and_parents` es una variante de `.parents` que te ofrece toda la ascendencia de un elemento, incluyendo el mismo elemento:

     for parent in link.parents:
        print(parent.name)
    # link
    # p
    # body
    # html
    # [document]

## Hacia los lados

Considera un documento sencillo como este:

    sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></a>", 'html.parser')
    print(sibling_soup.prettify())
    #   <a>
    #    <b>
    #     text1
    #    </b>
    #    <c>
    #     text2
    #    </c>
    #   </a>

Las etiquetas <b> y <c> están al mismo nivel: son hijas directas de la misma etiqueta. Las llamamos hermanas. Cuando un documento está bien formateado, las hermanas están al mismo nivel de sangría. Puedes usar también esta relación en el código que escribas.

### `.next_sibling` y `.previous_sibling`

Puedes usar `.next_sibling` y `.previous_sibling` para navegar entre elementos de la página que están al mismo nivel del árbol analizado:

    sibling_soup.b.next_sibling
    # <c>text2</c>

    sibling_soup.c.previous_sibling
    # <b>text1</b>

La etiqueta <b> tiene un `.next_sibling`, pero no `.previous_sibling`, porque no hay nada antes de la etiqueta <b> al mismo nivel del árbol. Por la misma razón, la etiqueta <c> tiene un `.previous_sibling` pero no un `.next_sibling`:

    print(sibling_soup.b.previous_sibling)
    # None
    print(sibling_soup.c.next_sibling)
    # None

Las cadenas «text1» y «text2» no son hermanas, porque no tienen la misma madre:

    sibling_soup.b.string
    # 'text1'

    print(sibling_soup.b.string.next_sibling)
    # None

En documentos reales, los `.next_sibling` o `.previous_sibling` de una etiqueta normalmente serán cadenas que contengan espacios en blanco. Retomando el documento de «Las tres hermanas»:

    # <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    # <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    # <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;

Podrías pensar que la `.next_sibling` de la primera etiqueta <a> podría ser la segunda etiqueta <a>. Pero realmente es una cadena de caracteres: la coma y el salto de línea que separan la primera etiqueta <a> de la segunda:

    link = soup.a
    link
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

    link.next_sibling
    # ',\n '

La segunda etiqueta <a> es realmente la `.next_sibling` de la coma:

    link.next_sibling.next_sibling
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>

### `.next_siblings` y `.previous_siblings`

Puedes iterar sobre las hermanas de una etiqueta con `.next_siblings` o `.previuos_siblings`:

    for sibling in soup.a.next_siblings:
        print(repr(sibling))
    # ',\n'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # ' and\n'
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
    # '; and they lived at the bottom of a well.'

    for sibling in soup.find(id="link3").previous_siblings:
        print(repr(sibling))
    # ' and\n'
    # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
    # ',\n'
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
    # 'Once upon a time there were three little sisters; and their names were\n'

## Hacia delante y hacia atrás

Échale un vistazo al comienzo del documento de «Las tres hermanas»:

    # <html><head><title>The Dormouse's story</title></head>
    # <p class="title"><b>The Dormouse's story</b></p>

Un analizador HTML toma esta cadena de caracteres y la convierte en una serie de eventos: «se abre una etiqueta <html>», «se abre una etiqueta <head>», «se abre una etiqueta <title>», «se añade una cadena», «se cierra la etiqueta <title>», «se abre una etiqueta <p>» y así sucesivamente. Beautiful Soup ofrece herramientas para reconstruir el análisis inicial del documento.

### `.next_element` y `.previous_element`

El atributo `.next_element` de una cadena o etiqueta apunta a cualquiera que fue analizado inmediatamente después. Podría ser igual que `.next_sibling`, pero normalmente es drásticamente diferente.

Aquí está la etiqueta final <a> en el documento de «Las tres hermanas». Su `..next_sibling` es una cadena: la terminación de la oración fue interrumpida por el comienzo de la etiqueta <a>.:

    last_a_tag = soup.find("a", id="link3")
    last_a_tag
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

    last_a_tag.next_sibling
    # ';\nand they lived at the bottom of a well.'

Pero el `.next_element` de esa etiqueta <a>, lo que fue analizado inmediatamente después de la etiqueta <a>, no es el resto de la oración: es la palabra «Tillie»:

    last_a_tag.next_element
    # 'Tillie'

Esto se debe a que en el marcado original, la palabra «Tillie» aparece antes del punto y coma. El analizador se encontró con una etiqueta <a>, después la palabra «Tillie», entonces la etiqueta de cierre </a>, después el punto y coma y el resto de la oración. El punto y coma está al mismo nivel que la etiqueta <a>, pero la palabra «Tillie» se encontró primera.

El atributo `.previous_element` es exactamente el opuesto de `.next_element`. Apunta a cualquier elemento que fue analizado inmediatamente antes que este:

    last_a_tag.previous_element
    # ' and\n'
    last_a_tag.previous_element.next_element
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

### `.next_elements` y `.previous_elements`

Ya te estarás haciendo a la idea. Puedes usar estos iteradores para moverte hacia delante y hacia atrás en el documento tal y como fue analizado:

    for element in last_a_tag.next_elements:
        print(repr(element))
    # 'Tillie'
    # ';\nand they lived at the bottom of a well.'
    # '\n'
    # <p class="story">...</p>
    # '...'
    # '\n'

# Buscar en el árbol

Beautiful Soup define una gran cantidad de métodos para buscar en el árbol analizado, pero todos son muy similares. Dedicaré mucho tiempo explicando los dos métodos más populares: `find()` y `find_all()`. Los otros métodos toman casi los mismos argumentos, así que los cubriré brevemente.

De nuevo, usaré el documento de «Las tres hermanas» como ejemplo:

    html_doc = """
    <html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>

    <p class="story">Once upon a time there were three little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>

    <p class="story">...</p>
    """

    from bs4 import BeautifulSoup
    soup = BeautifulSoup(html_doc, 'html.parser')

Empleando en un filtro un argumento como `find_all()`, puedes «acercar» aquellas partes del documento en las que estés interesado.

## Tipos de filtros

Antes de entrar en detalle sobre `find_all()` y métodos similares, me gustaría mostrar ejemplos de diferentes filtros que puedes utilizar en estos métodos. Estos filtros aparecen una y otra vez a lo largo de la API. Puedes usarlos para filtrar basándote en el nombre de una etiqueta, en sus atributos, en el texto de una cadena, o en alguna combinación de estos.

### Una cadena

El filtro más simple es una cadena. Pasa una cadena a un método de búsqueda y Beautiful Soup buscará un resultado para esa cadena exactamente. Este código encuentra todas las etiquetas <b> en el documento:

    soup.find_all('b')
    # [<b>The Dormouse's story</b>]

Si pasas un cadena de bytes, Beautiful Soup asumirá que la cadena está codificada como UTF-8. Puedes evitar esto pasando una cadena Unicode.

### Una expresión regular

Si pasas un objeto que sea una expresión regular, Beautiful Soup filtrará mediante dicho expresión regular usando si su método `search()`. Este código encuentra todas las etiquetas cuyo nombre empiece por la letra «b»; en este caso, las etiquetas <body> y <b>:

    import re
    for tag in soup.find_all(re.compile("^b")):
        print(tag.name)
    # body
    # b

Este código encuentra todas las etiquetas cuyo nombre contiene la letra “t”:

    for tag in soup.find_all(re.compile("t")):
        print(tag.name)
    # html
    # title

### `True`

El valor `True` empareja todo lo que pueda. Este código encuentra todas las etiquetas del documento, pero ninguna de las cadenas de texto:

    for tag in soup.find_all(True):
        print(tag.name)
    # html
    # head
    # title
    # body
    # p
    # b
    # p
    # a
    # a
    # a
    # p

### Una función

Si ninguna de las formas de búsqueda anteriores te sirven, define una función que tome un elemento como su único argumento. La función debería devolver `True` si el argumento se corresponde con lo indicado en la función, y `Falso` en cualquier otro caso.

Esta es una función que devuelve `True` si una etiqueta tiene definida el atributo «class» pero no el atributo «id»:

    def has_class_but_no_id(tag):
        return tag.has_attr('class') and not tag.has_attr('id')

Pasa esta función a `find_all()` y obtendrás todas las etiquetas <p>:

    soup.find_all(has_class_but_no_id)
    # [<p class="title"><b>The Dormouse's story</b></p>,
    #  <p class="story">Once upon a time there were…bottom of a well.</p>,
    #  <p class="story">...</p>]

Esta función solo devuelve las etiquetas <p>. No obtiene las etiquetas <a>, porque esas etiquetas definen ambas «class» y «id». No devuelve etiquetas como <html> y <title> porque dichas etiquetas no definen «class».

Si pasas una función para filtrar un atributo en específico como `href`, el argumento que se pasa a la función será el valor de dicho atributo, no toda la etiqueta. Esta es una función que encuentra todas las etiquetas <a> cuyo atributo `href` no empareja con una expresión regular:

    import re
    def not_lacie(href):
        return href and not re.compile("lacie").search(href)

    soup.find_all(href=not_lacie)
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

La función puede ser tan complicada como la necesites. Esta es una función que devuelve `True` si una etiqueta está rodeada por objetos string:

    from bs4 import NavigableString
    def surrounded_by_strings(tag):
        return (isinstance(tag.next_element, NavigableString)
                and isinstance(tag.previous_element, NavigableString))

    for tag in soup.find_all(surrounded_by_strings):
        print(tag.name)
    # body
    # p
    # a
    # a
    # a
    # p

    .. _a list:

### Una lista

Si pasas una lista, Beautiful Soup hará una búsqueda por cadenas con cualquier elemento en dicha lista. Este código encuentra todas las etiquetas <a> y todas las etiquetas <b>:

    soup.find_all(["a", "b"])
    # [<b>The Dormouse's story</b>,
    #  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Ahora ya estamos listos para entrar en detalle en los métodos de búsqueda.

## `find_all()`

Firma del método: find_all(name, attrs, recursive, string, limit, **kwargs)

El método `find_all()` busca por los descendientes de una etiqueta y obtiene todos aquellos que casan con tus filtros. He mostrado varios ejemplos en Tipos de filtros, pero aquí hay unos cuantos más:

    soup.find_all("title")
    # [<title>The Dormouse's story</title>]

    soup.find_all("p", "title")
    # [<p class="title"><b>The Dormouse's story</b></p>]

    soup.find_all("a")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.find_all(id="link2")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

    import re
    soup.find(string=re.compile("sisters"))
    # 'Once upon a time there were three little sisters; and their names were\n'

Algunos de estos deberían ser familiares, pero otros son nuevos. ¿Qué significa pasar un valor para `string`, o `id`? ¿Por qué `find_all("p", "title")` encuentra una etiqueta <p> con la clase CSS «title»? Echemos un vistazo a los argumentos de `find_all()`.

### El argumento `name`

Pasa un valor para `name` y notarás que Beautiful Soup solo considera etiquetas con ciertos nombres. Las cadenas de texto se ignorarán, como aquellas etiquetas cuyo nombre no emparejen.

Este es el uso más simple:

    soup.find_all("title")
    # [<title>The Dormouse's story</title>]

Recuerda de Tipos de filtros que el valor para `name` puede ser una cadena, una expresión regular, una lista, una función, o el valor True.

### El argumento palabras-clave

Cualquier argumento que no se reconozca se tomará como un filtro para alguno de los atributos de una etiqueta. Si pasas un valor para un argumento llamado `id`, Beautiful Soup filtrará el atributo “id” de cada una de las etiquetas:

    soup.find_all(id='link2')
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Si pasas un valor para `href`, Beautiful Soup filtrará el atributo `href` de cada uno de las etiquetas:

    soup.find_all(href=re.compile("elsie"))
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

Puedes filtrar un atributo basándote en una cadena, una expresión regular, una lista, una función, o el valor True.

Este código busca todas las etiquetas cuyo atributo `id` tiene un valor, sin importar qué valor es:

    soup.find_all(id=True)
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Puedes filtrar varios atributos al mismo tiempo pasando más de un argumento palabra-clave:

    soup.find_all(href=re.compile("elsie"), id='link1')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

Algunos atributos, como los atributos data-* en HTML5, tienen nombres que no pueden ser usados como nombres de argumentos palabra-clave:

    data_soup = BeautifulSoup('<div data-foo="value">foo!</div>', 'html.parser')
    data_soup.find_all(data-foo="value")
    # SyntaxError: keyword can't be an expression

Puedes usar estos atributos en búsquedas insertándolos en un diccionario y pasándolo a `find_all()` como el argumento `attrs`:

    data_soup.find_all(attrs={"data-foo": "value"})
    # [<div data-foo="value">foo!</div>]

No puedes usar un argumento palabra-clave para buscar por el nombre HTML de un elemento, porque BeautifulSoup usa el argumento `name` para guardar el nombre de la etiqueta. En lugar de esto, puedes darle valor a “name” en el argumento `attrs`:

    name_soup = BeautifulSoup('<input name="email"/>', 'html.parser')
    name_soup.find_all(name="email")
    # []
    name_soup.find_all(attrs={"name": "email"})
    # [<input name="email"/>]

### Buscando por clase CSS

Es muy útil para buscar una etiqueta que tenga una clase CSS específica, pero el nombre del atributo CSS, «class», es una palabra reservada de Python. Usar `class` como argumento ocasionaría un error sintáctico. Desde Beautiful Soup 4.1.2, se puede buscar por una clase CSS usando el argumento palabra-clave `class_`:

    soup.find_all("a", class_="sister")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Como con cualquier argumento palabra-clave, puede pasar una cadena de caracteres a `class_`, una expresión regular, una función, o `True`:

    soup.find_all(class_=re.compile("itl"))
    # [<p class="title"><b>The Dormouse's story</b></p>]

    def has_six_characters(css_class):
        return css_class is not None and len(css_class) == 6

    soup.find_all(class_=has_six_characters)
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Recuerda que una sola etiqueta puede tener varios valores para su atributo «class». Cuando se busca por una etiqueta que case una cierta clase CSS, se está intentando emparejar por cualquiera de sus clases CSS:

    css_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser')
    css_soup.find_all("p", class_="strikeout")
    # [<p class="body strikeout"></p>]

    css_soup.find_all("p", class_="body")
    # [<p class="body strikeout"></p>]

Puedes también buscar por la cadena de caracteres exacta del atributo `class`:

    css_soup.find_all("p", class_="body strikeout")
    # [<p class="body strikeout"></p>]

Pero buscar por variantes de la cadena de caracteres no funcionará:

    css_soup.find_all("p", class_="strikeout body")
    # []

Si quieres buscar por las etiquetas que casen dos o más clases CSS, deberías usar un selector CSS:

    css_soup.select("p.strikeout.body")
    # [<p class="body strikeout"></p>]

En versiones antiguas de Beautiful Soup, que no soportan el atajo `class_`, puedes usar el truco del `attrs` mencionado arriba. Crea un diccionario cuyo valor para «class» sea la cadena de caracteres (o expresión regular, o lo que sea) que quieras buscar:

    soup.find_all("a", attrs={"class": "sister"})
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Para buscar etiquetas que coincidan con dos o más clases CSS a la vez, usa el método de selección CSS `Tag.select()` descrito aquí:

    css_soup.select("p.strikeout.body")
    # [<p class="body strikeout"></p>]

### El argumento `string`

Con `string` puedes buscar por cadenas de caracteres en vez de etiquetas. Como con `name` y argumentos palabras-clave, puedes pasar una cadena, una expresión regular, una lista, una función, o el valor True. Aquí hay algunos ejemplos:

    soup.find_all(string="Elsie")
    # ['Elsie']

    soup.find_all(string=["Tillie", "Elsie", "Lacie"])
    # ['Elsie', 'Lacie', 'Tillie']

    soup.find_all(string=re.compile("Dormouse"))
    # ["The Dormouse's story", "The Dormouse's story"]

    def is_the_only_string_within_a_tag(s):
        """Return True if this string is the only child of its parent tag."""
        return (s == s.parent.string)

    soup.find_all(string=is_the_only_string_within_a_tag)
    # ["The Dormouse's story", "The Dormouse's story", 'Elsie', 'Lacie', 'Tillie', '...']

Aunque `string` es para encontrar cadenas, puedes combinarlo con argumentos que permitan buscar etiquetas: Beautiful Soup encontrará todas las etiquetas cuyo `.string` case con tu valor para `string`. Este código encuentra las etiquetas <a> cuyo `.string` es «Elsie»:

    soup.find_all("a", string="Elsie")
    # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

El argumento `string` es nuevo en Beautiful Soup 4.4.0. En versiones anteriores se llamaba `text`:

    soup.find_all("a", text="Elsie")
    # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

### El argumento``limit``

`find_all()` devuelve todas las etiquetas y cadenas que emparejan con tus filtros. Esto puede tardar un poco si el documento es grande. Si no necesitas todos los resultados, puedes pasar un número para `limit`. Esto funciona tal y como lo hace la palabra LIMIT en SQL. Indica a Beautiful Soup dejar de obtener resultados después de haber encontrado un cierto número.

Hay tres enlaces en el documento de «Las tres hermanas», pero este código tan solo obtiene los dos primeros:

    soup.find_all("a", limit=2)
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

### El argumento `recursive`

Si llamas a `mytag.find_all()`, Beautiful Soup examinará todos los descendientes de `mytag`: sus hijos, los hijos de sus hijos, y así sucesivamente. Si solo quieres que Beautiful Soup considere hijos directos, puedes pasar `recursive=False`. Observa las diferencias aquí:

    soup.html.find_all("title")
    # [<title>The Dormouse's story</title>]

    soup.html.find_all("title", recursive=False)
    # []

Aquí está esa parte del documento:

    <html>
     <head>
      <title>
       The Dormouse's story
      </title>
     </head>
    ...

La etiqueta <title> va después de la etiqueta <html>, pero no está directamente debajo de la etiqueta <html>: la etiqueta <head> está en medio de ambas. Beautiful Soup encuentra la etiqueta <title> cuando se permite observar todos los descendientes de la etiqueta <html>, pero cuando `recursive=False` restringe a los hijos directos de la etiqueta <html>, no se encuentra nada.

Beautiful Soup ofrece mucho métodos de análisis del árbol (descritos más adelante), y la mayoría toman los mismos argumentos que `find_all()`: `name`, `attrs`, `string`, `limit`, y los argumentos palabras-clave. Pero el argumento `recursive` es diferente: `find_all()` y `find()` son los únicos métodos que lo soportan. Pasar `recursive=False` en un método como `find_parents()` no sería muy útil.

## Llamar a una etiqueta es como llamar a `find_all()`

Como `find_all()` es el método más popular en la API de búsqueda de Beautiful Soup, puedes usar un atajo para usarlo. Si utilizas el objeto `BeautifulSoup` o un objeto `Tag` como si fuesen una función, entonces es lo mismo que llamar a `find_all()` en esos objetos. Estos dos líneas de código son equivalentes:

    soup.find_all("a")
    soup("a")

Estas dos líneas de código son también equivalentes:

    soup.title.find_all(string=True)
    soup.title(string=True)

## `find()`

Firma del método: find(name, attrs, recursive, string, **kwargs)

El método `find_all()` examina todo el documento buscando por resultados, pero a veces solo quieres encontrar un resultado. Si sabes que un documento solo tiene una etiqueta <body>, es una pérdida de tiempo examinar todo el documento buscando más emparejamientos. En lugar de pasar `limit=1` siempre que se llame a `find_all(), puedes usar el método ``find()`. Estas dos líneas de código son casi equivalentes:

    soup.find_all('title', limit=1)
    # [<title>The Dormouse's story</title>]

    soup.find('title')
    # <title>The Dormouse's story</title>

La única diferencia es que `find_all()` devuelve una lista conteniendo un resultado, y `find()` devuelve solo el resultado.

Si `find_all()` no encuentra nada, devuelve una lista vacía. Si `find()` no encuentra nada, devuelve `None`:

    print(soup.find("nosuchtag"))
    # None

¿Recuerdas el truco de `soup.head.title` de Navegar usando nombres de etiquetas? Ese truco funciona porque se llama repetidamente a `find()`:

    soup.head.title
    # <title>The Dormouse's story</title>

    soup.find("head").find("title")
    # <title>The Dormouse's story</title>

## `find_parents()` y `find_parent()`

Firma del método: find_parents(name, attrs, string, limit, **kwargs)

Firma del método: find_parent(name, attrs, string, **kwargs)

He pasado bastante tiempo cubriendo `find_all()` y `find()`. La API de Beautiful Soup define otros diez métodos para buscar por el árbol, pero no te asustes. Cinco de estos métodos son básicamente iguales a `find_all()`, y los otros cinco son básicamente iguales a `find()`. La única diferencia reside en qué partes del árbol buscan.

Primero consideremos `find_parents()` y `find_paren()`. Recuerda que `find_all()` y `find()` trabajan bajando por el árbol, examinando a los descendientes de una etiqueta. Estos métodos realizan lo contrario: trabajan subiendo por el árbol, buscando a las madres de las etiquetas (o cadenas). Probémoslos, empezando por una cadena de caracteres que esté bien enterrada en el documento de «Las tres hermanas»:

    a_string = soup.find(string="Lacie")
    a_string
    # 'Lacie'

    a_string.find_parents("a")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

    a_string.find_parent("p")
    # <p class="story">Once upon a time there were three little sisters; and their names were
    #  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
    #  and they lived at the bottom of a well.</p>

    a_string.find_parents("p", class_="title")
    # []

Una de la tres etiquetas <a> is la madre directa de la cadena en cuestión, así que nuestra búsqueda la encuentra. Una de las tres etiquetas <p> es una madre indirecta de la cadena, y nuestra búsqueda también la encuentra. Hay una etiqueta <p> con la clase CSS «title» en algún sitio del documento, pero no en ninguno de las madres de la cadena, así que no podemos encontrarla con `find_parents()`.

Puedes haber deducido la conexión entre `find_parent()` y `find_parents()`, y los atributos .parent y .parents mencionados anteriormente. La conexión es muy fuerte. Estos métodos de búsqueda realmente usan `.parents` para iterar sobre todas las madres, y comprobar cada una con el filtro provisto para ver si emparejan.

## `find_next_siblings()` y `find_next_sibling()`

Firma del método: find_next_siblings(name, attrs, string, limit, **kwargs)

Firma del método: find_next_sibling(name, attrs, string, **kwargs)

Estos métodos usan next_siblings para iterar sobre el resto de los hermanos de un elemento en el árbol. El método `find_next_siblings()` devuelve todos los hermanos que casen, y `find_next_sibling()` solo devuelve el primero de ellos:

    first_link = soup.a
    first_link
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

    first_link.find_next_siblings("a")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    first_story_paragraph = soup.find("p", "story")
    first_story_paragraph.find_next_sibling("p")
    # <p class="story">...</p>

## `find_previous_siblings()` y `find_previous_sibling()`

Firma del método: find_previous_siblings(name, attrs, string, limit, **kwargs)

Firma del método: find_previous_sibling(name, attrs, string, **kwargs)

Estos métodos emplean .previous_siblings para iterar sobre los hermanos de un elemento que les precede en el árbol. El método `find_previous_siblings()` devuelve todos los hermanos que emparejan, y `find_previous_sibling()` solo devuelve el primero de ellos:

    last_link = soup.find("a", id="link3")
    last_link
    # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

    last_link.find_previous_siblings("a")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

    first_story_paragraph = soup.find("p", "story")
    first_story_paragraph.find_previous_sibling("p")
    # <p class="title"><b>The Dormouse's story</b></p>

## `find_all_next()` y `find_next()`

Firma del método: find_all_next(name, attrs, string, limit, **kwargs)

Firma del método: find_next(name, attrs, string, **kwargs)

Estos métodos usan .next_elements para iterar sobre cualesquiera etiquetas y cadenas que vayan después de ella en el documento. El método `find_all_next()` devuelve todos los resultados, y `find_next()` solo devuelve el primero:

    first_link = soup.a
    first_link
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

    first_link.find_all_next(string=True)
    # ['Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
    #  ';\nand they lived at the bottom of a well.', '\n', '...', '\n']

    first_link.find_next("p")
    # <p class="story">...</p>

En el primer ejemplo, la cadena «Elsie» apareció, aunque estuviese contenida en la etiqueta <a> desde la que comenzamos. En el segundo ejemplo, la última etiqueta <p> en el documento apareció, aunque no esté en la misma parte del árbol que la etiqueta <a> desde la que comenzamos. Para estos métodos, todo lo que importa es que un elemento cumple con el filtro, y que aparezca en el documento después del elemento inicial.

## `find_all_previous()` y `find_previous()`

Firma del método: find_all_previous(name, attrs, string, limit, **kwargs)

Firma del método: find_previous(name, attrs, string, **kwargs)

Estos métodos usan .previous_elements para iterar sobre las etiquetas y cadenas que iban antes en el documento. El método `find_all_previous()` devuelve todos los resultados, y `find_previous()` solo devuelve el primero:

    first_link = soup.a
    first_link
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

    first_link.find_all_previous("p")
    # [<p class="story">Once upon a time there were three little sisters; ...</p>,
    #  <p class="title"><b>The Dormouse's story</b></p>]

    first_link.find_previous("title")
    # <title>The Dormouse's story</title>

La llamada a `find_all_previous("p")` encontró el primer párrafo en el documento (el que tiene la clase=»title»), pero también encuentra el segundo párrafo, la etiqueta <p> que contiene la etiqueta <a> con la que comenzamos. Esto no debería ser demasiado sorprendente: estamos buscando todas las etiquetas que aparecen en el documento después de la etiqueta con la que se comienza. Una etiqueta <p> que contiene una <a> debe aparecer antes de la etiqueta <a> que contiene.

## Selectores CSS mediante la propiedad `.css`

Los objetos `BeautifulSoup` y `Tag` soportan los selectores CSS a través de su atributo `.css`. El paquete Soup Sieve, disponible a través de PyPI como `soupsieve`, gestiona la implementación real del selector. Si instalaste Beautiful Soup mediante `pip`, Soup Sieve se instaló al mismo tiempo, así que no tienes que hacer nada adicional.

La documentación de Soup Sieve lista todos los selectores CSS soportados actualmente, pero estos son algunos de los básicos. Puedes encontrar etiquetas:

    soup.css.select("title")
    # [<title>The Dormouse's story</title>]

    soup.css.select("p:nth-of-type(3)")
    # [<p class="story">...</p>]

Encontrar etiquetas dentro de otras etiquetas:

    soup.css.select("body a")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.css.select("html head title")
    # [<title>The Dormouse's story</title>]

Encontrar etiquetas directamente después de otras etiquetas:

    soup.css.select("head > title")
    # [<title>The Dormouse's story</title>]

    soup.css.select("p > a")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.css.select("p > a:nth-of-type(2)")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

    soup.css.select("p > #link1")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

    soup.css.select("body > a")
    # []

Encontrar los hijos de etiquetas:

    soup.css.select("#link1 ~ .sister")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie"  id="link3">Tillie</a>]

    soup.css.select("#link1 + .sister")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Encontrar etiquetas por su clase CSS:

    soup.css.select(".sister")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.css.select("[class~=sister]")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Encontrar etiquetas por su ID:

    soup.css.select("#link1")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

    soup.css.select("a#link2")
    # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Encontrar etiquetas que casen con cualquier selector que estés en una lista de selectores:

    soup.css.select("#link1,#link2")
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Comprobar la existencia de un atributo:

    soup.css.select('a[href]')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Encontrar etiquetas por el valor de un atributo:

    soup.css.select('a[href="http://example.com/elsie"]')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

    soup.css.select('a[href^="http://example.com/"]')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.css.select('a[href$="tillie"]')
    # [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.css.select('a[href*=".com/el"]')
    # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

Hay también un método llamado `select_one()`, que encuentra solo la primera etiqueta que case con un selector:

    soup.css.select_one(".sister")
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

Por conveniencia, puedes llamar a `select()` y `select_one()` sobre el objeto `BeautifulSoup` o `Tag`, omitiendo la propiedad `.css`:

    soup.select('a[href$="tillie"]')
    # [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

    soup.select_one(".sister")
    # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

El soporte de selectores CSS es conveniente para personas que ya conocen la sintaxis de los selectores CSS. Puedes hacer todo esto con la API de Beautiful Soup. Si todo lo que necesitas son los selectores CSS, deberías saltarte Beautiful Soup y analizar el documento con `lxml`: es mucho más rápido. Pero Soup Sieve te permite combinar selectores CSS con la API de Beautiful Soup.

### Características avanzadas de Soup Sieve

Soup Sieve ofrece una API más amplia más allá de los métodos `select()` y `select_one()`, y puedes acceder a casi toda esa API a través del atributo `.css` de `Tag` o `Beautiful Soup`. Lo que sigue es solo una lista de los métodos soportados; ve a la documentación de Soup Sieve para la documentación completa.

El método `iselect()` funciona igualmente que `select()`, solo que devuelve un generador en vez de una lista:

    [tag['id'] for tag in soup.css.iselect(".sister")]
    # ['link1', 'link2', 'link3']

El método `closest()` devuelve la madre más cercana de una `Tag` dada que case con un selector CSS, similar al método `find_parent()` de Beautiful Soup:

    elsie = soup.css.select_one(".sister")
    elsie.css.closest("p.story")
    # <p class="story">Once upon a time there were three little sisters; and their names were
    #  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
    #  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
    #  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
    #  and they lived at the bottom of a well.</p>

El método `match()` devuelve un booleano dependiendo de si una `Tag` específica casa con un selector o no:

    # elsie.css.match("#link1")
    True

    # elsie.css.match("#link2")
    False

El método `filter()` devuelve un subconjunto de los hijos directos de una etiqueta que casen con un selector:

    [tag.string for tag in soup.find('p', 'story').css.filter('a')]
    # ['Elsie', 'Lacie', 'Tillie']

El método `escape()` formatea los identificadores CSS que de otra forma serían inválidos:

    soup.css.escape("1-strange-identifier")
    # '\\31 -strange-identifier'

### Espacios de nombres en selectores CSS

Si has analizado XML que define espacios de nombres, puedes usarlos en selectores CSS:

    from bs4 import BeautifulSoup
    xml = """<tag xmlns:ns1="http://namespace1/" xmlns:ns2="http://namespace2/">
     <ns1:child>I'm in namespace 1</ns1:child>
     <ns2:child>I'm in namespace 2</ns2:child>
    </tag> """
    namespace_soup = BeautifulSoup(xml, "xml")

    namespace_soup.css.select("child")
    # [<ns1:child>I'm in namespace 1</ns1:child>, <ns2:child>I'm in namespace 2</ns2:child>]

    namespace_soup.css.select("ns1|child")
    # [<ns1:child>I'm in namespace 1</ns1:child>]

Beautiful Soup intenta usar prefijos de espacios de nombres que tengan sentido basándose en lo que vio al analizar el documento, pero siempre puedes indicar tu propio diccionario de abreviaciones:

    namespaces = dict(first="http://namespace1/", second="http://namespace2/")
    namespace_soup.css.select("second|child", namespaces=namespaces)
    # [<ns1:child>I'm in namespace 2</ns1:child>]

### Historia del soporte de selectores CSS

La propiedad `.css` fue añadida en Beautiful Soup 4.12.0. Anterior a esta, solo los métodos convenientes `.select()` y `select_one()` se soportaban.

La integración de Soup Sieve fue añadida en Beautiful Soup 4.7.0. Versiones anteriores tenían el método `.select()`, pero solo los selectores CSS más comunes eran admitidos.

# Modificar el árbol

La mayor fortaleza de Beautiful Soup reside en buscar en el árbol analizado, pero puedes también modificar el árbol y escribir tus cambios como un nuevo documento HTML o XML.

## Cambiar nombres de etiquetas y atributos

Cubrí esto anteriormente, en `Tag.attrs`, pero vale la pena repetirlo. Puedes renombrar una etiqueta, cambiar el valor de sus atributos, añadir nuevos atributos, y eliminar atributos:

    soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
    tag = soup.b

    tag.name = "blockquote"
    tag['class'] = 'verybold'
    tag['id'] = 1
    tag
    # <blockquote class="verybold" id="1">Extremely bold</blockquote>

    del tag['class']
    del tag['id']
    tag
    # <blockquote>Extremely bold</blockquote>

## Modificar `.string`

Si quieres establecer el `.string` de una etiqueta a una nueva cadena de caracteres, los contenidos de la etiqueta se pueden reemplazar con esa cadena:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')

    tag = soup.a
    tag.string = "New link text."
    tag
    # <a href="http://example.com/">New link text.</a>

Ten cuidado: si una etiqueta contiene otras, ellas y todo su contenido serán destruidos.

## `append()`

Puedes añadir al contenido de una etiqueta con `Tag.append()`. Funciona como llamar a `.append()` en una lista de Python:

    soup = BeautifulSoup("<a>Foo</a>", 'html.parser')
    new_string = soup.a.append("Bar")

    soup
    # <a>FooBar</a>
    soup.a.contents
    # ['Foo', 'Bar']
    new_string
    # 'Bar'

`Tag.append()` devuelve el elemento recién añadido.

## `extend()`

Desde Beautiful Soup 4.7.0, `Tag` también soporta un método llamado `.extend()`, el cual añade todos los elementos de una lista a una `Tag`, en orden:

    soup = BeautifulSoup("<a>Soup</a>", 'html.parser')
    soup.a.extend(["'s", " ", "on"])

    soup
    # <a>Soup's on</a>
    soup.a.contents
    # ['Soup', ''s', ' ', 'on']

`Tag.extend()` devuelve la lista de elementos recién añadidos.

## `NavigableString()` y `.new_tag()`

Si necesitas añadir una cadena a un documento, sin problema—puedes pasar una cadena de Python a `append()`, o puedes llamar al constructor de `NavigableString`:

    from bs4 import NavigableString
    soup = BeautifulSoup("<b></b>", 'html.parser')
    tag = soup.b
    tag.append("Hello")
    new_string = NavigableString(" there")
    tag.append(new_string)
    tag
    # <b>Hello there.</b>
    tag.contents
    # ['Hello', ' there']

Si quieres crear un comentario o cualquier otra subclase de `NavigableString`, solo llama al constructor:

    from bs4 import Comment
    new_comment = Comment("Nice to see you.")
    tag.append(new_comment)
    tag
    # <b>Hello there<!--Nice to see you.--></b>
    tag.contents
    # ['Hello', ' there', 'Nice to see you.']

(Esto es una nueva característica en Beautiful Soup 4.4.0.)

¿Qué ocurre si necesitas crear una etiqueta totalmente nueva? La mejor solución es llamar al método de construcción (`factory method`) `BeautifulSoup.new_tag()`:

    soup = BeautifulSoup("<b></b>", 'html.parser')
    original_tag = soup.b

    new_tag = soup.new_tag("a", href="http://www.example.com")
    original_tag.append(new_tag)
    original_tag
    # <b><a href="http://www.example.com"></a></b>

    new_tag.string = "Link text."
    original_tag
    # <b><a href="http://www.example.com">Link text.</a></b>

Solo el primer argumento, el nombre de la etiqueta, es obligatorio.

(El argumento ``string`` en ``new_tag`` fue introducido en Beautiful Soup 4.13.0.)

Debido a que los métodos de inserción devuelven los elementos recién insertados, puedes crear, insertar, y obtener un elemento en un paso:

    soup = BeautifulSoup("<html></html>", 'html.parser')
    html = soup.html

    title = html.append(soup.new_tag("title"))

## `insert()`

`Tag.insert()` es justo como `Tag.append()`, excepto que el nuevo elemento no necesariamente va al final del `.contents` de su madre. Se insertará en la posición numérica que le hayas indicado, similar a `.insert()` en una lista de Python:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    tag = soup.a

    new_string = tag.insert(1, "but did not endorse ")
    tag
    # <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
    tag.contents
    # ['I linked to ', 'but did not endorse', <i>example.com</i>]
    new_string
    # 'but did not endorse '

Puedes pasar más de un elemento a `Tag.insert()`. Todos los elementos se insertarán, comenzando en la posición numérica que hayas pasado.

`Tag.insert()` devuelve la lista de elementos recién insertados.

## `insert_before()` y `insert_after()`

El método `insert_before()` inserta etiquetas o cadenas inmediatamente antes de algo en el árbol analizado:

    soup = BeautifulSoup("<b>leave</b>", 'html.parser')
    tag = soup.new_tag("i")
    tag.string = "Don't"
    soup.b.string.insert_before(tag)
    soup.b
    # <b><i>Don't</i>leave</b>

El método `insert_after()` inserta etiquetas o cadenas inmediatamente después de algo en el árbol analizado:

    div = soup.new_tag('div')
    div.string = 'ever'
    soup.b.i.insert_after(" you ", div)
    soup.b
    # <b><i>Don't</i> you <div>ever</div> leave</b>
    soup.b.contents
    # [<i>Don't</i>, ' you', <div>ever</div>, 'leave']

Ambos métodos devuelven la lista de los nuevos elementos insertados.

## `clear()`

`Tag.clear()` quita los contenidos de una etiqueta:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    tag = soup.a

    tag.clear()
    tag
    # <a href="http://example.com/"></a>

## `extract()`

`PageElement.extract()` elimina una etiqueta o una cadena de caracteres del árbol. Devuelve la etiqueta o la cadena que fue extraída:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    a_tag = soup.a

    i_tag = soup.i.extract()

    a_tag
    # <a href="http://example.com/">I linked to</a>

    i_tag
    # <i>example.com</i>

    print(i_tag.parent)
    # None

En este punto tienes realmente dos árboles analizados: uno anclado en el objeto `BeautifulSoup` que usaste para analizar el documento, y uno anclado en la etiqueta que fue extraída. Puedes llamar a `extract` en el hijo del elemento que extrajiste:

    my_string = i_tag.string.extract()
    my_string
    # 'example.com'

    print(my_string.parent)
    # None
    i_tag
    # <i></i>

## `decompose()`

`Tag.decompose()` quita una etiqueta del árbol, y luego lo destruye completamente y su contenido también:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    a_tag = soup.a
    i_tag = soup.i

    i_tag.decompose()
    a_tag
    # <a href="http://example.com/">I linked to</a>

El comportamiento de una `Tag` o `NavigableString` descompuesta no está definido y no deberías usarlo para nada. Si no estás seguro si algo ha sido descompuesto, puedes comprobar su propiedad `.decomposed` (nuevo en Beautiful Soup 4.9.0):

    i_tag.decomposed
    # True

    a_tag.decomposed
    # False

## `replace_with()`

`PageElement.replace_with()` elimina una etiqueta o cadena del árbol, y lo reemplaza con una o más etiquetas de tu elección:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    a_tag = soup.a

    new_tag = soup.new_tag("b")
    new_tag.string = "example.com"
    a_tag.i.replace_with(new_tag)

    a_tag
    # <a href="http://example.com/">I linked to <b>example.com</b></a>

    bold_tag = soup.new_tag("b")
    bold_tag.string = "example"
    i_tag = soup.new_tag("i")
    i_tag.string = "net"
    a_tag.b.replace_with(bold_tag, ".", i_tag)

    a_tag
    # <a href="http://example.com/">I linked to <b>example</b>.<i>net</i></a>

`replace_with()` devuelve la etiqueta o cadena que se reemplazó, así que puedes examinarla o añadirla de nuevo a otra parte del árbol.

La capacidad de pasar múltiples argumentos a replace_with() es nueva en Beautiful Soup 4.10.0.

## `wrap()`

`PageElement.wrap()` envuelve un elemento en la etiqueta que especificas. Devuelve la nueva envoltura:

    soup = BeautifulSoup("<p>I wish I was bold.</p>", 'html.parser')
    soup.p.string.wrap(soup.new_tag("b"))
    # <b>I wish I was bold.</b>

    soup.p.wrap(soup.new_tag("div"))
    # <div><p><b>I wish I was bold.</b></p></div>

Este método es nuevo en Beautiful Soup 4.0.5.

## `unwrap()`

`Tag.unwrap()` es el opuesto de `wrap()`. Reemplaza una etiqueta con lo que haya dentro de lo que haya en esa etiqueta. Es bueno para eliminar anotaciones:

    markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    a_tag = soup.a

    a_tag.i.unwrap()
    a_tag
    # <a href="http://example.com/">I linked to example.com</a>

Como `replace_with()`, `unwrap()` devuelve la etiqueta que fue reemplazada.

## `smooth()`

Tras llamar a un puñado de métodos que modifican el árbol analizado, puedes acabar con dos o más objetos `NavigableString` uno al lado del otro. Beautiful Soup no tiene ningún problema con esto, pero como no puede ocurrir en un documento recién analizado, puedes no esperar un comportamiento como el siguiente:

    soup = BeautifulSoup("<p>A one</p>", 'html.parser')
    soup.p.append(", a two")

    soup.p.contents
    # ['A one', ', a two']

    print(soup.p.encode())
    # b'<p>A one, a two</p>'

    print(soup.p.prettify())
    # <p>
    #  A one
    #  , a two
    # </p>

Puedes llamar a `Tag.smooth()` para limpiar el árbol analizado consolidando cadenas adyacentes:

    soup.smooth()

    soup.p.contents
    # ['A one, a two']

    print(soup.p.prettify())
    # <p>
    #  A one, a two
    # </p>

Este método es nuevo en Beautiful Soup 4.8.0.

# Salida

## Pretty-printing

El método `prettify()` convertirá un árbol analizado de Beautiful Soup en una cadena de caracteres Unicode bien formateado, con una línea para cada etiqueta y cada cadena:

    markup = '<html><head><body><a href="http://example.com/">I linked to <i>example.com</i></a>'
    soup = BeautifulSoup(markup, 'html.parser')
    soup.prettify()
    # '<html>\n <head>\n </head>\n <body>\n  <a href="http://example.com/">\n...'

    print(soup.prettify())
    # <html>
    #  <head>
    #  </head>
    #  <body>
    #   <a href="http://example.com/">
    #    I linked to
    #    <i>
    #     example.com
    #    </i>
    #   </a>
    #  </body>
    # </html>

Puedes llamar `prettify()` a alto nivel sobre el objeto `BeautifulSoup`, o sobre cualquiera de sus objetos `Tag`:

    print(soup.a.prettify())
    # <a href="http://example.com/">
    #  I linked to
    #  <i>
    #   example.com
    #  </i>
    # </a>

Como añade un espacio en blanco (en la forma de saltos de líneas), `prettify()` cambia el sentido del documento HTML y no debe ser usado para reformatearlo. El objetivo de `prettify()` es ayudarte a entender visualmente la estructura del documento en el que trabajas.

## Non-pretty printing

Si tan solo quieres una cadena, sin ningún formateo adornado, puedes llamar a `str()` en un objeto `BeautifulSoup`, o sobre una `Tag` dentro de él:

    str(soup)
    # '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

    str(soup.a)
    # '<a href="http://example.com/">I linked to <i>example.com</i></a>'

La función `str()` devuelve una cadena codificada en UTF-8. Mira Codificaciones para otras opciones.

Puedes también llamar a `encode()` para obtener un bytestring, y `decode()` para obtener Unicode.

## Formatos de salida

Si le das a Beautiful Soup un documento que contenga entidades HTML como «&lquot;», serán convertidas a caracteres Unicode:

    soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.", 'html.parser')
    str(soup)
    # '“Dammit!” he said.'

Si después conviertes el documento a bytestring, los caracteres Unicode serán convertidos a UTF-8. No obtendrás de nuevo las entidades HTML:

    soup.encode("utf8")
    # b'\xe2\x80\x9cDammit!\xe2\x80\x9d he said.'

Por defecto, los únicos caracteres que se formatean en la salida son ampersands y comillas anguladas simples. Estas se transforman en «&amp;», «&lt;» y «&gt;», así Beautiful Soup no genera inadvertidamente HTML o XML inválido:

    soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>", 'html.parser')
    soup.p
    # <p>The law firm of Dewey, Cheatem, &amp; Howe</p>

    soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
    soup.a
    # <a href="http://example.com/?foo=val1&amp;bar=val2">A link</a>

Puedes cambiar este comportamiento dando un valor al argumento `formatter` de `prettify()`, `encode()` o `decode()`. Beautiful Soup reconoce cinco posibles valores para `formatter`.

El valor por defecto es `formatter="minimal"`. Las cadenas solo serán procesadas lo suficiente como para asegurar que Beautiful Soup genera HTML/XML válido:

    french = "<p>Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;</p>"
    soup = BeautifulSoup(french, 'html.parser')
    print(soup.prettify(formatter="minimal"))
    # <p>
    #  Il a dit &lt;&lt;Sacré bleu!&gt;&gt;
    # </p>

Si pasas `formatter="html"`, Beautiful Soup convertirá caracteres Unicode a entidades HTML cuando sea posible:

    print(soup.prettify(formatter="html"))
    # <p>
    #  Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;
    # </p>

Si pasas `formatter="html5"`, es similar a `formatter="html"`, pero Beautiful Soup omitirá la barra de cierre en etiquetas HTML vacías como «br»:

    br = BeautifulSoup("<br>", 'html.parser').br

    print(br.encode(formatter="html"))
    # b'<br/>'

    print(br.encode(formatter="html5"))
    # b'<br>'

Además, cualquier atributo cuyos valores son la cadena de caracteres vacía se convertirán en atributos booleanos al estilo HTML:

    option = BeautifulSoup('<option selected=""></option>').option
    print(option.encode(formatter="html"))
    # b'<option selected=""></option>'

    print(option.encode(formatter="html5"))
    # b'<option selected></option>'

(Este comportamiento es nuevo a partir de Beautiful Soup 4.10.0.)

Si pasas `formatter=None`, Beautiful Soup no modificará en absoluto las cadenas a la salida. Esta es la opción más rápida, pero puede ocasionar que Beautiful Soup genere HTML/XML inválido, como en estos ejemplos:

    print(soup.prettify(formatter=None))
    # <p>
    #  Il a dit <<Sacré bleu!>>
    # </p>

    link_soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
    print(link_soup.a.encode(formatter=None))
    # b'<a href="http://example.com/?foo=val1&bar=val2">A link</a>'

### Objetos formatter

Si necesitas un control más sofisticado sobre tu salida, puedes instanciar uno de las clases formatters de Beautiful Soup y pasar dicho objeto a `formatter`.

class HTMLFormatter

Usado para personalizar las reglas de formato para documentos HTML.

Aquí está el formatter que convierte cadenas de caracteres a mayúsculas, como si están en un nodo de texto o en el valor de un atributo:

    from bs4.formatter import HTMLFormatter
    def uppercase(str):
        return str.upper()

    formatter = HTMLFormatter(uppercase)

    print(soup.prettify(formatter=formatter))
    # <p>
    #  IL A DIT <<SACRÉ BLEU!>>
    # </p>

    print(link_soup.a.prettify(formatter=formatter))
    # <a href="HTTP://EXAMPLE.COM/?FOO=VAL1&BAR=VAL2">
    #  A LINK
    # </a>

Este es el formatter que incrementa la sangría cuando se realiza pretty-printing:

    formatter = HTMLFormatter(indent=8)
    print(link_soup.a.prettify(formatter=formatter))
    # <a href="http://example.com/?foo=val1&bar=val2">
    #         A link
    # </a>

class XMLFormatter

Usado para personalizar las reglas de formateo para documentos XML.

### Escribir tu propio formatter

Crear una subclase a partir de `HTMLFormatter` p `XMLFormatter` te dará incluso más control sobre la salida. Por ejemplo, Beautiful Soup ordena por defecto los atributos en cada etiqueta:

    attr_soup = BeautifulSoup(b'<p z="1" m="2" a="3"></p>', 'html.parser')
    print(attr_soup.p.encode())
    # <p a="3" m="2" z="1"></p>

Para detener esto, puedes modificar en la subclase creada el método `Formatter.attributes()`, que controla los atributos que se ponen en la salida y en qué orden. Esta implementación también filtra el atributo llamado «m» cuando aparezca:

    class UnsortedAttributes(HTMLFormatter):
        def attributes(self, tag):
            for k, v in tag.attrs.items():
                if k == 'm':
                    continue
                yield k, v

    print(attr_soup.p.encode(formatter=UnsortedAttributes()))
    # <p z="1" a="3"></p>

Una última advertencia: si creas un objeto `CData`, el texto dentro de ese objeto siempre se muestra exactamente como aparece, sin ningún formato. Beautiful Soup llamará a la función de sustitución de entidad, por si hubieses escrito una función a medida que cuenta todas las cadenas en el documento o algo así, pero ignorará el valor de retorno:

    from bs4.element import CData
    soup = BeautifulSoup("<a></a>", 'html.parser')
    soup.a.string = CData("one < three")
    print(soup.a.prettify(formatter="html"))
    # <a>
    #  <![CDATA[one < three]]>
    # </a>

## `get_text()`

Si solo necesitas el texto legible dentro de un documento o etiqueta, puedes usar el método `get_text()`. Devuelve todo el texto dentro del documento o dentro de la etiqueta, como una sola cadena caracteres Unicode:

    markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
    soup = BeautifulSoup(markup, 'html.parser')

    soup.get_text()
    '\nI linked to example.com\n'
    soup.i.get_text()
    'example.com'

Puedes especificar una cadena que usará para unir los trozos de texto:

    # soup.get_text("|")
    '\nI linked to |example.com|\n'

Puedes indicar a Beautiful Soup que quite los espacios en blanco del comienzo y el final de cada trozo de texto:

    # soup.get_text("|", strip=True)
    'I linked to|example.com'

Pero en ese punto puedas querer usar mejor el generador .stripped_strings, y procesar el texto por tu cuenta:

    [text for text in soup.stripped_strings]
    # ['I linked to', 'example.com']

A partir de Beautiful Soup versión 4.9.0, cuando lxml o html.parser se usan, el contenido de las etiquetas <script>, <style>, y <template> no se consideran texto, ya que esas etiquetas no son parte de la parte legible del contenido de la página.

A partir de Beautiful Soup versión 4.10.0, puedes llamar a get_text(), .strings, o .stripped_strings en un objeto NavigableString. Devolverá el propio objeto, o nada, así que la única razón para hacerlo es cuando estás iterando sobre una lista mixta.

A partir de Beautiful Soup 4.13, puedes llamar a .string en un objeto NavigableString. Se devolverá a sí mismo, así que, de nuevo, la única razón para hacer esto es cuando estás iterando sobre una lista heterogénea.

# Especificar el analizador a usar

Si lo único que necesitas es analizar algún HTML, puedes ponerlo en el constructor de `BeautifulSoup`, y probablemente irá bien. Beautiful Soup elegirá un analizador por ti y analizará los datos. Pero hay algunos argumentos adicionales que puedes pasar al constructor para cambiar el analizador que se usa.

El primer argumento del constructor de `BeautifulSoup` es una cadena o un gestor de archivos abierto–el marcado que quieres analizar. El segundo argumento es cómo quieres que el marcado analizado.

Si no especificas nada, obtendrás el mejor analizador HTML que tengas instalado. Beautiful Soup clasifica al analizador de lxml como el mejor, después el de html5lib, y luego el analizador integrado en Python. Puedes sobrescribir esto especificando uno de los siguientes:

- El tipo de marcado que quieres analizar. Actualmente se soportan «html», «xml», y «html5».

- El nombre de la librería del analizador que quieras usar. Actualmente se soportan «lxml», «html5lib», y «html.parser» (el analizador HTML integrado de Python).

La sección Instalar un analizador contraste los analizadores admitidos.

Si no tienes un analizador apropiado instalado, Beautiful Soup ignorará tu petición y elegirá un analizador diferente. Ahora mismo, el único analizador XML es lxml. Si no tienes lxml instalado, solicitar un analizador XML no te dará uno, y pedir por «lxml» tampoco funcionará.

## Diferencias entre analizadores

Beautiful Soup presenta la misma interfaz que varios analizadores, pero cada uno es diferente. Analizadores diferentes crearán árboles analizados diferentes a partir del mismo documento. La mayores diferencias están entre los analizadores HTML y los XML. Este es un documento corto, analizado como HTML usando el analizador que viene con Python:

    BeautifulSoup("<a><b/></a>", "html.parser")
    # <a><b></b></a>

Como una sola etiqueta <b/> no es HTML válido, html.parser lo convierte a un par <b><b/>.

Aquí está el mismo documento analizado como XML (correr esto requiere que tengas instalado lxml). Debe notarse que la etiqueta independiente <b/> se deja sola, y que en el documento se incluye una declaración XML en lugar de introducirlo en una etiqueta <html>:

    print(BeautifulSoup("<a><b/></a>", "xml"))
    # <?xml version="1.0" encoding="utf-8"?>
    # <a><b/></a>

Hay también diferencias entre analizadores HTML. Si le das a Beautiful Soup un documento HTML perfectamente formado, esas diferencias no importan. Un analizador será más rápido que otro, pero todos te darán una estructura de datos que será exactamente como el documento HTML original.

Pero si el documento no está perfectamente formado, analizadores diferentes darán diferentes resultados. A continuación se presenta un documento corto e incorrecto analizado usando el analizador HTML de lxml. Debe considerarse que la etiqueta <a> es envuelta en las etiquetas <body> y <html>, y que la etiqueta colgada </p> simplemente se ignora:

    BeautifulSoup("<a></p>", "lxml")
    # <html><body><a></a></body></html>

Este es el mismo documento analizado usando html5lib:

    BeautifulSoup("<a></p>", "html5lib")
    # <html><head></head><body><a><p></p></a></body></html>

En lugar de ignorar la etiqueta colgada </p>, html5lib la empareja con una etiqueta inicial <p>. html5lib también añade una etiqueta <head> vacía; lxml no se molesta.

Este es el mismo documento analizado usando el analizador HTML integrado en Python:

    BeautifulSoup("<a></p>", "html.parser")
    # <a></a>

Como lxml, este analizador ignora la etiqueta clausura </p>. A diferencia de html5lib o lxml, este analizador no intenta crear un documento HTML bien formado añadiendo las etiquetas <html> o <body>.

Como el documento «<a></p>» es inválido, ninguna de estas técnicas es la forma “correcta” de gestionarlo. El analizador de html5lib usa técnicas que son parte del estándar de HTML5, así que es la que más se puede aproximar a ser la manera correcta, pero las tres técnicas son legítimas.

Las diferencias entre analizadores pueden afectar a tu script. Si estás planeando en distribuir tu script con otras personas, o ejecutarlo en varias máquinas, deberías especificar un analizador en el constructor de `BeautifulSoup`. Eso reducirá las probabilidad que tus usuarios analicen un documento diferentemente de la manera en la que tú lo analizas.

# Codificaciones

Cualquier documento HTML o XML está escrito en una codificación específica como ASCII o UTF-8. Pero cuando cargas ese documento en Beautiful Soup, descubrirás que se convierte en Unicode:

    markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
    soup = BeautifulSoup(markup, 'html.parser')
    soup.h1
    # <h1>Sacré bleu!</h1>
    soup.h1.string
    # 'Sacr\xe9 bleu!'

No es magia (seguro que eso sería genial). Beautiful Soup usa una sublibrería llamada Unicode, Dammit para detectar la codificación de un documento y convertirlo a Unicode. La codificación auto detectada está disponible con el atributo `.original_encoding` del objeto `Beautiful Soup`:

    soup.original_encoding
    # 'utf-8'

Si `.original_encoding` es `None`, significa que el documento ya se encontraba en formato Unicode cuando fue pasado a Beautiful Soup:

    markup = "<h1>Sacré bleu!</h1>"
    soup = BeautifulSoup(markup, 'html.parser')
    print(soup.original_encoding)
    # None

Unicode, Dammit estima correctamente la mayor parte del tiempo, pero a veces se equivoca. A veces estima correctamente, pero solo después de una búsqueda byte a byte del documento que tarda mucho tiempo. Si ocurre que sabes a priori la codificación del documento, puedes evitar errores y retrasos pasándola al constructor de `BeautifulSoup` con `from_encoding`.

Este es un documento escrito es ISO-8859-8. El documento es tan corto que Unicode, Dammit no da en el clave, y lo identifica erróneamente como ISO-8859-7:

    markup = b"<h1>\xed\xe5\xec\xf9</h1>"
    soup = BeautifulSoup(markup, 'html.parser')
    print(soup.h1)
    # <h1>νεμω</h1>
    print(soup.original_encoding)
    # iso-8859-7

Podemos arreglarlo pasándole el correcto a `from_encoding`:

    soup = BeautifulSoup(markup, 'html.parser', from_encoding="iso-8859-8")
    print(soup.h1)
    # <h1>םולש</h1>
    print(soup.original_encoding)
    # iso8859-8

Si no sabes cuál es la codificación correcta, pero sabes que Unicode, Dammit está suponiendo mal, puedes pasarle las opciones mal estimadas con `exclude_encodings`:

    soup = BeautifulSoup(markup, 'html.parser', exclude_encodings=["iso-8859-7"])
    print(soup.h1)
    # <h1>םולש</h1>
    print(soup.original_encoding)
    # WINDOWS-1255

Windows-1255 no es correcto al 100%, pero esa codificación es una superconjunto compatible con ISO-8859-8, así que se acerca lo suficiente. (`exlcude_encodings` es una nueva característica en Beautiful Soup 4.4.0).

En casos raros (normalmente cuando un documento UTF-8 contiene texto escrito en una codificación completamente diferente), la única manera para obtener Unicode es reemplazar algunos caracteres con el carácter Unicode especial «REPLACEMENT CHARACTER» (U+FFFD, �). Si Unicode, Dammit necesita hacer esto, establecerá el atributo `.contains_replacement_characters` a `True` en el objeto `UnicodeDammit` o `BeautifulSoup`. Esto te permite saber si la representación Unicode no es una representación exacta de la original—algún dato se ha perdido. Si un documento contiene �, pero `contains_replacement_characteres` es `False`, sabrás que � estaba allí originalmente (como lo está en este párrafo) y no implica datos perdidos.

## Codificación de salida

Cuando escribas completamente un documento desde Beautiful Soup, obtienes un documento UTF-8, incluso cuando el documento no está en UTF-8 por el que empezar. Este es un documento escrito con la codificación Latin-1:

    markup = b'''
     <html>
      <head>
       <meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
      </head>
      <body>
       <p>Sacr\xe9 bleu!</p>
      </body>
     </html>
    '''

    soup = BeautifulSoup(markup, 'html.parser')
    print(soup.prettify())
    # <html>
    #  <head>
    #   <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
    #  </head>
    #  <body>
    #   <p>
    #    Sacré bleu!
    #   </p>
    #  </body>
    # </html>

Fíjate bien que la etiqueta <meta> ha sido reescrita para reflejar el hecho de que el documento está ahora en UTF-8.

Si no quieres UTF-8, puedes pasar una codificación a `prettify()`:

    print(soup.prettify("latin-1"))
    # <html>
    #  <head>
    #   <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
    # ...

También puedes llamar a encode() sobre el objeto `BeautifulSoup`, o cualquier elemento en el objeto, como si fuese una cadena de Python:

    soup.p.encode("latin-1")
    # b'<p>Sacr\xe9 bleu!</p>'

    soup.p.encode("utf-8")
    # b'<p>Sacr\xc3\xa9 bleu!</p>'

Cualesquiera caracteres que no puedan ser representados en la codificación que has elegido se convierten en referencias a entidades numéricas XML. Este es un documento que incluye el carácter Unicode SNOWMAN:

    markup = u"<b>\N{SNOWMAN}</b>"
    snowman_soup = BeautifulSoup(markup, 'html.parser')
    tag = snowman_soup.b

El carácter SNOWMAN puede ser parte de un documento UTF-8 (se parece a ☃), pero no hay representación para ese carácter en ISO-Latin-1 o ASCII, así que se convierte en «&#9731» para esas codificaciones:

    print(tag.encode("utf-8"))
    # b'<b>\xe2\x98\x83</b>'

    print(tag.encode("latin-1"))
    # b'<b>&#9731;</b>'

    print(tag.encode("ascii"))
    # b'<b>&#9731;</b>'

## Unicode, Dammit

Puedes usar Unicode, Dammit sin usar Beautiful Soup. Es útil cuando tienes datos en una codificación desconocida y solo quieres convertirlo a Unicode:

    from bs4 import UnicodeDammit
    dammit = UnicodeDammit(b"\xc2\xabSacr\xc3\xa9 bleu!\xc2\xbb")
    print(dammit.unicode_markup)
    # «Sacré bleu!»
    dammit.original_encoding
    # 'utf-8'

Los estimaciones de Unicode, Dammit será mucho más precisas si instalas una de estas librerías de Python: `charset-normalizer`, `chardet`, o `cchardet`. Cuanto más datos le des a Unicode, Dammit, con mayor exactitud estimará. Si tienes alguna sospecha sobre las codificaciones que podrían ser, puedes pasárselas en una lista:

    dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
    print(dammit.unicode_markup)
    # Sacré bleu!
    dammit.original_encoding
    # 'latin-1'

Unicode, Dammit tiene dos características especiales que Beautiful Soup no usa.

### Comillas inteligentes

Puedes usar Unicode, Dammit para convertir las comillas inteligentes de Microsoft a entidades HTML o XML:

    markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"

    UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
    # '<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'

    UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
    # '<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

Puedes también convertir las comillas inteligentes de Microsoft a comillas ASCII:

    UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
    # '<p>I just "love" Microsoft Word\'s smart quotes</p>'

Con suerte encontrarás esta característica útil, pero Beautiful Soup no la usa. Beautiful Soup prefiere el comportamiento por defecto, el cual es convertir las comillas inteligentes de Microsoft a caracteres Unicode junto al resto de cosas:

    UnicodeDammit(markup, ["windows-1252"]).unicode_markup
    # '<p>I just “love” Microsoft Word’s smart quotes</p>'

### Codificaciones inconsistentes

A veces un documento está mayoritariamente en UTF-8, pero contiene caracteres Windows-1252 como (de nuevo) comillas inteligentes de Microsoft. Esto puede ocurrir cuando un sitio web incluye datos de múltiples fuentes. Puedes usar `UnicodeDammit.detwingle()` para convertir dicho documento en puro UTF-8. Este un ejemplo sencillo:

    snowmen = (u"\N{SNOWMAN}" * 3)
    quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
    doc = snowmen.encode("utf8") + quote.encode("windows_1252")

Este documento es un desastre. Los muñecos de nieve están en UTF-8 y las comillas están en Windows-1252. Puedes mostrar los muñecos de nieve o las comillas, pero no ambos:

    print(doc)
    # ☃☃☃�I like snowmen!�

    print(doc.decode("windows-1252"))
    # â˜ƒâ˜ƒâ˜ƒ“I like snowmen!”

Decodificar el documento en UTF-8 provoca un `UnicodeDecodeError`, y decodificarlo como Windows-1252 te da un galimatías. Afortunadamente, `UnicodeDammit.detwingle()` convertirá la cadena en puro UTF-8, permitiéndote decodificarlo en Unicode y mostrar el muñeco de nieve y marcas de comillas simultáneamente:

    new_doc = UnicodeDammit.detwingle(doc)
    print(new_doc.decode("utf8"))
    # ☃☃☃“I like snowmen!”

`UnicodeDammit.detwingle()` solo sabe cómo gestionar Windows-1252 embebido en UTF-8 (o viceversa, supongo), pero este es el caso más común.

Fíjate que debes saber que debes llamar a `UnicodeDammit.detwingle()` en tus datos antes de pasarlo a `BeautifulSoup` o el constructor de `UnicodeDammit`. Beautiful Soup asume que un documento tiene una sola codificación, la que sea. Si quieres pasar un documento que contiene ambas UTF-8 y Windows-1252, es probable que piense que todo el documento es Windows-1252, y el documento se parecerá a `â˜ƒâ˜ƒâ˜ƒ“I like snowmen!”`.

`UnicodeDammit.detwingle()` es nuevo en Beautiful Soup 4.1.0.

# Números de línea

Los analizadores de `html.parser` y `html5lib` pueden llevar la cuenta de los lugares en el documento original donde se han encontrado cada etiqueta. Puedes acceder a esta información con `Tag.sourceline` (número de línea) y `Tag.sourcepos` (posición del comienzo de una etiqueta en una línea):

    markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
    soup = BeautifulSoup(markup, 'html.parser')
    for tag in soup.find_all('p'):
        print(repr((tag.sourceline, tag.sourcepos, tag.string)))
    # (1, 0, 'Paragraph 1')
    # (3, 4, 'Paragraph 2')

Debe destacarse que los dos analizadores entienden cosas ligeramente diferentes por `sourceline` y `sourcepos`. Para html.parser, estos números representan la posición del signo «menor» inicial. Para html5lib, estos números representan la posición del signo «mayor» final:

    soup = BeautifulSoup(markup, 'html5lib')
    for tag in soup.find_all('p'):
        print(repr((tag.sourceline, tag.sourcepos, tag.string)))
    # (2, 0, 'Paragraph 1')
    # (3, 6, 'Paragraph 2')

Puedes interrumpir esta característica pasado `store_line_numbers=False` en el constructor de `BeautifulSoup`:

    markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
    soup = BeautifulSoup(markup, 'html.parser', store_line_numbers=False)
    print(soup.p.sourceline)
    # None

Esta característica es nueva en 4.8.1, y los analizadores basados en lxml no la soportan.

# Comparar objetos por igualdad

Beautiful Soup indica que dos objetos `NavigableString` o `Tag` son iguales cuando representan al mismo marcado HTML o XML. En este ejemplo, las dos etiquetas <b> son tratadas como iguales, aunque están en diferentes partes del objeto árbol, porque ambas son «<b>pizza</b>»:

    markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
    soup = BeautifulSoup(markup, 'html.parser')
    first_b, second_b = soup.find_all('b')
    print(first_b == second_b)
    # True

    print(first_b.previous_element == second_b.previous_element)
    # False

Si quieres saber si dos variables se refieren a exactamente el mismo objeto, usa `is`:

    print(first_b is second_b)
    # False

# Copiar objetos de Beautiful Soup

Puedes usar `copy.copy()` para crear una copia de cualquier `Tag` o `NavigableString`:

    import copy
    p_copy = copy.copy(soup.p)
    print(p_copy)
    # <p>I want <b>pizza</b> and more <b>pizza</b>!</p>

La copia se considera igual que la original, ya que representa el mismo marcado que el original, pero no son el mismo objeto:

    print(soup.p == p_copy)
    # True

    print(soup.p is p_copy)
    # False

La única diferencia real es que la copia está completamente desconectada del objeto árbol de Beautiful Soup, como si `extract()` hubiese sido llamada sobre ella. Esto se debe a que dos objetos `Tag` no pueden ocupar el mismo espacio simultáneamente.

>     print(p_copy.parent)
>     # None

Puedes usar `Tag.copy_self()` para crear una copia de un `Tag` sin copiar sus contenidos.

>     original = BeautifulSoup('<a id="a_tag" class="link">the <i>link</i></a>', 'html.parser')
>     print(original.a)
>     # <a class="link" id="a_tag">the <i>link</a>
>     print(original.a.copy_self())
>     # <a class="link" id="a_tag"></a>

(Tag.copy_self() fue introducido en Beautiful Soup 4.13.0.)

# Interfaz de búsqueda a bajo nivel

Casi todo el mundo que usa Beautiful Soup para extraer información de un documento puede conseguir lo que necesita usando los métodos descritos en Buscar en el árbol. Sin embargo, hay una interfaz de búsqueda a bajo nivel que te permite definir cualquier comportamiento de coincidencia. Entre bastidores, las partes de Beautiful Soup que la mayoría de personas usan– `find_all()` y similares—están usando realmente este interfaz a bajo nivel, y tú puedes usarla directamente.

(Acceso a la interfaz de búsqueda a bajo nivel es una nueva funcionalidad incluida en Beautiful Soup 4.13.0.)

## Elemento de filtrado personalizado

La clase `ElementFilter` es tu punto de entrada a la interfaz a bajo nivel. Para usarla, define una función que tome como argumento un objeto `PageElement` (que puede ser un `Tag` o un `NavigableString`). La función debe devolver `True` si el elemento cumple tu propio criterio, y `False` si no lo hace.

Esta función de ejemplo busca etiquetas y cadenas de caracteres con contenido, pero se salta cadenas con solo espacios en blanco:

    from bs4 import Tag, NavigableString
    def non_whitespace_element_func(tag_or_string):
       """
       return True for:
       * all Tag objects
       * NavigableString objects that contain non-whitespace text
       """
       return (
           isinstance(tag_or_string, Tag) or
           (isinstance(tag_or_string, NavigableString) and
               tag_or_string.strip() != ""))

Una vez que tienes la función, pásasela al constructor de `ElementFilter`:

    from bs4.filter import ElementFilter
    non_whitespace_filter = ElementFilter(non_whitespace_element_func)

Puedes usar así este objeto `ElementFilter` como primer argumento de cualquiera de los métodos en Buscar en el árbol. Cualquier criterio que hayas definido en tu función será usado en lugar de la lógica de coincidencia por defecto de Beautiful Soup:

    from bs4 import BeautifulSoup
    small_doc = """
    <p>
     <b>bold</b>
     <i>italic</i>
     and
     <u>underline</u>
    </p>
    """
    soup = BeautifulSoup(small_doc, 'html.parser')

    soup.find('p').find_all(non_whitespace_filter, recursive=False)
    # [<b>bold</b>, <i>italic</i>, '\n  and\n  ', <u>underline</u>]

    soup.find("b").find_next(non_whitespace_filter)
    # 'bold'

    soup.find("i").find_next_siblings(non_whitespace_filter)
    # ['\n  and\n  ', <u>underline</u>]

Todas las coincidencias potenciales pasarán por tu función, y los únicos objetos `PageElement` devueltos serán aquellos para los que tu función ha devuelto `True`.

Para resumir el comportamiento de coincidencia basada en funciones,

- Una función pasada como primer argumento a un método de búsqueda (o equivalentemente, usando el argumento `name`) considera solo objetos `Tag`.

- Una función pasada a un método de búsqueda usando el argumento `string` considera solo objetos `NavigableString`.

- Una función pasada a un método de búsqueda usando un objeto `ElementFilter` considera objetos tanto `Tag` como `NavigableString`.

### Iteración sobre elementos personalizada

ElementFilter.filter()

Al pasar una instancia de `ElementFilter` a uno de los métodos de búsqueda en el árbol de Beautiful Soup, puedes personalizar completamente lo que Beautiful Soup entiende por coincidencia en un elemento mientras itera sobre el árbol analizado. Al usar el método `ElementFilter.filter()` puedes también personalizar completamente lo que Beautiful Soup entiende por iterar sobre el árbol analizado.

El método `ElementFilter.filter()` toma un generador que devuelve un flujo de objetos `PageElement`. No hay restricción sobre los objetos `PageElement` que aparecen, ni las veces que aparecen, ni en qué orden. Teóricamente, no necesitan siquiera ser del mismo documento `BeautifulSoup`. Puedes cualquier cosa que tenga sentido para ti.

Aquí hay un ejemplo tonto: un generador que recorre aleatoriamente hacia adelante y hacia atrás el árbol analizado:

    import random
    def random_walk(starting_location):
        location = starting_location
        while location is not None:
            yield location
            if random.random() < 0.5:
                location = location.next_element
            else:
                location = location.previous_element
            if location is None:
                return

Pasa este generador al `ElementFilter.filter()` de ejemplo y Beautiful Soup deambulará aleatoriamente por el árbol analizado, aplicando la función `non_whitespace_filter` a cada elemento que encuentra, y devolviendo todas las coincidencias—potencialmente devolviendo un objeto más de una vez:

    [x for x in non_whitespace_filter.filter(random_walk(soup.b))]
    # [<b>bold</b>, 'bold', <b>bold</b>, <p><b>bold</b>...]

    [x for x in non_whitespace_filter.filter(random_walk(soup.b))]
    # [<b>bold</b>, <b>bold</b>, 'bold', <i>italic</i>, <i>italic</i>, ...]

(Ten en cuenta que, a diferencia de otros ejemplos de código en esta documentación, este ejemplo puede dar resultados diferentes cada vez que lo ejecutes debido al recorrido aleatorio. Es muy improbable, pero esta función podría deambular por el árbol analizado para siempre y nunca acabar.)

# Personalización avanzada del analizador

Beautiful Soup ofrece numerosas vías para personalizar la manera en la que el analizador trata HTML o XML entrante. Esta sección cubre las técnicas de personalizadas usadas más comúnmente.

## Analizar solo parte del documento

Digamos que quieres usar Beautiful Soup para observar las etiquetas <a> de un documento. Es un malgasto de tiempo y memoria analizar todo el documento y después recorrerlo una y otra vez buscando etiquetas <a>. Sería mucho más rápido ignorar todo lo que no sea una etiqueta <a> desde el principio. La clase `SoupStrainer` te permite elegir qué partes de un documento entrante se analizan. Tan solo crea un `SoupStrainer` y pásalo al constructor de `BeautifulSoup` en el argumento `parse_only`.

(Ten en cuenta que esta característica no funcionará si estás usando el analizador de html5lib. Si usas html5lib, todo el documento será analizado, no importa el resto. Esto es porque html5lib constantemente reorganiza el árbol analizado conforme trabaja, y si alguna parte del documento no consigue introducirse en el árbol analizado, se quedará colgado. Para evitar confusión en los ejemplos más abajo forzaré a Beautiful Soup a que use el analizador integrado de Python).

class SoupStrainer

La clase `SoupStrainer` toma los mismos argumentos que un típico método de Buscar en el árbol: name, attrs, string, y **kwargs. Estos son tres objetos `SoupStrainer`:

    from bs4 import SoupStrainer

    only_a_tags = SoupStrainer("a")

    only_tags_with_id_link2 = SoupStrainer(id="link2")

    def is_short_string(string):
        return string is not None and len(string) < 10

    only_short_strings = SoupStrainer(string=is_short_string)

Voy a traer de nuevo el documento de «Las tres hermanas» una vez más, y veremos cómo parece el documento cuando es analizado con estos tres objetos `SoupStrainer`:

    html_doc = """<html><head><title>The Dormouse's story</title></head>
    <body>
    <p class="title"><b>The Dormouse's story</b></p>

    <p class="story">Once upon a time there were three little sisters; and their names were
    <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
    <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
    <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
    and they lived at the bottom of a well.</p>

    <p class="story">...</p>
    """

    print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
    # <a class="sister" href="http://example.com/elsie" id="link1">
    #  Elsie
    # </a>
    # <a class="sister" href="http://example.com/lacie" id="link2">
    #  Lacie
    # </a>
    # <a class="sister" href="http://example.com/tillie" id="link3">
    #  Tillie
    # </a>

    print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
    # <a class="sister" href="http://example.com/lacie" id="link2">
    #  Lacie
    # </a>

    print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
    # Elsie
    # ,
    # Lacie
    # and
    # Tillie
    # ...
    #

El comportamiento de `SoupStrainer` es como sigue:

- Cuando una etiqueta coincide, se guarda (incluyendo todos sus contenidos, tanto si coinciden también como si no).

- Cuando una etiqueta no coincide, la etiqueta propia no se guarda, pero el análisis continúa entre sus contenidos para buscar otras etiquetas que sí coincidan.

## Personalizar atributos multivaluados

En un documento HTML, a un atributo como `class` se le da una lista de valores, y a un atributo como `id` se le da un solo valor, porque la especificación de HTML trata a esos atributos de manera diferente:

    markup = '<a class="cls1 cls2" id="id1 id2">'
    soup = BeautifulSoup(markup, 'html.parser')
    soup.a['class']
    # ['cls1', 'cls2']
    soup.a['id']
    # 'id1 id2'

Puedes interrumpir esto pasando `multi_values_attributes=None`. Entonces a todos los atributos se les dará un solo valor:

    soup = BeautifulSoup(markup, 'html.parser', multi_valued_attributes=None)
    soup.a['class']
    # 'cls1 cls2'
    soup.a['id']
    # 'id1 id2'

Puedes personalizar este comportamiento un poco pasando un diccionario a `multi_values_attributes`. Si lo necesitas, échale un vistazo a `HTMLTreeBuilder.DEFAULT_CDATA_LIST_ATTRIBUTES` para ver la configuración que Beautiful Soup usa por defecto, que está basada en la especificación HTML.

(Esto es una nueva característica en Beautiful Soup 4.8.0).

## Gestionar atributos duplicados

Cuando se use el analizador de `html.parser`, puedes usar el argumento del constructor `on_duplicate_attribute` para personalizar qué hace Beautiful Soup cuando encuentra una etiqueta que define el mismo atributo más de una vez:

    markup = '<a href="http://url1/" href="http://url2/">'

El comportamiento por defecto es usar el último valor encontrado en la etiqueta:

    soup = BeautifulSoup(markup, 'html.parser')
    soup.a['href']
    # http://url2/

    soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='replace')
    soup.a['href']
    # http://url2/

Con `on_duplicate_attribute='ignore'` puedes indicar a Beautiful Soup que use el primer valor encontrado e ignorar el resto:

    soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='ignore')
    soup.a['href']
    # http://url1/

(lxml y html5lib siempre lo hacen así; su comportamiento no puede ser configurado desde Beautiful Soup.)

Si necesitas más, puedes pasar una función que sea llamada en cada valor duplicado:

    def accumulate(attributes_so_far, key, value):
        if not isinstance(attributes_so_far[key], list):
            attributes_so_far[key] = [attributes_so_far[key]]
        attributes_so_far[key].append(value)

    soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute=accumulate)
    soup.a['href']
    # ["http://url1/", "http://url2/"]

(Esto es una nueva característica en Beautiful Soup 4.9.1.)

## Instanciar subclases personalizadas

Cuando un analizador indica a Beautiful Soup sobre una etiqueta o una cadena, Beautiful Soup instanciará un objeto `Tag` o `NavigableString` para contener esa información. En lugar de ese comportamiento por defecto, puedes indicar a Beautiful Soup que instancia subclases de `Tag` o `NavigableString`, subclases que defines con comportamiento personalizado:

    from bs4 import Tag, NavigableString
    class MyTag(Tag):
        pass

    class MyString(NavigableString):
        pass

    markup = "<div>some text</div>"
    soup = BeautifulSoup(markup, 'html.parser')
    isinstance(soup.div, MyTag)
    # False
    isinstance(soup.div.string, MyString)
    # False

    my_classes = { Tag: MyTag, NavigableString: MyString }
    soup = BeautifulSoup(markup, 'html.parser', element_classes=my_classes)
    isinstance(soup.div, MyTag)
    # True
    isinstance(soup.div.string, MyString)
    # True

Esto puede ser útil cuando se incorpore Beautiful Soup en un framework de pruebas.

(Esto es una nueva característica de Beautiful Soup 4.8.1.)

# Resolución de problemas

## `diagnose()`

Si estás teniendo problemas para entender qué hace Beautiful Soup a un documento, pasa el documento a la función `diagnose()`. (Nuevo en Beautiful Soup 4.2.0) Beautiful Soup imprimirá un informe mostrándote cómo manejan el documento diferentes analizadores, y te dirán si te falta un analizador que Beautiful Soup podría estar usando:

    from bs4.diagnose import diagnose
    with open("bad.html") as fp:
        data = fp.read()

    diagnose(data)

    # Diagnostic running on Beautiful Soup 4.2.0
    # Python version 2.7.3 (default, Aug  1 2012, 05:16:07)
    # I noticed that html5lib is not installed. Installing it may help.
    # Found lxml version 2.3.2.0
    #
    # Trying to parse your data with html.parser
    # Here's what html.parser did with the document:
    # ...

Tan solo mirando a la salida de diagnose() puede mostrarte cómo resolver el problema. Incluso si no, puedes pegar la salida de `diagnose()` cuando pidas ayuda.

## Errores analizando un documento

Hay dos tipos diferentes de errores de análisis. Hay veces en que se queda colgado, donde le das a Beautiful Soup un documento y lanza una excepción, normalmente un `HTMLParser.HTMLParseError`. Y hay comportamientos inesperados, donde un árbol analizado de Beautiful Soup parece muy diferente al documento usado para crearlo.

Casi ninguno de estos problemas resultan ser problemas con Beautiful Soup. Esto no es porque Beautiful Soup sea una increíble y bien escrita pieza de software. Es porque Beautiful Soup no incluye ningún código de análisis. En lugar de eso, depende de análisis externos. Si un analizador no está funcionando en un documento concreto, la mejor solución es probar con otro analizador. Échale un vistazo a Instalar un analizador para más detalles y una comparativa entre analizadores. Si esto no es de ayuda, tal vez tengas que inspeccionar el árbol del documento que se encuentra dentro del objeto `BeautifulSoup` para ver dónde se encuentra realmente el marcado que estás buscando.

## Problemas de incompatibilidad entre versiones

- `SyntaxError: Invalid syntax` (on the line `ROOT_TAG_NAME = '[document]'`): Causado por ejecutar una versión antigua de Beautiful Soup de Python 2 bajo Python 3, sin portar el código.

- `ImportError: No module named HTMLParser` - Causado por ejecutar una versión antigua de Beautiful Soup de Python 2 bajo Python 3.

- `ImportError: No module named html.parser` - Causado por ejecutar una versión de Beautiful Soup de Python 3 bajo Python 2.

- `ImportError: No module named BeautifulSoup` - Causado por ejecutar código de Beautiful Soup 3 en un sistema que no tiene BS3 instalado. O al escribir código de Beautiful Soup 4 sin saber que el nombre del paquete se cambió a `bs4`.

- `ImportError: No module named bs4` - Causado por ejecutar código de Beautiful Soup 4 en un sistema que no tiene BS4 instalado.

## Analizar XML

Por defecto, Beautiful Soup analiza documentos HTML. Para analizar un documento como XML, pasa «xml» como segundo argumento al constructor de `BeautifulSoup`:

    soup = BeautifulSoup(markup, "xml")

Necesitarás tener lxml instalado.

## Otros problemas de análisis

- Si tu script funciona en un ordenador pero no en otro, o en un entorno virtual pero no en otro, o fuera del entorno virtual pero no dentro, probablemente sea porque los dos entornos tienen diferentes librerías de analizadores disponibles. Por ejemplo, tal vez has desarrollado el script en un ordenador que solo tenga html5lib instalado. Mira Diferencias entre analizadores por qué esto es importante, y soluciona el problema especificando una librería de análisis en el constructor de `Beautiful Soup`.

- Debido a que las etiquetas y atributos HTML son sensibles a mayúsculas y minúsculas, los tres analizadores HTML convierten los nombres de las etiquetas y atributos a minúscula. Esto es, el marcado <TAG></TAG> se convierte en <tag></tag>. Si quieres preservar la mezcla entre minúscula y mayúscula o mantener las mayúsculas en etiquetas y atributos, necesitarás analizar el documento como XML.

## Diverso

- `UnicodeEncodeError: 'charmap' codec can't encode character '\xfoo' in position bar` (o cualquier otro `UnicodeEncodeError`) - Este problema aparece principalmente en dos situaciones. Primero, cuando intentas mostrar un carácter Unicode que tu consola no sabe cómo mostrar (mira esta página en la wiki de Python). Segundo, cuando estás escribiendo en un archivo y pasas un carácter Unicode que no se soporta en tu codificación por defecto. En este caso, la solución más simple es codificar explícitamente la cadena Unicode en UTF-8 con `u.encode("utf8")`.

- `KeyError: [attr]` - Causado por acceder a `tag['attr']` cuando la etiqueta en cuestión no define el atributo `'attr'`. Los errores más comunes son `KeyError: 'href'` y `KeyError: 'class`. Usa `tag.get('attr')` si no estás seguro si `attr` está definido, tal y como harías con un diccionario de Python.

- `AttributeError: 'ResultSet' object has no attribute 'foo'` - Esto normalmente ocurre cuando esperas que `find_all()` devuelva una sola etiqueta o cadena. Pero `find_all()` devuelve una lista de etiquetas y cadenas—un objeto `ResultSet`. Tienes que iterar sobre la lista y comprobar el `.foo` de cada uno, O, si solo quieres un resultado, tienes que usar `find()` en lugar de `find_all()`.

- `AttributeError: 'NoneType' object has no attribute 'foo'` - Esto normalmente ocurre porque llamaste a `find()` y después intentaste acceder al atributo `.foo` del resultado. Pero en tu caso, `find()` no encontró nada, así que devolvió `None`, en lugar de devolver una etiqueta o una cadena de caracteres. Necesitas averiguar por qué `find()` no está devolviendo nada.

- `AttributeError: 'NavigableString' object has no attribute 'foo'` - Esto ocurre normalmente porque estás tratando una cadena de caracteres como si fuese una etiqueta. Puedes estar iterando sobre una lista, esperando que tan solo contenga etiquetas, pero en realidad contiene tanto etiquetas como cadenas.

## Mejorar el rendimiento

Beautiful Soup nunca será tan rápido como los analizadores en los que se basa. Si el tiempo de respuesta es crítico, si estás pagando por tiempo de uso por hora, o si hay alguna otra razón por la que el tiempo de computación es más valioso que el tiempo del programador, deberías olvidarte de Beautiful Soup y trabajar directamente sobre lxml.

Dicho esto, hay cosas que puedes hacer para aumentar la velocidad de Beautiful Soup. Si no estás usando lxml como el analizador que hay por debajo, mi consejo es que empieces a usarlo. Beautiful Soup analiza documentos significativamente más rápido usando lxml que usando html.parser o html5lib.

Puedes aumentar la velocidad de detección de codificación significativamente instalando la librería cchardet.

Analizar solo parte del documento no te ahorrará mucho tiempo de análisis, pero puede ahorrar mucha memoria, y hará que buscar en el documento sea mucho más rápido.

# Traducir esta documentación

Las nuevas traducciones de la documentación de Beautiful Soup se agradecen enormemente. Las traducciones deberían estar bajo la licencia MIT, tal y como están Beautiful Soup y su documentación en inglés.

Hay dos maneras para que tu traducción se incorpore a la base de código principal y al sitio de Beautiful Soup:

1. Crear una rama del repositorio de Beautiful Soup, añadir tus traducciones, y proponer una fusión (merge) con la rama principal, lo mismo que se haría con una propuesta de código del código fuente.

2. Enviar un mensaje al grupo de discusión de Beautiful Soup con un enlace a tu traducción, o adjuntar tu traducción al mensaje.

Utiliza la traducción china o portugués-brasileño como tu modelo. En particular, por favor, traduce el archivo fuente `doc/index.rst`, en vez de la versión HTML de la documentación. Esto hace posible que la documentación se pueda publicar en una variedad de formatos, no solo HTML.

# Beautiful Soup 3

Beautiful Soup 3 es la serie de lanzamientos anterior, y no está siendo activamente desarrollada. Actualmente está empaquetada con las distribuciones de Linux más grandes:

`$` `apt`-`get` `install` `python`-`beautifulsoup`

También está publicada a través de PyPI como `BeautifulSoup`.:

`$` `easy_install` `BeautifulSoup`

`$` `pip` `install` `BeautifulSoup`

También puedes descargar un tarball de Beautiful Soup 3.2.0.

Si ejecutaste `easy_install beautifulsoup` o `easy_install BeautifulSoup`, pero tu código no funciona, instalaste por error Beautiful Soup 3. Necesitas ejecutar `easy_install beautifulsoup4`.

La documentación de Beautiful Soup 3 está archivada online.

## Actualizar el código a BS4

La mayoría del código escrito con Beautiful Soup 3 funcionará con Beautiful Soup 4 con un cambio simple. Todo lo que debes hacer es cambiar el nombre del paquete de `BeautifulSoup` a `bs4`. Así que esto:

    from BeautifulSoup import BeautifulSoup

se convierte en esto:

    from bs4 import BeautifulSoup

- Si obtienes el `ImportError` «No module named BeautifulSoup`», tu problema es que estás intentando ejecutar código de Beautiful Soup 3, pero solo tienes instalado Beautiful Soup 4.

- Si obtienes el `ImportError` «No module named bs4», tu problema es que estás intentando ejecutar código Beautiful Soup 4, pero solo tienes Beautiful Soup 3 instalado.

Aunque BS4 es mayormente compatible con la versión anterior BS3, la mayoría de sus métodos han quedado obsoletos y dados nuevos nombres para que cumplan con PEP 8. Hay muchos otros renombres y cambios, y algunos de ellos rompen con la compatibilidad hacia atrás.

Esto es todo lo que necesitarás saber para convertir tu código y hábitos BS3 a BS4:

### Necesitas un analizador

Beautiful Soup 3 usaba el `SGMLParser` de Python, un módulo que fue obsoleto y quitado en Python 3.0. Beautiful Soup 4 usa `html.parser` por defecto, pero puedes conectar lxml o html5lib y usar esos. Mira Instalar un analizador para una comparación.

Como `html.parser` no es el mismo analizador que `SGMLParser`, podrías encontrarte que Beautiful Soup 4 te de un árbol analizado diferente al que te da Beautiful Soup 3 para el mismo marcado. Si cambias `html.parser` por lxml o html5lib, puedes encontrarte que el árbol analizado también cambia. Si esto ocurre, necesitarás actualizar tu código de scraping para gestionar el nuevo árbol.

### Nombre de las propiedades

Renombré tres atributos para evitar usar palabras que tienen un significado especial en Python. A diferencia de mis cambios a los nombres de los métodos (que verás en forma de avisos de no soporte futuro), estos cambios no preservan compatibilidad hacia atrás. Si usabas estos atributos en BS3, tu código no funcionará en BS4 hasta que los cambies.

- `UnicodeDammit.unicode` -> `UnicodeDammit.unicode_markup`

- `Tag.next` -> `Tag.next_element`

- `Tag.previous` -> `Tag.previous_element`

### Generadores

Algunos de los generadores solían devolver `None` después de que hayan terminado, y después paran. Eso era un error. Ahora el generador tan solo se detiene.

### XML

Ya no hay una clase `BeautifulStoneSoup` para analizar XML. Para analizar XML pasas «xml» como el segundo argumento del constructor de `BeautifulSoup`. Por la misma razón, el constructor de `BeautifulSoup` ya no reconoce el argumento `isHTML`.

La gestión de Beautiful Soup sobre las etiquetas XML sin elementos ha sido mejorada. Previamente cuando analizabas XML tenías que indicar explícitamente qué etiquetas eran consideradas etiquetas sin elementos. El argumento `selfClosingTags` al constructor ya no se reconoce. En lugar de ello, Beautiful Soup considera cualquier etiqueta vacía como una etiqueta sin elementos. Si añades un hijo a una etiqueta sin elementos, deja de ser una etiqueta sin elementos.

### Entidades

Una entidad HTML o XML entrante siempre se convierte al correspondiente carácter Unicode. Beautiful Soup 3 tenía varias formas solapadas para gestionar entidades, las cuales se han eliminado. El constructor de `BeautifulSoup` ya no reconoce los argumentos `smartQuotesTo` o `convertEntities` (Unicode, Dammit aún tiene `smart_quotes_to`, pero por defecto ahora transforma las comillas inteligentes a Unicode). Las constantes `HTML_ENTITIES`, `XML_ENTITIES`, y `XHTML_ENTITIES` han sido eliminadas, ya que configuran una característica (transformando algunas pero no todas las entidades en caracteres Unicode) que ya no existe.

Si quieres volver a convertir caracteres Unicode en entidades HTML a la salida, en lugar de transformarlos a caracteres UTF-8, necesitas usar un *formatter* de salida.

### Otro

Tag.string ahora funciona recursivamente. Si una etiqueta A contiene una sola etiqueta B y nada más, entonces A.string es el mismo que B.string (Antes, era `None`).

Los atributos multivaluados como `class` tienen listas de cadenas de caracteres como valores, no cadenas. Esto podría afectar la manera en la que buscas por clases CSS.

Objetos `Tag` ahora implementan el método `__hash__`, de tal manera que dos objetos `Tag` se consideran iguales si generan el mismo marcado. Esto puede cambiar el comportamiento de tus scripts si insertas los objetos `Tag` en un diccionario o conjunto.

Si pasas a unos de los métodos `find*` una cadena y un argumento específico de una etiqueta como name, Beautiful Soup buscará etiquetas que casen con tu criterio específico de la etiqueta y cuyo Tag.string case con tu valor para la cadena. No encontrará las cadenas mismas. Anteriormente, Beautiful Soup ignoraba el argumento específico de la etiqueta y buscaba por cadenas de caracteres.

El constructor de `Beautiful Soup` ya no reconoce el argumento `markupMassage`. Es ahora responsabilidad del analizador gestionar el marcado correctamente.

Los analizadores alternativos, que rara vez se utilizaban, como `ICantBelieveItsBeautifulSoup` y `BeautifulSOAP` se han eliminado. Ahora es decisión del analizador saber cómo gestionar marcado ambiguo.

El método `prettify()` ahora devuelve una cadena Unicode, no un bytestring.