Pular para o conteúdo

BeautifulSoup

Introdução

Um arquivo HTML normalmente possui dezenas de elementos. BeautifulSoup permite montar uma estrutura de dados com esses elementos e fazer a análise de forma rápida e automática. Para montar a estrutura de dados, deve-se usar um analisador. O analisador padrão (vem com o BeautifulSoup) é o “html.parser“.

No exemplo abaixo, é usado o módulo Requests para obter a página oficial do Python; em seguida é usado o “html.parser” para montar a estrutura de dados da página (pode-se também usar outros analisadores como lxml e html5lib); no final, é usado o método prettify() para exibir o código da página de forma estruturada.

import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
print(pagina.prettify())

São exemplos de métodos do BeautifulSoup:

  • findAll() – permite extrair uma lista de objetos tag (é possível especificar o nome da tag e quaisquer atributos da tag);
  • find() – retorna a primeira tag encontrada com a especificação dada;
  • select() – faz seleção de elementos de acordo com definições CSS.

Exemplos

  • Exemplo: usa Requests para obter a página oficial do Python; em seguida, usa o BeautifulSoup com o analisador padrão “html.parser” para criar uma estrutura de dados com os elementos HTML. O método select() é então usado para localizar o título e os três primeiros metadados da página.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
# Título da página
titulo = pagina.select('title')
print("\nTipo de objeto: " + str(type(titulo)))
print("Linha do Título: " + str(titulo))
print("Conteudo do titulo: " + titulo[0].getText())
# Metas da página
print('\nMETA 0:' + str(pagina.select('meta')[0]))
print('META 1:' + str(pagina.select('meta')[1]))
print('META 2:' + str(pagina.select('meta')[2]) + "\n")

A resposta é

Tipo de objeto: <class 'bs4.element.ResultSet'>
Linha do Título: [<title>Welcome to Python.org</title>]
Conteudo do titulo: Welcome to Python.org

META 0:<meta charset="utf-8"/>
META 1:<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
META 2:<meta content="Python.org" name="application-name"/>
  • Exemplo: o método select() encontra múltiplas instâncias para ‘meta’ na página oficial do Python e retorna essas instâncias em uma lista.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
lista = pagina.select('meta')
for i in lista:
   print(i)

A lista de linhas metadados encontradas na página Python é mostrada abaixo.

<meta charset="utf-8"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<meta content="Python.org" name="application-name"/>
<meta content="The official home of the Python Programming Language" name="msapplication-tooltip"/>
<meta content="Python.org" name="apple-mobile-web-app-title"/>
<meta content="yes" name="apple-mobile-web-app-capable"/>
<meta content="black" name="apple-mobile-web-app-status-bar-style"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<meta content="True" name="HandheldFriendly"/>
<meta content="telephone=no" name="format-detection"/>
<meta content="on" http-equiv="cleartype"/>
<meta content="false" http-equiv="imagetoolbar"/>
<meta content="/static/metro-icon-144x144-precomposed.png" name="msapplication-TileImage"/>
<meta content="#3673a5" name="msapplication-TileColor"/>
<meta content="#3673a5" name="msapplication-navbutton-color"/>
<meta content="The official home of the Python Programming Language" name="description"/>
<meta content="Python programming language object oriented web free open source software license documentation download community" name="keywords"/>
<meta content="website" property="og:type"/>
<meta content="Python.org" property="og:site_name"/>
<meta content="Welcome to Python.org" property="og:title"/>
<meta content="The official home of the Python Programming Language" property="og:description"/>
<meta content="https://www.python.org/static/opengraph-icon-200x200.png" property="og:image"/>
<meta content="https://www.python.org/static/opengraph-icon-200x200.png" property="og:image:secure_url"/>
  • Exemplo: o script obtém informações sobre o título da página Python de duas formas diferentes.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")

print("\n*** Primeira forma")
titulo = pagina.select('title')
print("Tipo de objeto: " + str(type(titulo)))
print("Linha do Título: " + str(titulo))
print("Conteudo do título: " + titulo[0].getText())

