Merge ~phoenixsite/beautifulsoup:master into beautifulsoup:master

Proposed by Carlos Romero
Status: Merged
Merged at revision: c9fe6065804af362c1c65ea85fece3cac31c2e82
Proposed branch: ~phoenixsite/beautifulsoup:master
Merge into: beautifulsoup:master
Diff against target: 4117 lines (+4095/-0)
3 files modified
doc.es/Makefile (+130/-0)
doc.es/source/conf.py (+256/-0)
doc.es/source/index.rst (+3709/-0)
Reviewer Review Type Date Requested Status
Leonard Richardson Approve
Review via email: mp+458246@code.launchpad.net

Commit message

Added Spanish documentation

To post a comment you must log in.
Revision history for this message
Chris Papademetrious (chrispitude) wrote :

Hi Carlos - I have a pull request for documentation suggestions here:

https://code.launchpad.net/~chrispitude/beautifulsoup/+git/beautifulsoup/+merge/456268

When future updates are made to the English documentation, will you be making corresponding updates to the Spanish documentation?

Revision history for this message
Leonard Richardson (leonardr) wrote :

Carlos, can you translate the following sentence for me so that I can link to the Spanish documentation from the other languages?

"This document is also available in Spanish."

Revision history for this message
Leonard Richardson (leonardr) wrote :

I've put up the documentation here:

https://www.crummy.com/software/BeautifulSoup/bs4/doc.es/

I used machine translation to come up with "Este documento también está disponible en una traducción al español." for the link text; let me know if something else is better.

Thanks again for your hard work on this!

review: Approve
Revision history for this message
Carlos Romero (phoenixsite) wrote :

Hello, Chris. Yes, whenever the English documentation is updated in the master branch I will try to translate it to Spanish. If you need the translation on another branch before releasing it to master, just let me know in the comment section of the proposal or just create a post on the discussion group. I will try to look it up. Thank you.

Revision history for this message
Carlos Romero (phoenixsite) wrote :

Hello Leonard, that translation is good. Maybe it would be better "Este documento también está disponible en español.", but the difference is not really that important. Maybe better for future updates.

