Web Scraping con BeautifulSoup en Python

Web Scraping es una técnica utilizada para la extracción de información dentro de una web, simulando la navegación humana. Con esta información podemos obtener el contenido de las etiquetas de un documento HTML o XML con el fin de recopilar información sobre el contenido de una página web, como los valores y contenidos de sus etiquetas, metadatos, enlaces dentro de etiquetas de tipo <a>, etc. En resumen, y haciendo una traducción literal, se trata de «escarbar una web«.

Estas técnicas se conocen con varios nombres, como web crawlingscreen scrapingweb extractioncrawl spiderweb-botspider robotdata miningharvests, …

Se puede aprovechar esta técnica para obtener grandes cantidades de información haciendo uso de búsquedas en páginas web, que junto al uso de expresiones regulares podemos obtener la información deseada.

Como ejemplos de uso, sin entrar en detalle ya que no es el objetivo de esta entrada:

  • Marketing de contenidos: recopilar información de una web para generar nuestro propio contenido con datos estadísticos.
  • Visibilidad en redes sociales: interactuar con usuarios en redes sociales.
  • Imagen y visibilidad de marca en Internet: tener bajo control la posición de nuestra web en Google obteniendo la posición de las entradas en sus búsquedas, o la presencia de nuestra marca en las entradas de un foro.

También podremos usar esta técnica para obtener información valiosa para realizar auditorías de seguridad, obteniendo los formularios para pruebas de XSS (Cross-site Scripting), llamadas a funciones de JavaScript, información relevante de etiquetas <meta> con información de la Web, …

Existen varios problemas a la hora de realizar esta técnica:

  • Sesiones de navegación necesarias para acceder al contenido web.
  • Uso de JavaScript para mostrar el contenido, lo que nos obligará a utilizar un motor para obtener este contenido.
  • Bloqueos en la aplicación web, por superar el número máximo de peticiones, no solicitar todos los elementos de la página, problemas con el user-agent, bloqueo de la IP o uso de reCaptcha.
  • También las implicaciones legales que tienen este tipo de técnicas, ya que en ocasiones podrían llegar a ser denunciables.

Para ello vamos a utilizar la librería BeautifulSoup de Python, que junto a urllib nos permitirá recopilar cualquier información contenida en una página web.

A continuación se muestran los comandos más habituales para empezar a trabajar con esta técnica. Al final de la entrada podrás consultar el enlace oficial con muchos más comandos y ejemplos para ampliar conocimientos.

Vamos al lío!!!


Objetivo

Vamos a realizar el parseo de información contenida en documentos HTML y XML.

Haremos uso de la librería BeautifulSoup:

  • Librería utilizada para realizar operaciones de Web Scraping desde Python.
  • Parsea y permite extraer información de documentos HTML.
  • Soporta múltiples parsers para tratar documentos XML/XHTML y HTML.
  • Genera una estructura de árbol con todos los elementos del documento parseado.
  • Permite de manera muy sencilla buscar elementos HTML, tales como enlaces, formularios o cualquier otra etiqueta HTML.

Escenario

Vamos a contemplar dos escenarios para la extracción del contenido:

  • Documento HTML a través de una URL.
  • Documento XML a través de un archivo .xml

Librerías necesarias

Instalaremos las librerías bs4 y lxml para el parseo.

pip install bs4
pip install lxml

También podemos instalar el parser para HTML5, pero en mi caso no fue necesario:

pip install html5lib

OJO!!! Si no funcionara la instalación con pip probar a instalar el paquete desde los repositorios oficiales. Por ejemplo, la librería lxml solo la pude instalar en mi Debian con el comando apt-get install python-lxml, ya que con pip me daba error.


Extracción HTML

Vamos a extraer el contenido de este blog utilizando la URL como fuente de datos.

Instancia BeautifulSoup

Primero importamos las librerías recién instaladas:

from bs4 import BeautifulSoup
import urllib

Leemos el contenido de la URL y la guardamos en una variable:

contents = urllib.urlopen("https://tiraquelibras.com/blog").read()

Creamos una instancia BeautifulSoup indicando como primer argumento el contenido HTML y como segundo el parser, con el fin de acceder y navegar por el documento y acceder a su contenido.

bs = BeautifulSoup(contents, "lxml")

Ahora ya podemos acceder al contenido de todas sus etiquetas.

Acceso al contenido

A continuación vamos a ver los comandos necesarios para acceder al contenido del documento extraído.

Etiquetas

Obtenemos el contenido de las etiquetas indicando el nombre de esta. Por ejemplo, mostramos el contenido de las etiquetas <title> y <meta>:

>>> bs.title
<title>Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI</title>
>>> bs.meta
<meta charset="unicode-escape"/>

Por defecto solo se muestra el contenido de la primera coincidencia. Más adelante veremos como obtener todos los resultados.

Contenido de etiquetas