print("\n*** Segunda forma")
print("Nome do elemento HTML: " + str(pagina.title.name))
print("Tag completa: " + str(pagina.title))
print("Conteúdo da tag: " + str(pagina.title.string) + "\n")

A resposta é

*** Primeira forma
Tipo de objeto: <class 'bs4.element.ResultSet'>
Linha do Título: [<title>Welcome to Python.org</title>]
Conteudo do título: Welcome to Python.org

*** Segunda forma
Nome do elemento HTML: title
Tag completa: <title>Welcome to Python.org</title>
Conteúdo da tag: Welcome to Python.org
  • Exemplo: o programa informa a quantidade de vários elementos HTML na página oficial do Python.
import requests, bs4
r = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(r.text, "html.parser")

div = pagina.select('div span')
print("\nNúmero de div: " + str(len(div)))

img = pagina.select('img')
print("\nNúmero de imagens: " + str(len(img)))

ul = pagina.select('ul')
print("\nNúmero de ul: " + str(len(ul)))

li = pagina.select('li')
print("\nNúmero de li: " + str(len(li)))

span = pagina.select('span')
print("\nNúmero de span: " + str(len(span)))

A resposta é mostrada abaixo.

Número de div: 72

Número de imagens: 1

Número de ul: 28

Número de li: 163

Número de span: 72
  • Exemplo: o programa usa o método find() para achar o primeiro parágrafo da página oficial Python.
import requests, bs4
r = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(r.text, "html.parser")

print('\n*** Primeiro Parágrafo')
print(pagina.find('p').text)

O primeiro parágrafo da página é

*** Primeiro Parágrafo
Notice: While JavaScript is not essential for this website, your interaction with the content will be limited. Please turn JavaScript on for the full experience.
  • Exemplo: o script usa o método find_all() para gerar a lista com todos os parágrafos da página oficial do Python. Depois imprime apenas o primeiro e o terceiro parágrafos.
import requests, bs4
r = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(r.text, "html.parser")

paragrafos = pagina.find_all('p')
qtde = len(paragrafos)
print('\nQuantidade de parágrafos:', qtde)

print('\n*** Primeiro Parágrafo')
print(paragrafos[0].text)

print('\n*** Terceiro Parágrafo')
print(paragrafos[2].text)

A resposta é

Quantidade de parágrafos: 23

*** Primeiro Parágrafo
Notice: While JavaScript is not essential for this website, your interaction with the content will be limited. Please turn JavaScript on for the full experience. 

*** Terceiro Parágrafo
Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3
  • Exemplo: o programa verifica a quantidade de links que a página Python possui. Em seguida, lista apenas os links que possuem o valor ‘http://’ no atributo ‘href’ (os links com ‘https’ não entram na seleção).
import requests, re, bs4
r = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(r.text, "html.parser")

links = pagina.find_all('a')
print('Quantidade de links =', len(links))
links = pagina.findAll('a', attrs={'href': re.compile("^http://")})
print('Quantidade de links com http =', len(links))
   
print('\n*** Links com http')
for link in links:
  print(link.get('href'))

O método re.compile() verifica se o atributo ‘href’ tem um valor que começa (caractere ^) com a expressão ‘http://’. Abaixo a resposta encontrada.

Quantidade de links = 209
Quantidade de links com http = 32

