Como raspar dados de uma consulta AJAX usando python?

Olá,

Estou tentando raspar dados a partir de uma consulta AJAX em um site, o que acontece em muitos locais públicos.
Como exemplo, estou tentando obter os dados da seguinte consulta no site do clickbus: https://www.clickbus.com.br/onibus/recife-pe/natal-rn?departureDate=2019-12-19 .
Vi vários tutoriais mas não consigo segui-los os sites que eles usam estão aparentemente com uma configuração diferente de quanto eles foram criados. Alguém tem um tutorial que eu possa seguir?

2 Curtidas

Sites em AJAX são populados depois do carregamento da página pelo usuário, com informações enviadas pelo servidor a partir de, geralmente, um javascript.
Para podermos raspá-los precisamos simular uma requisição um pouco mais complexa que o normal. Tentando fazer uma requisição “do jeito comum” nesse site do exemplo, o primeiro problema encontrado é o seguinte:

Input:
>>> requests.get("https://www.clickbus.com.br/refresh/recife-pe/natal-rn?departureDate=2019-12-19&cached=0").text

Output:
<html>\r\n<head>\r\n **<META NAME="robots" CONTENT="noindex,nofollow">** \r\n<script src="/_Incapsula_Resource?SWJIYLWA=5074a744e2e3d891814e9a2dace20bd4,719d34d31c8e3a6e6fffd425f7e032f3">\r\n</script>\r\n<body>\r\n</body></html>'

Provavelmente batemos em uma proteção anti crawler do site.

Para poder simular um comportamento mais humano e conseguir uma resposta consistente do site, uma das principais formas é modificar o “header” da requisição, alterando o User-Agent do navegador e outros parâmetros que o site espera receber. Para saber quais são esses é só olhar na aba de Requisições de Rede do navegador (Ctrl+Shift+E no Firefox), selecionar a requisição certa e procurar os Request Headers.
No caso desse site temos, portanto:

headers = {'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36',
           "Host": "www.clickbus.com.br",
           "Accept": "text/css,*/*;q=0.1",
           "Accept-Language": "pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
           "Accept-Encoding": "gzip, deflate, br",
           "DNT": "1",
           "Connection": "keep-alive"}

Atenção para o fato de que os headers são dicionários e seus parâmetros são uma dupla de chave e valor em formato de string, até mesmo o valor “1” da chave “DNT”.

Fazendo uma requisição com a biblioteca requests, e especificando o uso destes headers conseguimos receber o conteúdo que procuramos:

Input:
r = requests.get("https://www.clickbus.com.br/refresh/recife-pe/natal-rn?departureDate=2019-12-19&cached=0", headers=headers)

A resposta dada pelo site, enfim, é recebida como um dicionário gigante, em formato json, que podemos ler a partir da seguinte linha:

conteudo = r.json()

Finalmente, por coincidência, este json parece muito com um arquivo HTML, então podemos tentar parseá-lo com BeautifulSoup:

from bs4 import BeautifulSoup as bs
sopa = bs(conteudo["departure"], "html.parser")
for item in sopa.findAll("div"):
    print(item.text)

2 Curtidas

Olá João,

Tentei reproduzir aqui o seu script mas não tive sucesso, acho que é algo a ver com os headers. Existem mais headers na request, eu deveria copiar todas ou só as que você enumerou? Aproveitando, por que tu enumerou só essas?

Abraços,

[Atualização] , mesmo eu copiando todos os headers e mudando o navegador a minha requisição dentro da requisição (ou seja, as viagens) continua “Request unsuccessful. Incapsula incident ID: 217000950452348482-938142722862679550”

Só as Request Headers.
Testei novamente hoje com o código abaixo e funcionou. Tente rodar esse abaixo.

import requests
import json
from bs4 import BeautifulSoup as bs

headers = {'User-Agent': 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0',
          "Host": "www.clickbus.com.br",
           "Accept": "*/*",
           "Accept-Language": "pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
           "Accept-Encoding": "gzip, deflate, br",
           "X-Requested-With": "XMLHttpRequest",
           "DNT": "1",
           "Connection": "keep-alive",
           "Referer": "https://www.clickbus.com.br/onibus/recife-pe/natal-rn?departureDate=2019-12-19",
           "Cookie": "PHPSESSID=3r7id2l57625q0odkp5renr2j7; visid_incap_2103645=hVCiT3OyQTCMoV4o6V1IwifU510AAAAAQUIPAAAAAACpKuhsS2SXN13ANt9f8Daj; nlbi_2103645=LPTkDntFgHVV6gVxbTj2RAAAAACTlkWeTim7JQ89k+u3a7XG; incap_ses_684_2103645=GBeMZaabYCj2umidoQ9+CSfU510AAAAAORWwJSTSUKkTi4xlqiol6Q==",
           "Cache-Control": "max-age=0",
           "TE": "Trailers"}