Para obtener el contenido de las etiquetas, si lo tuviera, indicamos la función string a continuación del nombre de la etiqueta. También podemos usar la función text. Por ejemplo, la etiqueta <meta> no tiene contenido alguno, pero <title> sí:

>>> bs.meta.string
>>> bs.title.string
u'Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI'

>>> bs.title.text
u'Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI'

Atributos y valores

Cada etiqueta se trata como un diccionario, ya que contiene atributos y valores. Por ejemplo, la etiqueta <meta> no tiene contenido, pero si atributos y valores, por lo que si hacemos una consulta al diccionario por su atributo como key nos muestra el contenido como value:

>>> bs.meta
<meta charset="unicode-escape"/>
>>> bs.meta.content
>>> bs.meta['charset']
u'UTF-8'

Por ejemplo, podemos obtener los enlaces del documento, indicados en etiquetas de tipo <a>, usando la función get() para obtener el valor de un atributo href en concreto:

>>> bs.a
<a class="skip-link screen-reader-text" href="#main">Skip to content</a>
>>> bs.a.get('href')
'#main'

Etiqueta anterior / siguiente

Para acceder a la siguiente o anterior etiqueta en el documento usamos las funciones nextprevious, respectivamente.

Importante tener en cuenta que para mostrar la siguiente o anterior etiqueta es necesario concatenar varias veces estas funciones, de tal forma que una solo me muestra el siguiente valor del árbol, osea el contenido de la etiqueta con la función siguiente y el retorno de carro para la anterior. Lo veremos mejor en el siguiente ejemplo:

>>> bs.title
<title>Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI</title>
>>> bs.title.next
u'Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI'

>>> bs.title.next.next
u'\n'

>>> bs.title.next.next.next
<link href="//tiraquelibras.com" rel="dns-prefetch"/>

Y a la inversa:

>>> bs.title.previous
u'\n'

>>> bs.title.previous.previous
<link href="http://gmpg.org/xfn/11" rel="profile"/>

# SI BUSCAMOS EL PRIMER LINK OBTENEMOS EL RESULTADO ANTERIOR:
>>> bs.link
<link href="http://gmpg.org/xfn/11" rel="profile"/>

Podemos concatener estos comandos con otras funciones, por ejemplo si usamos la función string mostramos únicamente el contenido.

>>> bs.link.next.next
<title>Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI</title>

>>> bs.link.next.next.string
u'Tira que libras \u2026 \u2013 Blog de ciberseguridad y TI'

Etiqueta padre

Con la función parent obtenemos la etiqueta del nivel inmediatamente superior a la que pertenece la que estamos consultando.

Por ejemplo, para mostrar la etiqueta a la que pertenece el primer párrafo o etiqueta <p>, observamos como se muestra la etiqueta padre y a continuación sus etiquetas hijas:

>>> bs.p
<p class="site-description">Blog de ciberseguridad y TI</p>

>>> bs.p.parent
<div class="center-brand">\n<h1 class="site-title"><a href="https://blog.tiraquelibras.com/" rel="home">Tira que libras \u2026</a></h1>\n<p class="site-description">Blog de ciberseguridad y TI</p>\n</div>

Mostrar todos los resultados

Los comandos vistos hasta ahora solo muestran el primer resultado de la consulta, pero si queremos ver todos los resultados debemos de usar la función find_all() indicando la etiqueta a buscar.