*** Links com http
http://brochure.getpython.info/
http://wiki.python.org/moin/Languages
http://python.org/dev/peps/
http://planetpython.org/
http://pyfound.blogspot.com/
http://pycon.blogspot.com/
http://docs.python.org/3/tutorial/introduction.html#using-python-as-a-calculator
http://pyfound.blogspot.com/2022/02/we-are-hiring-contract-developers-to.html
http://pyfound.blogspot.com/2022/01/announcing-python-software-foundation.html
http://pyfound.blogspot.com/2021/12/georgi-ker-awarded-psf-community.html
http://www.djangoproject.com/
http://www.pylonsproject.org/
http://bottlepy.org
http://tornadoweb.org
http://flask.pocoo.org/
http://www.web2py.com/
http://wiki.python.org/moin/TkInter
http://www.riverbankcomputing.co.uk/software/pyqt/intro
http://www.wxpython.org/
http://www.scipy.org
http://pandas.pydata.org/
http://ipython.org
http://buildbot.net/
http://trac.edgewall.org/
http://roundup.sourceforge.net/
http://www.ansible.com
http://brochure.getpython.info/
http://wiki.python.org/moin/Languages
http://python.org/dev/peps/
http://planetpython.org/
http://pyfound.blogspot.com/
http://pycon.blogspot.com/
  • Exemplo: o programa localiza inicialmente o primeiro link da página. Depois, o programa exibe o elemento pai do link e o primeiro parágrafo que aparece após o link.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
primeiro_link = pagina.a
print("\n*** Primeiro link")
print(primeiro_link)
print("\n*** Pai do primeiro link")
print(primeiro_link.parent)
print("\n*** Primeiro parágrafo após link")
print(primeiro_link.find_next("p"))

A resposta é

*** Primeiro link
<a href="#content" title="Skip to content">Skip to content</a>

*** Pai do primeiro link
<div class="skip-link screen-reader-text">
<a href="#content" title="Skip to content">Skip to content</a>
</div>

*** Primeiro parágrafo após link
<p>The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. <a href="//docs.python.org/3/tutorial/controlflow.html#defining-functions">More about defining functions in Python 3</a></p>

BeautifulSoup permite navegar entre os diversos elementos de uma página HTML como, por exemplo, elemento pai (.parent), elemento irmão (.next_sibling e .previous_sibling), elemento filho (.children), próximo elemento (.next_element), elemento anterior (.previous_element), etc.

  • Exemplo: o script verifica quantas tags na página possuem a classe .header-banner e/ou a classe menu.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
print("\n*** Seleciona classes")
classes = pagina.select(".header-banner, .menu")
print("Quantidade de elementos selecionados =", len(classes))

Abaixo é apresentada a resposta do programa.

*** Seleciona classes
Quantidade de elementos selecionados = 29
  • Exemplo: são selecionados todos os parágrafos da página Python. Os parágrafos são então exibidos ao lado da numeração da linha de código.
import requests, bs4
resp = requests.get('https://www.python.org')
pagina = bs4.BeautifulSoup(resp.text, "html.parser")
for parag in pagina.find_all('p'):
    print(parag.sourceline, parag.string)

O resultado da execução é

120 None
482 None
496 None
510 None
525 None
539 None
550 None
576 Whether you're new to programming or an experienced developer, it's easy to learn and use Python.
577 Start with our Beginner’s Guide
582 Python source code and installers are available for download for all versions!
583 None
588 Documentation for Python's standard library, along with tutorials and guides, are available online.
589 docs.python.org
594 None
595 jobs.python.org
607 More
642 More
695 More
708 None
722 More
762 None
763 None
1029 None

Note que a linha 576 mostra o conteúdo do parágrafo. O formato da tag na página é

<p>Whether you're new to programming or an experienced developer, it's easy to learn and use Python.</p>

O valor None aparece quando a tag não tem filhos ou tem mais de um filho. Por exemplo, a linha 120 da página Python é mostrada abaixo. Ela possui dois filhos (o primeiro é <strong>…</strong>, o segundo é o texto do parágrafo), por isso o resultado é None.

 <p><strong>Notice:</strong> While JavaScript is not essential for this website, your interaction with the content will be limited. Please turn JavaScript on for the full experience. </p>

Quando o parágrafo só tem um filho, o texto do filho é mostrado. É o caso da linha 607 da página.

<p class="give-me-more"><a href="https://blog.python.org" title="More News">More</a></p>
  • Exemplo: o programa usa o analisador lxml para identificar os links da página do Python. O método fromstring(), usado no exemplo, fornece um objeto HtmlElement. O método xpath() deste tipo de objeto pode ser usado para localizar e extrair informações do documento HTML.