r = requests.get("https://www.clickbus.com.br/refresh/recife-pe/natal-rn?departureDate=2019-12-19&cached=0", headers=headers)
conteudo = r.json()
sopa = bs(conteudo["search"]["departure"], "html.parser")
for item in sopa.findAll("div"):
    print(item.attrs)

Enumerei somente aquelas porque foram as que meu navegador enviou no momento em que estava testando hahaha

Eu também encontrei este problema “Request unsuccessful. Incapsula incident…” nas minhas primeiras tentativas. Vou tentar reproduzi-lo novamente para ver o que pode ser feito.

Engraçado, os meus headers nunca dão esse DNT.
Eu acho que eu posso estar olhando na request errada. Qual das requests você olha?

Olá @voigt.jessica e @jovemadulto
Eu não sou bom em achar a requisição certa no Network do Inspecionar Elemento e também não sei que headers são obrigatórios. Isso daria um curso bem legal
Uma coisa que faço quando encontro a certa isso é clicar na requisição com o botão direito do mouse e copiar as cURL. Depois jogar o texto no https://curl.trillworks.com/, que transforma já no código para requests
Bom, mas não consegui encontrar a requisição certa por isso fiz com selenium mesmo. O meu curso do Coda deste ano foi sobre isso

from selenium import webdriver
import os
import time
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Local do geckodriver no seu computador
firefoxPath = "/home/abraji/Documentos/Code/geckodriver"
# Link inicial de busca
link = 'https://www.clickbus.com.br/'

# Dados da busca
origem = "Recife, PE"
destino = "Natal, RN"
data_viagem = "19/12/2019"

# Chama o webdriver para o Mozilla
driver = webdriver.Firefox(executable_path=firefoxPath)
# Se a conexão for ruim pode colocar pausa até carregar browser ou página
# time.sleep(6)
# Acessa o site
driver.get(link)

# Preenche o primeiro campo - a partir do item que encontrei com o Inspecionar Elemento
# No caso estou usando as marcações XPATH
driver.find_element_by_xpath("//*[@id='widget-vertical-origin-place']").send_keys(origem)

# Preenche o segundo campo
driver.find_element_by_xpath("//*[@id='widget-vertical-destination-place']").send_keys(destino)

# Clica no campo três antes por precaução, para fechar a janela anterior
# Em campos com click também é uma boa prática colocar wait para assegurar que o botão/local vai estar disponível
WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, "//*[@id='widget-vertical-departure-date']"))).click()

# Preenche o terceiro campo
driver.find_element_by_xpath("//*[@id='widget-vertical-departure-date']").send_keys(data_viagem)

# Clica em Somente ida
WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, "//*[@id='search-widget-vertical']/div[3]/div/label[1]"))).click()

# Clica no botão Buscar
WebDriverWait(driver, 20).until(EC.element_to_be_clickable((By.XPATH, "//*[@id='search-widget-vertical']/div[5]/div/button"))).click()

# Busca as informações das passagens
try:
    resultado = driver.find_element_by_xpath("//*[@id='search-results']/div[2]/div").text
except NoSuchElementException:
    resultado = "não encontrou"

# Transforma em lista, que depois pode ser limpa para uso final
file_list = resultado.split('\n')

# Fecha o driver
driver.quit()

Lógico que depois você pode criar iterações para fazer essa busca em vários destinos

Desculpe a demora @voigt.jessica.

É uma boa ideia mesmo tratar disso em um curso. Vou tentar criar um material mais formal e quem sabe liberar, né?

Mas é relativamente simples achar requisição certa pra ser analisada.
Vejam na imagem:

Uma vez que nós descobrimos que a página é dinâmica, podemos concluir com 99% de certeza que os dados são recebidos através de uma requisição XHR (retângulo vermelho) e retorna um JSON.

Na aba do Monitor de Redes podemos filtrar os tipos que nos interessa (linha azul). Limpando este log e recarregando a página novamente (forçando a atualização de todos os elementos, ou seja, sem o cache do navegador -> Ctrl+F5) vemos que só existe esta opção:

E para ter uma ideia de que é isso mesmo que queremos antes de levar pro código, podemos analisar diretamente no Monitor de Redes a resposta.
Assim temos certeza que ao fazer esse GET receberemos o que desejamos.

2 Curtidas

Nossa, eu estou olhando a requisição correta, copiando os headers e simplesmente não sei porque não aceita a requisição:

Os headers:

image

O código:

import requests
import json
from bs4 import BeautifulSoup as bs
import pandas as pd