>>> bs.find_all('meta')
[<meta charset="unicode-escape"/>, <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>, <meta content="WordPress 5.1" name="generator"/>, <meta content="website" name="og:type" property="og:type"/>, <meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/04/cropped-favicon.png" name="og:image" property="og:image"/>, <meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/03/cropped-office-2539844_1280-2.jpg" name="og:image" property="og:image"/>, <meta content="Blog de ciberseguridad y TI" name="og:description" property="og:description"/>, <meta content="es_ES" name="og:locale" property="og:locale"/>, <meta content="Tira que libras ..." name="og:site_name" property="og:site_name"/>, <meta content="summary" name="twitter:card" property="twitter:card"/>, <meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/04/cropped-favicon-270x270.png" name="msapplication-TileImage"/>, <meta content="width=device-width, user-scalable=yes, initial-scale=1.0, minimum-scale=0.1, maximum-scale=10.0" name="viewport"/>]

Para verlo mejor formateado usamos un diccionario y le hacemos un bucle for:

>>> metas = bs.find_all('meta')
>>> for meta in metas:
...  print meta
...
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<meta content="WordPress 5.1" name="generator"/>
<meta content="website" name="og:type" property="og:type"/>
<meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/04/cropped-favicon.png" name="og:image" property="og:image"/>
<meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/03/cropped-office-2539844_1280-2.jpg" name="og:image" property="og:image"/>
<meta content="Blog de ciberseguridad y TI" name="og:description" property="og:description"/>
<meta content="es_ES" name="og:locale" property="og:locale"/>
<meta content="Tira que libras ..." name="og:site_name" property="og:site_name"/>
<meta content="summary" name="twitter:card" property="twitter:card"/>
<meta content="https://blog.tiraquelibras.com/wp-content/uploads/2018/04/cropped-favicon-270x270.png" name="msapplication-TileImage"/>
<meta content="width=device-width, user-scalable=yes, initial-scale=1.0, minimum-scale=0.1, maximum-scale=10.0" name="viewport"/>

Con esta función podemos buscar todos los enlaces contenidos en el documento consultado:

>>> enlaces = bs.find_all('a')
>>> for enlace in enlaces:
...  print enlace
...
<a class="skip-link screen-reader-text" href="#main">Skip to content</a>
<a href="https://blog.tiraquelibras.com/" rel="home">Tira que libras …</a>
<a aria-current="page" href="https://blog.tiraquelibras.com/">Inicio</a>
<a href="https://blog.tiraquelibras.com/?page_id=49">Info</a>
<a href="https://blog.tiraquelibras.com/?page_id=151">Política</a>
<a href="https://blog.tiraquelibras.com/?page_id=66">Contacto</a>
...

Buscar por id y class

Podemos buscar capas o etiquetas concretas buscando por su identificador o clase. Para el primero indicamos el nombre del identificador, pero para el segundo debemos de indicar un diccionario de atributos para aplicar el filtro.

  • La sintaxis para id sería -> find_all(‘<tag_name>’, id='<id_name>’)
  • La sintaxis para class sería -> find_all(‘<tag_name>’, {‘class’:'<class_name>’})

Buscar por id:

>>> bs.find_all('footer', id="colophon")
[<footer class="site-footer" id="colophon" role="contentinfo">\n<div class="footer-copy">\n<div class="container">\n<div class="row">\n<div class="col-12 col-sm-12">\n<div class="site-credits">\xa9 2019 Tira que libras ...</div>\n<div class="site-info">\n<a href="https://wordpress.org/">Powered by WordPress</a>\n<span class="sep"> - </span>\n<a href="http://themeegg.com">Miteri by ThemeEgg</a>\n</div><!-- .site-info -->\n</div>\n</div>\n</div><!-- .container -->\n</div><!-- .footer-copy -->\n</footer>]

Buscar por class:

>>> bs.find_all('h1', {'class':'site-title'})
[<h1 class="site-title"><a href="https://blog.tiraquelibras.com/" rel="home">Tira que libras \u2026</a></h1>]

Auditoría Web – Formularios

Para buscar los formularios indicamos el nombre de esta etiqueta directamente. Esto nos es muy útil para auditorías Web, con el fin de identificar dónde hacer pruebas de XSS, por ejemplo.

>>> forms = bs.find_all('form')


>>> for form in forms:
...  print form
...
<form action="https://blog.tiraquelibras.com/" class="search-form clear" method="get" role="search">
<label>
<span class="screen-reader-text">Search for:</span>
<input class="search-field" id="s" miteri="search" name="s" placeholder="Search …" value=""/>
</label>
<button class="search-submit" miteri="submit">
<i class="fa fa-search"></i> <span class="screen-reader-text">
                Search</span>
</button>
</form>
...

 


Extracción XML

Vamos a extraer el contenido utilizando un archivo .xml como fuente de datos.

Archivo XML

Vamos a tomar como ejemplo el archivo bs.xml con el siguiente contenido:

<note>
  <to>Manolo</to>
  <frm>Lorena</frm>
  <heading>Recordatorio</heading>
  <body>¡Hacer la compra al salir del trabajo!</body>
</note>

Instancia BeautifulSoup

Primero accedemos al documento:

xml = open("bs.xml", "r").read()

Y luego creamos la instancia de BeautifulSoup indicando el parser a utilizar, en este caso xml aunque valdría lxml también:

bs = BeautifulSoup(xml, "xml")

Acceso al contenido

El acceso al contenido es exactamente igual que lo indicado para los archivos HTML, por lo que no vamos a repetir los pasos anteriores de nuevo.

A continuación indicamos unos sencillos ejemplos de extracción sobre la instancia generada:

>>> bs.note
<note>\n<to>Manolo</to>\n<frm>Lorena</frm>\n<heading>Recordatorio</heading>\n<body>\u0e22\u0e01Hacer la compra al salir del trabajo!</body>\n</note>

>>> bs.note.string
>>>

>>> bs.note.to
<to>Manolo</to>

>>> bs.note.to.string
u'Manolo'

Conclusión

Con esta técnica no solo podemos obtener información valiosa de un archivo HTML o XML, en resumen de una web, foro, chat o de un enlace en concreto, sino que podemos obtener información importante para la realización de una auditoría Web o de un análisis de contenido en busca de enlaces maliciosos, por ejemplo.


Enlaces relacionados

Enlace oficial de BeautifulSoup, pincha aquí.

Videotutorial de DragonJar muy interesante sobre este asunto, pincha aquí.