Webbskrapning
Låt oss dela upp webbsidorna i två kategorier: statiska och dynamiska. Statiska sidor gör inget efter att de är inladdade i browsern; all information skickas över när sidan laddas. Dessa är lätta att skrapa, man gör request efter request och datat är i form av HTML som kan parsas t.ex. av Beautiful Soup och konverteras till text.
Dynamiska sidor gör saker i browsern efter de laddats, t.ex. ytterligare anrop till webbservern (eller andra servrar, t.ex. OpenStreetMap) och hämtar mera data, i majoriteten av fallen med hjälp av Javascript-program som skickas med den första webbsidan. Dessa är svåra-till-omöjliga att skrapa. Det går inte utan att tolka den hämtade sidan på samma sätt som browsern. Vill man automatisera detta måste man använda en s.k. Headless Browser och programmera den att göra det man vill.
Båda typerna av sidor kan ibland kräva autentisering, alltså inloggning. För att verifiera autentiseringen kan man efter inloggning få ett tilläg till query-string i URL:en (?auth=xlaHoHO923xa). Den måste hänga med hela tiden i alla acceser. Ett annat alternativ är att sätta en session cookie. Den kan till exempel utgöras av en hash av användarnamnet och inloggningstiden. Den lagras också i servern och alla accesser verifieras mot den. Så fungerar registeringen på enklare sidor, t.ex. DN.se, SvD.se m.fl. nyhetssiter där de ekonomiska värdena är små.
De idag vanligaste autentiseringsmetoderna är:
- Lösenordshash: lagras, skickas med varje request till servern (bara ok om https)
- Session Cookies: servern skapar en cookie, andvänds i varje request (bara ok om https)
- Tokens (JSON Web Tokens, JWT): servern skapar ett signerat token, används i varje request (ok även vid http, signeringen kan inte förfalskas utan att känna serverns privata nyckel)
- Passkeys (Web Authentication, WebAuthn): det moderna sättet, använder öppen och privat nyckel som användaren har, servern verifierar signaturen, användaren veriferar servern, funkar med hårdvarutokens som BankID och Yubikey.
För både statiska och dynamiska sidor krävs programmering för att skrapning skall bli praktisk. För dynamiska sidor kan det bli mycket krångligt och resurskrävande, kanske till och med omöjligt. Det måste göras specifikt för den sida man vill ta hem.
Att skrapa en komplicerad sida kräver eftertanke och analys av hur sidan fungerar. Finns ett API som hus de sociala medierna använder man med fördel det, varvid allt blir otroligt mycket lättare. Men gratis-API:er har i regel begränsningar på hur stora datamängder som returneras och på hur många anrop man får göra per tidsenhet. Betal-API:erna har också begränsningar men de är generösare (ju mer man betalar…).
Om det inte är publicerat vilka API:er som finns och hur de används, kan man kika på trafiken genom att använda browserns debug- eller inspektions-sidor. För Chrome:
- Gå till webbsidan
- Högerklicka och välj Inspect. Inspektionsfönstret öppnas.
- Leta upp Network i bandmenyn överst i inspektionsfönstret. Välj den.
- Filtrera på XHR eller Fetch
- Ladda om sidan
- Leta efter requests som hämtar JSON-data
Firefox har liknande funktioner. Hittar man nåt där kan man själv göra samma API-anrop med wget eller curl, man gör alltså manuellt exakt samma sak som browsern. Det går mycket fortare och man får alla data direkt.
Skrapa en statisk sida
Vi använder Beautiful Soup. Först måsta man ladda in Python-modulerna som behövs i en virtuell miljö i Linux-skalet:
python3 -m venv ~/venv/default . ~/venv/default/bin/activate pip install requests beautifulsoup4
Här är programmet:
import requests
from bs4 import BeautifulSoup
import json
import sys
def static_to_json(url, output_file="output.json"):
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }
response = requests.get(url, headers=headers, timeout=10)
print(f"Returvärde: {response.raise_for_status()}")
soup = BeautifulSoup(response.text, 'html.parser')
title = soup.title.string if soup.title else "Ingen title"
full_text = soup.get_text(separator=' ', strip=True)
structured_content = []
for element in soup.find_all(['h1', 'h2', 'h3', 'p']):
structured_content.append({
"tag": element.name,
"text": element.get_text(strip=True)
})
data = {
"url": url,
"title": title,
"full_text": full_text,
"structured_content": structured_content
}
with open(output_file, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print(f"{url} finns nu i {output_file}")
if __name__ == "__main__":
static_to_json(sys.argv[1])
Spara programmet i scrape1.py och kör så här:
python scrape1.py https://www.dn.se/
Och kika på outputen så här:
jq -r .full_text output.json | more
Skrapa en dynamisk sida
När en webbsida använder Javascipt för att göra API-anrop för att ladda innehåll så finns inget i webbsidan från början, bara ett enkelt skal med Javascript-programmet och möjligen lite administrativa data för grundläggande layout. Tekniken kallas Client Side Rendering (CSR). För att komma vidare med en sådan sida behöver vi en Headless Browser. Man kan använda Firefox eller Chrome och få dem att dumpa sidan som text eller bild och sedan processa dessa, ev. med AI. Men enklare är att använda Selenium eller Playwright. Eller, kanske inte enklare men det ger definitivt ett renare resultat!
Först måste vi installera Python-modulen i Linux-skalet och sedan måste den i sin tur installera Chromium (renderingskomponenten från Chrome).
pip install playwright beautifulsoup4 playwright install chromium
Här är programmet för dynamiska sidor med Javascript. Det är krångligare, använder asynkron I/O i Python eftersom Playwright och Chromium använder det. Anropen till API:erna som sker bakom kulliserna är asynkrona (man väntar inte på dem, de säger till när de är klara):
import asyncio
import json
import sys
from playwright.async_api import async_playwright
from bs4 import BeautifulSoup
# Playwright behöver asynkron I/O, vi väntar på db-accesser. Använd asyncio.
async def scrape_dynamic_webpage(url, output_file="dynamic_output.json"):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
page = await context.new_page()
try:
print(f"Laddar: {url}...")
await page.goto(url, wait_until="domcontentloaded")
# Vänta tills nätverkstrafiken är klar (= Javascript har hämtat klart).
#
await page.wait_for_load_state("networkidle")
# Eller vänta in ett visst element, om man nu vet att det innebär "klart".
# await page.wait_for_selector(".content-class")
content = await page.content()
soup = BeautifulSoup(content, 'html.parser')
for script_or_style in soup(["script", "style"]): script_or_style.decompose()
title = soup.title.string if soup.title else "Ingen title"
full_text = soup.get_text(separator=' ', strip=True)
structured_content = []
for element in soup.find_all(['h1', 'h2', 'h3', 'p']):
structured_content.append({
"tag": element.name,
"text": element.get_text(strip=True)
})
data = {
"url": url,
"title": title,
"full_text": full_text,
"structured_content": structured_content
}
with open(output_file, "w") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print(f"Dynamiska sidan {url} finns nu i {output_file}")
print("Titta med: jq -r .full_text dynamic_output.json | more")
except Exception as e:
print(f"Fel: {e}")
finally:
await browser.close()
if __name__ == "__main__":
asyncio.run(scrape_dynamic_webpage(sys.argv[1]))
Spara programmet i scrape2.py och kör så här:
python scrape2.py https://www.dn.se/
Och kika på outputen så här. Jämför gärna resultaten från scrape1.py och scrape2.py!
jq -r .full_text dynamic_output.json | more