Problemas comunes y sus soluciones
Developer / Founder @ Digital Tack
Mail: david.hernandez@digitaltack.com
Twitter: @David_Baltha
Es una técnica utilizada mediante programas de software para extraer información de sitios web.
– Wikipedia
HTTP Request
HTTP Response
$response = file_get_contents('https://2022.drupalcamp.es/sessions/webscrapping-problemas-comunes-y-sus-soluciones');
curl 'https://2022.drupalcamp.es/sessions/webscrapping-problemas-comunes-y-sus-soluciones'
await fetch("https://2022.drupalcamp.es/sessions/webscrapping-problemas-comunes-y-sus-soluciones", {
"method": "GET",
});
requests.get('https://2022.drupalcamp.es/sessions/webscrapping-problemas-comunes-y-sus-soluciones')
PHP
Bash/curl
Javascript
Python
HTML
JSON
CSV
IMÁGEN
VÍDEO
'.session_title' // Selector
'//main/section//article/div/div/div[1]/h1' // XPATH
'.field--name-field-experience-level > div:nth-child(2)' // Selector
'//main/section//article/div/div/div[4]/div[1]/div[2]' // XPATH
XPath es un formato que nos permite identificar cualquier etiqueta del HTML o XML.
<html>
<a class="menu" href=""></a>
<div>
<a class="link" href=""></a>
</div>
</html>
Seleccionar todos los enlaces:
//a
Seleccionar el div:
/html/div
Seleccionar segundo enlace:
//a[2]
Usando atributos
//a[@class="link"]
<html>
<a class="menu" href=""></a>
<div>
<a class="link" href=""></a>
</div>
</html>
/html//a[@class="link"]
Try XPath for Firefox
doc = document_fromstring(html)
authors = doc.xpath("//div[@class='author']")
for author in authors:
name = author.cssselect('.name')
$doc = str_get_html($html);
$authors = $doc->find("//div[@class='author']");
foreach ($authors as $author) {
$name = $author->find('.name');
}
Python LXML
PHP SimpleHTMLDOM
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const authors = doc.evaluate("//div[@class='author']")
for (const author of authors) {
const name = author.querySelector('.name')
}
Javascript puro en el front, JSDOM en el back
Vamos a intentar extraer los autores de todas las charlas de la DrupalCamp:
Node | PHP | Python | |
---|---|---|---|
HTTP requests | https (core) | guzzle | requests |
Intérprete | jsdom | symfony crawler | lxml |
import requests
from lxml.html import fromstring
def make_request():
url = 'https://2022.drupalcamp.es/programa'
response = requests.get(url)
if not response.ok:
raise Exception
return response.text
def extract_authors(html):
dom = fromstring(html)
author_xpath = '//div[@class="schedule-wrapper"]//span[@class="author"]'
authors = [e.text_content() for e in dom.xpath(author_xpath)]
return authors
html = make_request()
authors = extract_authors(html)
print(authors)
const https = require('https')
const jsdom = require("jsdom")
const { JSDOM } = jsdom
const options = {
hostname: '2022.drupalcamp.es',
port: 443,
path: '/programa',
method: 'GET'
}
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
res.on('data', html => {
const dom = new JSDOM(html).window.document
const author_elements = dom.querySelectorAll(".author")
const authors = Array.from(author_elements).map(author => author.textContent)
console.log(authors)
})
})
req.on('error', error => {
console.error(error)
})
req.end()
Si una web renderiza mediante ajax y no tiene autentificación, podemos extraer directamente de sus APIs.
No hay una manera 100% eficaz de impedir que te conectes a una API con acceso abierto.
await fetch("https://www.congreso.es/busqueda-de-diputados?p_p_id=diputadomodule&p_p_lifecycle=2&p_p_state=normal&p_p_mode=view&p_p_resource_id=searchDiputados&p_p_cacheability=cacheLevelPage", {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"Sec-GPC": "1"
},
"referrer": "https://www.congreso.es/busqueda-de-diputados",
"body": "_diputadomodule_idLegislatura=14&_diputadomodule_genero=0&_diputadomodule_grupo=all&_diputadomodule_tipo=0&_diputadomodule_nombre=&_diputadomodule_apellidos=&_diputadomodule_formacion=all&_diputadomodule_filtroProvincias=%5B%5D&_diputadomodule_nombreCircunscripcion=",
"method": "POST",
"mode": "cors"
});
¿Qué parámetros son necesarios de la petición? ¿Cuáles no? ¿Podemos obtener otros datos?
El servidor a veces bloquea una IP cuando llegan muchas peticiones muy rápidamente desde la misma IP.
Proxies
En otras ocasiones, se bloquea un rango de IPs: por país, por ejemplo.
El User-Agent identifica al navegador que hace la petición.
Muchas herramientas para hacer peticiones, usan User-Agents propios.
Estos User-Agents muchas veces son bloqueados.
Identifica y usa el user agent de un navegador "normal"
Otra defensa habitual es bloquear temporalmente las peticiones que llegan de una IP que hace muchas peticiones muy rápidamente, algo que un "usuario real" no podría hacer.
Waits/Sleeps, colas de procesos...
Hay webs que para acceder a la información tienes que iniciar sesión. O APIs que necesitan un token para acceder...
Autentificación por cookie, por token...
¡Cuidado! No guardes y reutilices siempre la misma cookie, ya que puede caducar!
Prepárate para que eliminen tu cuenta o te bloqueen la IP. Webs grandes suelen estar alerta de "comportamientos sospechosos".
import requests
from requests.structures import CaseInsensitiveDict
url = "https://es.privalia.com/api/authentication/front/v1/login"
headers = CaseInsensitiveDict()
headers["User-Agent"] = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0"
headers["Accept"] = "*/*"
headers["Accept-Language"] = "es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3"
headers["Accept-Encoding"] = "gzip, deflate, br"
headers["Referer"] = "https://es.privalia.com/gr/portal"
headers["Content-Type"] = "application/json"
headers["User-Agent-Custom"] = "vp-groot-mobile"
headers["mpDistinctId"] = "1812a72405cd5-08b3112aedd2ed8-97f2736-1fa400-1812a72405dd3"
headers["formName"] = "portal"
headers["Origin"] = "https://es.privalia.com"
headers["DNT"] = "1"
headers["Connection"] = "keep-alive"
headers["Sec-Fetch-Dest"] = "empty"
headers["Sec-Fetch-Mode"] = "cors"
headers["Sec-Fetch-Site"] = "same-origin"
headers["TE"] = "trailers"
data = '{"email":"******************","password":"*********","rememberMe":true,"returnUrl":null,"siteId":66}'
resp = requests.post(url, headers=headers, data=data)
print(resp.cookies)
print(resp.cookies)
<RequestsCookieJar[
<Cookie .ASPXAUTH=FFEFFC7008943DA732410C27C86EBDC6CF5336210D5A19FDB6CA2C3F8
C2EA5DA45DB86E03DF869B3B60CAFDE2F0179A9571541FFC746B61994D5948C34DC4A4DB4F3
4571882D9A458FBDC9EC7B8B6EE714CBBCCCBD1EC4475A8A96032E7EB0926E3C3EA9CF75BFC
310E923333F6851EA1E9A22F2 for .es.privalia.com/>,
<Cookie CrmSegment={"MemberId":84715914,"CrmSegment":"0"} for .es.privalia.com/>,
<Cookie CheckAutologin=True for es.privalia.com/>,
<Cookie context=1KyMEKfXkPXDT/tW0CMbBvbGw1lZ7Bvfe3fheZGzt5I=+t5lR93qaB12Af+9F
LAU8Q==&nhl7USxL3Xy3+2WD7Fbc1Cdl2w4Gn8GLgOXNCUmE8iQ=XrBxbQMAPWeaB5boIvumDA=
=&v1Qjw3n3ulZI7AqymWiKQyEsgasusmL/wSOYpo8aAv8=XrBxbQMAPWeaB5boIvumDA==&yCW6
rcZdpl0aFFRaKOa1Q53DrYB9UYF7P36N5tOeqiQ=zWc2qPgnGPRJ2o/quhOVIEiVm51nEdLi9HT
CAguj8g4= for es.privalia.com/>,
<Cookie dataContext=1w8gZTIT3pHupcXLu5VZGc96/AU3TdhquZneJakjWXY=T7XSQY/f7F4rT
/TCSorLb5jNct5xIVyUcCbwGEHn8Ck=&fkXJY5zgM99oBzkfu7b6RmRwz7NgHzKZg9jJiQ5Yr0M
=gYk3h76bt0Fbmx7KIRgkS/a3KZp80TcUzvnx4rhU5hI=&j9cBT5cXtAP8wEYE034g1FdciIYFl
A8qEm9vhLUhvHg=XrBxbQMAPWeaB5boIvumDA== for es.privalia.com/>,
<Cookie infoMember=mid=84715914 for es.privalia.com/>
]>
cookies = resp.cookies
url = "https://es.privalia.com/ns-sd/frontservices/navigation/graphql"
headers = CaseInsensitiveDict()
headers["User-Agent"] = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0"
headers["Accept"] = "*/*"
# ...
headers["DNT"] = "1"
headers["Connection"] = "keep-alive"
headers["Sec-Fetch-Dest"] = "empty"
headers["Sec-Fetch-Mode"] = "cors"
headers["Sec-Fetch-Site"] = "same-origin"
headers["Sec-GPC"] = "1"
headers["TE"] = "trailers"
data = '{"query":"n query getHeader($isPrivaliaOrBenelux: Boolean!, $previewDate: String, $siteId: Int = 1, $userparam: String) {n header(siteId: $siteId) {n cart {n label: basketTextn url: basketUrln isActive: isBasketActiven __typenamen }n lateralMenu {n title: menuTitlen links: lateralMenuSections {n sections {n ...lateralMenuSubSectionFragmentn }n }n lateralMenuBottomSections {n sections {n ...lateralMenuSubSectionFragmentn }n }n isActive: isLateralMenuActiven countryBlock {n countryListTitlen countryMainList {n countryTitleMainn countriesList {n flagUrln countryCoden flagTextn }n }n selectedCountryn }n faciliti {n logon textn }n }n misc {n baseUrln }n sponsorship {n label: godfatherTextn isActive: isSponsorshipActiven }n whoWeAre {n label: whoWeAreTextn isActive: isWhoWeAreActiven }n }n homes(previewDate: $previewDate, siteId: $siteId, tracking: false, userparam: $userparam) {n idn __typenamen ... on Home {n namen displayNamen upperDisplayNamen subNavBanner {n imageUrln linkn tagn }n modules {n ... on SectionBannerModule {n __typenamen idn namen displayNamen isCurrentn hideOnSubNavigationn banners {n idn }n }n }n subNavModules {n __typenamen titlen showAlln ... on SubNavCategoryModule {n items {n ...subNavModuleItemFragmentn }n }n ... on SubNavSaleModule {n items {n ...subNavModuleItemFragmentn }n }n ... on SubNavVPassModule {n items {n ...subNavModuleItemFragmentn }n }n }n }n ... on SpecialHome {n namen displayNamen subNavBanner {n imageUrln linkn tagn }n }n }n memberInformation @include(if: $isPrivaliaOrBenelux) {n ... on MemberInformationModule {n isPremiumn premiumMessagen }n }n }n fragment subNavModuleItemFragment on SubNavModuleItem {n __typenamen idn linkUrln namen ... on Category {n hasItemsn defaultNamen mediaUrls {n v2n v3n }n }n }n fragment lateralMenuSubSectionFragment on LateralMenuSection {n ...lateralMenuSectionFragmentn lateralMenuSubSections {n ...lateralMenuSectionFragmentn }n }n fragment lateralMenuSectionFragment on LateralMenuSection {n contentn defaultUrln textn cssClassn loggedInn }n","variables":{"isPrivaliaOrBenelux":true,"siteId":66}}'
resp = requests.post(url, headers=headers, data=data, cookies=cookies)
print(resp.status_code)
Hay webs que cambian automáticamente su estructura del HTML (generando clases de CSS automáticamente, modificando las etiquetas de HTML...).
Aunque puedas hacer scrapping de estas páginas, el sistema quedará inútil en un tiempo breve.
Soluciones experimentales: exportar la web a una imagen y utilizar reconocimiento de imágenes, ya que no depende de la estructura del HTML
La web actual se renderiza por javascript (Vue, React, Angular...).
Si haces una request a este tipo de webs verás un HTML mínimo que carga un script de JS que renderiza la web después.
Si es muy difícil conectarse a la API, hay otras alternativas.
Replicamos el comportamiento de un navegador con herramientas que usen headless browsers
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
executablePath: '/usr/bin/google-chrome',
args: [
"--disable-gpu",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-sandbox",
]
})
const page = await browser.newPage()
await page.goto('https://quehacenlosdiputados.es/diputados', {
waitUntil: 'networkidle0',
})
const deputies = await page.evaluate(() => {
const deputy_elements = document.querySelectorAll('.c-deputy-card h4')
return Array.from(deputy_elements, deputy_element => deputy_element.textContent)
})
console.log(deputies)
await browser.close()
})()
Frente a herramientas de testing E2E, que tienen un comportamiento muy similar, Puppeteer nos da ciertas utilidades para extraer los datos del browser.
El captcha está pensado para evitar que una máquina no pueda acceder a la información, permitiendo solo pasar a humanos
Captcha solvers
El 2FA está pensado como herramienta de seguridad para evitar hackeos de nuestras cuentas, pero también impiden que podamos acceder mediante técnicas de web scraping.
Sin solución por el momento
Hay páginas que de manera aleatoria nos piden "autentificarnos" o mostrar que somos humanos. Puede ser un captcha, introducir un código mandado al correo, cerrar un popup...
Dependiendo de la implementación, podremos sortearlo o no.