Web scraping
Problemas comunes y sus soluciones
David Hernández
¿Quién SOY?
Developer @ Political Watch
Mail: david.hernandez@politicalwatch.es
hola@davidhernandez.info
Twitter: @David_Baltha
¿Qué vamos a ver?
¿Qué es el web scraping?
〞
Es una técnica utilizada mediante programas de software para extraer información de sitios web.
– Wikipedia
HTTP Request
HTTP Response
¿Cómo funciona la web?
conocimientos previos
REALIZANDO UNA PETICIón http
REALIZANDO UNA PETICIón http
$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
¿Podemos hacer una petición a una página?
interprentando la respuesta
HTML
JSON
CSV
IMÁGEN
VÍDEO
HTML/XML: XPATH / CSS SELECTORs
HTML/XML: XPATH / CSS SELECTORs
'.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
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"]
XPATH
<html>
<a class="menu" href=""></a>
<div>
<a class="link" href=""></a>
</div>
</html>
/html//a[@class="link"]
La consola, nuestra mejor amiga
CONSEJO: Plugin to test xpath
Try XPath for Firefox
Procesando HTML: HTML PARSERS
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
WEB Scraping básico
make
HTTP
REQUEST
parse
HTTP
RESPONSE
Prueba de web scraping básico
Vamos a intentar extraer los autores de todas las charlas de la DrupalCamp:
tecnologías que podemos usar
Node | PHP | Python | |
---|---|---|---|
HTTP requests | https (core) | guzzle | requests |
Intérprete | jsdom | symfony crawler | lxml |
Ejemplo en Python
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)
Ejemplo en JAVASCRIPT
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()
Identificando APIs
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.
Ejemplo práctico
Busquemos el listado de diputados
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"
});
Obtengamos el índice de iniciativas
¿Qué parámetros son necesarios?
¿Qué parámetros son necesarios de la petición? ¿Cuáles no? ¿Podemos obtener otros datos?
WEB Scraping avanzado
HTTP
REQUEST
PROBLEMS
HTTP
RESPONSE
PROBLEMS
problemas de las peticiones HTTP
- Filtrado por IP
- Filtrado por User-Agent
- Limitación temporal
- Autentificación para acceder
Filtrado por IP
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.
Bloqueo por USER AGENT
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"
Limitación temporal
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...
Autentificación para acceder
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...
Algunas web con autentificación
- Periódicos digitales
- Tiendas online: Privalia
Scrapeando Privalia
- Analiza el comportamiento de la web
- Replica el proceso de inicio de sesión
- Replica la petición que tiene los datos
¡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".
Replica la petición
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)
Guardamos la cookie
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/>
]>
Usa las cookies para la petición
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)
problemas de la Interpretación HTML
- HTML cambiante
- Renderizado mediante Javascript
HTML Cambiante
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
Renderizado mediante javascript
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
Scrapeando mediante Puppeteer
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()
})()
¿PORQUé puppeteer?
Frente a herramientas de testing E2E, que tienen un comportamiento muy similar, Puppeteer nos da ciertas utilidades para extraer los datos del browser.
Problemas sin solución
problemas SIN SOLUCIón
- Captchas
- Two factor authentications
- Autentificaciones aleatorias
Captchas
El captcha está pensado para evitar que una máquina no pueda acceder a la información, permitiendo solo pasar a humanos
Captcha solvers
Two factor authentication (2Fa)
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
Autentificaciones aleatorias
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.
¿Preguntas?
Bibliografía
Ejemplos
Web Scraping
By david_hernandez
Web Scraping
- 1,291