diff options
author | Leonard Richardson <leonardr@segfault.org> | 2024-01-13 13:30:34 -0500 |
---|---|---|
committer | Leonard Richardson <leonardr@segfault.org> | 2024-01-13 13:30:34 -0500 |
commit | c9fe6065804af362c1c65ea85fece3cac31c2e82 (patch) | |
tree | f0613eb2c4f80ccb0e605c31af059ba1a2310797 | |
parent | 05b797a8950cd09f0f9670e4da5ea3737ab6fa07 (diff) | |
parent | 09334755d84debde8d574905d5e8906e586b5e0b (diff) |
Bringing in Carlos Romero's Spanish translation of the documentation.
-rw-r--r-- | doc.es/Makefile | 130 | ||||
-rw-r--r-- | doc.es/source/6.1.jpg | bin | 0 -> 22619 bytes | |||
-rw-r--r-- | doc.es/source/conf.py | 256 | ||||
-rw-r--r-- | doc.es/source/index.rst | 3709 |
4 files changed, 4095 insertions, 0 deletions
diff --git a/doc.es/Makefile b/doc.es/Makefile new file mode 100644 index 0000000..8c833d2 --- /dev/null +++ b/doc.es/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/BeautifulSoup.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/BeautifulSoup.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/BeautifulSoup" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/BeautifulSoup" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc.es/source/6.1.jpg b/doc.es/source/6.1.jpg Binary files differnew file mode 100644 index 0000000..97014f0 --- /dev/null +++ b/doc.es/source/6.1.jpg diff --git a/doc.es/source/conf.py b/doc.es/source/conf.py new file mode 100644 index 0000000..42fcf6d --- /dev/null +++ b/doc.es/source/conf.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +# +# Beautiful Soup documentation build configuration file, created by +# sphinx-quickstart on Thu Jan 26 11:22:55 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Beautiful Soup' +copyright = u'2004-2024, Leonard Richardson' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '4' +# The full version, including alpha/beta/rc tags. +release = '4.12.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = "es" + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'BeautifulSoupdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'BeautifulSoup.tex', u'Beautiful Soup Documentation', + u'Leonard Richardson', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'beautifulsoup', u'Beautiful Soup Documentation', + [u'Leonard Richardson'], 1) +] + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Beautiful Soup' +epub_author = u'Leonard Richardson' +epub_publisher = u'Leonard Richardson' +epub_copyright = u'2012, Leonard Richardson' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True diff --git a/doc.es/source/index.rst b/doc.es/source/index.rst new file mode 100644 index 0000000..53c47f5 --- /dev/null +++ b/doc.es/source/index.rst @@ -0,0 +1,3709 @@ +.. _manual: + +================================= + Documentación de Beautiful Soup +================================= + +.. py:module:: bs4 + +.. image:: 6.1.jpg + :align: right + :alt: "El lacayo-pez empezó por sacarse de debajo del brazo una gran carta, + casi tan grande como él." + +`Beautiful Soup <http://www.crummy.com/software/BeautifulSoup/>`_ es una +librería de Python para extraer datos de archivos en formato HTML y XML. +Trabaja 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.12.1. Los ejemplos en este +documento fueron escritos para Python 3.8. + +Podrías estar buscando la documentación de `Beautiful Soup 3 +<http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html>`_. +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: + +* `这篇文档当然还有中文版. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/>`_ +* このページは日本語で利用できます(`外部リンク <http://kondou.com/BS4/>`_) +* `이 문서는 한국어 번역도 가능합니다. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ko/>`_ +* `Este documento também está disponível em Português do Brasil. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ptbr>`_ +* `Эта документация доступна на русском языке. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ru/>`_ + +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 +<https://groups.google.com/forum/?fromgroups#!forunm/beautifulsoup>`_. +Si tienes algún problema relacionado con el análisis de un documento HTML, +asegúrate de mencionar :ref:`lo que la función diagnose() dice <diagnose>` +sobre dicho documento. + +Cuando informes de algún error en esta documentación, por favor, +indica la traducción que estás leyendo. + +=============== + 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 :py:class:`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: + +:kbd:`$ 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): + +:kbd:`$ easy_install beautifulsoup4` + +:kbd:`$ pip install beautifulsoup4` + +(El paquete :py:class:`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 +<http://www.crummy.com/software/BeautifulSoup/download/4.x/>`_ e +instalarlo con ``setup.py``: + +:kbd:`$ 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. + +.. _parser-installation: + + +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 <http://lxml.de/>`_. +Dependiendo de tu instalación, puedes instalar lxml con uno de los +siguientes comandos: + +:kbd:`$ apt-get install python-lxml` + +:kbd:`$ easy_install lxml` + +:kbd:`$ pip install lxml` + +Otra alternativa es usar el analizador de Python de +`html5lib <http://code.google.com/p/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: + +:kbd:`$ apt-get install python-html5lib` + +:kbd:`$ easy_install html5lib` + +:kbd:`$ 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 | * No tan rápido como lxml, | +| | | * Rapidez decente | menos tolerante que | +| | | * Tolerante (en Python 3.2) | html5lib. | ++-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+ +| Analizador HTML de | ``BeautifulSoup(markup, "lxml")`` | * Muy rápido | * Dependencia externa de C | +| lxml | | * Tolerante | | ++-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+ +| Analizador XML de | ``BeautifulSoup(markup, "lxml-xml")`` | * Muy rápido | * Dependencia externa de C | +| lxml | ``BeautifulSoup(markup, "xml")`` | * El único analizador XML | | +| | | actualmente soportado | | ++-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+ +| html5lib | ``BeautifulSoup(markup, "html5lib")`` | * Extremadamente tolerante | * Muy lento | +| | | * Analiza las páginas de la misma | * Dependencia externa de | +| | | manera que un navegador web | Python | +| | | * Crea HTML5 válido | | ++-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+ + +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 :py:class:`BeautifulSoup`. +Puedes pasar una cadena de caracteres o abrir un manejador 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é 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: :py:class:`Tag`, +:py:class:`NavigableString`, :py:class:`BeautifulSoup` y :py:class:`Comment`. + +.. py:class:: Tag + + Un objeto :py:class:`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. + + .. py:attribute:: 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> + + .. py:attribute:: 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 + + .. _multivalue: + + 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 :py:class:`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 + +.. py:class:: NavigableString + +----------------------------- + +Un *string* corresponde a un trozo de texto en una etiqueta. Beautiful Soup usa la clase +:py:class:`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 :py:class:`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 +:py:class:`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 +:ref:`replace_with()`:: + + tag.string.replace_with("No longer bold") + tag + # <b class="boldest">No longer bold</b> + +:py:class:`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 :py:class:`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. + +.. py:class:: BeautifulSoup + +--------------------------- + +El objeto :py:class:`BeautifulSoup` representa el documento analizado +en su conjunto. Para la mayoría de propósitos, puedes usarlo como un objeto +:py:class:`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 :py:class:`BeautifulSoup` en cualquiera de +los métodos definidos en `Modificar el árbol`_, como si fuese un :py:class:`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 :py:class:`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 +================== + +:py:class:`Tag`, :py:class:`NavigableString` y +:py:class:`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 :py:class:`Comment`. + +.. py: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 :py:class:`Comment` es solo un tipo especial de :py:class:`NavigableString`:: + + comment + # 'Hey, buddy. Want to buy a used parser' + +Pero cuando aparece como parte de un documento HTML, un :py:class:`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 :py:class:`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)`. + +.. py:class:: Stylesheet + +Una subclase de :py:class:`NavigableString` que representa hojas de estilo +CSS embebidas; esto es, cualquier cadena en una etiqueta +``<style>`` durante el análisis del documento. + +.. py:class:: Script + +Una subclase de :py:class:`NavigableString` que representa +JavaScript embebido; esto es, cualquier cadena en una etiqueta +``<script>`` durante el análisis del documento. + +.. py:class:: Template + +Una subclase de :py:class: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 :py:class:`NavigableString` +para contener tipos especiales de cadenas de caracteres que pueden +ser encontradas en documentos XML. Como :py:class:`Comment`, estas +clases son subclases de :py:class:`NavigableString` que añaden +algo extra a la cadena de caracteres en la salida. + +.. py:class:: Declaration + +Una subclase de :py:class:`NavigableString` que representa la +`declaración <https://www.w3.org/TR/REC-xml/#sec-prolog-dtd>`_ al +principio de un documento XML. + +.. py:class:: Doctype + +Una subclase de :py:class:`NavigableString` que representa la +`declaración del tipo de documento <https://www.w3.org/TR/REC-xml/#dt-doctype>`_ +que puede encontrarse cerca del comienzo de un documento XML. + +.. py:class:: CData + +Una subclase de :py:class:`NavigableString` que representa una +`sección CData <https://www.w3.org/TR/REC-xml/#sec-cdata-sect>`_. + +.. py:class:: ProcessingInstruction + +Una subclase de :py:class:`NavigableString` que representa el contenido de +una `instrucción de procesamiento XML <https://www.w3.org/TR/REC-xml/#sec-pi>`_. + + +====================== + 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 :py:class:`BeautifulSoup` por sí solo ya tiene hijos. En este caso, +la etiqueta <html> is hija del objeto :py:class:`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 +:py:class:`BeautifulSoup` tiene un hijo directo (la etiqueta <html>), pero +tiene otros muchos descendientes:: + + len(list(soup.children)) + # 1 + len(list(soup.descendants)) + # 26 + +.. _.string: + +``.string`` +----------- + +Si una etiqueta tiene solo un hijo, y dicho hijo es un :py:class:`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 + +.. _string-generators: + +``.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: + +``.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 :py:class:`BeautifulSoup` +mismo:: + + html_tag = soup.html + type(html_tag.parent) + # <class 'bs4.BeautifulSoup'> + +Y el ``.parent`` de un objeto :py:class:`BeautifulSoup` se define como ``None``:: + + print(soup.parent) + # None + +.. _.parents: + +``.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] + +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> + +.. _sibling-generators: + +``.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. + +.. _element-generators: + +``.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. + +.. _a string: + +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. + +.. _a regular expression: + +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 + +.. _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>] + +.. _the value True: + +``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 + +.. a function: + +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 + +Ahora ya estamos listos para entrar en detalle en los métodos +de búsqueda. + +``find_all()`` +============== + +Firma del método: find_all(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`recursive +<recursive>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <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()``. + +.. _name: + +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`_. + +.. _kwargs: + +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"/>] + +.. _attrs: + +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>] + +:ref:`Recuerda <multivalue>` 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>] + +.. _string: + +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>] + +.. _limit: + +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>] + +.. _recursive: + +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 :py:class:`BeautifulSoup` o un objeto :py:class:`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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`recursive +<recursive>`, :ref:`string <string>`, :ref:`**kwargs <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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`) + +Firma del método: find_parent(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`) + +Firma del método: find_next_sibling(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`) + +Estos métodos usan :ref:`next_siblings <sibling-generators>` +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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`) + +Firma del método: find_previous_sibling(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`) + +Estos métodos emplean :ref:`.previous_siblings <sibling-generators>` 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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`) + +Firma del método: find_next(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`) + +Estos métodos usan :ref:`.next_elements <element-generators>` 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(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`) + +Firma del método: find_previous(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`) + +Estos métodos usan :ref:`.previous_elements <element-generators>` +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 :py:class:`BeautifulSoup` y :py:class:`Tag` soportan los selectores +CSS a través de su atributo ``.css``. El paquete `Soup Sieve <https://facelessuser.github.io/soupsieve/>`_, +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 <https://facelessuser.github.io/soupsieve/selectors/>`_, 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 :py:class:`BeautifulSoup` o :py:class:`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 :py:class:`Tag` o :py:class:`Beautiful Soup`. Lo que +sigue es solo una lista de los métodos soportados; ve a `la documentación de +Soup Sieve <https://facelessuser.github.io/soupsieve/>`_ 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 :py:class:`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 :py:class:`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 :py:class:`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') + soup.a.append("Bar") + + soup + # <a>FooBar</a> + soup.a.contents + # ['Foo', 'Bar'] + +``extend()`` +============ + +Desde Beautiful Soup 4.7.0, :py:class:`Tag` también soporta un método +llamado ``.extend()``, el cual añade todos los elementos de una lista +a una :py:class:`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'] + +``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 :py:class:`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 :py:class:`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. + +``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. Funciona +como ``.insert()`` es 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 + + 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>] + +``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'] + +``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 :py:class:`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 :py:class:`Tag` o :py:class:`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(): + +``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 :py:class:`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 +======== + +.. _.prettyprinting: + +*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 :py:class:`BeautifulSoup`, +o sobre cualquiera de sus objetos :py:class:`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 :py:class:`BeautifulSoup`, o +sobre una :py:class:`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. + +.. _output_formatters: + +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("“Dammit!” 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 +"&", "<" y ">", 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, & 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&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 <<Sacré bleu!>></p>" + soup = BeautifulSoup(french, 'html.parser') + print(soup.prettify(formatter="minimal")) + # <p> + # Il a dit <<Sacré bleu!>> + # </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 <<Sacré bleu!>> + # </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``. + +.. py: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> + +.. py:class:: XMLFormatter + +Usado para personalizar las reglas de formateo para documentos XML. + +Escribir tu propio *formatter* +------------------------------ + +Crear una subclase a partir de :py:class:`HTMLFormatter` p :py:class:`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 :py:class:`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 +:ref:`.stripped_strings <string-generators>`, y procesar el texto +por tu cuenta:: + + [text for text in soup.stripped_strings] + # ['I linked to', 'example.com'] + +*A partir de Beautiful Soup version 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 de Beautiful Soup version 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.* + +================================== + Especificar el analizador a usar +================================== + +Si lo único que necesitas es analizar algún HTML, puedes ponerlo en +el constructor de :py:class:`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 :py:class:`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 :py:class:`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 +sub-librerí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 +:py:class:`Beautiful Soup`:: + + soup.original_encoding + 'utf-8' + +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 :py:class:`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 :py:class:`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 :py:class:`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 "☃" para esas codificaciones:: + + print(tag.encode("utf-8")) + # b'<b>\xe2\x98\x83</b>' + + print(tag.encode("latin-1")) + # b'<b>☃</b>' + + print(tag.encode("ascii")) + # b'<b>☃</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 “love” Microsoft Word’s smart quotes</p>' + + UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup + # '<p>I just “love” Microsoft Word’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 :py:class:`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 :py:class:`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 :py:class:`NavigableString` o :py:class:`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 +:py:class:`Tag` o :py:class:`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:: + + print(p_copy.parent) + # None + +Esto es porque dos diferentes objetos :py:class:`Tag` no pueden ocupar +el mismo espacio al mismo tiempo. + +========================================= + 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 :py:class:`SoupStrainer` te permite elegir qué partes de un +documento entrante se analizan. Tan solo crea un :py:class:`SoupStrainer` y +pásalo al constructor de :py:class:`BeautifulSoup` en el argumento ``parse_only``. + +(Debe notarse 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). + +.. py:class:: SoupStrainer + +La clase :py:class:`SoupStrainer` toma los mismos argumentos que un típico +método de `Buscar en el árbol`_: :ref:`name <name>`, :ref:`attrs <attrs>`, +:ref:`string <string>`, y :ref:`**kwargs <kwargs>`. Estos son tres objetos +:py:class:`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 :py:class:`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 + # ... + # + +Puedes también pasar un :py:class:`SoupStrainer` en cualquiera de los métodos +cubiertos en `Buscar en el árbol`_. Esto probablemente no sea terriblemente útil, +pero pensé en mencionarlo:: + + soup = BeautifulSoup(html_doc, 'html.parser') + soup.find_all(only_short_strings) + # ['\n\n', '\n\n', 'Elsie', ',\n', 'Lacie', ' and\n', 'Tillie', + # '\n\n', '...', '\n'] + +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 :py:class:`Tag` o :py:class:`NavigableString` +para contener esa información. En lugar de ese comportamiento por defecto, +puedes indicar a Beautiful Soup que instancia `subclases` de :py:class:`Tag` o +:py:class:`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: + +``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 mostrate 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 +detalles y una comparativa de analizadores. + +Los errores de análisis más comunes son ``HTMLParser.HTMLParseError: +malformed start tag`` y ``HTMLParser.HTMLParseError: bad end +tag``. Ambos son generados por la librería del analizador HTML +incluido en Python, y la solución es :ref:`instalar lxml o html5lib. +<parser-installation>` + +El comportamiento inesperado más común es que no puedas encontrar +una etiqueta que sabes que está en el documento. La viste llegar, pero +``find_all()`` devuelve ``[]`` o ``find()`` devuelve ``None``. Esto +es otro problema común con el analizador HTML integrado en Python, el cual +a veces omite etiquetas que no entiende. De nuevo, la mejor solución es +:ref:`instalar lxml o html5lib. <parser-installation>`. + +Problemas de incompatibilidad de versiones +========================================== + +* ``SyntaxError: Invalid syntax`` (on the line ``ROOT_TAG_NAME = + '[document]'``): Causado por ejecutar una version antigua de Beautiful + Soup de Python 2 bajo Python 3, sin convertir el código. + +* ``ImportError: No module named HTMLParser`` - Causado por ejecutar + una version antigua de Beautiful Soup de Python 2 bajo Python 3. + +* ``ImportError: No module named html.parser`` - Causado por ejecutar + una version 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. + +.. _parsing-xml: + +Analizar XML +============ + +Por defecto, Beautiful Soup analiza documentos HTML. Para analizar +un documento como XML, pasa "xml" como el segundo argumento al +constructor :py:class:`BeautifulSoup`:: + + soup = BeautifulSoup(markup, "xml") + +Necesitarás :ref:`tener lxml instalado <parser-installation>`. + +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, es probable porque los dos entornos tienen + diferentes librerías de analizadores disponibles. Por ejemplo, + puedes haber desarrollado el script en un ordenador que solo + tenga html5lib instalado. Mira `Diferencias entre analizadores`_ + por qué esto importa, y solucionar el problema especificando una + librería de análisis en el constructor de :py:class:`Beautiful Soup`. + +* Porque `las etiquetas y atributos de HTML son sensibles a mayúsculas + y minúsculas <http://www.w3.org/TR/html5/syntax.html#syntax>`_, + los tres analizadores HTML convierten los nombres de las etiquetas y + atributos a minúscula. Esto es, el marcado <TAG></TAG> se convierte + a <tag></tag>. Si quieres preservar la mezcla entre minúscula y + mayúscula o mantener las mayúsculas en etiquetas y atributos, + necesitarás :ref:`analizar el documento como XML. <parsing-xml>` + +.. _misc: + +Diversos +======== + +* ``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 <http://wiki.python.org/moin/PrintFails>`_). 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 <http://lxml.de/>`_. + +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 :ref:`empieces a usarlo <parser-installation>`. +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 <http://pypi.python.org/pypi/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 +============================== + +Nuevas traducciones de la documentación de Beautiful Soup se agradecen +enormemente. Las traducciones deberían estar bajo la licencia del 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/source/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: + +:kbd:`$ apt-get install python-beautifulsoup` + +También está publicada a través de PyPI como :py:class:`BeautifulSoup`.: + +:kbd:`$ easy_install BeautifulSoup` + +:kbd:`$ pip install BeautifulSoup` + +También puedes `descargar un tarball de Beautiful Soup 3.2.0 +<http://www.crummy.com/software/BeautifulSoup/bs3/download/3.x/BeautifulSoup-3.2.0.tar.gz>`_. + +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 +<http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html>`_. + +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 :py:class:`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 <http://www.python.org/dev/peps/pep-0008/>`_. +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 los métodos +--------------------- + +* ``renderContents`` -> ``encode_contents`` +* ``replaceWith`` -> ``replace_with`` +* ``replaceWithChildren`` -> ``unwrap`` +* ``findAll`` -> ``find_all`` +* ``findAllNext`` -> ``find_all_next`` +* ``findAllPrevious`` -> ``find_all_previous`` +* ``findNext`` -> ``find_next`` +* ``findNextSibling`` -> ``find_next_sibling`` +* ``findNextSiblings`` -> ``find_next_siblings`` +* ``findParent`` -> ``find_parent`` +* ``findParents`` -> ``find_parents`` +* ``findPrevious`` -> ``find_previous`` +* ``findPreviousSibling`` -> ``find_previous_sibling`` +* ``findPreviousSiblings`` -> ``find_previous_siblings`` +* ``getText`` -> ``get_text`` +* ``nextSibling`` -> ``next_sibling`` +* ``previousSibling`` -> ``previous_sibling`` + +Algunos argumentos del constructor de Beautiful Soup fueron renombrados +por la misma razón: + +* ``BeautifulSoup(parseOnlyThese=...)`` -> ``BeautifulSoup(parse_only=...)`` +* ``BeautifulSoup(fromEncoding=...)`` -> ``BeautifulSoup(from_encoding=...)`` + +Renombré un método para compatibilidad con Python 3: + +* ``Tag.has_key()`` -> ``Tag.has_attr()`` + +Renombré un atributo para usar terminología más precisa: + +* ``Tag.isSelfClosing`` -> ``Tag.is_empty_element`` + +Renombré tres atributos para evitar usar palabras que tienen un significado +especial en Python. A diferencia de otros, estos cambios no soportan +*compatibilidad hacia atrás*. Si usaste estos atributos en BS3, tu código +se romperá en BS4 hasta que lo cambies. + +* ``UnicodeDammit.unicode`` -> ``UnicodeDammit.unicode_markup`` +* ``Tag.next`` -> ``Tag.next_element`` +* ``Tag.previous`` -> ``Tag.previous_element`` + +Estos métodos sobras desde la API de Beautiful Soup 2. Han quedado +obsoletos desde 2006, y no deberían usarse en absoluto: + +* ``Tag.fetchNextSiblings`` +* ``Tag.fetchPreviousSiblings`` +* ``Tag.fetchPrevious`` +* ``Tag.fetchPreviousSiblings`` +* ``Tag.fetchParents`` +* ``Tag.findChild`` +* ``Tag.findChildren`` + + +Generadores +----------- + +Le di a los generadores nombres que cumplan con PEP 8, y se transformaron +en propiedades: + +* ``childGenerator()`` -> ``children`` +* ``nextGenerator()`` -> ``next_elements`` +* ``nextSiblingGenerator()`` -> ``next_siblings`` +* ``previousGenerator()`` -> ``previous_elements`` +* ``previousSiblingGenerator()`` -> ``previous_siblings`` +* ``recursiveChildGenerator()`` -> ``descendants`` +* ``parentGenerator()`` -> ``parents`` + +Así que en lugar de esto:: + + for parent in tag.parentGenerator(): + ... + +Puedes escribir esto:: + + for parent in tag.parents: + ... + +(Pero el código antiguo seguirá funcionando). + +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 +para. + +Hay dos nuevos generadores, :ref:`.strings y .stripped_strings +<string-generators>`. ``.strings`` devuelve objetos NavigableString, +y ``.stripped_strings`` devuelve cadenas de Python cuyos espacios +en blanco al comienzo y al final han sido quitados. + +XML +--- + +Ya no hay una clase ``BeautifulStoneSoup`` para analizar XML. Para +analizar XML pasas "xml" como el segundo argumento del constructor +de :py:class:`BeautifulSoup`. Por la misma razón, el constructor +de :py:class:`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 +:py:class:`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 :ref:`*formatter* de salida <output_formatters>`. + +Otro +---- + +:ref:`Tag.string <.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 :py:class:`Tag` ahora implementan el método ``__hash__``, de tal +manera que dos objetos :py:class:`Tag` se consideran iguales si generan +el mismo marcado. Esto puede cambiar el comportamiento de tus scripts +si insertas los objetos :py:class:`Tag` en un diccionario o conjunto. + +Si pasas a unos de los métodos ``find*`` una :ref:`cadena <string>` y +un argumento específico de una etiqueta como :ref:`name <name>`, Beautiful +Soup buscará etiquetas que casen con tu criterio específico de la etiqueta +y cuyo :ref:`Tag.string <.string>` case con tu valor para la :ref:`cadena <string>`. +`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 :py:class:`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. |