h1 = {'Host': 'www.clickbus.com.br',
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0', 
      'Accept': '*/*', 
      'Accept-Language': 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3',
      'Accept-Encoding': 'gzip, deflate, br', 
      'X-NewRelic-ID': 'XA4DUVZACQoCVFVWDgQ=',
      'X-Requested-With': 'XMLHttpRequest',
      'Connection': 'keep-alive',
      'Referer': 'https://www.clickbus.com.br/onibus/recife-pe/natal-rn?departureDate=2019-12-19',
      'Cookie': 'visid_incap_2103645=RdTocET7RKG7smmbB5C13mhr5V0AAAAAQUIPAAAAAACCqdGxzWSUB8vNKut/IH4k; _gcl_au=1.1.2111274864.1575317546; ua_medium=branded; ua_source=direct; ua_gclid=undefined; ua_campaign=undefined; ua_term=undefined; ua_referrer=undefined; cbFirstAccess=1575317546104; __kdtv=t%3D1575317546851%3Bi%3D046ea749dbe711783cf178e11e6cc5518fb0eaf7; _kdt=%7B%22t%22%3A1575317546851%2C%22i%22%3A%22046ea749dbe711783cf178e11e6cc5518fb0eaf7%22%7D; _ga=GA1.3.1956546053.1575317547; _fbp=fb.2.1575317547939.439435084; _gu=10b9dc0c-f56f-493b-b321-1f8badd0e4b9; _gw=2.u%5B%2C%2C%2C%2C%5Dv%5B~fmtsp%2C~0%2C~1%5Da(); _hjid=1325f04f-0f0e-4c3b-ab28-3872aea21203; _spl_pv=7; sback_browser=0-14632200-15710644048ba580944578c1e0dc744650794946f92b5a8ae412261159755da48a5423b9c0-12989444-1778121141,6425217988-1575557358; sback_client=56d48438785e6155bb34e47e; sback_customer=$2QcxgWUJd1VZNDMhdmYUVTWZFDbRdkezkVVsJGa0oWcalFbxJlNUdTTrlneZtGRw0UZzw2Y0pWTN9kMjtmM6ZlT2$12; sback_partner=false; intent_media_prefs=; sback_total_sessions=3; sb_days=1575317554262; im_puid=13ef7f7a-cc26-480f-9d47-294a238603cb; _gid=GA1.3.1208204646.1575470298; sback_refresh_wp=no; PHPSESSID=n8t0f7q4o8e0ch9pq2bnronevp; nlbi_2103645=FfeDZuSe3FzkZimPbTj2RAAAAAAQ5ksuQVeG0DRvANKF8U3X; incap_ses_297_2103645=4LPCSX/r3WYixd4w9CcfBOkY6V0AAAAAiTvYPtKezDX6S/05yrS9PQ==; _gs=2.s()c%5BDesktop%2CFirefox%2C180%3A691%3A45241%3A%2CWindows%2C189.110.89.81%5D; _st_ses=2568276014972506; _cm_ads_activation_retry=false; _sptid=217; _spcid=245; _st_cart_script=helper_clickbus.js; _st_cart_url=/; _st_no_user=1; sback_access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhcGkuc2JhY2sudGVjaCIsImlhdCI6MTU3NTU1NzM1OSwiZXhwIjoxNTc1NjQzNzU5LCJhcGkiOiJ2MiIsImRhdGEiOnsiY2xpZW50X2lkIjoiNTZkNDg0Mzg3ODVlNjE1NWJiMzRlNDdlIiwiY2xpZW50X2RvbWFpbiI6ImNsaWNrYnVzLmNvbS5iciIsImN1c3RvbWVyX2lkIjoiNWRhNDhhNTRjZThmZTQxMjYwNzcyNjk3IiwiY3VzdG9tZXJfYW5vbnltb3VzIjp0cnVlLCJjb25uZWN0aW9uX2lkIjoiNWRlNTcwMzFlZmM1Y2RlZGY2NWQzYjc0IiwiYWNjZXNzX2xldmVsIjoiY3VzdG9tZXIifX0.I7F7wxJWTzosT5-IxmBYEAzEoN3u6G4-Fzc-KPqvgSM.WrWrDrEiDrDrDrEiKqDrHe; sback_current_session=1; sback_customer_w=true; im_snid=0773c59e-57c3-450b-a5a7-2566ec1b8cef', 
      'TE': 'Trailers'
      }

r = requests.get("https://www.clickbus.com.br/refresh/recife-pe/natal-rn?departureDate=2019-12-19&cached=0", headers=h1)
r.content

Ontem usei os seus headers e tinha funcionado. Hoje nem mesmo os seus headers estão funcionando mais.

1 Curtida

Reinaldo, você tem o conteúdo do curso no github? Olhei rapidamente a sua resposta mas acho que vou precisar de algo mais comentado para entender.

1 Curtida

Olá Jessica. Sim, no Github tem os scripts comentados e links para apresentação, materiais de leitura e instalações necessárias