import requests
import lxml.html as lx

pagina = requests.get('https://www.python.org/')
objHTML = lx.fromstring(pagina.content)
links = objHTML.xpath('//a')

for link in links:
    print("***** HREF:", link.get("href"))
    print("      TEXTO:", link.text_content())

Uma parte da resposta gerada pelo programa é mostrada abaixo.

***** HREF: https://www.facebook.com/pythonlang?fref=ts
      TEXTO: Facebook
***** HREF: https://twitter.com/ThePSF
      TEXTO: Twitter
***** HREF: /community/irc/
      TEXTO: Chat on IRC
  • Exemplo: O script usa lxml para extrair informações sobre jogos da empresa STEAM.
import requests
import lxml.html as lx

objHTML = lx.fromstring(pagina.content)
links = objHTML.xpath('//div[@id="tab_newreleases_content"]/a')

i = 0
for link in links:
    i = i + 1
    print("**************")
    print("Jogo", i)
    # Título do jogo
    titulo = link.xpath('.//div[@class="tab_item_name"]/text()')[0]
    print("Título: ", titulo)
    # Preço do jogo
    preco = link.xpath('.//div[@class="discount_final_price"]/text()')[0]
    print("Preço:", preco)
    # Informações do jogo
    informacoes = link.xpath('.//div[@class="tab_item_top_tags"]')[0].text_content()
    print("Informações:", informacoes)
    # Plataformas do jogo
    plataformas=[]
    lista_plataformas = link.xpath('.//div[@class="tab_item_details"]')
    for plataforma in lista_plataformas:
        temp = plataforma.xpath('.//span[contains(@class, "platform_img")]')
        nome = [t.get('class').split(' ')[-1] for t in temp]
        plataformas.append(nome)
    print("Plataformas:", plataformas[0])
print("**************")

Note que inicialmente o programa identifica os links que estão abaixo da div com id=”tab_newreleases_content”. As informações desejadas (título, preço, informações do jogo e plataformas em que o jogo pode ser usado) estão dentro das tags <a>…</a> como mostra a estrutura abaixo (as informações de cada jogo foi substituída por ‘XXXXX’). Portanto, ao recuperar os links, o programa recupera as informações dos jogos. É interessante também notar que os nomes das plataformas fazem parte dos nomes das classes que começam por “platform_img”. Assim se o jogo pode ser usado no Linux, haverá um span com classe “platform_img linux”.

<a href="XXXXX">
   <div class="tab_item_cap"><img class="tab_item_cap_img" src="XXXXX" ></div>
   <div class="discount_block tab_item_discount no_discount" data-price-final="  ">
        <div class="discount_prices">
             <div class="discount_final_price">XXXXX</div>
        </div>
   </div>		
   <div class="tab_item_content">
        <div class="tab_item_name">XXXXX</div>
	<div class="tab_item_details">
             <span class="platform_img XXXXX"></span>
             <span class="platform_img XXXXX"></span>
             <div class="tab_item_top_tags">
                  <span class="top_tag">XXXXX</span>
                  <span class="top_tag">XXXXX</span>
                  <span class="top_tag">XXXXX</span>
             </div>
	</div>
   </div>
   <div style="clear: both;"></div>
</a>

Abaxo, uma parte da resposta obtida pelo programa.

**************
Jogo 25
Título:  Aperture Desk Job
Preço: Grátis
Informações: Grátis para Jogar, Um Só Jogador, Engraçado, Casual
Plataformas: ['win', 'linux']
**************
Jogo 26
Título:  ELDEN RING
Preço: R$ 249,90
Informações: Soulslike, Relaxante, Fantasia Sombria, RPG
Plataformas: ['win']
**************

Outra forma de fazer esse exemplo pode ser encontrada em https://timber.io/blog/an-intro-to-web-scraping-with-lxml-and-python/.