Thanks to you and all the contributors to this great package. Keep it up!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/doc.es/Makefile b/doc.es/Makefile
0new file mode 1006440new file mode 100644
index 0000000..8c833d2
--- /dev/null
+++ b/doc.es/Makefile
@@ -0,0 +1,130 @@
1# Makefile for Sphinx documentation
2#
3
4# You can set these variables from the command line.
5SPHINXOPTS =
6SPHINXBUILD = sphinx-build
7PAPER =
8BUILDDIR = build
9
10# Internal variables.
11PAPEROPT_a4 = -D latex_paper_size=a4
12PAPEROPT_letter = -D latex_paper_size=letter
13ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
14
15.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
16
17help:
18 @echo "Please use \`make <target>' where <target> is one of"
19 @echo " html to make standalone HTML files"
20 @echo " dirhtml to make HTML files named index.html in directories"
21 @echo " singlehtml to make a single large HTML file"
22 @echo " pickle to make pickle files"
23 @echo " json to make JSON files"
24 @echo " htmlhelp to make HTML files and a HTML help project"
25 @echo " qthelp to make HTML files and a qthelp project"
26 @echo " devhelp to make HTML files and a Devhelp project"
27 @echo " epub to make an epub"
28 @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
29 @echo " latexpdf to make LaTeX files and run them through pdflatex"
30 @echo " text to make text files"
31 @echo " man to make manual pages"
32 @echo " changes to make an overview of all changed/added/deprecated items"
33 @echo " linkcheck to check all external links for integrity"
34 @echo " doctest to run all doctests embedded in the documentation (if enabled)"
35
36clean:
37 -rm -rf $(BUILDDIR)/*
38
39html:
40 $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
41 @echo
42 @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
43
44dirhtml:
45 $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
46 @echo
47 @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
48
49singlehtml:
50 $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
51 @echo
52 @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
53
54pickle:
55 $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
56 @echo
57 @echo "Build finished; now you can process the pickle files."
58
59json:
60 $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
61 @echo
62 @echo "Build finished; now you can process the JSON files."
63
64htmlhelp:
65 $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
66 @echo
67 @echo "Build finished; now you can run HTML Help Workshop with the" \
68 ".hhp project file in $(BUILDDIR)/htmlhelp."
69
70qthelp:
71 $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
72 @echo
73 @echo "Build finished; now you can run "qcollectiongenerator" with the" \
74 ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
75 @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/BeautifulSoup.qhcp"
76 @echo "To view the help file:"
77 @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/BeautifulSoup.qhc"
78
79devhelp:
80 $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
81 @echo
82 @echo "Build finished."
83 @echo "To view the help file:"
84 @echo "# mkdir -p $$HOME/.local/share/devhelp/BeautifulSoup"
85 @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/BeautifulSoup"
86 @echo "# devhelp"
87
88epub:
89 $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
90 @echo
91 @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
92
93latex:
94 $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
95 @echo
96 @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
97 @echo "Run \`make' in that directory to run these through (pdf)latex" \
98 "(use \`make latexpdf' here to do that automatically)."
99
100latexpdf:
101 $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
102 @echo "Running LaTeX files through pdflatex..."
103 make -C $(BUILDDIR)/latex all-pdf
104 @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
105
106text:
107 $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
108 @echo
109 @echo "Build finished. The text files are in $(BUILDDIR)/text."
110
111man:
112 $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
113 @echo
114 @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
115
116changes:
117 $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
118 @echo
119 @echo "The overview file is in $(BUILDDIR)/changes."
120
121linkcheck:
122 $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
123 @echo
124 @echo "Link check complete; look for any errors in the above output " \
125 "or in $(BUILDDIR)/linkcheck/output.txt."
126
127doctest:
128 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
129 @echo "Testing of doctests in the sources finished, look at the " \
130 "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/doc.es/source/6.1.jpg b/doc.es/source/6.1.jpg
0new file mode 100644131new file mode 100644
index 0000000..97014f0
1Binary files /dev/null and b/doc.es/source/6.1.jpg differ132Binary files /dev/null and b/doc.es/source/6.1.jpg differ
diff --git a/doc.es/source/conf.py b/doc.es/source/conf.py
2new file mode 100644133new file mode 100644
index 0000000..42fcf6d
--- /dev/null
+++ b/doc.es/source/conf.py
@@ -0,0 +1,256 @@
1# -*- coding: utf-8 -*-
2#
3# Beautiful Soup documentation build configuration file, created by
4# sphinx-quickstart on Thu Jan 26 11:22:55 2012.
5#
6# This file is execfile()d with the current directory set to its containing dir.
7#
8# Note that not all possible configuration values are present in this
9# autogenerated file.
10#
11# All configuration values have a default; values that are commented out
12# serve to show the default.
13
14import sys, os
15
16# If extensions (or modules to document with autodoc) are in another directory,
17# add these directories to sys.path here. If the directory is relative to the
18# documentation root, use os.path.abspath to make it absolute, like shown here.
19#sys.path.insert(0, os.path.abspath('.'))
20
21# -- General configuration -----------------------------------------------------
22
23# If your documentation needs a minimal Sphinx version, state it here.
24#needs_sphinx = '1.0'
25
26# Add any Sphinx extension module names here, as strings. They can be extensions
27# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
28extensions = []
29
30# Add any paths that contain templates here, relative to this directory.
31templates_path = ['_templates']
32
33# The suffix of source filenames.
34source_suffix = '.rst'
35
36# The encoding of source files.
37#source_encoding = 'utf-8-sig'
38
39# The master toctree document.
40master_doc = 'index'
41
42# General information about the project.
43project = u'Beautiful Soup'
44copyright = u'2004-2024, Leonard Richardson'
45
46# The version info for the project you're documenting, acts as replacement for
47# |version| and |release|, also used in various other places throughout the
48# built documents.
49#
50# The short X.Y version.
51version = '4'
52# The full version, including alpha/beta/rc tags.
53release = '4.12.0'
54
55# The language for content autogenerated by Sphinx. Refer to documentation
56# for a list of supported languages.
57language = "es"
58
59# There are two options for replacing |today|: either, you set today to some
60# non-false value, then it is used:
61#today = ''
62# Else, today_fmt is used as the format for a strftime call.
63#today_fmt = '%B %d, %Y'
64
65# List of patterns, relative to source directory, that match files and
66# directories to ignore when looking for source files.
67exclude_patterns = []
68
69# The reST default role (used for this markup: `text`) to use for all documents.
70#default_role = None
71
72# If true, '()' will be appended to :func: etc. cross-reference text.
73#add_function_parentheses = True
74
75# If true, the current module name will be prepended to all description
76# unit titles (such as .. function::).
77#add_module_names = True
78
79# If true, sectionauthor and moduleauthor directives will be shown in the
80# output. They are ignored by default.
81#show_authors = False
82
83# The name of the Pygments (syntax highlighting) style to use.
84pygments_style = 'sphinx'
85
86# A list of ignored prefixes for module index sorting.
87#modindex_common_prefix = []
88
89
90# -- Options for HTML output ---------------------------------------------------
91
92# The theme to use for HTML and HTML Help pages. See the documentation for
93# a list of builtin themes.
94html_theme = 'default'
95
96# Theme options are theme-specific and customize the look and feel of a theme
97# further. For a list of options available for each theme, see the
98# documentation.
99#html_theme_options = {}
100
101# Add any paths that contain custom themes here, relative to this directory.
102#html_theme_path = []
103
104# The name for this set of Sphinx documents. If None, it defaults to
105# "<project> v<release> documentation".
106#html_title = None
107
108# A shorter title for the navigation bar. Default is the same as html_title.
109#html_short_title = None
110
111# The name of an image file (relative to this directory) to place at the top
112# of the sidebar.
113#html_logo = None
114
115# The name of an image file (within the static path) to use as favicon of the
116# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
117# pixels large.
118#html_favicon = None
119
120# Add any paths that contain custom static files (such as style sheets) here,
121# relative to this directory. They are copied after the builtin static files,
122# so a file named "default.css" will overwrite the builtin "default.css".
123html_static_path = ['_static']
124
125# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
126# using the given strftime format.
127#html_last_updated_fmt = '%b %d, %Y'
128
129# If true, SmartyPants will be used to convert quotes and dashes to
130# typographically correct entities.
131#html_use_smartypants = True
132
133# Custom sidebar templates, maps document names to template names.
134#html_sidebars = {}
135
136# Additional templates that should be rendered to pages, maps page names to
137# template names.
138#html_additional_pages = {}
139
140# If false, no module index is generated.
141#html_domain_indices = True
142
143# If false, no index is generated.
144#html_use_index = True
145
146# If true, the index is split into individual pages for each letter.
147#html_split_index = False
148
149# If true, links to the reST sources are added to the pages.
150#html_show_sourcelink = True
151
152# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
153#html_show_sphinx = True
154
155# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
156#html_show_copyright = True
157
158# If true, an OpenSearch description file will be output, and all pages will
159# contain a <link> tag referring to it. The value of this option must be the
160# base URL from which the finished HTML is served.
161#html_use_opensearch = ''
162
163# This is the file name suffix for HTML files (e.g. ".xhtml").
164#html_file_suffix = None
165
166# Output file base name for HTML help builder.
167htmlhelp_basename = 'BeautifulSoupdoc'
168
169
170# -- Options for LaTeX output --------------------------------------------------
171
172# The paper size ('letter' or 'a4').
173#latex_paper_size = 'letter'
174
175# The font size ('10pt', '11pt' or '12pt').
176#latex_font_size = '10pt'
177
178# Grouping the document tree into LaTeX files. List of tuples
179# (source start file, target name, title, author, documentclass [howto/manual]).
180latex_documents = [
181 ('index', 'BeautifulSoup.tex', u'Beautiful Soup Documentation',
182 u'Leonard Richardson', 'manual'),
183]
184
185# The name of an image file (relative to this directory) to place at the top of
186# the title page.
187#latex_logo = None
188
189# For "manual" documents, if this is true, then toplevel headings are parts,
190# not chapters.
191#latex_use_parts = False
192
193# If true, show page references after internal links.
194#latex_show_pagerefs = False
195
196# If true, show URL addresses after external links.
197#latex_show_urls = False
198
199# Additional stuff for the LaTeX preamble.
200#latex_preamble = ''
201
202# Documents to append as an appendix to all manuals.
203#latex_appendices = []
204
205# If false, no module index is generated.
206#latex_domain_indices = True
207
208
209# -- Options for manual page output --------------------------------------------
210
211# One entry per manual page. List of tuples
212# (source start file, name, description, authors, manual section).
213man_pages = [
214 ('index', 'beautifulsoup', u'Beautiful Soup Documentation',
215 [u'Leonard Richardson'], 1)
216]
217
218
219# -- Options for Epub output ---------------------------------------------------
220
221# Bibliographic Dublin Core info.
222epub_title = u'Beautiful Soup'
223epub_author = u'Leonard Richardson'
224epub_publisher = u'Leonard Richardson'
225epub_copyright = u'2012, Leonard Richardson'
226
227# The language of the text. It defaults to the language option
228# or en if the language is not set.
229#epub_language = ''
230
231# The scheme of the identifier. Typical schemes are ISBN or URL.
232#epub_scheme = ''
233
234# The unique identifier of the text. This can be a ISBN number
235# or the project homepage.
236#epub_identifier = ''
237
238# A unique identification for the text.
239#epub_uid = ''
240
241# HTML files that should be inserted before the pages created by sphinx.
242# The format is a list of tuples containing the path and title.
243#epub_pre_files = []
244
245# HTML files shat should be inserted after the pages created by sphinx.
246# The format is a list of tuples containing the path and title.
247#epub_post_files = []
248
249# A list of files that should not be packed into the epub file.
250#epub_exclude_files = []
251
252# The depth of the table of contents in toc.ncx.
253#epub_tocdepth = 3
254
255# Allow duplicate toc entries.
256#epub_tocdup = True
diff --git a/doc.es/source/index.rst b/doc.es/source/index.rst
0new file mode 100644257new file mode 100644
index 0000000..53c47f5
--- /dev/null
+++ b/doc.es/source/index.rst
@@ -0,0 +1,3709 @@
1.. _manual:
2
3=================================
4 Documentación de Beautiful Soup
5=================================
6
7.. py:module:: bs4
8
9.. image:: 6.1.jpg
10 :align: right
11 :alt: "El lacayo-pez empezó por sacarse de debajo del brazo una gran carta,
12 casi tan grande como él."
13
14`Beautiful Soup <http://www.crummy.com/software/BeautifulSoup/>`_ es una
15librería de Python para extraer datos de archivos en formato HTML y XML.
16Trabaja con tu analizador favorito para ofrecer maneras bien definidas
17de navegar, buscar y modificar el árbol analizado. Puede llegar a ahorrar
18horas o días de trabajo a los programadores.
19
20Este manual ilustra con ejemplos la funcionalidades más importantes
21de Beautiful Soup 4. Te muestro las cosas para las que la librería es buena,
22cómo funciona, cómo usarla, cómo hacer lo que quieres y qué hacer cuando
23no se cumplen tus expectativas.
24
25Este documento cubre Beautiful Soup versión 4.12.1. Los ejemplos en este
26documento fueron escritos para Python 3.8.
27
28Podrías estar buscando la documentación de `Beautiful Soup 3
29<http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html>`_.
30Si es así, debes saber que Beautiful Soup 3 ya no se desarrolla y
31su soporte fue abandonado el 31 de diciembre de 2020. Si quieres
32conocer la diferencias entre Beautiful Soup 3 y Beautiful Soup 4,
33mira `Actualizar el código a BS4`_.
34
35Esta documentación ha sido traducida a otras lenguas por los usuarios
36de Beautiful Soup:
37
38* `这篇文档当然还有中文版. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.zh/>`_
39* このページは日本語で利用できます(`外部リンク <http://kondou.com/BS4/>`_)
40* `이 문서는 한국어 번역도 가능합니다. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ko/>`_
41* `Este documento também está disponível em Português do Brasil. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ptbr>`_
42* `Эта документация доступна на русском языке. <https://www.crummy.com/software/BeautifulSoup/bs4/doc.ru/>`_
43
44Cómo conseguir ayuda
45====================
46Si tienes alguna pregunta sobre BeautifulSoup, o si tienes problemas,
47`envía un correo electrónico al grupo de discusión
48<https://groups.google.com/forum/?fromgroups#!forunm/beautifulsoup>`_.
49Si tienes algún problema relacionado con el análisis de un documento HTML,
50asegúrate de mencionar :ref:`lo que la función diagnose() dice <diagnose>`
51sobre dicho documento.
52
53Cuando informes de algún error en esta documentación, por favor,
54indica la traducción que estás leyendo.
55
56===============
57 Inicio rápido
58===============
59
60Este es un documento HTML que usaré como ejemplo a lo largo de este
61documento. Es parte de una historia de `Alicia en el país de las maravillas`::
62
63 html_doc = """<html><head><title>The Dormouse's story</title></head>
64 <body>
65 <p class="title"><b>The Dormouse's story</b></p>
66
67 <p class="story">Once upon a time there were three little sisters; and their names were
68 <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
69 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
70 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
71 and they lived at the bottom of a well.</p>
72
73 <p class="story">...</p>
74 """
75
76Al procesar el documento de "Las tres hermanas" en Beautiful Soup, se nos
77devuelve un objeto :py:class:`BeautifulSoup`, que representa el
78documento como una estructura de datos anidada::
79
80 from bs4 import BeautifulSoup
81 soup = BeautifulSoup(html_doc, 'html.parser')
82
83 print(soup.prettify())
84 # <html>
85 # <head>
86 # <title>
87 # The Dormouse's story
88 # </title>
89 # </head>
90 # <body>
91 # <p class="title">
92 # <b>
93 # The Dormouse's story
94 # </b>
95 # </p>
96 # <p class="story">
97 # Once upon a time there were three little sisters; and their names were
98 # <a class="sister" href="http://example.com/elsie" id="link1">
99 # Elsie
100 # </a>
101 # ,
102 # <a class="sister" href="http://example.com/lacie" id="link2">
103 # Lacie
104 # </a>
105 # and
106 # <a class="sister" href="http://example.com/tillie" id="link3">
107 # Tillie
108 # </a>
109 # ; and they lived at the bottom of a well.
110 # </p>
111 # <p class="story">
112 # ...
113 # </p>
114 # </body>
115 # </html>
116
117Estas son algunas de las maneras sencillas para navegar
118por la estructura de datos::
119
120 soup.title
121 # <title>The Dormouse's story</title>
122
123 soup.title.name
124 # u'title'
125
126 soup.title.string
127 # u'The Dormouse's story'
128
129 soup.title.parent.name
130 # u'head'
131
132 soup.p
133 # <p class="title"><b>The Dormouse's story</b></p>
134
135 soup.p['class']
136 # u'title'
137
138 soup.a
139 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
140
141 soup.find_all('a')
142 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
143 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
144 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
145
146 soup.find(id="link3")
147 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
148
149Una tarea frecuente es extraer todas las URL encontradas en las etiquetas
150<a> de una página::
151
152 for link in soup.find_all('a'):
153 print(link.get('href'))
154 # http://example.com/elsie
155 # http://example.com/lacie
156 # http://example.com/tillie
157
158Otra tarea habitual es extraer todo el texto de una página::
159
160 print(soup.get_text())
161 # The Dormouse's story
162 #
163 # The Dormouse's story
164 #
165 # Once upon a time there were three little sisters; and their names were
166 # Elsie,
167 # Lacie and
168 # Tillie;
169 # and they lived at the bottom of a well.
170 #
171 # ...
172
173¿Esto se parece a lo que necesitas? Si es así, sigue leyendo.
174
175=========================
176 Instalar Beautiful Soup
177=========================
178Si usas una versión reciente de Debian o Ubuntu Linux, puedes instalar
179Beautiful Soup con el gestor de paquetes del sistema:
180
181:kbd:`$ apt-get install python3-bs4`
182
183Beautiful Soup 4 está publicado en Pypi, así que si no puedes instalarlo
184con el gestor de paquetes, puedes instalarlo con ``easy_install`` o
185``pip``. El nombre del paquete es ``beautifulsoup4``. Asegúrate de que
186usas la versión correcta de ``pip`` o ``easy_install`` para tu versión
187de Python (podrían llamarse ``pip3`` y ``easy_install3``, respectivamente):
188
189:kbd:`$ easy_install beautifulsoup4`
190
191:kbd:`$ pip install beautifulsoup4`
192
193(El paquete :py:class:`BeautifulSoup` ``no`` es el que quieres. Ese es
194el lanzamiento anterior `Beautiful Soup 3`_. Muchos *software* utilizan
195BS3, así que aún está disponible, pero si estás escribiendo nuevo código,
196deberías instalar ``beautifulsoup4``).
197
198Si no tienes ``easy_install`` o ``pip`` instalados, puedes
199`descargar el código de Beautiful Soup 4 comprimido en un tarball
200<http://www.crummy.com/software/BeautifulSoup/download/4.x/>`_ e
201instalarlo con ``setup.py``:
202
203:kbd:`$ python setup.py install`
204
205Si aún así todo falla, la licencia de Beautiful Soup te permite
206empaquetar la librería completa con tu aplicación. Puedes descargar
207el *tarball*, copiar su directorio ``bs4`` en tu base de código y
208usar Beautiful Soup sin instalarlo en absoluto.
209
210Yo empleo Python 3.10 para desarrollar Beautiful Soup, aunque debería
211funcionar con otras versiones recientes.
212
213.. _parser-installation:
214
215
216Instalar un analizador
217======================
218
219Beautiful Soup soporta el analizador de HTML incluido en la librería
220estándar de Python, aunque también soporta varios analizadores de
221Python de terceros. Uno de ellos es el `analizador de lxml <http://lxml.de/>`_.
222Dependiendo de tu instalación, puedes instalar lxml con uno de los
223siguientes comandos:
224
225:kbd:`$ apt-get install python-lxml`
226
227:kbd:`$ easy_install lxml`
228
229:kbd:`$ pip install lxml`
230
231Otra alternativa es usar el analizador de Python de
232`html5lib <http://code.google.com/p/html5lib/>`_,
233el cual analiza HTML de la misma manera en la que lo haría
234un navegador web. Dependiendo de tu instalación, puedes instalar
235html5lib con uno de los siguientes comandos:
236
237:kbd:`$ apt-get install python-html5lib`
238
239:kbd:`$ easy_install html5lib`
240
241:kbd:`$ pip install html5lib`
242
243Esta tabla resume las ventajas e inconvenientes de cada librería de los analizadores:
244
245+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
246| Analizador | Uso típico | Ventajas | Desventajas |
247+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
248| html.parser de Python | ``BeautifulSoup(markup, "html.parser")`` | * Ya incluido | * No tan rápido como lxml, |
249| | | * Rapidez decente | menos tolerante que |
250| | | * Tolerante (en Python 3.2) | html5lib. |
251+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
252| Analizador HTML de | ``BeautifulSoup(markup, "lxml")`` | * Muy rápido | * Dependencia externa de C |
253| lxml | | * Tolerante | |
254+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
255| Analizador XML de | ``BeautifulSoup(markup, "lxml-xml")`` | * Muy rápido | * Dependencia externa de C |
256| lxml | ``BeautifulSoup(markup, "xml")`` | * El único analizador XML | |
257| | | actualmente soportado | |
258+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
259| html5lib | ``BeautifulSoup(markup, "html5lib")`` | * Extremadamente tolerante | * Muy lento |
260| | | * Analiza las páginas de la misma | * Dependencia externa de |
261| | | manera que un navegador web | Python |
262| | | * Crea HTML5 válido | |
263+-----------------------+--------------------------------------------+-----------------------------------+-----------------------------+
264
265Si puedes, te recomiendo que instales y uses lxml para mayor velocidad.
266
267Ten en cuenta que si un documento es inválido, analizadores diferentes
268generarán árboles de Beautiful Soup diferentes para él. Mira
269`Diferencias entre analizadores`_ para más detalle.
270
271==================
272 Haciendo la sopa
273==================
274
275Para analizar un documento pásalo al constructor de :py:class:`BeautifulSoup`.
276Puedes pasar una cadena de caracteres o abrir un manejador de archivos::
277
278 from bs4 import BeautifulSoup
279
280 with open("index.html") as fp:
281 soup = BeautifulSoup(fp, 'html.parser')
282
283 soup = BeautifulSoup("<html>a web page</html>", 'html.parser')
284
285Primero, el documento se convierte a Unicode, y las entidades HTML se
286convierten a caracteres Unicode::
287
288 print(BeautifulSoup("<html><head></head><body>Sacr&eacute; bleu!</body></html>", "html.parser"))
289 # <html><head></head><body>Sacré bleu!</body></html>
290
291Entonces Beautiful Soup analiza el documento usando el mejor analizador
292disponible. Usará un analizador HTML a no ser que se especifique que se
293use un analizador XML (ver `Analizar XML`_).
294
295==================
296 Tipos de objetos
297==================
298
299Beautiful Soup transforma un complejo documento HTML en un complejo árbol de objetos
300de Python. Pero tan solo tendrás que lidiar con cuatro `tipos` de objetos: :py:class:`Tag`,
301:py:class:`NavigableString`, :py:class:`BeautifulSoup` y :py:class:`Comment`.
302
303.. py:class:: Tag
304
305 Un objeto :py:class:`Tag` corresponde a una etiqueta XML o HTML en el documento
306 original.
307
308 ::
309
310 soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
311 tag = soup.b
312 type(tag)
313 # <class 'bs4.element.Tag'>
314
315 Las etiquetas tienen muchos atributos y métodos, y cubriré la mayoría de ellos en
316 `Navegar por el árbol`_ y `Buscar en el árbol`_. Por ahora, las características
317 más importantes de una etiqueta son su nombre y sus atributos.
318
319 .. py:attribute:: name
320
321 Toda etiqueta tiene un nombre::
322
323 tag.name
324 # 'b'
325
326
327 Si cambias el nombre de una etiqueta, el cambio se verá reflejado en
328 cualquier especificación generada por Beautiful Soup a partir de entonces::
329
330 tag.name = "blockquote"
331 tag
332 # <blockquote class="boldest">Extremely bold</blockquote>
333
334 .. py:attribute:: attrs
335
336 Una etiqueta HTML o XML puede tener cualquier cantidad de atributos.
337 La etiqueta ``<b id="boldest">`` tiene un atributo "id" cuyo valor
338 es "boldest". Puedes acceder a los atributos de una etiqueta
339 usándola como un diccionario::
340
341 tag = BeautifulSoup('<b id="boldest">bold</b>', 'html.parser').b
342 tag['id']
343 # 'boldest'
344
345 Puedes acceder a los atributos del diccionario directamente con ``.attrs``::
346
347 tag.attrs
348 # {'id': 'boldest'}
349
350 Puedes añadir, quitar y modificar los atributos de una etiqueta. De nuevo, esto
351 se realiza usando la etiqueta como un diccionario::
352
353 tag['id'] = 'verybold'
354 tag['another-attribute'] = 1
355 tag
356 # <b another-attribute="1" id="verybold"></b>
357
358 del tag['id']
359 del tag['another-attribute']
360 tag
361 # <b>bold</b>
362
363 tag['id']
364 # KeyError: 'id'
365 tag.get('id')
366 # None
367
368 .. _multivalue:
369
370 Atributos multivaluados
371 -----------------------
372
373 HTML 4 define algunos atributos que pueden tomar múltiples valores. HTML 5
374 elimina un par de ellos, pero define unos cuantos más. El atributo multivaluado
375 más común es ``class`` (esto es, una etiqueta puede tener más de una clase de CSS).
376 Otros incluyen ``rel``, ``rev``, ``accept-charset``, ``headers`` y ``accesskey``.
377 Por defecto, Beautiful Soup transforma los valores de un atributo multivaluado en
378 una lista::
379
380 css_soup = BeautifulSoup('<p class="body"></p>', 'html.parser')
381 css_soup.p['class']
382 # ['body']
383
384 css_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser')
385 css_soup.p['class']
386 # ['body', 'strikeout']
387
388 Si un atributo `parece` que tiene más de un valor, pero no es un atributo
389 multivaluado definido como tal por ninguna versión del estándar de HTML,
390 Beautiful Soup no modificará el atributo::
391
392 id_soup = BeautifulSoup('<p id="my id"></p>', 'html.parser')
393 id_soup.p['id']
394 # 'my id'
395
396 Cuando transformas una etiqueta en una cadena de caracteres, muchos atributos
397 se combinan::
398
399 rel_soup = BeautifulSoup('<p>Back to the <a rel="index first">homepage</a></p>', 'html.parser')
400 rel_soup.a['rel']
401 # ['index', 'first']
402 rel_soup.a['rel'] = ['index', 'contents']
403 print(rel_soup.p)
404 # <p>Back to the <a rel="index contents">homepage</a></p>
405
406 Puedes forzar que todos los atributos sean analizados como cadenas
407 de caracteres pasando ``multi_valued_attributes=None`` como argumento
408 clave en el constructor de :py:class:`BeautifulSoup`::
409
410 no_list_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser', multi_valued_attributes=None)
411 no_list_soup.p['class']
412 # 'body strikeout'
413
414 Puedes usar ``get_attribute_list`` para obtener un valor que siempre sea una lista,
415 sin importar si es un atributo multivaluado::
416
417 id_soup.p.get_attribute_list('id')
418 # ["my id"]
419
420 Si analizas un documento como XML, no hay atributos multivaluados::
421
422 xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
423 xml_soup.p['class']
424 # 'body strikeout'
425
426 Una vez más, puedes configurar esto usando el argumento ``multi_valued_attributes`` ::
427
428 class_is_multi= { '*' : 'class'}
429 xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml', multi_valued_attributes=class_is_multi)
430 xml_soup.p['class']
431 # ['body', 'strikeout']
432
433 Probablemente no tengas que hacer esto, pero si lo necesitas, usa los
434 parámetros por defecto como guía. Implementan las reglas descritas en la
435 especificación de HTML::
436
437 from bs4.builder import builder_registry
438 builder_registry.lookup('html').DEFAULT_CDATA_LIST_ATTRIBUTES
439
440.. py:class:: NavigableString
441
442-----------------------------
443
444Un *string* corresponde a un trozo de texto en una etiqueta. Beautiful Soup usa la clase
445:py:class:`NavigableString` para contener estos trozos de texto::
446
447 soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
448 tag = soup.b
449 tag.string
450 # 'Extremely bold'
451 type(tag.string)
452 # <class 'bs4.element.NavigableString'>
453
454Un :py:class:`NavigableString` es como una cadena de caracteres de Python Unicode,
455exceptuando que también soporta algunas de las características descritas en
456`Navegar por el árbol`_ y `Buscar en el árbol`_. Puedes convertir un objeto
457:py:class:`NavigableString` a una cadena de caracteres Unicode usando ``str``::
458
459 unicode_string = str(tag.string)
460 unicode_string
461 # 'Extremely bold'
462 type(unicode_string)
463 # <type 'str'>
464
465No puedes editar dicha cadena, pero puedes reemplazar una cadena por otra, usando
466:ref:`replace_with()`::
467
468 tag.string.replace_with("No longer bold")
469 tag
470 # <b class="boldest">No longer bold</b>
471
472:py:class:`NavigableString` soporta la mayoría de las características descritas en
473`Navegar por el árbol`_ y `Buscar en el árbol`_, pero no todas.
474En particular, como una cadena no puede contener nada (la manera en la que
475una etiqueta contiene una cadena de caracteres u otra etiqueta), *strings* no
476admiten los atributos `.contents`` o ``.string``, o el método ``find()``.
477
478Si quieres usar un :py:class:`NavigableString` fuera de Beautiful Soup,
479deberías llamar ``unicode()`` sobre él para convertirlo en una cadena de caracteres
480de Python Unicode. Si no, tu cadena arrastrará una referencia a todo el árbol analizado
481de Beautiful Soup, incluso cuando hayas acabado de utilizar Beautiful Soup. Esto es un
482gran malgasto de memoria.
483
484.. py:class:: BeautifulSoup
485
486---------------------------
487
488El objeto :py:class:`BeautifulSoup` representa el documento analizado
489en su conjunto. Para la mayoría de propósitos, puedes usarlo como un objeto
490:py:class:`Tag`. Esto significa que soporta la mayoría de métodos descritos
491en `Navegar por el árbol`_ and `Buscar en el árbol`_.
492
493Puedes también pasar un objeto :py:class:`BeautifulSoup` en cualquiera de
494los métodos definidos en `Modificar el árbol`_, como si fuese un :py:class:`Tag`.
495Esto te permite hacer cosas como combinar dos documentos analizados::
496
497 doc = BeautifulSoup("<document><content/>INSERT FOOTER HERE</document", "xml")
498 footer = BeautifulSoup("<footer>Here's the footer</footer>", "xml")
499 doc.find(text="INSERT FOOTER HERE").replace_with(footer)
500 # 'INSERT FOOTER HERE'
501 print(doc)
502 # <?xml version="1.0" encoding="utf-8"?>
503 # <document><content/><footer>Here's the footer</footer></document>
504
505Como un objeto :py:class:`BeautifulSoup` no corresponde realmente con una
506etiqueta HTML o XML, no tiene nombre ni atributos. Aún así, es útil
507comprobar su ``.name``, así que se le ha dado el ``.name`` especial
508"[document]"::
509
510 soup.name
511 # '[document]'
512
513Cadenas especiales
514==================
515
516:py:class:`Tag`, :py:class:`NavigableString` y
517:py:class:`BeautifulSoup` cubren la mayoría de todo lo que verás en
518un archivo HTML o XML, aunque aún quedan algunos remanentes. El principal
519que probablemente encuentres es el :py:class:`Comment`.
520
521.. py:class:: Comment
522
523::
524
525 markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
526 soup = BeautifulSoup(markup, 'html.parser')
527 comment = soup.b.string
528 type(comment)
529 # <class 'bs4.element.Comment'>
530
531El objeto :py:class:`Comment` es solo un tipo especial de :py:class:`NavigableString`::
532
533 comment
534 # 'Hey, buddy. Want to buy a used parser'
535
536Pero cuando aparece como parte de un documento HTML, un :py:class:`Comment`
537se muestra con un formato especial::
538
539 print(soup.b.prettify())
540 # <b>
541 # <!--Hey, buddy. Want to buy a used parser?-->
542 # </b>
543
544Para documentos HTML
545--------------------
546
547Beautiful Soup define algunas subclases de :py:class:`NavigableString`
548para contener cadenas de caracteres encontradas dentro de etiquetas
549HTML específicas. Esto hace más fácil tomar el cuerpo principal de la
550página, ignorando cadenas que probablemente representen directivas de
551programación encontradas dentro de la página. `(Estas clases son nuevas
552en Beautiful Soup 4.9.0, y el analizador html5lib no las usa)`.
553
554.. py:class:: Stylesheet
555
556Una subclase de :py:class:`NavigableString` que representa hojas de estilo
557CSS embebidas; esto es, cualquier cadena en una etiqueta
558``<style>`` durante el análisis del documento.
559
560.. py:class:: Script
561
562Una subclase de :py:class:`NavigableString` que representa
563JavaScript embebido; esto es, cualquier cadena en una etiqueta
564``<script>`` durante el análisis del documento.
565
566.. py:class:: Template
567
568Una subclase de :py:class:NavigableString` que representa plantillas
569HTML embebidas; esto es, cualquier cadena en una etiqueta ``<template>``
570durante el análisis del documento.
571
572Para documentos XML
573-------------------
574
575Beautiful Soup define algunas clases :py:class:`NavigableString`
576para contener tipos especiales de cadenas de caracteres que pueden
577ser encontradas en documentos XML. Como :py:class:`Comment`, estas
578clases son subclases de :py:class:`NavigableString` que añaden
579algo extra a la cadena de caracteres en la salida.
580
581.. py:class:: Declaration
582
583Una subclase de :py:class:`NavigableString` que representa la
584`declaración <https://www.w3.org/TR/REC-xml/#sec-prolog-dtd>`_ al
585principio de un documento XML.
586
587.. py:class:: Doctype
588
589Una subclase de :py:class:`NavigableString` que representa la
590`declaración del tipo de documento <https://www.w3.org/TR/REC-xml/#dt-doctype>`_
591que puede encontrarse cerca del comienzo de un documento XML.
592
593.. py:class:: CData
594
595Una subclase de :py:class:`NavigableString` que representa una
596`sección CData <https://www.w3.org/TR/REC-xml/#sec-cdata-sect>`_.
597
598.. py:class:: ProcessingInstruction
599
600Una subclase de :py:class:`NavigableString` que representa el contenido de
601una `instrucción de procesamiento XML <https://www.w3.org/TR/REC-xml/#sec-pi>`_.
602
603
604======================
605 Navegar por el árbol
606======================
607
608Aquí está el documento HTML de las "Tres hermanas" de nuevo::
609
610 html_doc = """
611 <html><head><title>The Dormouse's story</title></head>
612 <body>
613 <p class="title"><b>The Dormouse's story</b></p>
614
615 <p class="story">Once upon a time there were three little sisters; and their names were
616 <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
617 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
618 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
619 and they lived at the bottom of a well.</p>
620
621 <p class="story">...</p>
622 """
623
624 from bs4 import BeautifulSoup
625 soup = BeautifulSoup(html_doc, 'html.parser')
626
627Usaré este como ejemplo para enseñarte cómo mover una parte de un
628documento a otra.
629
630Bajar
631=====
632
633Las etiquetas pueden contener cadenas u otras etiquetas. Estos elementos
634son los hijos (`children`) de la etiqueta. Beautiful Soup ofrece muchos
635atributos para navegar e iterar por los hijos de una etiqueta.
636
637Debe notarse que las cadenas de Beautiful Soup no soportan ninguno
638de estos atributos, porque una cadena no puede tener hijos.
639
640Navegar usando nombres de etiquetas
641-----------------------------------
642
643La manera más simple de navegar por el árbol analizado es indicar
644el nombre de la etiqueta que quieres. Si quieres la etiqueta <head>,
645tan solo indica ``soup.head``::
646
647 soup.head
648 # <head><title>The Dormouse's story</title></head>
649
650 soup.title
651 # <title>The Dormouse's story</title>
652
653Puedes usar este truco una y otra vez para acercarte a una parte concreta
654del árbol analizado. Este código obtiene la primera etiqueta <b> dentro
655de la etiqueta <body>::
656
657 soup.body.b
658 # <b>The Dormouse's story</b>
659
660Usar el nombre de la etiqueta como atributo te dará solo la `primera`
661etiqueta con ese nombre::
662
663 soup.a
664 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
665
666Si necesitas obtener `todas` las etiquetas <a>, o cualquier
667cosa más complicada que la primera etiqueta con cierto nombre, tendrás
668que usar uno de los métodos descritos en `Buscar en el árbol`_, como
669`find_all()`::
670
671 soup.find_all('a')
672 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
673 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
674 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
675
676``.contents`` y ``.children``
677-----------------------------
678
679Los hijos de una etiqueta están disponibles en una lista llamada
680``.contents``::
681
682 head_tag = soup.head
683 head_tag
684 # <head><title>The Dormouse's story</title></head>
685
686 head_tag.contents
687 # [<title>The Dormouse's story</title>]
688
689 title_tag = head_tag.contents[0]
690 title_tag
691 # <title>The Dormouse's story</title>
692 title_tag.contents
693 # ['The Dormouse's story']
694
695El objeto :py:class:`BeautifulSoup` por sí solo ya tiene hijos. En este caso,
696la etiqueta <html> is hija del objeto :py:class:`BeautifulSoup`.::
697
698 len(soup.contents)
699 # 1
700 soup.contents[0].name
701 # 'html'
702
703Una cadena no tiene ``.contents``, porque no puede contener nada::
704
705 text = title_tag.contents[0]
706 text.contents
707 # AttributeError: 'NavigableString' object has no attribute 'contents'
708
709En lugar de obtenerlos como una lista, puedes iterar sobre los hijos
710de una etiqueta usando el generador ``.children``::
711
712 for child in title_tag.children:
713 print(child)
714 # The Dormouse's story
715
716Si quieres modificar los hijos de una etiqueta, emplea los métodos
717descritos en `Modificar el árbol`_. No modifiques la lista
718``.contents`` directamente: eso podría ocasionar problemas que pueden
719ser sutiles y difíciles de detectar.
720
721
722``.descendants``
723----------------
724
725Los atributos ``.contents`` y ``.children`` tan solo consideran los
726hijos `directos` de una etiqueta. Por ejemplo, la etiqueta <head>
727tiene un único hijo directo--la etiqueta <title>::
728
729 head_tag.contents
730 # [<title>The Dormouse's story</title>]
731
732Pero la etiqueta <title> tiene un hijo: la cadena "The Dormouse's
733story". Puede dar la sensación de que esa cadena es también hija de
734la etiqueta <head>. El atributo ``.descendants`` te permite iterar
735sobre `todos` los hijos de una etiqueta recursivamente: sus hijos,
736hijos de sus hijos directos, y así sucesivamente::
737
738 for child in head_tag.descendants:
739 print(child)
740 # <title>The Dormouse's story</title>
741 # The Dormouse's story
742
743La etiqueta <head> tiene un solo hijo, pero tiene dos descendientes:
744la etiqueta <title> y el hijo de la etiqueta <title>. El objeto
745:py:class:`BeautifulSoup` tiene un hijo directo (la etiqueta <html>), pero
746tiene otros muchos descendientes::
747
748 len(list(soup.children))
749 # 1
750 len(list(soup.descendants))
751 # 26
752
753.. _.string:
754
755``.string``
756-----------
757
758Si una etiqueta tiene solo un hijo, y dicho hijo es un :py:class:`NavigableString`,
759el hijo se obtiene mediante ``.string``::
760
761 title_tag.string
762 # 'The Dormouse's story'
763
764Si el único hijo de una etiqueta es otra etiqueta, y `esa`
765etiqueta tiene un ``.string``, entonces se considera que
766la etiqueta madre tiene el mismo ``.string`` que su hijo::
767
768 head_tag.contents
769 # [<title>The Dormouse's story</title>]
770
771 head_tag.string
772 # 'The Dormouse's story'
773
774Si una etiqueta contiene más una cadena, entonces no está claro
775a qué se debería referir ``.string``, así que ``.string``
776pasa a valer ``None``::
777
778 print(soup.html.string)
779 # None
780
781.. _string-generators:
782
783``.strings`` y ``stripped_strings``
784-----------------------------------
785
786Si hay más de una cosa dentro de una etiqueta, puedes seguir
787obteniendo las cadenas. Usa el generador ``.string``::
788
789 for string in soup.strings:
790 print(repr(string))
791 '\n'
792 # "The Dormouse's story"
793 # '\n'
794 # '\n'
795 # "The Dormouse's story"
796 # '\n'
797 # 'Once upon a time there were three little sisters; and their names were\n'
798 # 'Elsie'
799 # ',\n'
800 # 'Lacie'
801 # ' and\n'
802 # 'Tillie'
803 # ';\nand they lived at the bottom of a well.'
804 # '\n'
805 # '...'
806 # '\n'
807
808Estas cadenas tienden a tener muchos espacios en blanco extra, los
809cuales puedes quitar usando el generador ``.stripped_strings``::
810
811 for string in soup.stripped_strings:
812 print(repr(string))
813 # "The Dormouse's story"
814 # "The Dormouse's story"
815 # 'Once upon a time there were three little sisters; and their names were'
816 # 'Elsie'
817 # ','
818 # 'Lacie'
819 # 'and'
820 # 'Tillie'
821 # ';\n and they lived at the bottom of a well.'
822 # '...'
823
824Aquí, las cadenas que consisten completamente en espacios en blanco
825se ignoran, y espacios en blanco al principio y final de las cadenas
826se eliminan.
827
828Subir
829=====
830
831Continuando con la analogía del árbol genealógico, toda etiqueta
832tiene una `madre`: la etiqueta que la contiene.
833
834.. _.parent:
835
836``.parent``
837-----------
838
839Puedes acceder a la madre de una etiqueta con el atributo ``.parent``. En
840el ejemplo de "Las tres hermanas", la etiqueta <head> es la madre
841de la etiqueta <title>::
842
843 title_tag = soup.title
844 title_tag
845 # <title>The Dormouse's story</title>
846 title_tag.parent
847 # <head><title>The Dormouse's story</title></head>
848
849El texto de título tiene una madre: la etiqueta <title> que lo
850contiene::
851
852 title_tag.string.parent
853 # <title>The Dormouse's story</title>
854
855La madre de una etiqueta de alto nivel como <html> es el objeto :py:class:`BeautifulSoup`
856mismo::
857
858 html_tag = soup.html
859 type(html_tag.parent)
860 # <class 'bs4.BeautifulSoup'>
861
862Y el ``.parent`` de un objeto :py:class:`BeautifulSoup` se define como ``None``::
863
864 print(soup.parent)
865 # None
866
867.. _.parents:
868
869``.parents``
870------------
871
872Puedes iterar sobre todas las madres de los elementos con
873``.parents``. Este ejemplo usa ``.parent`` para moverse' de una
874etiqueta <a> en medio del documento a lo más alto del documento::
875
876 link = soup.a
877 link
878 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
879 for parent in link.parents:
880 print(parent.name)
881 # p
882 # body
883 # html
884 # [document]
885
886Hacia los lados
887===============
888
889Considera un documento sencillo como este::
890
891 sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></a>", 'html.parser')
892 print(sibling_soup.prettify())
893 # <a>
894 # <b>
895 # text1
896 # </b>
897 # <c>
898 # text2
899 # </c>
900 # </a>
901
902Las etiquetas <b> y <c> están al mismo nivel: son hijas directas de la misma
903etiqueta. Las llamamos `hermanas`. Cuando un documento está bien formateado,
904las hermanas están al mismo nivel de sangría. Puedes usar también esta
905relación en el código que escribas.
906
907``.next_sibling`` y ``.previous_sibling``
908-----------------------------------------
909
910Puedes usar ``.next_sibling`` y ``.previous_sibling`` para navegar
911entre elementos de la página que están al mismo nivel del árbol
912analizado::
913
914 sibling_soup.b.next_sibling
915 # <c>text2</c>
916
917 sibling_soup.c.previous_sibling
918 # <b>text1</b>
919
920La etiqueta <b> tiene un ``.next_sibling``, pero no ``.previous_sibling``,
921porque no hay nada antes de la etiqueta <b> `al mismo nivel del árbol`.
922Por la misma razón, la etiqueta <c> tiene un ``.previous_sibling`` pero no
923un ``.next_sibling``::
924
925 print(sibling_soup.b.previous_sibling)
926 # None
927 print(sibling_soup.c.next_sibling)
928 # None
929
930Las cadenas "text1" y "text2" `no` son hermanas, porque no tienen la misma
931madre::
932
933 sibling_soup.b.string
934 # 'text1'
935
936 print(sibling_soup.b.string.next_sibling)
937 # None
938
939En documentos reales, los ``.next_sibling`` o ``.previous_sibling`` de
940una etiqueta normalmente serán cadenas que contengan espacios en blanco.
941Retomando el documento de "Las tres hermanas"::
942
943 # <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
944 # <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
945 # <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
946
947Podrías pensar que la ``.next_sibling`` de la primera etiqueta <a> podría
948ser la segunda etiqueta <a>. Pero realmente es una cadena de caracteres:
949la coma y el salto de línea que separan la primera etiqueta <a> de la
950segunda::
951
952 link = soup.a
953 link
954 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
955
956 link.next_sibling
957 # ',\n '
958
959La segunda etiqueta <a> es realmente la ``.next_sibling`` de la coma::
960
961 link.next_sibling.next_sibling
962 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
963
964.. _sibling-generators:
965
966``.next_siblings`` y ``.previous_siblings``
967-------------------------------------------
968
969Puedes iterar sobre las hermanas de una etiqueta con ``.next_siblings`` o
970``.previuos_siblings``::
971
972 for sibling in soup.a.next_siblings:
973 print(repr(sibling))
974 # ',\n'
975 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
976 # ' and\n'
977 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
978 # '; and they lived at the bottom of a well.'
979
980 for sibling in soup.find(id="link3").previous_siblings:
981 print(repr(sibling))
982 # ' and\n'
983 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
984 # ',\n'
985 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
986 # 'Once upon a time there were three little sisters; and their names were\n'
987
988Hacia delante y hacia atrás
989===========================
990
991Échale un vistazo al comienzo del documento de "Las tres hermanas"::
992
993 # <html><head><title>The Dormouse's story</title></head>
994 # <p class="title"><b>The Dormouse's story</b></p>
995
996Un analizador HTML toma esta cadena de caracteres y la convierte en
997una serie de eventos: "se abre una etiqueta <html>", "se abre una
998etiqueta <head>", "se abre una etiqueta <title>", "se añade una cadena",
999"se cierra la etiqueta <title>", "se abre una etiqueta <p>" y así
1000sucesivamente. Beautiful Soup ofrece herramientas para reconstruir
1001el análisis inicial del documento.
1002
1003.. _element-generators:
1004
1005``.next_element`` y ``.previous_element``
1006-----------------------------------------
1007
1008El atributo ``.next_element`` de una cadena o etiqueta apunta a cualquiera
1009que fue analizado inmediatamente después. Podría ser igual que ``.next_sibling``,
1010pero normalmente es drásticamente diferente.
1011
1012Aquí está la etiqueta final <a> en el documento de "Las tres hermanas".
1013Su ``..next_sibling`` es una cadena: la terminación de la oración fue
1014interrumpida por el comienzo de la etiqueta <a>.::
1015
1016 last_a_tag = soup.find("a", id="link3")
1017 last_a_tag
1018 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
1019
1020 last_a_tag.next_sibling
1021 # ';\nand they lived at the bottom of a well.'
1022
1023Pero el ``.next_element`` de esa etiqueta <a>, lo que fue analizado
1024inmediatamente después de la etiqueta <a>, `no` es el resto de la
1025oración: es la palabra "Tillie"::
1026
1027 last_a_tag.next_element
1028 # 'Tillie'
1029
1030Esto se debe a que en el marcado original, la palabra "Tillie"
1031aparece antes del punto y coma. El analizador se encontró con
1032una etiqueta <a>, después la palabra "Tillie", entonces la etiqueta
1033de cierre </a>, después el punto y coma y el resto de la oración.
1034El punto y coma está al mismo nivel que la etiqueta <a>, pero
1035la palabra "Tillie" se encontró primera.
1036
1037El atributo ``.previous_element`` es exactamente el opuesto
1038de ``.next_element``. Apunta a cualquier elemento que
1039fue analizado inmediatamente antes que este::
1040
1041 last_a_tag.previous_element
1042 # ' and\n'
1043 last_a_tag.previous_element.next_element
1044 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
1045
1046``.next_elements`` y ``.previous_elements``
1047-------------------------------------------
1048
1049Ya te estarás haciendo a la idea. Puedes usar estos iteradores
1050para moverte hacia delante y hacia atrás en el documento tal y como
1051fue analizado::
1052
1053 for element in last_a_tag.next_elements:
1054 print(repr(element))
1055 # 'Tillie'
1056 # ';\nand they lived at the bottom of a well.'
1057 # '\n'
1058 # <p class="story">...</p>
1059 # '...'
1060 # '\n'
1061
1062======================
1063 Buscar en el árbol
1064======================
1065
1066Beautiful Soup define una gran cantidad de métodos para buscar en
1067el árbol analizado, pero todos son muy similares. Dedicaré mucho
1068tiempo explicando los dos métodos más populares: ``find()`` y
1069``find_all()``. Los otros métodos toman casi los mismos argumentos,
1070así que los cubriré brevemente.
1071
1072De nuevo, usaré el documento de "Las tres hermanas" como ejemplo::
1073
1074 html_doc = """
1075 <html><head><title>The Dormouse's story</title></head>
1076 <body>
1077 <p class="title"><b>The Dormouse's story</b></p>
1078
1079 <p class="story">Once upon a time there were three little sisters; and their names were
1080 <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
1081 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
1082 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
1083 and they lived at the bottom of a well.</p>
1084
1085 <p class="story">...</p>
1086 """
1087
1088 from bs4 import BeautifulSoup
1089 soup = BeautifulSoup(html_doc, 'html.parser')
1090
1091Empleando en un filtro un argumento como ``find_all()``, puedes
1092"acercar" aquellas partes del documento en las que estés interesado.
1093
1094Tipos de filtros
1095================
1096
1097Antes de entrar en detalle sobre ``find_all()`` y métodos similares,
1098me gustaría mostrar ejemplos de diferentes filtros que puedes
1099utilizar en estos métodos. Estos filtros aparecen una y otra vez a lo
1100largo de la API. Puedes usarlos para filtrar basándote en el nombre de
1101una etiqueta, en sus atributos, en el texto de una cadena, o en alguna
1102combinación de estos.
1103
1104.. _a string:
1105
1106Una cadena
1107----------
1108
1109El filtro más simple es una cadena. Pasa una cadena a un método de
1110búsqueda y Beautiful Soup buscará un resultado para esa cadena
1111exactamente. Este código encuentra todas las etiquetas <b> en el
1112documento::
1113
1114 soup.find_all('b')
1115 # [<b>The Dormouse's story</b>]
1116
1117Si pasas un cadena de *bytes*, Beautiful Soup asumirá que la cadena
1118está codificada como UTF-8. Puedes evitar esto pasando una cadena
1119Unicode.
1120
1121.. _a regular expression:
1122
1123Una expresión regular
1124---------------------
1125
1126Si pasas un objeto que sea una expresión regular, Beautiful Soup filtrará
1127mediante dicho expresión regular usando si su método ``search()``. Este
1128código encuentra todas las etiquetas cuyo nombre empiece por la letra
1129"b"; en este caso, las etiquetas <body> y <b>::
1130
1131 import re
1132 for tag in soup.find_all(re.compile("^b")):
1133 print(tag.name)
1134 # body
1135 # b
1136
1137Este código encuentra todas las etiquetas cuyo nombre contiene
1138la letra 't'::
1139
1140 for tag in soup.find_all(re.compile("t")):
1141 print(tag.name)
1142 # html
1143 # title
1144
1145.. _a list:
1146
1147Una lista
1148---------
1149
1150Si pasas una lista, Beautiful Soup hará una búsqueda por cadenas
1151con `cualquier` elemento en dicha lista. Este código encuentra
1152todas las etiquetas <a> `y` todas las etiquetas <b>::
1153
1154 soup.find_all(["a", "b"])
1155 # [<b>The Dormouse's story</b>,
1156 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1157 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1158 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1159
1160.. _the value True:
1161
1162``True``
1163--------
1164
1165El valor ``True`` empareja todo lo que pueda. Este código encuentra
1166``todas`` las etiquetas del documento, pero ninguna de las cadenas
1167de texto::
1168
1169 for tag in soup.find_all(True):
1170 print(tag.name)
1171 # html
1172 # head
1173 # title
1174 # body
1175 # p
1176 # b
1177 # p
1178 # a
1179 # a
1180 # a
1181 # p
1182
1183.. a function:
1184
1185Una función
1186-----------
1187
1188Si ninguna de las formas de búsqueda anteriores te sirven, define
1189una función que tome un elemento como su único argumento. La función
1190debería devolver ``True`` si el argumento se corresponde con lo indicado
1191en la función, y ``Falso`` en cualquier otro caso.
1192
1193Esta es una función que devuelve ``True`` si una etiqueta tiene
1194definida el atributo "class" pero no el atributo "id"::
1195
1196 def has_class_but_no_id(tag):
1197 return tag.has_attr('class') and not tag.has_attr('id')
1198
1199Pasa esta función a ``find_all()`` y obtendrás todas las etiquetas
1200<p>::
1201
1202 soup.find_all(has_class_but_no_id)
1203 # [<p class="title"><b>The Dormouse's story</b></p>,
1204 # <p class="story">Once upon a time there were…bottom of a well.</p>,
1205 # <p class="story">...</p>]
1206
1207Esta función solo devuelve las etiquetas <p>. No obtiene las etiquetas
1208<a>, porque esas etiquetas definen ambas "class" y "id". No devuelve
1209etiquetas como <html> y <title> porque dichas etiquetas no definen
1210"class".
1211
1212Si pasas una función para filtrar un atributo en específico como
1213``href``, el argumento que se pasa a la función será el valor de
1214dicho atributo, no toda la etiqueta. Esta es una función que
1215encuentra todas las etiquetas <a> cuyo atributo ``href`` *no*
1216empareja con una expresión regular::
1217
1218 import re
1219 def not_lacie(href):
1220 return href and not re.compile("lacie").search(href)
1221
1222 soup.find_all(href=not_lacie)
1223 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1224 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1225
1226La función puede ser tan complicada como la necesites. Esta es una
1227función que devuelve ``True`` si una etiqueta está rodeada por
1228objetos *string*::
1229
1230 from bs4 import NavigableString
1231 def surrounded_by_strings(tag):
1232 return (isinstance(tag.next_element, NavigableString)
1233 and isinstance(tag.previous_element, NavigableString))
1234
1235 for tag in soup.find_all(surrounded_by_strings):
1236 print(tag.name)
1237 # body
1238 # p
1239 # a
1240 # a
1241 # a
1242 # p
1243
1244Ahora ya estamos listos para entrar en detalle en los métodos
1245de búsqueda.
1246
1247``find_all()``
1248==============
1249
1250Firma del método: find_all(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`recursive
1251<recursive>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1252
1253El método ``find_all()`` busca por los descendientes de una etiqueta y
1254obtiene `todos` aquellos que casan con tus filtros. He mostrado varios
1255ejemplos en `Tipos de filtros`_, pero aquí hay unos cuantos más::
1256
1257 soup.find_all("title")
1258 # [<title>The Dormouse's story</title>]
1259
1260 soup.find_all("p", "title")
1261 # [<p class="title"><b>The Dormouse's story</b></p>]
1262
1263 soup.find_all("a")
1264 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1265 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1266 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1267
1268 soup.find_all(id="link2")
1269 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1270
1271 import re
1272 soup.find(string=re.compile("sisters"))
1273 # 'Once upon a time there were three little sisters; and their names were\n'
1274
1275Algunos de estos deberían ser familiares, pero otros son nuevos.
1276¿Qué significa pasar un valor para ``string``, o ``id``? ¿Por qué
1277``find_all("p", "title")`` encuentra una etiqueta <p> con la clase
1278CSS "title"? Echemos un vistazo a los argumentos de ``find_all()``.
1279
1280.. _name:
1281
1282El argumento ``name``
1283---------------------
1284
1285Pasa un valor para ``name`` y notarás que Beautiful Soup solo
1286considera etiquetas con ciertos nombres. Las cadenas de texto se
1287ignorarán, como aquellas etiquetas cuyo nombre no emparejen.
1288
1289Este es el uso más simple::
1290
1291 soup.find_all("title")
1292 # [<title>The Dormouse's story</title>]
1293
1294Recuerda de `Tipos de filtros`_ que el valor para ``name`` puede ser
1295`una cadena`_, `una expresión regular`_, `una lista`_, `una función`_,
1296o el valor `True`_.
1297
1298.. _kwargs:
1299
1300El argumento palabras-clave
1301---------------------------
1302
1303Cualquier argumento que no se reconozca se tomará como un filtro para alguno
1304de los atributos de una etiqueta. Si pasas un valor para un argumento llamado
1305``id``, Beautiful Soup filtrará el atributo 'id' de cada una de las etiquetas::
1306
1307 soup.find_all(id='link2')
1308 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1309
1310Si pasas un valor para ``href``, Beautiful Soup filtrará
1311el atributo ``href`` de cada uno de las etiquetas::
1312
1313 soup.find_all(href=re.compile("elsie"))
1314 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1315
1316Puedes filtrar un atributo basándote en `una cadena`_,
1317`una expresión regular`_, `una lista`_, `una función`_, o el valor
1318`True`_.
1319
1320Este código busca todas las etiquetas cuyo atributo ``id`` tiene
1321un valor, sin importar qué valor es::
1322
1323 soup.find_all(id=True)
1324 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1325 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1326 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1327
1328Puedes filtrar varios atributos al mismo tiempo pasando más de un argumento
1329palabra-clave::
1330
1331 soup.find_all(href=re.compile("elsie"), id='link1')
1332 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1333
1334Algunos atributos, como los atributos data-* en HTML5, tienen nombres que
1335no pueden ser usados como nombres de argumentos palabra-clave::
1336
1337 data_soup = BeautifulSoup('<div data-foo="value">foo!</div>', 'html.parser')
1338 data_soup.find_all(data-foo="value")
1339 # SyntaxError: keyword can't be an expression
1340
1341Puedes usar estos atributos en búsquedas insertándolos en un diccionario
1342y pasándolo a ``find_all()`` como el argumento ``attrs``::
1343
1344 data_soup.find_all(attrs={"data-foo": "value"})
1345 # [<div data-foo="value">foo!</div>]
1346
1347No puedes usar un argumento palabra-clave para buscar por el nombre
1348HTML de un elemento, porque BeautifulSoup usa el argumento ``name``
1349para guardar el nombre de la etiqueta. En lugar de esto, puedes
1350darle valor a 'name' en el argumento ``attrs``::
1351
1352 name_soup = BeautifulSoup('<input name="email"/>', 'html.parser')
1353 name_soup.find_all(name="email")
1354 # []
1355 name_soup.find_all(attrs={"name": "email"})
1356 # [<input name="email"/>]
1357
1358.. _attrs:
1359
1360Buscando por clase CSS
1361----------------------
1362
1363Es muy útil para buscar una etiqueta que tenga una clase CSS específica,
1364pero el nombre del atributo CSS, "class", es una palabra reservada de
1365Python. Usar ``class`` como argumento ocasionaría un error sintáctico.
1366Desde Beautiful Soup 4.1.2, se puede buscar por una clase CSS usando
1367el argumento palabra-clave ``class_``::
1368
1369 soup.find_all("a", class_="sister")
1370 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1371 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1372 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1373
1374Como con cualquier argumento palabra-clave, puede pasar una cadena
1375de caracteres a ``class_``, una expresión regular, una función, o
1376``True``::
1377
1378 soup.find_all(class_=re.compile("itl"))
1379 # [<p class="title"><b>The Dormouse's story</b></p>]
1380
1381 def has_six_characters(css_class):
1382 return css_class is not None and len(css_class) == 6
1383
1384 soup.find_all(class_=has_six_characters)
1385 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1386 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1387 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1388
1389:ref:`Recuerda <multivalue>` que una sola etiqueta puede tener varios
1390valores para su atributo "class". Cuando se busca por una etiqueta
1391que case una cierta clase CSS, se está intentando emparejar por
1392`cualquiera` de sus clases CSS::
1393
1394 css_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser')
1395 css_soup.find_all("p", class_="strikeout")
1396 # [<p class="body strikeout"></p>]
1397
1398 css_soup.find_all("p", class_="body")
1399 # [<p class="body strikeout"></p>]
1400
1401Puedes también buscar por la cadena de caracteres exacta del atributo
1402``class``::
1403
1404 css_soup.find_all("p", class_="body strikeout")
1405 # [<p class="body strikeout"></p>]
1406
1407Pero buscar por variantes de la cadena de caracteres no funcionará::
1408
1409 css_soup.find_all("p", class_="strikeout body")
1410 # []
1411
1412Si quieres buscar por las etiquetas que casen dos o más clases CSS,
1413deberías usar un selector CSS::
1414
1415 css_soup.select("p.strikeout.body")
1416 # [<p class="body strikeout"></p>]
1417
1418En versiones antiguas de Beautiful Soup, que no soportan el
1419atajo ``class_``, puedes usar el truco del ``attrs`` mencionado
1420arriba. Crea un diccionario cuyo valor para "class" sea la
1421cadena de caracteres (o expresión regular, o lo que sea) que
1422quieras buscar::
1423
1424 soup.find_all("a", attrs={"class": "sister"})
1425 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1426 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1427 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1428
1429.. _string:
1430
1431El argumento ``string``
1432-----------------------
1433
1434Con ``string`` puedes buscar por cadenas de caracteres en vez de
1435etiquetas. Como con ``name`` y argumentos palabras-clave, puedes
1436pasar `una cadena`_, `una expresión regular`_, `una lista`_, `una
1437función`_, o el valor `True`_.
1438Aquí hay algunos ejemplos::
1439
1440 soup.find_all(string="Elsie")
1441 # ['Elsie']
1442
1443 soup.find_all(string=["Tillie", "Elsie", "Lacie"])
1444 # ['Elsie', 'Lacie', 'Tillie']
1445
1446 soup.find_all(string=re.compile("Dormouse"))
1447 # ["The Dormouse's story", "The Dormouse's story"]
1448
1449 def is_the_only_string_within_a_tag(s):
1450 """Return True if this string is the only child of its parent tag."""
1451 return (s == s.parent.string)
1452
1453 soup.find_all(string=is_the_only_string_within_a_tag)
1454 # ["The Dormouse's story", "The Dormouse's story", 'Elsie', 'Lacie', 'Tillie', '...']
1455
1456
1457Aunque ``string`` es para encontrar cadenas, puedes combinarlo
1458con argumentos que permitan buscar etiquetas: Beautiful Soup
1459encontrará todas las etiquetas cuyo ``.string`` case con tu valor
1460para ``string``. Este código encuentra las etiquetas <a> cuyo
1461``.string`` es "Elsie"::
1462
1463 soup.find_all("a", string="Elsie")
1464 # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]
1465
1466El argumento ``string`` es nuevo en Beautiful Soup 4.4.0. En versiones
1467anteriores se llamaba ``text``::
1468
1469 soup.find_all("a", text="Elsie")
1470 # [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]
1471
1472.. _limit:
1473
1474El argumento``limit``
1475---------------------
1476
1477``find_all()`` devuelve todas las etiquetas y cadenas que emparejan
1478con tus filtros. Esto puede tardar un poco si el documento es grande.
1479Si no necesitas `todos` los resultados, puedes pasar un número para
1480``limit``. Esto funciona tal y como lo hace la palabra LIMIT en SQL.
1481Indica a Beautiful Soup dejar de obtener resultados después de
1482haber encontrado un cierto número.
1483
1484Hay tres enlaces en el documento de "Las tres hermanas", pero este
1485código tan solo obtiene los dos primeros::
1486
1487 soup.find_all("a", limit=2)
1488 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1489 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1490
1491.. _recursive:
1492
1493El argumento ``recursive``
1494--------------------------
1495
1496Si llamas a ``mytag.find_all()``, Beautiful Soup examinará todos los
1497descendientes de ``mytag``: sus hijos, los hijos de sus hijos, y
1498así sucesivamente. Si solo quieres que Beautiful Soup considere
1499hijos directos, puedes pasar ``recursive=False``. Observa las
1500diferencias aquí::
1501
1502 soup.html.find_all("title")
1503 # [<title>The Dormouse's story</title>]
1504
1505 soup.html.find_all("title", recursive=False)
1506 # []
1507
1508Aquí está esa parte del documento::
1509
1510 <html>
1511 <head>
1512 <title>
1513 The Dormouse's story
1514 </title>
1515 </head>
1516 ...
1517
1518La etiqueta <title> va después de la etiqueta <html>, pero no está
1519`directamente` debajo de la etiqueta <html>: la etiqueta <head>
1520está en medio de ambas. Beautiful Soup encuentra la etiqueta <title> cuando
1521se permite observar todos los descendientes de la etiqueta <html>,
1522pero cuando ``recursive=False`` restringe a los hijos directos
1523de la etiqueta <html>, no se encuentra nada.
1524
1525Beautiful Soup ofrece mucho métodos de análisis del árbol (descritos
1526más adelante), y la mayoría toman los mismos argumentos que ``find_all()``:
1527``name``, ``attrs``, ``string``, ``limit``, y los argumentos
1528palabras-clave. Pero el argumento ``recursive`` es diferente:
1529``find_all()`` y ``find()`` son los únicos métodos que lo soportan.
1530Pasar ``recursive=False`` en un método como ``find_parents()`` no sería
1531muy útil.
1532
1533Llamar a una etiqueta es como llamar a ``find_all()``
1534=====================================================
1535
1536Como ``find_all()`` es el método más popular en la API de búsqueda
1537de Beautiful Soup, puedes usar un atajo para usarlo. Si utilizas
1538el objeto :py:class:`BeautifulSoup` o un objeto :py:class:`Tag`
1539como si fuesen una función, entonces es lo mismo que llamar a
1540``find_all()`` en esos objetos. Estos dos líneas de código son
1541equivalentes::
1542
1543 soup.find_all("a")
1544 soup("a")
1545
1546Estas dos líneas de código son también equivalentes::
1547
1548 soup.title.find_all(string=True)
1549 soup.title(string=True)
1550
1551``find()``
1552==========
1553
1554Firma del método: find(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`recursive
1555<recursive>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1556
1557El método ``find_all()`` examina todo el documento buscando por
1558resultados, pero a veces solo quieres encontrar un resultado.
1559Si sabes que un documento solo tiene una etiqueta <body>, es una
1560pérdida de tiempo examinar todo el documento buscando más
1561emparejamientos. En lugar de pasar ``limit=1`` siempre que se llame
1562a ``find_all(), puedes usar el método ``find()``. Estas dos líneas
1563de código son `casi` equivalentes::
1564
1565 soup.find_all('title', limit=1)
1566 # [<title>The Dormouse's story</title>]
1567
1568 soup.find('title')
1569 # <title>The Dormouse's story</title>
1570
1571La única diferencia es que ``find_all()`` devuelve una lista
1572conteniendo un resultado, y ``find()`` devuelve solo el resultado.
1573
1574Si ``find_all()`` no encuentra nada, devuelve una lista vacía. Si
1575``find()`` no encuentra nada, devuelve ``None``::
1576
1577 print(soup.find("nosuchtag"))
1578 # None
1579
1580¿Recuerdas el truco de ``soup.head.title`` de `Navegar usando nombres
1581de etiquetas`_? Ese truco funciona porque se llama repetidamente a
1582``find()``::
1583
1584 soup.head.title
1585 # <title>The Dormouse's story</title>
1586
1587 soup.find("head").find("title")
1588 # <title>The Dormouse's story</title>
1589
1590``find_parents()`` y ``find_parent()``
1591======================================
1592
1593Firma del método: find_parents(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1594
1595Firma del método: find_parent(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1596
1597He pasado bastante tiempo cubriendo ``find_all()`` y ``find()``.
1598La API de Beautiful Soup define otros diez métodos para buscar por
1599el árbol, pero no te asustes. Cinco de estos métodos son básicamente
1600iguales a ``find_all()``, y los otros cinco son básicamente
1601iguales a ``find()``. La única diferencia reside en qué partes del
1602árbol buscan.
1603
1604Primero consideremos ``find_parents()`` y ``find_paren()``. Recuerda
1605que ``find_all()`` y ``find()`` trabajan bajando por el árbol,
1606examinando a los descendientes de una etiqueta. Estos métodos realizan
1607lo contrario: trabajan `subiendo` por el árbol, buscando a las madres
1608de las etiquetas (o cadenas). Probémoslos, empezando por una cadena
1609de caracteres que esté bien enterrada en el documento de "Las tres
1610hermanas"::
1611
1612 a_string = soup.find(string="Lacie")
1613 a_string
1614 # 'Lacie'
1615
1616 a_string.find_parents("a")
1617 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1618
1619 a_string.find_parent("p")
1620 # <p class="story">Once upon a time there were three little sisters; and their names were
1621 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1622 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
1623 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
1624 # and they lived at the bottom of a well.</p>
1625
1626 a_string.find_parents("p", class_="title")
1627 # []
1628
1629Una de la tres etiquetas <a> is la madre directa de la cadena
1630en cuestión, así que nuestra búsqueda la encuentra. Una de las
1631tres etiquetas <p> es una madre indirecta de la cadena, y nuestra
1632búsqueda también la encuentra. Hay una etiqueta <p> con la clase
1633CSS "title" `en algún sitio` del documento, pero no en ninguno
1634de las madres de la cadena, así que no podemos encontrarla con
1635``find_parents()``.
1636
1637Puedes haber deducido la conexión entre ``find_parent()`` y
1638``find_parents()``, y los atributos `.parent`_ y `.parents`_
1639mencionados anteriormente. La conexión es muy fuerte. Estos
1640métodos de búsqueda realmente usan ``.parents`` para iterar
1641sobre todas las madres, y comprobar cada una con el filtro
1642provisto para ver si emparejan.
1643
1644``find_next_siblings()`` y ``find_next_sibling()``
1645==================================================
1646
1647Firma del método: find_next_siblings(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1648
1649Firma del método: find_next_sibling(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1650
1651Estos métodos usan :ref:`next_siblings <sibling-generators>`
1652para iterar sobre el resto de los hermanos de un elemento en el
1653árbol. El método ``find_next_siblings()`` devuelve todos los
1654hermanos que casen, y ``find_next_sibling()`` solo devuelve
1655el primero de ellos::
1656
1657 first_link = soup.a
1658 first_link
1659 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
1660
1661 first_link.find_next_siblings("a")
1662 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1663 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1664
1665 first_story_paragraph = soup.find("p", "story")
1666 first_story_paragraph.find_next_sibling("p")
1667 # <p class="story">...</p>
1668
1669``find_previous_siblings()`` y ``find_previous_sibling()``
1670==========================================================
1671
1672Firma del método: find_previous_siblings(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1673
1674Firma del método: find_previous_sibling(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1675
1676Estos métodos emplean :ref:`.previous_siblings <sibling-generators>` para iterar sobre
1677los hermanos de un elemento que les precede en el árbol. El método
1678``find_previous_siblings()`` devuelve todos los hermanos que emparejan, y
1679``find_previous_sibling()`` solo devuelve el primero de ellos::
1680
1681 last_link = soup.find("a", id="link3")
1682 last_link
1683 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
1684
1685 last_link.find_previous_siblings("a")
1686 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1687 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1688
1689 first_story_paragraph = soup.find("p", "story")
1690 first_story_paragraph.find_previous_sibling("p")
1691 # <p class="title"><b>The Dormouse's story</b></p>
1692
1693
1694``find_all_next()`` y ``find_next()``
1695=====================================
1696
1697Firma del método: find_all_next(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1698
1699Firma del método: find_next(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1700
1701Estos métodos usan :ref:`.next_elements <element-generators>` para
1702iterar sobre cualesquiera etiquetas y cadenas que vayan después
1703de ella en el documento. El método ``find_all_next()`` devuelve
1704todos los resultados, y ``find_next()`` solo devuelve el primero::
1705
1706 first_link = soup.a
1707 first_link
1708 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
1709
1710 first_link.find_all_next(string=True)
1711 # ['Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
1712 # ';\nand they lived at the bottom of a well.', '\n', '...', '\n']
1713
1714 first_link.find_next("p")
1715 # <p class="story">...</p>
1716
1717En el primer ejemplo, la cadena "Elsie" apareció, aunque estuviese
1718contenida en la etiqueta <a> desde la que comenzamos. En el segundo
1719ejemplo, la última etiqueta <p> en el documento apareció, aunque no
1720esté en la misma parte del árbol que la etiqueta <a> desde la que
1721comenzamos. Para estos métodos, todo lo que importa es que un
1722elemento cumple con el filtro, y que aparezca en el documento
1723después del elemento inicial.
1724
1725``find_all_previous()`` y ``find_previous()``
1726=============================================
1727
1728Firma del método: find_all_previous(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`limit <limit>`, :ref:`**kwargs <kwargs>`)
1729
1730Firma del método: find_previous(:ref:`name <name>`, :ref:`attrs <attrs>`, :ref:`string <string>`, :ref:`**kwargs <kwargs>`)
1731
1732Estos métodos usan :ref:`.previous_elements <element-generators>`
1733para iterar sobre las etiquetas y cadenas que iban antes en el
1734documento. El método ``find_all_previous()`` devuelve todos los
1735resultados, y ``find_previous()`` solo devuelve el primero::
1736
1737 first_link = soup.a
1738 first_link
1739 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
1740
1741 first_link.find_all_previous("p")
1742 # [<p class="story">Once upon a time there were three little sisters; ...</p>,
1743 # <p class="title"><b>The Dormouse's story</b></p>]
1744
1745 first_link.find_previous("title")
1746 # <title>The Dormouse's story</title>
1747
1748La llamada a ``find_all_previous("p")`` encontró el primer
1749párrafo en el documento (el que tiene la clase="title"), pero
1750también encuentra el segundo párrafo, la etiqueta <p> que
1751contiene la etiqueta <a> con la que comenzamos. Esto no debería
1752ser demasiado sorprendente: estamos buscando todas las etiquetas
1753que aparecen en el documento después de la etiqueta con la que se
1754comienza. Una etiqueta <p> que contiene una <a> debe aparecer
1755antes de la etiqueta <a> que contiene.
1756
1757Selectores CSS mediante la propiedad ``.css``
1758=============================================
1759
1760Los objetos :py:class:`BeautifulSoup` y :py:class:`Tag` soportan los selectores
1761CSS a través de su atributo ``.css``. El paquete `Soup Sieve <https://facelessuser.github.io/soupsieve/>`_,
1762disponible a través de PyPI como ``soupsieve``, gestiona la implementación real
1763del selector. Si instalaste Beautiful Soup mediante ``pip``, Soup Sieve se
1764instaló al mismo tiempo, así que no tienes que hacer nada adicional.
1765
1766La documentación de Soup Sieve lista `todos los selectores CSS soportados
1767actualmente <https://facelessuser.github.io/soupsieve/selectors/>`_, pero
1768estos son algunos de los básicos. Puedes encontrar etiquetas::
1769
1770 soup.css.select("title")
1771 # [<title>The Dormouse's story</title>]
1772
1773 soup.css.select("p:nth-of-type(3)")
1774 # [<p class="story">...</p>]
1775
1776Encontrar etiquetas dentro de otras etiquetas::
1777
1778 soup.css.select("body a")
1779 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1780 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1781 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1782
1783 soup.css.select("html head title")
1784 # [<title>The Dormouse's story</title>]
1785
1786Encontrar etiquetas `directamente` después de otras etiquetas::
1787
1788 soup.css.select("head > title")
1789 # [<title>The Dormouse's story</title>]
1790
1791 soup.css.select("p > a")
1792 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1793 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1794 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1795
1796 soup.css.select("p > a:nth-of-type(2)")
1797 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1798
1799 soup.css.select("p > #link1")
1800 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1801
1802 soup.css.select("body > a")
1803 # []
1804
1805Encontrar los hijos de etiquetas::
1806
1807 soup.css.select("#link1 ~ .sister")
1808 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1809 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1810
1811 soup.css.select("#link1 + .sister")
1812 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1813
1814Encontrar etiquetas por su clase CSS::
1815
1816 soup.css.select(".sister")
1817 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1818 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1819 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1820
1821 soup.css.select("[class~=sister]")
1822 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1823 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1824 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1825
1826Encontrar etiquetas por su ID::
1827
1828 soup.css.select("#link1")
1829 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1830
1831 soup.css.select("a#link2")
1832 # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1833
1834Encontrar etiquetas que casen con cualquier selector que estés en una
1835lista de selectores::
1836
1837 soup.css.select("#link1,#link2")
1838 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1839 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
1840
1841Comprobar la existencia de un atributo::
1842
1843 soup.css.select('a[href]')
1844 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1845 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1846 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1847
1848Encontrar etiquetas por el valor de un atributo::
1849
1850 soup.css.select('a[href="http://example.com/elsie"]')
1851 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1852
1853 soup.css.select('a[href^="http://example.com/"]')
1854 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1855 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
1856 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1857
1858 soup.css.select('a[href$="tillie"]')
1859 # [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1860
1861 soup.css.select('a[href*=".com/el"]')
1862 # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
1863
1864Hay también un método llamado ``select_one()``, que encuentra solo
1865la primera etiqueta que case con un selector::
1866
1867 soup.css.select_one(".sister")
1868 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
1869
1870Por conveniencia, puedes llamar a ``select()`` y ``select_one()`` sobre
1871el objeto :py:class:`BeautifulSoup` o :py:class:`Tag`, omitiendo la
1872propiedad ``.css``::
1873
1874 soup.select('a[href$="tillie"]')
1875 # [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
1876
1877 soup.select_one(".sister")
1878 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
1879
1880El soporte de selectores CSS es conveniente para personas que ya conocen
1881la sintaxis de los selectores CSS. Puedes hacer todo esto con la API
1882de Beautiful Soup. Si todo lo que necesitas son los selectores CSS, deberías
1883saltarte Beautiful Soup y analizar el documento con ``lxml``: es mucho más
1884rápido. Pero Soup Sieve te permite `combinar` selectores CSS con la API
1885de Beautiful Soup.
1886
1887Características avanzadas de Soup Sieve
1888---------------------------------------
1889
1890Soup Sieve ofrece una API más amplia más allá de los métodos ``select()``
1891y ``select_one()``, y puedes acceder a casi toda esa API a través del
1892atributo ``.css`` de :py:class:`Tag` o :py:class:`Beautiful Soup`. Lo que
1893sigue es solo una lista de los métodos soportados; ve a `la documentación de
1894Soup Sieve <https://facelessuser.github.io/soupsieve/>`_ para la documentación
1895completa.
1896
1897El método ``iselect()`` funciona igualmente que ``select()``, solo que
1898devuelve un generador en vez de una lista::
1899
1900 [tag['id'] for tag in soup.css.iselect(".sister")]
1901 # ['link1', 'link2', 'link3']
1902
1903El método ``closest()`` devuelve la madre más cercana de una :py:class:`Tag` dada
1904que case con un selector CSS, similar al método ``find_parent()`` de
1905Beautiful Soup::
1906
1907 elsie = soup.css.select_one(".sister")
1908 elsie.css.closest("p.story")
1909 # <p class="story">Once upon a time there were three little sisters; and their names were
1910 # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
1911 # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
1912 # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
1913 # and they lived at the bottom of a well.</p>
1914
1915El método ``match()`` devuelve un booleano dependiendo de si
1916una :py:class:`Tag` específica casa con un selector o no::
1917
1918 # elsie.css.match("#link1")
1919 True
1920
1921 # elsie.css.match("#link2")
1922 False
1923
1924El método ``filter()`` devuelve un subconjunto de los hijos directos
1925de una etiqueta que casen con un selector::
1926
1927 [tag.string for tag in soup.find('p', 'story').css.filter('a')]
1928 # ['Elsie', 'Lacie', 'Tillie']
1929
1930El método ``escape()`` formatea los identificadores CSS que de otra
1931forma serían inválidos::
1932
1933 soup.css.escape("1-strange-identifier")
1934 # '\\31 -strange-identifier'
1935
1936Espacios de nombres en selectores CSS
1937-------------------------------------
1938Si has analizado XML que define espacios de nombres, puedes usarlos
1939en selectores CSS::
1940
1941 from bs4 import BeautifulSoup
1942 xml = """<tag xmlns:ns1="http://namespace1/" xmlns:ns2="http://namespace2/">
1943 <ns1:child>I'm in namespace 1</ns1:child>
1944 <ns2:child>I'm in namespace 2</ns2:child>
1945 </tag> """
1946 namespace_soup = BeautifulSoup(xml, "xml")
1947
1948 namespace_soup.css.select("child")
1949 # [<ns1:child>I'm in namespace 1</ns1:child>, <ns2:child>I'm in namespace 2</ns2:child>]
1950
1951 namespace_soup.css.select("ns1|child")
1952 # [<ns1:child>I'm in namespace 1</ns1:child>]
1953
1954Beautiful Soup intenta usar prefijos de espacios de nombres que tengan
1955sentido basándose en lo que vio al analizar el documento, pero siempre
1956puedes indicar tu propio diccionario de abreviaciones::
1957
1958 namespaces = dict(first="http://namespace1/", second="http://namespace2/")
1959 namespace_soup.css.select("second|child", namespaces=namespaces)
1960 # [<ns1:child>I'm in namespace 2</ns1:child>]
1961
1962Historia del soporte de selectores CSS
1963--------------------------------------
1964
1965La propiedad ``.css`` fue añadida en Beautiful Soup 4.12.0. Anterior a esta,
1966solo los métodos convenientes ``.select()`` y ``select_one()`` se
1967soportaban.
1968
1969La integración de Soup Sieve fue añadida en Beautiful Soup 4.7.0. Versiones
1970anteriores tenían el método ``.select()``, pero solo los selectores CSS
1971más comunes eran admitidos.
1972
1973
1974====================
1975 Modificar el árbol
1976====================
1977
1978La mayor fortaleza de Beautiful Soup reside en buscar en el árbol
1979analizado, pero puedes también modificar el árbol y escribir tus
1980cambios como un nuevo documento HTML o XML.
1981
1982Cambiar nombres de etiquetas y atributos
1983========================================
1984
1985Cubrí esto anteriormente, en :py:class:`Tag.attrs`, pero vale la pena
1986repetirlo. Puedes renombrar una etiqueta, cambiar el valor de sus
1987atributos, añadir nuevos atributos, y eliminar atributos::
1988
1989 soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
1990 tag = soup.b
1991
1992 tag.name = "blockquote"
1993 tag['class'] = 'verybold'
1994 tag['id'] = 1
1995 tag
1996 # <blockquote class="verybold" id="1">Extremely bold</blockquote>
1997
1998 del tag['class']
1999 del tag['id']
2000 tag
2001 # <blockquote>Extremely bold</blockquote>
2002
2003Modificar ``.string``
2004=====================
2005
2006Si quieres establecer el ``.string`` de una etiqueta a una nueva cadena de
2007caracteres, los contenidos de la etiqueta se pueden reemplazar con esa cadena::
2008
2009 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2010 soup = BeautifulSoup(markup, 'html.parser')
2011
2012 tag = soup.a
2013 tag.string = "New link text."
2014 tag
2015 # <a href="http://example.com/">New link text.</a>
2016
2017Ten cuidado: si una etiqueta contiene otras, ellas y todo su contenido
2018serán destruidos.
2019
2020``append()``
2021============
2022
2023Puedes añadir al contenido de una etiqueta con ``Tag.append()``.
2024Funciona como llamar a ``.append()`` en una lista de Python::
2025
2026 soup = BeautifulSoup("<a>Foo</a>", 'html.parser')
2027 soup.a.append("Bar")
2028
2029 soup
2030 # <a>FooBar</a>
2031 soup.a.contents
2032 # ['Foo', 'Bar']
2033
2034``extend()``
2035============
2036
2037Desde Beautiful Soup 4.7.0, :py:class:`Tag` también soporta un método
2038llamado ``.extend()``, el cual añade todos los elementos de una lista
2039a una :py:class:`Tag`, en orden::
2040
2041 soup = BeautifulSoup("<a>Soup</a>", 'html.parser')
2042 soup.a.extend(["'s", " ", "on"])
2043
2044 soup
2045 # <a>Soup's on</a>
2046 soup.a.contents
2047 # ['Soup', ''s', ' ', 'on']
2048
2049``NavigableString()`` y ``.new_tag()``
2050======================================
2051
2052Si necesitas añadir una cadena a un documento, sin problema--puedes
2053pasar una cadena de Python a ``append()``, o puedes llamar al constructor
2054de :py:class:`NavigableString`::
2055
2056 from bs4 import NavigableString
2057 soup = BeautifulSoup("<b></b>", 'html.parser')
2058 tag = soup.b
2059 tag.append("Hello")
2060 new_string = NavigableString(" there")
2061 tag.append(new_string)
2062 tag
2063 # <b>Hello there.</b>
2064 tag.contents
2065 # ['Hello', ' there']
2066
2067Si quieres crear un comentario o cualquier otra subclase
2068de :py:class:`NavigableString`, solo llama al constructor::
2069
2070 from bs4 import Comment
2071 new_comment = Comment("Nice to see you.")
2072 tag.append(new_comment)
2073 tag
2074 # <b>Hello there<!--Nice to see you.--></b>
2075 tag.contents
2076 # ['Hello', ' there', 'Nice to see you.']
2077
2078`(Esto es una nueva característica en Beautiful Soup 4.4.0.)`
2079
2080¿Qué ocurre si necesitas crear una etiqueta totalmente nueva? La mejor
2081solución es llamar al método de construcción (`factory method`)
2082``BeautifulSoup.new_tag()``::
2083
2084 soup = BeautifulSoup("<b></b>", 'html.parser')
2085 original_tag = soup.b
2086
2087 new_tag = soup.new_tag("a", href="http://www.example.com")
2088 original_tag.append(new_tag)
2089 original_tag
2090 # <b><a href="http://www.example.com"></a></b>
2091
2092 new_tag.string = "Link text."
2093 original_tag
2094 # <b><a href="http://www.example.com">Link text.</a></b>
2095
2096Solo el primer argumento, el nombre de la etiqueta, es
2097obligatorio.
2098
2099``insert()``
2100============
2101
2102``Tag.insert()`` es justo como ``Tag.append()``, excepto que el nuevo
2103elemento no necesariamente va al final del ``.contents`` de su madre.
2104Se insertará en la posición numérica que le hayas indicado. Funciona
2105como ``.insert()`` es una lista de Python::
2106
2107 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2108 soup = BeautifulSoup(markup, 'html.parser')
2109 tag = soup.a
2110
2111 tag.insert(1, "but did not endorse ")
2112 tag
2113 # <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
2114 tag.contents
2115 # ['I linked to ', 'but did not endorse', <i>example.com</i>]
2116
2117``insert_before()`` y ``insert_after()``
2118========================================
2119
2120El método ``insert_before()`` inserta etiquetas o cadenas
2121inmediatamente antes de algo en el árbol analizado::
2122
2123 soup = BeautifulSoup("<b>leave</b>", 'html.parser')
2124 tag = soup.new_tag("i")
2125 tag.string = "Don't"
2126 soup.b.string.insert_before(tag)
2127 soup.b
2128 # <b><i>Don't</i>leave</b>
2129
2130El método ``insert_after()`` inserta etiquetas o cadenas
2131inmediatamente después de algo en el árbol analizado::
2132
2133 div = soup.new_tag('div')
2134 div.string = 'ever'
2135 soup.b.i.insert_after(" you ", div)
2136 soup.b
2137 # <b><i>Don't</i> you <div>ever</div> leave</b>
2138 soup.b.contents
2139 # [<i>Don't</i>, ' you', <div>ever</div>, 'leave']
2140
2141``clear()``
2142===========
2143
2144``Tag.clear()`` quita los contenidos de una etiqueta::
2145
2146 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2147 soup = BeautifulSoup(markup, 'html.parser')
2148 tag = soup.a
2149
2150 tag.clear()
2151 tag
2152 # <a href="http://example.com/"></a>
2153
2154``extract()``
2155=============
2156
2157``PageElement.extract()`` elimina una etiqueta o una cadena de caracteres
2158del árbol. Devuelve la etiqueta o la cadena que fue extraída::
2159
2160 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2161 soup = BeautifulSoup(markup, 'html.parser')
2162 a_tag = soup.a
2163
2164 i_tag = soup.i.extract()
2165
2166 a_tag
2167 # <a href="http://example.com/">I linked to</a>
2168
2169 i_tag
2170 # <i>example.com</i>
2171
2172 print(i_tag.parent)
2173 # None
2174
2175En este punto tienes realmente dos árboles analizados: uno anclado en el
2176objeto :py:class:`BeautifulSoup` que usaste para analizar el documento, y
2177uno anclado en la etiqueta que fue extraída. Puedes llamar a ``extract``
2178en el hijo del elemento que extrajiste::
2179
2180 my_string = i_tag.string.extract()
2181 my_string
2182 # 'example.com'
2183
2184 print(my_string.parent)
2185 # None
2186 i_tag
2187 # <i></i>
2188
2189
2190``decompose()``
2191===============
2192
2193``Tag.decompose()`` quita una etiqueta del árbol, y luego `lo destruye
2194completamente y su contenido también`::
2195
2196 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2197 soup = BeautifulSoup(markup, 'html.parser')
2198 a_tag = soup.a
2199 i_tag = soup.i
2200
2201 i_tag.decompose()
2202 a_tag
2203 # <a href="http://example.com/">I linked to</a>
2204
2205El comportamiento de una :py:class:`Tag` o :py:class:`NavigableString` descompuesta
2206no está definido y no deberías usarlo para nada. Si no estás seguro si algo
2207ha sido descompuesto, puedes comprobar su propiedad ``.decomposed``
2208`(nuevo en Beautiful Soup 4.9.0)`::
2209
2210 i_tag.decomposed
2211 # True
2212
2213 a_tag.decomposed
2214 # False
2215
2216
2217.. _replace_with():
2218
2219``replace_with()``
2220==================
2221
2222``PageElement.replace_with()`` elimina una etiqueta o cadena del árbol,
2223y lo reemplaza con una o más etiquetas de tu elección::
2224
2225 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2226 soup = BeautifulSoup(markup, 'html.parser')
2227 a_tag = soup.a
2228
2229 new_tag = soup.new_tag("b")
2230 new_tag.string = "example.com"
2231 a_tag.i.replace_with(new_tag)
2232
2233 a_tag
2234 # <a href="http://example.com/">I linked to <b>example.com</b></a>
2235
2236 bold_tag = soup.new_tag("b")
2237 bold_tag.string = "example"
2238 i_tag = soup.new_tag("i")
2239 i_tag.string = "net"
2240 a_tag.b.replace_with(bold_tag, ".", i_tag)
2241
2242 a_tag
2243 # <a href="http://example.com/">I linked to <b>example</b>.<i>net</i></a>
2244
2245
2246``replace_with()`` devuelve la etiqueta o cadena que se reemplazó,
2247así que puedes examinarla o añadirla de nuevo a otra parte del árbol.
2248
2249`La capacidad de pasar múltiples argumentos a replace_with() es nueva
2250en Beautiful Soup 4.10.0.`
2251
2252
2253``wrap()``
2254==========
2255
2256``PageElement.wrap()`` envuelve un elemento en la etiqueta que especificas.
2257Devuelve la nueva envoltura::
2258
2259 soup = BeautifulSoup("<p>I wish I was bold.</p>", 'html.parser')
2260 soup.p.string.wrap(soup.new_tag("b"))
2261 # <b>I wish I was bold.</b>
2262
2263 soup.p.wrap(soup.new_tag("div"))
2264 # <div><p><b>I wish I was bold.</b></p></div>
2265
2266`Este método es nuevo en Beautiful Soup 4.0.5.`
2267
2268``unwrap()``
2269============
2270
2271``Tag.unwrap()`` es el opuesto de ``wrap()``. Reemplaza una
2272etiqueta con lo que haya dentro de lo que haya en esa etiqueta.
2273Es bueno para eliminar anotaciones::
2274
2275 markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2276 soup = BeautifulSoup(markup, 'html.parser')
2277 a_tag = soup.a
2278
2279 a_tag.i.unwrap()
2280 a_tag
2281 # <a href="http://example.com/">I linked to example.com</a>
2282
2283Como ``replace_with()``, ``unwrap()`` devuelve la etiqueta que fue
2284reemplazada.
2285
2286``smooth()``
2287============
2288
2289Tras llamar a un puñado de métodos que modifican el árbol analizado, puedes
2290acabar con dos o más objetos :py:class:`NavigableString` uno al lado del otro.
2291Beautiful Soup no tiene ningún problema con esto, pero como no puede ocurrir
2292en un documento recién analizado, puedes no esperar un comportamiento como
2293el siguiente::
2294
2295 soup = BeautifulSoup("<p>A one</p>", 'html.parser')
2296 soup.p.append(", a two")
2297
2298 soup.p.contents
2299 # ['A one', ', a two']
2300
2301 print(soup.p.encode())
2302 # b'<p>A one, a two</p>'
2303
2304 print(soup.p.prettify())
2305 # <p>
2306 # A one
2307 # , a two
2308 # </p>
2309
2310Puedes llamar a ``Tag.smooth()`` para limpiar el árbol analizado consolidando
2311cadenas adyacentes::
2312
2313 soup.smooth()
2314
2315 soup.p.contents
2316 # ['A one, a two']
2317
2318 print(soup.p.prettify())
2319 # <p>
2320 # A one, a two
2321 # </p>
2322
2323`Este método es nuevo en Beautiful Soup 4.8.0.`
2324
2325========
2326 Salida
2327========
2328
2329.. _.prettyprinting:
2330
2331*Pretty-printing*
2332=================
2333
2334El método ``prettify()`` convertirá un árbol analizado de Beautiful Soup
2335en una cadena de caracteres Unicode bien formateado, con una línea
2336para cada etiqueta y cada cadena::
2337
2338 markup = '<html><head><body><a href="http://example.com/">I linked to <i>example.com</i></a>'
2339 soup = BeautifulSoup(markup, 'html.parser')
2340 soup.prettify()
2341 # '<html>\n <head>\n </head>\n <body>\n <a href="http://example.com/">\n...'
2342
2343 print(soup.prettify())
2344 # <html>
2345 # <head>
2346 # </head>
2347 # <body>
2348 # <a href="http://example.com/">
2349 # I linked to
2350 # <i>
2351 # example.com
2352 # </i>
2353 # </a>
2354 # </body>
2355 # </html>
2356
2357Puedes llamar ``prettify()`` a alto nivel sobre el objeto :py:class:`BeautifulSoup`,
2358o sobre cualquiera de sus objetos :py:class:`Tag`::
2359
2360 print(soup.a.prettify())
2361 # <a href="http://example.com/">
2362 # I linked to
2363 # <i>
2364 # example.com
2365 # </i>
2366 # </a>
2367
2368Como añade un espacio en blanco (en la forma de saltos de líneas),
2369``prettify()`` cambia el sentido del documento HTML y no debe ser
2370usado para reformatearlo. El objetivo de ``prettify()`` es ayudarte
2371a entender visualmente la estructura del documento en el que trabajas.
2372
2373*Non-pretty printing*
2374=====================
2375
2376Si tan solo quieres una cadena, sin ningún formateo adornado,
2377puedes llamar a ``str()`` en un objeto :py:class:`BeautifulSoup`, o
2378sobre una :py:class:`Tag` dentro de él::
2379
2380 str(soup)
2381 # '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'
2382
2383 str(soup.a)
2384 # '<a href="http://example.com/">I linked to <i>example.com</i></a>'
2385
2386La función ``str()`` devuelve una cadena codificada en UTF-8. Mira
2387`Codificaciones`_ para otras opciones.
2388
2389Puedes también llamar a ``encode()`` para obtener un bytestring, y
2390``decode()`` para obtener Unicode.
2391
2392.. _output_formatters:
2393
2394Formatos de salida
2395==================
2396
2397Si le das a Beautiful Soup un documento que contenga entidades HTML
2398como "&lquot;", serán convertidas a caracteres Unicode::
2399
2400 soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.", 'html.parser')
2401 str(soup)
2402 # '“Dammit!” he said.'
2403
2404Si después conviertes el documento a bytestring, los caracteres Unicode
2405serán convertidos a UTF-8. No obtendrás de nuevo las entidades HTML::
2406
2407 soup.encode("utf8")
2408 # b'\xe2\x80\x9cDammit!\xe2\x80\x9d he said.'
2409
2410Por defecto, los únicos caracteres que se formatean en la salida son
2411ampersands y comillas anguladas simples. Estas se transforman en
2412"&amp;", "&lt;" y "&gt;", así Beautiful Soup no genera inadvertidamente
2413HTML o XML inválido::
2414
2415 soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>", 'html.parser')
2416 soup.p
2417 # <p>The law firm of Dewey, Cheatem, &amp; Howe</p>
2418
2419 soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
2420 soup.a
2421 # <a href="http://example.com/?foo=val1&amp;bar=val2">A link</a>
2422
2423Puedes cambiar este comportamiento dando un valor al argumento
2424``formatter`` de ``prettify()``, ``encode()`` o ``decode()``.
2425Beautiful Soup reconoce cinco posibles valores para ``formatter``.
2426
2427El valor por defecto es ``formatter="minimal"``. Las cadenas solo
2428serán procesadas lo suficiente como para asegurar que Beautiful Soup
2429genera HTML/XML válido::
2430
2431 french = "<p>Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;</p>"
2432 soup = BeautifulSoup(french, 'html.parser')
2433 print(soup.prettify(formatter="minimal"))
2434 # <p>
2435 # Il a dit &lt;&lt;Sacré bleu!&gt;&gt;
2436 # </p>
2437
2438Si pasas ``formatter="html"``, Beautiful Soup convertirá caracteres
2439Unicode a entidades HTML cuando sea posible::
2440
2441 print(soup.prettify(formatter="html"))
2442 # <p>
2443 # Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;
2444 # </p>
2445
2446Si pasas ``formatter="html5"``, es similar a
2447``formatter="html"``, pero Beautiful Soup omitirá la barra de
2448cierre en etiquetas HTML vacías como "br"::
2449
2450 br = BeautifulSoup("<br>", 'html.parser').br
2451
2452 print(br.encode(formatter="html"))
2453 # b'<br/>'
2454
2455 print(br.encode(formatter="html5"))
2456 # b'<br>'
2457
2458Además, cualquier atributo cuyos valores son la cadena de
2459caracteres vacía se convertirán en atributos booleanos al
2460estilo HTML::
2461
2462 option = BeautifulSoup('<option selected=""></option>').option
2463 print(option.encode(formatter="html"))
2464 # b'<option selected=""></option>'
2465
2466 print(option.encode(formatter="html5"))
2467 # b'<option selected></option>'
2468
2469*(Este comportamiento es nuevo a partir de Beautiful Soup 4.10.0.)*
2470
2471Si pasas ``formatter=None``, Beautiful Soup no modificará en absoluto
2472las cadenas a la salida. Esta es la opción más rápida, pero puede
2473ocasionar que Beautiful Soup genere HTML/XML inválido, como en estos
2474ejemplos::
2475
2476 print(soup.prettify(formatter=None))
2477 # <p>
2478 # Il a dit <<Sacré bleu!>>
2479 # </p>
2480
2481 link_soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>', 'html.parser')
2482 print(link_soup.a.encode(formatter=None))
2483 # b'<a href="http://example.com/?foo=val1&bar=val2">A link</a>'
2484
2485*Objetos Formatter*
2486-------------------
2487
2488Si necesitas un control más sofisticado sobre tu salida, puedes
2489instanciar uno de las clases *formatters* de Beautiful Soup y pasar
2490dicho objeto a ``formatter``.
2491
2492.. py:class:: HTMLFormatter
2493
2494Usado para personalizar las reglas de formato para documentos HTML.
2495
2496Aquí está el *formatter* que convierte cadenas de caracteres a mayúsculas,
2497como si están en un nodo de texto o en el valor de un atributo::
2498
2499 from bs4.formatter import HTMLFormatter
2500 def uppercase(str):
2501 return str.upper()
2502
2503 formatter = HTMLFormatter(uppercase)
2504
2505 print(soup.prettify(formatter=formatter))
2506 # <p>
2507 # IL A DIT <<SACRÉ BLEU!>>
2508 # </p>
2509
2510 print(link_soup.a.prettify(formatter=formatter))
2511 # <a href="HTTP://EXAMPLE.COM/?FOO=VAL1&BAR=VAL2">
2512 # A LINK
2513 # </a>
2514
2515Este es el *formatter* que incrementa la sangría cuando se realiza
2516*pretty-printing*::
2517
2518 formatter = HTMLFormatter(indent=8)
2519 print(link_soup.a.prettify(formatter=formatter))
2520 # <a href="http://example.com/?foo=val1&bar=val2">
2521 # A link
2522 # </a>
2523
2524.. py:class:: XMLFormatter
2525
2526Usado para personalizar las reglas de formateo para documentos XML.
2527
2528Escribir tu propio *formatter*
2529------------------------------
2530
2531Crear una subclase a partir de :py:class:`HTMLFormatter` p :py:class:`XMLFormatter`
2532te dará incluso más control sobre la salida. Por ejemplo, Beautiful Soup
2533ordena por defecto los atributos en cada etiqueta::
2534
2535 attr_soup = BeautifulSoup(b'<p z="1" m="2" a="3"></p>', 'html.parser')
2536 print(attr_soup.p.encode())
2537 # <p a="3" m="2" z="1"></p>
2538
2539Para detener esto, puedes modificar en la subclase creada
2540el método ``Formatter.attributes()``, que controla los atributos
2541que se ponen en la salida y en qué orden. Esta implementación también
2542filtra el atributo llamado "m" cuando aparezca::
2543
2544 class UnsortedAttributes(HTMLFormatter):
2545 def attributes(self, tag):
2546 for k, v in tag.attrs.items():
2547 if k == 'm':
2548 continue
2549 yield k, v
2550
2551 print(attr_soup.p.encode(formatter=UnsortedAttributes()))
2552 # <p z="1" a="3"></p>
2553
2554Una última advertencia: si creas un objeto :py:class:`CData`, el texto
2555dentro de ese objeto siempre se muestra `exactamente como aparece, sin
2556ningún formato`. Beautiful Soup llamará a la función de sustitución de
2557entidad, por si hubieses escrito una función a medida que cuenta
2558todas las cadenas en el documento o algo así, pero ignorará el
2559valor de retorno::
2560
2561 from bs4.element import CData
2562 soup = BeautifulSoup("<a></a>", 'html.parser')
2563 soup.a.string = CData("one < three")
2564 print(soup.a.prettify(formatter="html"))
2565 # <a>
2566 # <![CDATA[one < three]]>
2567 # </a>
2568
2569
2570``get_text()``
2571==============
2572
2573Si solo necesitas el texto legible dentro de un documento o etiqueta, puedes
2574usar el método ``get_text()``. Devuelve todo el texto dentro del documento o
2575dentro de la etiqueta, como una sola cadena caracteres Unicode::
2576
2577 markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
2578 soup = BeautifulSoup(markup, 'html.parser')
2579
2580 soup.get_text()
2581 '\nI linked to example.com\n'
2582 soup.i.get_text()
2583 'example.com'
2584
2585Puedes especificar una cadena que usará para unir los trozos
2586de texto::
2587
2588 # soup.get_text("|")
2589 '\nI linked to |example.com|\n'
2590
2591Puedes indicar a Beautiful Soup que quite los espacios en blanco del
2592comienzo y el final de cada trozo de texto::
2593
2594 # soup.get_text("|", strip=True)
2595 'I linked to|example.com'
2596
2597Pero en ese punto puedas querer usar mejor el generador
2598:ref:`.stripped_strings <string-generators>`, y procesar el texto
2599por tu cuenta::
2600
2601 [text for text in soup.stripped_strings]
2602 # ['I linked to', 'example.com']
2603
2604*A partir de Beautiful Soup version 4.9.0, cuando lxml o html.parser
2605se usan, el contenido de las etiquetas <script>, <style>, y <template>
2606no se consideran texto, ya que esas etiquetas no son parte de la parte
2607legible del contenido de la página.*
2608
2609*A partir de de Beautiful Soup version 4.10.0, puedes llamar a get_text(),
2610.strings, o .stripped_strings en un objeto NavigableString. Devolverá
2611el propio objeto, o nada, así que la única razón para hacerlo es cuando
2612estás iterando sobre una lista mixta.*
2613
2614==================================
2615 Especificar el analizador a usar
2616==================================
2617
2618Si lo único que necesitas es analizar algún HTML, puedes ponerlo en
2619el constructor de :py:class:`BeautifulSoup`, y probablemente irá bien.
2620Beautiful Soup elegirá un analizador por ti y analizará los datos.
2621Pero hay algunos argumentos adicionales que puedes pasar al constructor
2622para cambiar el analizador que se usa.
2623
2624El primer argumento del constructor de :py:class:`BeautifulSoup` es una cadena
2625o un gestor de archivos abierto--el marcado que quieres analizar. El segundo
2626argumento es `cómo` quieres que el marcado analizado.
2627
2628Si no especificas nada, obtendrás el mejor analizador HTML que tengas
2629instalado. Beautiful Soup clasifica al analizador de lxml como el mejor,
2630después el de html5lib, y luego el analizador integrado en Python. Puedes
2631sobrescribir esto especificando uno de los siguientes:
2632
2633* El tipo de marcado que quieres analizar. Actualmente se soportan
2634 "html", "xml", y "html5".
2635
2636* El nombre de la librería del analizador que quieras usar. Actualmente se
2637 soportan "lxml", "html5lib", y "html.parser" (el analizador HTML integrado
2638 de Python).
2639
2640La sección `Instalar un analizador`_ contraste los analizadores admitidos.
2641
2642Si no tienes un analizador apropiado instalado, Beautiful Soup ignorará
2643tu petición y elegirá un analizador diferente. Ahora mismo, el único
2644analizador XML es lxml. Si no tienes lxml instalado, solicitar un
2645analizador XML no te dará uno, y pedir por "lxml" tampoco funcionará.
2646
2647Diferencias entre analizadores
2648==============================
2649
2650Beautiful Soup presenta la misma interfaz que varios analizadores,
2651pero cada uno es diferente. Analizadores diferentes crearán
2652árboles analizados diferentes a partir del mismo documento. La mayores
2653diferencias están entre los analizadores HTML y los XML. Este es un
2654documento corto, analizado como HTML usando el analizador que viene
2655con Python::
2656
2657 BeautifulSoup("<a><b/></a>", "html.parser")
2658 # <a><b></b></a>
2659
2660Como una sola etiqueta <b/> no es HTML válido, html.parser lo convierte a
2661un par <b><b/>.
2662
2663Aquí está el mismo documento analizado como XML (correr esto requiere que
2664tengas instalado lxml). Debe notarse que la etiqueta independiente
2665<b/> se deja sola, y que en el documento se incluye una declaración XML
2666en lugar de introducirlo en una etiqueta <html>::
2667
2668 print(BeautifulSoup("<a><b/></a>", "xml"))
2669 # <?xml version="1.0" encoding="utf-8"?>
2670 # <a><b/></a>
2671
2672Hay también diferencias entre analizadores HTML. Si le das a Beautiful
2673Soup un documento HTML perfectamente formado, esas diferencias no
2674importan. Un analizador será más rápido que otro, pero todos te darán
2675una estructura de datos que será exactamente como el documento HTML
2676original.
2677
2678Pero si el documento no está perfectamente formado, analizadores
2679diferentes darán diferentes resultados. A continuación se presenta
2680un documento corto e incorrecto analizado usando el analizador
2681HTML de lxml. Debe considerarse que la etiqueta <a> es envuelta
2682en las etiquetas <body> y <html>, y que la etiqueta colgada </p>
2683simplemente se ignora::
2684
2685 BeautifulSoup("<a></p>", "lxml")
2686 # <html><body><a></a></body></html>
2687
2688Este es el mismo documento analizado usando html5lib::
2689
2690 BeautifulSoup("<a></p>", "html5lib")
2691 # <html><head></head><body><a><p></p></a></body></html>
2692
2693En lugar de ignorar la etiqueta colgada </p>, html5lib la empareja
2694con una etiqueta inicial <p>. html5lib también añade una etiqueta <head>
2695vacía; lxml no se molesta.
2696
2697Este es el mismo documento analizado usando el analizador HTML integrado
2698en Python::
2699
2700 BeautifulSoup("<a></p>", "html.parser")
2701 # <a></a>
2702
2703Como lxml, este analizador ignora la etiqueta clausura </p>.
2704A diferencia de html5lib o lxml, este analizador no intenta
2705crear un documento HTML bien formado añadiendo las etiquetas
2706<html> o <body>.
2707
2708Como el documento "<a></p>" es inválido, ninguna de estas técnicas
2709es la forma 'correcta' de gestionarlo. El analizador de html5lib usa
2710técnicas que son parte del estándar de HTML5, así que es la que más
2711se puede aproximar a ser la manera correcta, pero las tres técnicas
2712son legítimas.
2713
2714Las diferencias entre analizadores pueden afectar a tu script. Si
2715estás planeando en distribuir tu script con otras personas, o
2716ejecutarlo en varias máquinas, deberías especificar un analizador
2717en el constructor de :py:class:`BeautifulSoup`. Eso reducirá
2718las probabilidad que tus usuarios analicen un documento diferentemente
2719de la manera en la que tú lo analizas.
2720
2721================
2722 Codificaciones
2723================
2724
2725Cualquier documento HTML o XML está escrito en una codificación
2726específica como ASCII o UTF-8. Pero cuando cargas ese documento en
2727Beautiful Soup, descubrirás que se convierte en Unicode::
2728
2729 markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
2730 soup = BeautifulSoup(markup, 'html.parser')
2731 soup.h1
2732 # <h1>Sacré bleu!</h1>
2733 soup.h1.string
2734 # 'Sacr\xe9 bleu!'
2735
2736No es magia (seguro que eso sería genial). Beautiful Soup usa una
2737sub-librería llamada `Unicode, Dammit`_ para detectar la codificación
2738de un documento y convertirlo a Unicode. La codificación auto detectada
2739está disponible con el atributo ``.original_encoding`` del objeto
2740:py:class:`Beautiful Soup`::
2741
2742 soup.original_encoding
2743 'utf-8'
2744
2745Unicode, Dammit estima correctamente la mayor parte del tiempo, pero
2746a veces se equivoca. A veces estima correctamente, pero solo después
2747de una búsqueda byte a byte del documento que tarda mucho tiempo.
2748Si ocurre que sabes a priori la codificación del documento, puedes
2749evitar errores y retrasos pasándola al constructor de :py:class:`BeautifulSoup`
2750con ``from_encoding``.
2751
2752Este es un documento escrito es ISO-8859-8. El documento es tan corto que
2753Unicode, Dammit no da en el clave, y lo identifica erróneamente como
2754ISO-8859-7::
2755
2756 markup = b"<h1>\xed\xe5\xec\xf9</h1>"
2757 soup = BeautifulSoup(markup, 'html.parser')
2758 print(soup.h1)
2759 # <h1>νεμω</h1>
2760 print(soup.original_encoding)
2761 # iso-8859-7
2762
2763Podemos arreglarlo pasándole el correcto a ``from_encoding``::
2764
2765 soup = BeautifulSoup(markup, 'html.parser', from_encoding="iso-8859-8")
2766 print(soup.h1)
2767 # <h1>םולש</h1>
2768 print(soup.original_encoding)
2769 # iso8859-8
2770
2771Si no sabes cuál es la codificación correcta, pero sabes que Unicode, Dammit
2772está suponiendo mal, puedes pasarle las opciones mal estimadas con
2773``exclude_encodings``::
2774
2775 soup = BeautifulSoup(markup, 'html.parser', exclude_encodings=["iso-8859-7"])
2776 print(soup.h1)
2777 # <h1>םולש</h1>
2778 print(soup.original_encoding)
2779 # WINDOWS-1255
2780
2781Windows-1255 no es correcto al 100%, pero esa codificación es
2782una superconjunto compatible con ISO-8859-8, así que se acerca
2783lo suficiente. (``exlcude_encodings`` es una nueva característica
2784en Beautiful Soup 4.4.0).
2785
2786En casos raros (normalmente cuando un documento UTF-8 contiene texto
2787escrito en una codificación completamente diferente), la única manera
2788para obtener Unicode es reemplazar algunos caracteres con el carácter
2789Unicode especial "REPLACEMENT CHARACTER" (U+FFFD, �). Si Unicode, Dammit
2790necesita hacer esto, establecerá el atributo ``.contains_replacement_characters``
2791a ``True`` en el objeto ``UnicodeDammit`` o :py:class:`BeautifulSoup`. Esto
2792te permite saber si la representación Unicode no es una representación
2793exacta de la original--algún dato se ha perdido. Si un documento contiene �,
2794pero ``contains_replacement_characteres`` es ``False``, sabrás que �
2795estaba allí originalmente (como lo está en este párrafo) y no implica
2796datos perdidos.
2797
2798Codificación de salida
2799======================
2800
2801Cuando escribas completamente un documento desde Beautiful Soup,
2802obtienes un documento UTF-8, incluso cuando el documento no está en UTF-8
2803por el que empezar. Este es un documento escrito con la codificación Latin-1::
2804
2805 markup = b'''
2806 <html>
2807 <head>
2808 <meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
2809 </head>
2810 <body>
2811 <p>Sacr\xe9 bleu!</p>
2812 </body>
2813 </html>
2814 '''
2815
2816 soup = BeautifulSoup(markup, 'html.parser')
2817 print(soup.prettify())
2818 # <html>
2819 # <head>
2820 # <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
2821 # </head>
2822 # <body>
2823 # <p>
2824 # Sacré bleu!
2825 # </p>
2826 # </body>
2827 # </html>
2828
2829Fíjate bien que la etiqueta <meta> ha sido reescrita para reflejar el hecho
2830de que el documento está ahora en UTF-8.
2831
2832Si no quieres UTF-8, puedes pasar una codificación a ``prettify()``::
2833
2834 print(soup.prettify("latin-1"))
2835 # <html>
2836 # <head>
2837 # <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
2838 # ...
2839
2840También puedes llamar a encode() sobre el objeto :py:class:`BeautifulSoup`, o
2841cualquier elemento en el objeto, como si fuese una cadena de Python::
2842
2843 soup.p.encode("latin-1")
2844 # b'<p>Sacr\xe9 bleu!</p>'
2845
2846 soup.p.encode("utf-8")
2847 # b'<p>Sacr\xc3\xa9 bleu!</p>'
2848
2849Cualesquiera caracteres que no puedan ser representados en la codificación
2850que has elegido se convierten en referencias a entidades numéricas XML.
2851Este es un documento que incluye el carácter Unicode SNOWMAN::
2852
2853 markup = u"<b>\N{SNOWMAN}</b>"
2854 snowman_soup = BeautifulSoup(markup, 'html.parser')
2855 tag = snowman_soup.b
2856
2857El carácter SNOWMAN puede ser parte de un documento UTF-8 (se parece a ☃),
2858pero no hay representación para ese carácter en ISO-Latin-1 o ASCII,
2859así que se convierte en "&#9731" para esas codificaciones::
2860
2861 print(tag.encode("utf-8"))
2862 # b'<b>\xe2\x98\x83</b>'
2863
2864 print(tag.encode("latin-1"))
2865 # b'<b>&#9731;</b>'
2866
2867 print(tag.encode("ascii"))
2868 # b'<b>&#9731;</b>'
2869
2870Unicode, Dammit
2871===============
2872
2873Puedes usar Unicode, Dammit sin usar Beautiful Soup. Es útil cuando
2874tienes datos en una codificación desconocida y solo quieres convertirlo
2875a Unicode::
2876
2877 from bs4 import UnicodeDammit
2878 dammit = UnicodeDammit(b"\xc2\xabSacr\xc3\xa9 bleu!\xc2\xbb")
2879 print(dammit.unicode_markup)
2880 # «Sacré bleu!»
2881 dammit.original_encoding
2882 # 'utf-8'
2883
2884Los estimaciones de Unicode, Dammit será mucho más precisas si instalas
2885una de estas librerías de Python: ``charset-normalizer``, ``chardet``,
2886o ``cchardet``. Cuanto más datos le des a Unicode, Dammit, con mayor exactitud
2887estimará. Si tienes alguna sospecha sobre las codificaciones que podrían ser, puedes
2888pasárselas en una lista::
2889
2890 dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
2891 print(dammit.unicode_markup)
2892 # Sacré bleu!
2893 dammit.original_encoding
2894 # 'latin-1'
2895
2896Unicode, Dammit tiene dos características especiales que Beautiful Soup no usa.
2897
2898Comillas inteligentes
2899---------------------
2900
2901Puedes usar Unicode, Dammit para convertir las comillas inteligentes de Microsoft
2902a entidades HTML o XML::
2903
2904 markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"
2905
2906 UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
2907 # '<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'
2908
2909 UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
2910 # '<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'
2911
2912Puedes también convertir las comillas inteligentes de Microsoft a comillas ASCII::
2913
2914 UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
2915 # '<p>I just "love" Microsoft Word\'s smart quotes</p>'
2916
2917Con suerte encontrarás esta característica útil, pero Beautiful Soup no la usa.
2918Beautiful Soup prefiere el comportamiento por defecto, el cual es convertir
2919las comillas inteligentes de Microsoft a caracteres Unicode junto al resto
2920de cosas::
2921
2922 UnicodeDammit(markup, ["windows-1252"]).unicode_markup
2923 # '<p>I just “love” Microsoft Word’s smart quotes</p>'
2924
2925Codificaciones inconsistentes
2926-----------------------------
2927
2928A veces un documento está mayoritariamente en UTF-8, pero contiene
2929caracteres Windows-1252 como (de nuevo) comillas inteligentes de Microsoft.
2930Esto puede ocurrir cuando un sitio web incluye datos de múltiples fuentes.
2931Puedes usar ``UnicodeDammit.detwingle()`` para convertir dicho documento en
2932puro UTF-8. Este un ejemplo sencillo::
2933
2934 snowmen = (u"\N{SNOWMAN}" * 3)
2935 quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
2936 doc = snowmen.encode("utf8") + quote.encode("windows_1252")
2937
2938Este documento es un desastre. Los muñecos de nieve están en UTF-8 y las
2939comillas están en Windows-1252. Puedes mostrar los muñecos de nieve o
2940las comillas, pero no ambos::
2941
2942 print(doc)
2943 # ☃☃☃�I like snowmen!�
2944
2945 print(doc.decode("windows-1252"))
2946 # ☃☃☃“I like snowmen!”
2947
2948Decodificar el documento en UTF-8 provoca un ``UnicodeDecodeError``, y
2949decodificarlo como Windows-1252 te da un galimatías. Afortunadamente,
2950``UnicodeDammit.detwingle()`` convertirá la cadena en puro UTF-8,
2951permitiéndote decodificarlo en Unicode y mostrar el muñeco de nieve
2952y marcas de comillas simultáneamente::
2953
2954 new_doc = UnicodeDammit.detwingle(doc)
2955 print(new_doc.decode("utf8"))
2956 # ☃☃☃“I like snowmen!”
2957
2958``UnicodeDammit.detwingle()`` solo sabe cómo gestionar Windows-1252 embebido
2959en UTF-8 (o viceversa, supongo), pero este es el caso más común.
2960
2961Fíjate que debes saber que debes llamar a ``UnicodeDammit.detwingle()``
2962en tus datos antes de pasarlo a :py:class:`BeautifulSoup` o el constructor
2963de ``UnicodeDammit``. Beautiful Soup asume que un documento tiene una
2964sola codificación, la que sea. Si quieres pasar un documento que contiene
2965ambas UTF-8 y Windows-1252, es probable que piense que todo el documento
2966es Windows-1252, y el documento se parecerá a ```☃☃☃“I like snowmen!”``.
2967
2968``UnicodeDammit.detwingle()`` es nuevo en Beautiful Soup 4.1.0.
2969
2970==================
2971 Números de línea
2972==================
2973
2974Los analizadores de ``html.parser`` y ``html5lib`` pueden llevar la cuenta
2975de los lugares en el documento original donde se han encontrado cada etiqueta.
2976Puedes acceder a esta información con ``Tag.sourceline`` (número de línea) y
2977``Tag.sourcepos`` (posición del comienzo de una etiqueta en una línea)::
2978
2979 markup = "<p\n>Paragraph 1</p>\n <p>Paragraph 2</p>"
2980 soup = BeautifulSoup(markup, 'html.parser')
2981 for tag in soup.find_all('p'):
2982 print(repr((tag.sourceline, tag.sourcepos, tag.string)))
2983 # (1, 0, 'Paragraph 1')
2984 # (3, 4, 'Paragraph 2')
2985
2986Debe destacarse que los dos analizadores entienden cosas ligeramente
2987diferentes por ``sourceline`` y ``sourcepos``. Para html.parser, estos
2988números representan la posición del signo "menor" inicial. Para html5lib,
2989estos números representan la posición del signo "mayor" final::
2990
2991 soup = BeautifulSoup(markup, 'html5lib')
2992 for tag in soup.find_all('p'):
2993 print(repr((tag.sourceline, tag.sourcepos, tag.string)))
2994 # (2, 0, 'Paragraph 1')
2995 # (3, 6, 'Paragraph 2')
2996
2997Puedes interrumpir esta característica pasado ``store_line_numbers=False``
2998en el constructor de :py:class:`BeautifulSoup`::
2999
3000 markup = "<p\n>Paragraph 1</p>\n <p>Paragraph 2</p>"
3001 soup = BeautifulSoup(markup, 'html.parser', store_line_numbers=False)
3002 print(soup.p.sourceline)
3003 # None
3004
3005`Esta característica es nueva en 4.8.1, y los analizadores basados en lxml no la
3006soportan.`
3007
3008===============================
3009 Comparar objetos por igualdad
3010===============================
3011
3012Beautiful Soup indica que dos objetos :py:class:`NavigableString` o :py:class:`Tag`
3013son iguales cuando representan al mismo marcado HTML o XML. En este ejemplo,
3014las dos etiquetas <b> son tratadas como iguales, aunque están en diferentes
3015partes del objeto árbol, porque ambas son "<b>pizza</b>"::
3016
3017 markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
3018 soup = BeautifulSoup(markup, 'html.parser')
3019 first_b, second_b = soup.find_all('b')
3020 print(first_b == second_b)
3021 # True
3022
3023 print(first_b.previous_element == second_b.previous_element)
3024 # False
3025
3026Si quieres saber si dos variables se refieren a exactamente el mismo
3027objeto, usa `is`::
3028
3029 print(first_b is second_b)
3030 # False
3031
3032==================================
3033 Copiar objetos de Beautiful Soup
3034==================================
3035
3036Puedes usar ``copy.copy()`` para crear una copia de cualquier
3037:py:class:`Tag` o :py:class:`NavigableString`::
3038
3039 import copy
3040 p_copy = copy.copy(soup.p)
3041 print(p_copy)
3042 # <p>I want <b>pizza</b> and more <b>pizza</b>!</p>
3043
3044La copia se considera igual que la original, ya que representa el mismo
3045marcado que el original, pero no son el mismo objeto::
3046
3047 print(soup.p == p_copy)
3048 # True
3049
3050 print(soup.p is p_copy)
3051 # False
3052
3053La única diferencia real es que la copia está completamente desconectada
3054del objeto árbol de Beautiful Soup, como si ``extract()`` hubiese sido
3055llamada sobre ella::
3056
3057 print(p_copy.parent)
3058 # None
3059
3060Esto es porque dos diferentes objetos :py:class:`Tag` no pueden ocupar
3061el mismo espacio al mismo tiempo.
3062
3063=========================================
3064 Personalización avanzada del analizador
3065=========================================
3066
3067Beautiful Soup ofrece numerosas vías para personalizar la manera en la que
3068el analizador trata HTML o XML entrante. Esta sección cubre las técnicas
3069de personalizadas usadas más comúnmente.
3070
3071Analizar solo parte del documento
3072=================================
3073
3074Digamos que quieres usar Beautiful Soup para observar las etiquetas <a> de un
3075documento. Es un malgasto de tiempo y memoria analizar todo el documento y
3076después recorrerlo una y otra vez buscando etiquetas <a>. Sería mucho más
3077rápido ignorar todo lo que no sea una etiqueta <a> desde el principio.
3078La clase :py:class:`SoupStrainer` te permite elegir qué partes de un
3079documento entrante se analizan. Tan solo crea un :py:class:`SoupStrainer` y
3080pásalo al constructor de :py:class:`BeautifulSoup` en el argumento ``parse_only``.
3081
3082(Debe notarse que *esta característica no funcionará si estás usando el
3083analizador de html5lib*. Si usas html5lib, todo el documento será analizado,
3084no importa el resto. Esto es porque html5lib constantemente reorganiza el
3085árbol analizado conforme trabaja, y si alguna parte del documento no
3086consigue introducirse en el árbol analizado, se quedará colgado. Para evitar
3087confusión en los ejemplos más abajo forzaré a Beautiful Soup a que use
3088el analizador integrado de Python).
3089
3090.. py:class:: SoupStrainer
3091
3092La clase :py:class:`SoupStrainer` toma los mismos argumentos que un típico
3093método de `Buscar en el árbol`_: :ref:`name <name>`, :ref:`attrs <attrs>`,
3094:ref:`string <string>`, y :ref:`**kwargs <kwargs>`. Estos son tres objetos
3095:py:class:`SoupStrainer`::
3096
3097 from bs4 import SoupStrainer
3098
3099 only_a_tags = SoupStrainer("a")
3100
3101 only_tags_with_id_link2 = SoupStrainer(id="link2")
3102
3103 def is_short_string(string):
3104 return string is not None and len(string) < 10
3105
3106 only_short_strings = SoupStrainer(string=is_short_string)
3107
3108Voy a traer de nuevo el documento de "Las tres hermanas" una vez más,
3109y veremos cómo parece el documento cuando es analizado con estos
3110tres objetos :py:class:`SoupStrainer`::
3111
3112 html_doc = """<html><head><title>The Dormouse's story</title></head>
3113 <body>
3114 <p class="title"><b>The Dormouse's story</b></p>
3115
3116 <p class="story">Once upon a time there were three little sisters; and their names were
3117 <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
3118 <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
3119 <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
3120 and they lived at the bottom of a well.</p>
3121
3122 <p class="story">...</p>
3123 """
3124
3125 print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
3126 # <a class="sister" href="http://example.com/elsie" id="link1">
3127 # Elsie
3128 # </a>
3129 # <a class="sister" href="http://example.com/lacie" id="link2">
3130 # Lacie
3131 # </a>
3132 # <a class="sister" href="http://example.com/tillie" id="link3">
3133 # Tillie
3134 # </a>
3135
3136 print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
3137 # <a class="sister" href="http://example.com/lacie" id="link2">
3138 # Lacie
3139 # </a>
3140
3141 print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
3142 # Elsie
3143 # ,
3144 # Lacie
3145 # and
3146 # Tillie
3147 # ...
3148 #
3149
3150Puedes también pasar un :py:class:`SoupStrainer` en cualquiera de los métodos
3151cubiertos en `Buscar en el árbol`_. Esto probablemente no sea terriblemente útil,
3152pero pensé en mencionarlo::
3153
3154 soup = BeautifulSoup(html_doc, 'html.parser')
3155 soup.find_all(only_short_strings)
3156 # ['\n\n', '\n\n', 'Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
3157 # '\n\n', '...', '\n']
3158
3159Personalizar atributos multivaluados
3160====================================
3161
3162En un documento HTML, a un atributo como ``class`` se le da una lista
3163de valores, y a un atributo como ``id`` se le da un solo valor, porque
3164la especificación de HTML trata a esos atributos de manera diferente::
3165
3166 markup = '<a class="cls1 cls2" id="id1 id2">'
3167 soup = BeautifulSoup(markup, 'html.parser')
3168 soup.a['class']
3169 # ['cls1', 'cls2']
3170 soup.a['id']
3171 # 'id1 id2'
3172
3173Puedes interrumpir esto pasando ``multi_values_attributes=None``. Entonces
3174a todos los atributos se les dará un solo valor::
3175
3176 soup = BeautifulSoup(markup, 'html.parser', multi_valued_attributes=None)
3177 soup.a['class']
3178 # 'cls1 cls2'
3179 soup.a['id']
3180 # 'id1 id2'
3181
3182Puedes personalizar este comportamiento un poco pasando un diccionario
3183a ``multi_values_attributes``. Si lo necesitas, échale un vistazo a
3184``HTMLTreeBuilder.DEFAULT_CDATA_LIST_ATTRIBUTES`` para ver la configuración
3185que Beautiful Soup usa por defecto, que está basada en la especificación
3186HTML.
3187
3188`(Esto es una nueva característica en Beautiful Soup 4.8.0).`
3189
3190Gestionar atributos duplicados
3191==============================
3192
3193Cuando se use el analizador de ``html.parser``, puedes usar
3194el argumento del constructor ``on_duplicate_attribute`` para personalizar
3195qué hace Beautiful Soup cuando encuentra una etiqueta que define el mismo
3196atributo más de una vez::
3197
3198 markup = '<a href="http://url1/" href="http://url2/">'
3199
3200El comportamiento por defecto es usar el último valor encontrado en la
3201etiqueta::
3202
3203 soup = BeautifulSoup(markup, 'html.parser')
3204 soup.a['href']
3205 # http://url2/
3206
3207 soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='replace')
3208 soup.a['href']
3209 # http://url2/
3210
3211Con ``on_duplicate_attribute='ignore'`` puedes indicar a Beautiful Soup que
3212use el `primer` valor encontrado e ignorar el resto::
3213
3214 soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='ignore')
3215 soup.a['href']
3216 # http://url1/
3217
3218(lxml y html5lib siempre lo hacen así; su comportamiento no puede ser
3219configurado desde Beautiful Soup.)
3220
3221Si necesitas más, puedes pasar una función que sea llamada en cada valor duplicado::
3222
3223 def accumulate(attributes_so_far, key, value):
3224 if not isinstance(attributes_so_far[key], list):
3225 attributes_so_far[key] = [attributes_so_far[key]]
3226 attributes_so_far[key].append(value)
3227
3228 soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute=accumulate)
3229 soup.a['href']
3230 # ["http://url1/", "http://url2/"]
3231
3232
3233`(Esto es una nueva característica en Beautiful Soup 4.9.1.)`
3234
3235Instanciar subclases personalizadas
3236===================================
3237
3238Cuando un analizador indica a Beautiful Soup sobre una etiqueta o una cadena,
3239Beautiful Soup instanciará un objeto :py:class:`Tag` o :py:class:`NavigableString`
3240para contener esa información. En lugar de ese comportamiento por defecto,
3241puedes indicar a Beautiful Soup que instancia `subclases` de :py:class:`Tag` o
3242:py:class:`NavigableString`, subclases que defines con comportamiento
3243personalizado::
3244
3245 from bs4 import Tag, NavigableString
3246 class MyTag(Tag):
3247 pass
3248
3249
3250 class MyString(NavigableString):
3251 pass
3252
3253
3254 markup = "<div>some text</div>"
3255 soup = BeautifulSoup(markup, 'html.parser')
3256 isinstance(soup.div, MyTag)
3257 # False
3258 isinstance(soup.div.string, MyString)
3259 # False
3260
3261 my_classes = { Tag: MyTag, NavigableString: MyString }
3262 soup = BeautifulSoup(markup, 'html.parser', element_classes=my_classes)
3263 isinstance(soup.div, MyTag)
3264 # True
3265 isinstance(soup.div.string, MyString)
3266 # True
3267
3268
3269Esto puede ser útil cuando se incorpore Beautiful Soup en un *framework*
3270de pruebas.
3271
3272`(Esto es una nueva característica de Beautiful Soup 4.8.1.)`
3273
3274=========================
3275 Resolución de problemas
3276=========================
3277
3278.. _diagnose:
3279
3280``diagnose()``
3281==============
3282
3283Si estás teniendo problemas para entender qué hace Beautiful Soup a un
3284documento, pasa el documento a la función ``diagnose()``. (Nuevo en
3285Beautiful Soup 4.2.0) Beautiful Soup imprimirá un informe mostrándote
3286cómo manejan el documento diferentes analizadores, y te dirán si
3287te falta un analizador que Beautiful Soup podría estar usando::
3288
3289 from bs4.diagnose import diagnose
3290 with open("bad.html") as fp:
3291 data = fp.read()
3292
3293 diagnose(data)
3294
3295 # Diagnostic running on Beautiful Soup 4.2.0
3296 # Python version 2.7.3 (default, Aug 1 2012, 05:16:07)
3297 # I noticed that html5lib is not installed. Installing it may help.
3298 # Found lxml version 2.3.2.0
3299 #
3300 # Trying to parse your data with html.parser
3301 # Here's what html.parser did with the document:
3302 # ...
3303
3304Tan solo mirando a la salida de diagnose() puede mostrate cómo resolver
3305el problema. Incluso si no, puedes pegar la salida de ``diagnose()``
3306cuando pidas ayuda.
3307
3308Errores analizando un documento
3309===============================
3310
3311Hay dos tipos diferentes de errores de análisis. Hay veces en que
3312se queda colgado, donde le das a Beautiful Soup un documento y
3313lanza una excepción, normalmente un ``HTMLParser.HTMLParseError``. Y hay
3314comportamientos inesperados, donde un árbol analizado de Beautiful Soup
3315parece muy diferente al documento usado para crearlo.
3316
3317Casi ninguno de estos problemas resultan ser problemas con Beautiful Soup.
3318Esto no es porque Beautiful Soup sea una increíble y bien escrita pieza
3319de software. Es porque Beautiful Soup no incluye ningún código de
3320análisis. En lugar de eso, depende de análisis externos. Si un analizador
3321no está funcionando en un documento concreto, la mejor solución es probar
3322con otro analizador. Échale un vistazo a `Instalar un analizador`_ para
3323detalles y una comparativa de analizadores.
3324
3325Los errores de análisis más comunes son ``HTMLParser.HTMLParseError:
3326malformed start tag`` y ``HTMLParser.HTMLParseError: bad end
3327tag``. Ambos son generados por la librería del analizador HTML
3328incluido en Python, y la solución es :ref:`instalar lxml o html5lib.
3329<parser-installation>`
3330
3331El comportamiento inesperado más común es que no puedas encontrar
3332una etiqueta que sabes que está en el documento. La viste llegar, pero
3333``find_all()`` devuelve ``[]`` o ``find()`` devuelve ``None``. Esto
3334es otro problema común con el analizador HTML integrado en Python, el cual
3335a veces omite etiquetas que no entiende. De nuevo, la mejor solución es
3336:ref:`instalar lxml o html5lib. <parser-installation>`.
3337
3338Problemas de incompatibilidad de versiones
3339==========================================
3340
3341* ``SyntaxError: Invalid syntax`` (on the line ``ROOT_TAG_NAME =
3342 '[document]'``): Causado por ejecutar una version antigua de Beautiful
3343 Soup de Python 2 bajo Python 3, sin convertir el código.
3344
3345* ``ImportError: No module named HTMLParser`` - Causado por ejecutar
3346 una version antigua de Beautiful Soup de Python 2 bajo Python 3.
3347
3348* ``ImportError: No module named html.parser`` - Causado por ejecutar
3349 una version de Beautiful Soup de Python 3 bajo Python 2.
3350
3351* ``ImportError: No module named BeautifulSoup`` - Causado por ejecutar
3352 código de Beautiful Soup 3 en un sistema que no tiene BS3 instalado. O
3353 al escribir código de Beautiful Soup 4 sin saber que el nombre del paquete
3354 se cambió a ``bs4``.
3355
3356* ``ImportError: No module named bs4`` - Causado por ejecutar código de
3357 Beautiful Soup 4 en un sistema que no tiene BS4 instalado.
3358
3359.. _parsing-xml:
3360
3361Analizar XML
3362============
3363
3364Por defecto, Beautiful Soup analiza documentos HTML. Para analizar
3365un documento como XML, pasa "xml" como el segundo argumento al
3366constructor :py:class:`BeautifulSoup`::
3367
3368 soup = BeautifulSoup(markup, "xml")
3369
3370Necesitarás :ref:`tener lxml instalado <parser-installation>`.
3371
3372Otros problemas de análisis
3373===========================
3374
3375* Si tu script funciona en un ordenador pero no en otro, o en un
3376 entorno virtual pero no en otro, o fuera del entorno virtual
3377 pero no dentro, es probable porque los dos entornos tienen
3378 diferentes librerías de analizadores disponibles. Por ejemplo,
3379 puedes haber desarrollado el script en un ordenador que solo
3380 tenga html5lib instalado. Mira `Diferencias entre analizadores`_
3381 por qué esto importa, y solucionar el problema especificando una
3382 librería de análisis en el constructor de :py:class:`Beautiful Soup`.
3383
3384* Porque `las etiquetas y atributos de HTML son sensibles a mayúsculas
3385 y minúsculas <http://www.w3.org/TR/html5/syntax.html#syntax>`_,
3386 los tres analizadores HTML convierten los nombres de las etiquetas y
3387 atributos a minúscula. Esto es, el marcado <TAG></TAG> se convierte
3388 a <tag></tag>. Si quieres preservar la mezcla entre minúscula y
3389 mayúscula o mantener las mayúsculas en etiquetas y atributos,
3390 necesitarás :ref:`analizar el documento como XML. <parsing-xml>`
3391
3392.. _misc:
3393
3394Diversos
3395========
3396
3397* ``UnicodeEncodeError: 'charmap' codec can't encode character
3398 '\xfoo' in position bar`` (o cualquier otro
3399 ``UnicodeEncodeError``) - Este problema aparece principalmente
3400 en dos situaciones. Primero, cuando intentas mostrar un carácter
3401 Unicode que tu consola no sabe cómo mostrar (mira `esta página en la
3402 wiki de Python <http://wiki.python.org/moin/PrintFails>`_). Segundo,
3403 cuando estás escribiendo en un archivo y pasas un carácter Unicode
3404 que no se soporta en tu codificación por defecto. En este caso,
3405 la solución más simple es codificar explícitamente la cadena Unicode
3406 en UTF-8 con ``u.encode("utf8")``.
3407
3408* ``KeyError: [attr]`` - Causado por acceder a ``tag['attr']`` cuando
3409 la etiqueta en cuestión no define el atributo ``'attr'``. Los
3410 errores más comunes son ``KeyError: 'href'`` y ``KeyError: 'class``.
3411 Usa ``tag.get('attr')`` si no estás seguro si ``attr`` está definido,
3412 tal y como harías con un diccionario de Python.
3413
3414* ``AttributeError: 'ResultSet' object has no attribute 'foo'`` - Esto
3415 normalmente ocurre cuando esperas que ``find_all()`` devuelva
3416 una sola etiqueta o cadena. Pero ``find_all()`` devuelve una
3417 `lista` de etiquetas y cadenas--un objeto ``ResultSet``. Tienes que
3418 iterar sobre la lista y comprobar el ``.foo`` de cada uno, O, si solo
3419 quieres un resultado, tienes que usar ``find()`` en lugar de
3420 ``find_all()``.
3421
3422* ``AttributeError: 'NoneType' object has no attribute 'foo'`` - Esto
3423 normalmente ocurre porque llamaste a ``find()`` y después intentaste
3424 acceder al atributo ``.foo`` del resultado. Pero en tu caso, ``find()``
3425 no encontró nada, así que devolvió ``None``, en lugar de devolver
3426 una etiqueta o una cadena de caracteres. Necesitas averiguar por qué
3427 ``find()`` no está devolviendo nada.
3428
3429* ``AttributeError: 'NavigableString' object has no attribute
3430 'foo'`` - Esto ocurre normalmente porque estás tratando una
3431 cadena de caracteres como si fuese una etiqueta. Puedes estar iterando
3432 sobre una lista, esperando que tan solo contenga etiquetas, pero en
3433 realidad contiene tanto etiquetas como cadenas.
3434
3435
3436Mejorar el rendimiento
3437======================
3438
3439Beautiful Soup nunca será tan rápido como los analizadores en los que
3440se basa. Si el tiempo de respuesta es crítico, si estás pagando por
3441tiempo de uso por hora, o si hay alguna otra razón por la que el tiempo
3442de computación es más valioso que el tiempo del programador, deberías
3443olvidarte de Beautiful Soup y trabajar directamente sobre
3444`lxml <http://lxml.de/>`_.
3445
3446Dicho esto, hay cosas que puedes hacer para aumentar la velocidad de
3447Beautiful Soup. Si no estás usando lxml como el analizador que hay
3448por debajo, mi consejo es que :ref:`empieces a usarlo <parser-installation>`.
3449Beautiful Soup analiza documentos significativamente más rápido usando
3450lxml que usando html.parser o html5lib.
3451
3452Puedes aumentar la velocidad de detección de codificación significativamente
3453instalando la librería `cchardet <http://pypi.python.org/pypi/cchardet/>`_.
3454
3455`Analizar solo parte del documento`_ no te ahorrará mucho tiempo de análisis, pero puede
3456ahorrar mucha memoria, y hará que `buscar` en el documento sea mucho más rápido.
3457
3458==============================
3459 Traducir esta documentación
3460==============================
3461
3462Nuevas traducciones de la documentación de Beautiful Soup se agradecen
3463enormemente. Las traducciones deberían estar bajo la licencia del MIT, tal
3464y como están Beautiful Soup y su documentación en inglés.
3465
3466Hay dos maneras para que tu traducción se incorpore a la base de código
3467principal y al sitio de Beautiful Soup:
3468
34691. Crear una rama del repositorio de Beautiful Soup, añadir tus
3470 traducciones, y proponer una fusión (*merge*) con la rama principal, lo
3471 mismo que se haría con una propuesta de código del código fuente.
3472
34732. Enviar un mensaje al grupo de discusión de Beautiful Soup con un
3474 enlace a tu traducción, o adjuntar tu traducción al mensaje.
3475
3476Utiliza la traducción china o portugués-brasileño como tu modelo. En
3477particular, por favor, traduce el archivo fuente ``doc/source/index.rst``,
3478en vez de la versión HTML de la documentación. Esto hace posible que la
3479documentación se pueda publicar en una variedad de formatos, no solo HTML.
3480
3481==================
3482 Beautiful Soup 3
3483==================
3484
3485Beautiful Soup 3 es la serie de lanzamientos anterior, y no está siendo
3486activamente desarrollada. Actualmente está empaquetada con las
3487distribuciones de Linux más grandes:
3488
3489:kbd:`$ apt-get install python-beautifulsoup`
3490
3491También está publicada a través de PyPI como :py:class:`BeautifulSoup`.:
3492
3493:kbd:`$ easy_install BeautifulSoup`
3494
3495:kbd:`$ pip install BeautifulSoup`
3496
3497También puedes `descargar un tarball de Beautiful Soup 3.2.0
3498<http://www.crummy.com/software/BeautifulSoup/bs3/download/3.x/BeautifulSoup-3.2.0.tar.gz>`_.
3499
3500Si ejecutaste ``easy_install beautifulsoup`` o ``easy_install BeautifulSoup``,
3501pero tu código no funciona, instalaste por error Beautiful Soup 3. Necesitas
3502ejecutar ``easy_install beautifulsoup4``.
3503
3504`La documentación de Beautiful Soup 3 está archivada online
3505<http://www.crummy.com/software/BeautifulSoup/bs3/documentation.html>`_.
3506
3507Actualizar el código a BS4
3508==========================
3509
3510La mayoría del código escrito con Beautiful Soup 3 funcionará
3511con Beautiful Soup 4 con un cambio simple. Todo lo que debes hacer
3512es cambiar el nombre del paquete de :py:class:`BeautifulSoup` a
3513``bs4``. Así que esto::
3514
3515 from BeautifulSoup import BeautifulSoup
3516
3517se convierte en esto::
3518
3519 from bs4 import BeautifulSoup
3520
3521* Si obtienes el ``ImportError`` "No module named BeautifulSoup`", tu
3522 problema es que estás intentando ejecutar código de Beautiful Soup 3,
3523 pero solo tienes instalado Beautiful Soup 4.
3524
3525* Si obtienes el ``ImportError`` "No module named bs4", tu problema
3526 es que estás intentando ejecutar código Beautiful Soup 4, pero solo
3527 tienes Beautiful Soup 3 instalado.
3528
3529Aunque BS4 es mayormente compatible con la versión anterior BS3, la
3530mayoría de sus métodos han quedado obsoletos y dados nuevos nombres
3531para que `cumplan con PEP 8 <http://www.python.org/dev/peps/pep-0008/>`_.
3532Hay muchos otros renombres y cambios, y algunos de ellos rompen
3533con la compatibilidad hacia atrás.
3534
3535Esto es todo lo que necesitarás saber para convertir tu código y hábitos BS3 a
3536BS4:
3537
3538Necesitas un analizador
3539-----------------------
3540
3541Beautiful Soup 3 usaba el ``SGMLParser`` de Python, un módulo que
3542fue obsoleto y quitado en Python 3.0. Beautiful Soup 4 usa
3543``html.parser`` por defecto, pero puedes conectar lxml o html5lib
3544y usar esos. Mira `Instalar un analizador`_ para una comparación.
3545
3546Como ``html.parser`` no es el mismo analizador que ``SGMLParser``,
3547podrías encontrarte que Beautiful Soup 4 te de un árbol analizado
3548diferente al que te da Beautiful Soup 3 para el mismo marcado. Si
3549cambias ``html.parser`` por lxml o html5lib, puedes encontrarte
3550que el árbol analizado también cambia. Si esto ocurre, necesitarás
3551actualizar tu código de *scraping* para gestionar el nuevo árbol.
3552
3553Nombre de los métodos
3554---------------------
3555
3556* ``renderContents`` -> ``encode_contents``
3557* ``replaceWith`` -> ``replace_with``
3558* ``replaceWithChildren`` -> ``unwrap``
3559* ``findAll`` -> ``find_all``
3560* ``findAllNext`` -> ``find_all_next``
3561* ``findAllPrevious`` -> ``find_all_previous``
3562* ``findNext`` -> ``find_next``
3563* ``findNextSibling`` -> ``find_next_sibling``
3564* ``findNextSiblings`` -> ``find_next_siblings``
3565* ``findParent`` -> ``find_parent``
3566* ``findParents`` -> ``find_parents``
3567* ``findPrevious`` -> ``find_previous``
3568* ``findPreviousSibling`` -> ``find_previous_sibling``
3569* ``findPreviousSiblings`` -> ``find_previous_siblings``
3570* ``getText`` -> ``get_text``
3571* ``nextSibling`` -> ``next_sibling``
3572* ``previousSibling`` -> ``previous_sibling``
3573
3574Algunos argumentos del constructor de Beautiful Soup fueron renombrados
3575por la misma razón:
3576
3577* ``BeautifulSoup(parseOnlyThese=...)`` -> ``BeautifulSoup(parse_only=...)``
3578* ``BeautifulSoup(fromEncoding=...)`` -> ``BeautifulSoup(from_encoding=...)``
3579
3580Renombré un método para compatibilidad con Python 3:
3581
3582* ``Tag.has_key()`` -> ``Tag.has_attr()``
3583
3584Renombré un atributo para usar terminología más precisa:
3585
3586* ``Tag.isSelfClosing`` -> ``Tag.is_empty_element``
3587
3588Renombré tres atributos para evitar usar palabras que tienen un significado
3589especial en Python. A diferencia de otros, estos cambios no soportan
3590*compatibilidad hacia atrás*. Si usaste estos atributos en BS3, tu código
3591se romperá en BS4 hasta que lo cambies.
3592
3593* ``UnicodeDammit.unicode`` -> ``UnicodeDammit.unicode_markup``
3594* ``Tag.next`` -> ``Tag.next_element``
3595* ``Tag.previous`` -> ``Tag.previous_element``
3596
3597Estos métodos sobras desde la API de Beautiful Soup 2. Han quedado
3598obsoletos desde 2006, y no deberían usarse en absoluto:
3599
3600* ``Tag.fetchNextSiblings``
3601* ``Tag.fetchPreviousSiblings``
3602* ``Tag.fetchPrevious``
3603* ``Tag.fetchPreviousSiblings``
3604* ``Tag.fetchParents``
3605* ``Tag.findChild``
3606* ``Tag.findChildren``
3607
3608
3609Generadores
3610-----------
3611
3612Le di a los generadores nombres que cumplan con PEP 8, y se transformaron
3613en propiedades:
3614
3615* ``childGenerator()`` -> ``children``
3616* ``nextGenerator()`` -> ``next_elements``
3617* ``nextSiblingGenerator()`` -> ``next_siblings``
3618* ``previousGenerator()`` -> ``previous_elements``
3619* ``previousSiblingGenerator()`` -> ``previous_siblings``
3620* ``recursiveChildGenerator()`` -> ``descendants``
3621* ``parentGenerator()`` -> ``parents``
3622
3623Así que en lugar de esto::
3624
3625 for parent in tag.parentGenerator():
3626 ...
3627
3628Puedes escribir esto::
3629
3630 for parent in tag.parents:
3631 ...
3632
3633(Pero el código antiguo seguirá funcionando).
3634
3635Algunos de los generadores solían devolver ``None`` después de que hayan
3636terminado, y después paran. Eso era un error. Ahora el generador tan solo
3637para.
3638
3639Hay dos nuevos generadores, :ref:`.strings y .stripped_strings
3640<string-generators>`. ``.strings`` devuelve objetos NavigableString,
3641y ``.stripped_strings`` devuelve cadenas de Python cuyos espacios
3642en blanco al comienzo y al final han sido quitados.
3643
3644XML
3645---
3646
3647Ya no hay una clase ``BeautifulStoneSoup`` para analizar XML. Para
3648analizar XML pasas "xml" como el segundo argumento del constructor
3649de :py:class:`BeautifulSoup`. Por la misma razón, el constructor
3650de :py:class:`BeautifulSoup` ya no reconoce el argumento ``isHTML``.
3651
3652La gestión de Beautiful Soup sobre las etiquetas XML sin elementos ha sido
3653mejorada. Previamente cuando analizabas XML tenías que indicar
3654explícitamente qué etiquetas eran consideradas etiquetas sin elementos.
3655El argumento ``selfClosingTags`` al constructor ya no se reconoce.
3656En lugar de ello, Beautiful Soup considera cualquier etiqueta vacía como
3657una etiqueta sin elementos. Si añades un hijo a una etiqueta sin elementos,
3658deja de ser una etiqueta sin elementos.
3659
3660Entidades
3661---------
3662
3663Una entidad HTML o XML entrante siempre se convierte al correspondiente
3664carácter Unicode. Beautiful Soup 3 tenía varias formas solapadas para
3665gestionar entidades, las cuales se han eliminado. El constructor de
3666:py:class:`BeautifulSoup` ya no reconoce los argumentos ``smartQuotesTo``
3667o ``convertEntities`` (`Unicode, Dammit`_ aún tiene ``smart_quotes_to``,
3668pero por defecto ahora transforma las comillas inteligentes a Unicode).
3669Las constantes ``HTML_ENTITIES``, ``XML_ENTITIES``, y ``XHTML_ENTITIES``
3670han sido eliminadas, ya que configuran una característica (transformando
3671algunas pero no todas las entidades en caracteres Unicode) que ya no
3672existe.
3673
3674Si quieres volver a convertir caracteres Unicode en entidades HTML
3675a la salida, en lugar de transformarlos a caracteres UTF-8, necesitas
3676usar un :ref:`*formatter* de salida <output_formatters>`.
3677
3678Otro
3679----
3680
3681:ref:`Tag.string <.string>` ahora funciona recursivamente. Si una
3682etiqueta A contiene una sola etiqueta B y nada más, entonces
3683A.string es el mismo que B.string (Antes, era ``None``).
3684
3685Los `atributos multivaluados`_ como ``class`` tienen listas de cadenas
3686de caracteres como valores, no cadenas. Esto podría afectar la manera
3687en la que buscas por clases CSS.
3688
3689Objetos :py:class:`Tag` ahora implementan el método ``__hash__``, de tal
3690manera que dos objetos :py:class:`Tag` se consideran iguales si generan
3691el mismo marcado. Esto puede cambiar el comportamiento de tus scripts
3692si insertas los objetos :py:class:`Tag` en un diccionario o conjunto.
3693
3694Si pasas a unos de los métodos ``find*`` una :ref:`cadena <string>` y
3695un argumento específico de una etiqueta como :ref:`name <name>`, Beautiful
3696Soup buscará etiquetas que casen con tu criterio específico de la etiqueta
3697y cuyo :ref:`Tag.string <.string>` case con tu valor para la :ref:`cadena <string>`.
3698`No` encontrará las cadenas mismas. Anteriormente, Beautiful Soup ignoraba el
3699argumento específico de la etiqueta y buscaba por cadenas de caracteres.
3700
3701El constructor de :py:class:`Beautiful Soup` ya no reconoce el argumento
3702`markupMassage`. Es ahora responsabilidad del analizador gestionar el marcado
3703correctamente.
3704
3705Los analizadores alternativos, que rara vez se utilizaban, como
3706``ICantBelieveItsBeautifulSoup`` y ``BeautifulSOAP`` se han eliminado.
3707Ahora es decisión del analizador saber cómo gestionar marcado ambiguo.
3708
3709El método ``prettify()`` ahora devuelve una cadena Unicode, no un bytestring.

Subscribers

People subscribed via source and target branches