Kilka miesięcy temu chciałem zbudować codzienny zestaw najważniejszych artykułów z Hacker News dla zespołu w Thunderbit. Pierwszy odruch? Po prostu dodać stronę do zakładek i przeglądać ją każdego ranka. Wytrzymało to jakieś trzy dni — potem zorientowałem się, że codziennie tracę 20 minut tylko na czytanie nagłówków i kopiowanie linków do arkusza kalkulacyjnego.
Hacker News to jedno z najbogatszych i najbardziej skoncentrowanych źródeł wiedzy technologicznej w internecie — około 13 milionów wizyt miesięcznie, mniej więcej 1 300 nowych wpisów dziennie i około 13 000 komentarzy każdego dnia. Niezależnie od tego, czy śledzisz rodzące się trendy technologiczne, monitorujesz swoją markę, budujesz pipeline rekrutacyjny z wątków „Who’s Hiring”, czy po prostu chcesz wiedzieć, czym żyje społeczność deweloperów, ręczne śledzenie tego wszystkiego to walka z góry przegrana.
Dobra wiadomość: scrapowanie Hacker News w Pythonie jest zaskakująco proste. W tym poradniku pokażę Ci dwie kompletne metody — scrapowanie HTML z BeautifulSoup oraz oficjalne API HN Firebase — a do tego omówię paginację, eksport danych, praktyki gotowe do wdrożenia oraz skrót bez kodu na wypadek, gdy Python wydaje się przesadą.
Dlaczego warto scrapować Hacker News w Pythonie?
Hacker News to nie jest kolejny zwykły agregator linków. To kuratorowany, napędzany społecznością feed, w którym najciekawsze historie technologiczne wypływają na szczyt dzięki głosom użytkowników i aktywnej dyskusji. Publiczność jest mocno przesunięta w stronę specjalistów technologicznych (około 76% mężczyzn, główna grupa wiekowa 25–34 lata), a 66% ruchu bezpośredniego pokazuje, że to lojalna, nawykowa społeczność — nie przypadkowi odwiedzający.
Oto, po co ludzie scrapują dane z HN:
| Zastosowanie | Co otrzymujesz |
|---|---|
| Codzienny digest technologiczny | Najlepsze historie, wyniki i linki trafiające do skrzynki mailowej lub Slacka |
| Monitoring marki / konkurencji | Powiadomienia, gdy Twoja firma lub produkt zostanie wspomniany |
| Analiza trendów | Śledzenie, które technologie, języki lub tematy zyskują popularność z czasem |
| Rekrutacja | Parsowanie wątków „Who’s Hiring” pod kątem ofert pracy, stacków technologicznych i sygnałów płacowych |
| Research treści | Wyszukiwanie tematów o wysokim potencjale, o których warto pisać lub które warto udostępniać |
| Analiza sentymentu | Ocenianie opinii społeczności o produktach, premierach i zmianach w branży |
Firmy o łącznej wartości przekraczającej 400 miliardów dolarów — Stripe, Dropbox, Airbnb — przypisują Hacker News kluczowe wczesne opinie i pierwszych użytkowników. Drew Houston opublikował demo Dropboxa na HN w kwietniu 2007 roku, strona wskoczyła na 1. miejsce, a lista oczekujących na betę urosła z 5 000 do 75 000 użytkowników w jeden dzień. Dane z HN są nie tylko ciekawe — są też realnie wartościowe biznesowo.
Dane są publicznie dostępne, ale struktura serwisu sprawia, że ręczne zbieranie ich jest uciążliwe. Automatyzacja w Pythonie to po prostu praktyczniejsze rozwiązanie.
Dwa sposoby na scrapowanie Hacker News w Pythonie: przegląd
Ten poradnik obejmuje dwie kompletne, działające metody:
- Scrapowanie HTML z
requests+ BeautifulSoup — pobierasz surowy HTML z news.ycombinator.com i parsujesz go, aby wyciągnąć dane o wpisach. Świetne do nauki podstaw scrapowania i pobierania dokładnie tego, co widać na stronie. - Oficjalne API Hacker News Firebase — wysyłasz żądania bezpośrednio do endpointów JSON, bez parsowania HTML. Lepsze do niezawodnych pipeline’ów danych, pobierania komentarzy i danych historycznych.
Oto porównanie obok siebie, które pomoże Ci wybrać właściwą opcję:
| Kryterium | Scrapowanie HTML (requests + BS4) | API HN Firebase | Thunderbit (bez kodu) |
|---|---|---|---|
| Złożoność konfiguracji | Średnia (selektory HTML) | Niska (endpointy JSON) | Brak (rozszerzenie Chrome w 2 kliknięciach) |
| Aktualność danych | Front page w czasie rzeczywistym | W czasie rzeczywistym (dowolny element po ID) | W czasie rzeczywistym |
| Ryzyko limitów | Średnie (robots.txt podaje 30 s opóźnienia crawl) | Niskie (oficjalne, dość hojne) | Zarządzane przez Thunderbit |
| Dostęp do komentarzy | Trudny (zagnieżdżony HTML) | Łatwy (rekurencyjne ID elementów) | Funkcja scrapowania podstron |
| Dane historyczne | Ograniczone | Przez Algolia Search API | Brak |
| Najlepsze do | Nauki podstaw scrapowania | Niezawodnych pipeline’ów danych | Osób nietechnicznych, szybkich eksportów |
Obie metody zawierają pełny, gotowy do uruchomienia kod Python. A jeśli po prostu chcesz dostać dane bez pisania jakiegokolwiek kodu, też pokażę Ci taką opcję.
Zanim zaczniesz
- Poziom trudności: początkujący do średnio zaawansowanego
- Czas potrzebny: około 15–20 minut na każdą metodę
- Czego potrzebujesz:
- Python 3.11+ zainstalowany
- Terminal lub edytor kodu
- Przeglądarka Chrome (jeśli chcesz podejrzeć HTML HN albo wypróbować opcję bez kodu)
- Rozszerzenie Thunderbit do Chrome (opcjonalnie, do metody bez kodu)

Konfiguracja środowiska Python
Zanim dotkniemy jakichkolwiek danych z HN, przygotujmy środowisko. Polecam utworzyć wirtualne środowisko, żeby zależności projektu były czyste i odseparowane.
# Utwórz i aktywuj wirtualne środowisko
python3 -m venv hn-scraper
# macOS/Linux:
source hn-scraper/bin/activate
# Windows:
hn-scraper\Scripts\activate
# Zainstaluj pakiety potrzebne do obu metod
pip install requests==2.33.1 beautifulsoup4==4.14.3 pandas==3.0.2 openpyxl==3.1.5
Do dalszych, bardziej produkcyjnych scenariuszy (cache, retry) przydadzą się też:
pip install requests-cache==1.3.1 tenacity==9.1.4
Bez specjalnych kluczy API, bez tokenów uwierzytelniających. Dane HN są publiczne.
Metoda 1: scrapowanie Hacker News w Pythonie z użyciem BeautifulSoup
To klasyczne podejście — pobierasz HTML, parsujesz go i wyciągasz potrzebne dane. W ten sposób większość osób uczy się web scrapingu, a prosta, tabelaryczna struktura HN sprawia, że to świetne środowisko do ćwiczeń.
Krok 1: pobierz stronę główną Hacker News
Otwórz edytor i utwórz plik scrape_hn_bs4.py. Oto kod startowy:
import requests
from bs4 import BeautifulSoup
url = "https://news.ycombinator.com/news"
headers = {"User-Agent": "Mozilla/5.0 (educational HN scraper)"}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")
print(f"Status: {response.status_code}, Page length: {len(response.text)} chars")
Uruchom go. Powinieneś zobaczyć Status: 200 i długość strony około 40 000–50 000 znaków. To surowy HTML strony głównej HN załadowany do pamięci, gotowy do parsowania.
Krok 2: zrozum strukturę HTML
HN korzysta z układu opartego na tabelach — bez nowoczesnego CSS grid czy flexboxa. Każda historia na stronie składa się z dwóch kluczowych wierszy <tr>:
- Wiersz historii (
<tr class="athing submission">): zawiera pozycję, tytuł i link - Wiersz metadanych (następny
<tr>): zawiera liczbę punktów, autora, czas i liczbę komentarzy
Najważniejsze selektory:
span.titleline > a— tytuł wpisu i URLspan.score— liczba głosów (np. „118 points”)a.hnuser— nazwa użytkownika autoraspan.age— czas publikacji- Ostatni
<a>w.subtextz tekstem zawierającym „comment” — liczba komentarzy
Jeśli klikniesz prawym przyciskiem myszy na dowolny tytuł w Chrome i wybierzesz „Inspect”, zobaczysz coś takiego:
<span class="titleline">
<a href="https://darkbloom.dev">Darkbloom – Private inference on idle Macs</a>
</span>
A poniżej wiersz z metadanymi:
<span class="score" id="score_47788542">118 points</span>
by <a href="user?id=twapi" class="hnuser">twapi</a>
<span class="age" title="2026-04-16T04:06:39 1776312399">
<a href="item?id=47788542">2 hours ago</a>
</span>
| <a href="item?id=47788542">65 comments</a>
Zrozumienie tych selektorów jest kluczowe — jeśli HN kiedykolwiek zmieni markup, trzeba będzie je zaktualizować. (Spoiler: metoda API całkowicie omija ten problem.)
Krok 3: wyciągnij tytuły, linki i wyniki
Teraz właściwa część. Przejdziemy po każdym wierszu historii, pobierzemy tytuł i link z wiersza historii, a następnie score z wiersza metadanych znajdującego się bezpośrednio pod nim.
import requests
from bs4 import BeautifulSoup
from pprint import pprint
url = "https://news.ycombinator.com/news"
headers = {"User-Agent": "Mozilla/5.0 (educational HN scraper)"}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")
stories = []
story_rows = soup.select("tr.athing")
for row in story_rows:
# Tytuł i URL z wiersza historii
title_tag = row.select_one("span.titleline > a")
if not title_tag:
continue
title = title_tag.get_text()
link = title_tag.get("href", "")
# Metadane z następnego wiersza
meta_row = row.find_next_sibling("tr")
score = 0
author = ""
comments = 0
if meta_row:
if score_tag := meta_row.select_one("span.score"):
score = int(score_tag.get_text().replace(" points", ""))
if author_tag := meta_row.select_one("a.hnuser"):
author = author_tag.get_text()
# Liczba komentarzy: ostatni <a> z tekstem zawierającym "comment"
for a_tag in meta_row.select("a"):
text = a_tag.get_text()
if "comment" in text:
comments = int(text.split("\xa0")[0])
stories.append({
"title": title,
"url": link,
"score": score,
"author": author,
"comments": comments,
})
# Filtrujemy historie z 50+ punktami i sortujemy po score
top_stories = sorted(
[s for s in stories if s["score"] >= 50],
key=lambda x: x["score"],
reverse=True,
)
pprint(top_stories[:10])
Kilka uwag do kodu:
- Operator walrus (
:=) działa od Pythona 3.8. Pozwala przypisać i sprawdzić w jednej linii — przydatne dla opcjonalnych elementów, takich jakspan.score, które nie występują w każdym wierszu (np. wpisy rekrutacyjne nie mają wyniku). - HN używa
\xa0(twardej spacji) między liczbą a słowem „comments”, więc dzielimy po tym znaku. - Wpisy prowadzące do innych stron HN (np. posty „Ask HN”) będą miały względne URL-e zaczynające się od
item?id=. Warto dodać wtedy prefikshttps://news.ycombinator.com/.
Krok 4: uruchom i zobacz wyniki
Zapisz plik i uruchom:
python scrape_hn_bs4.py
Powinieneś zobaczyć wynik podobny do tego:
[{'author': 'twapi',
'comments': 65,
'score': 118,
'title': 'Darkbloom – Private inference on idle Macs',
'url': 'https://darkbloom.dev'},
{'author': 'sebg',
'comments': 203,
'score': 247,
'title': 'Show HN: I built an open-source Perplexity alternative',
'url': 'https://github.com/...'},
...]
To 30 historii z pierwszej strony. Ale HN ma przecież setki aktywnych wpisów naraz. Paginację omówimy w dalszej części.
Metoda 2: scrapowanie Hacker News w Pythonie z użyciem oficjalnego API
HN Firebase API to oficjalnie rekomendowany sposób dostępu do danych Hacker News. Bez uwierzytelniania, bez kluczy API, bez parsowania HTML. Dostajesz czyste odpowiedzi JSON. Ja używam tej metody do wszystkiego, co musi działać niezawodnie w produkcji.
Kluczowe endpointy API, które warto znać
Bazowy adres to https://hacker-news.firebaseio.com/v0/. Oto najważniejsze endpointy:
| Endpoint | Co zwraca | Przykład |
|---|---|---|
/v0/topstories.json | Tablica do 500 ID najlepszych historii | [47788542, 47787901, ...] |
/v0/newstories.json | Do 500 najnowszych ID historii | Ten sam format |
/v0/beststories.json | Do 500 najlepszych ID historii | Ten sam format |
/v0/askstories.json | Do 200 ID wpisów typu „Ask HN” | Ten sam format |
/v0/showstories.json | Do 200 ID wpisów typu „Show HN” | Ten sam format |
/v0/jobstories.json | Do 200 ID ofert pracy | Ten sam format |
/v0/item/{id}.json | Pełne szczegóły dowolnego elementu (historia, komentarz, ankieta) | Obiekt JSON |
/v0/user/{username}.json | Profil użytkownika | Obiekt JSON |
/v0/maxitem.json | Aktualne maksymalne ID elementu | Liczba całkowita (np. 47789427) |
Przykładowy element historii wygląda tak:
{
"by": "twapi",
"descendants": 65,
"id": 47788542,
"kids": [47789171, 47788769, 47788762],
"score": 118,
"time": 1776312399,
"title": "Darkbloom – Private inference on idle Macs",
"type": "story",
"url": "https://darkbloom.dev"
}
Pole kids zawiera ID bezpośrednich komentarzy potomnych. Każdy komentarz jest osobnym elementem i może mieć własne kids — właśnie tak zbudowane jest drzewo komentarzy.
Krok 1: pobierz ID najpopularniejszych historii
Utwórz plik scrape_hn_api.py:
import requests
import time
from pprint import pprint
API_BASE = "https://hacker-news.firebaseio.com/v0"
# Pobieramy ID top stories
response = requests.get(f"{API_BASE}/topstories.json")
story_ids = response.json()
print(f"Got {len(story_ids)} top story IDs")
# Output: Got 500 top story IDs
500 ID historii w jednym żądaniu — bez parsowania, bez selektorów, po prostu tablica JSON.
Krok 2: pobierz szczegóły historii po ID
Teraz potrzebujemy właściwych danych. Tutaj pojawia się problem „fan-out”: 500 historii oznacza 500 osobnych wywołań API. W moich testach pojedyncze żądanie o element trwa około 1,2 sekundy sekwencyjnie. Dla 500 historii to mniej więcej 10 minut.
Dla większości zastosowań nie potrzebujesz wszystkich 500. Oto kod pobierający top 30:
def fetch_story(story_id):
"""Pobiera szczegóły pojedynczej historii z API HN."""
resp = requests.get(f"{API_BASE}/item/{story_id}.json")
return resp.json()
# Pobieramy szczegóły dla 30 najlepszych historii
stories = []
for sid in story_ids[:30]:
story = fetch_story(sid)
if story and story.get("type") == "story":
stories.append({
"title": story.get("title", ""),
"url": story.get("url", ""),
"score": story.get("score", 0),
"author": story.get("by", ""),
"comments": story.get("descendants", 0),
"time": story.get("time", 0),
"id": story.get("id"),
})
time.sleep(0.1) # Bądź uprzejmy — krótka przerwa między żądaniami
# Sortujemy po score i pokazujemy top 10
top = sorted(stories, key=lambda x: x["score"], reverse=True)[:10]
pprint(top)
time.sleep(0.1) dodaje niewielkie opóźnienie z grzeczności. Firebase API nie podaje oficjalnego limitu, ale bombardowanie jakiegokolwiek API bez pauz to zła praktyka.
Krok 3: scrapowanie komentarzy (rekurencyjne przejście po drzewie)
Tutaj API naprawdę błyszczy na tle scrapowania HTML. Komentarze na HN są głęboko zagnieżdżone — odpowiedzi na odpowiedzi na odpowiedzi. W HTML oznacza to walkę ze skomplikowaną, zagnieżdżoną strukturą tabel. W API każde kids zawiera ID dzieci, więc po prostu przechodzisz po drzewie rekurencyjnie.
def fetch_comments(item_id, depth=0, max_depth=3):
"""Rekurencyjnie pobiera komentarze do max_depth."""
item = requests.get(f"{API_BASE}/item/{item_id}.json").json()
if not item or item.get("type") != "comment":
return []
comments = [{
"author": item.get("by", "[deleted]"),
"text": item.get("text", ""),
"depth": depth,
"id": item.get("id"),
}]
if depth < max_depth and item.get("kids"):
for kid_id in item["kids"]:
comments.extend(fetch_comments(kid_id, depth + 1, max_depth))
time.sleep(0.05)
return comments
# Przykład: pobieramy komentarze dla top story
if stories:
top_story = stories[0]
top_story_full = requests.get(f"{API_BASE}/item/{top_story['id']}.json").json()
if top_story_full.get("kids"):
print(f"\nComments for: {top_story['title']}")
all_comments = []
for kid_id in top_story_full["kids"][:5]: # Pierwsze 5 komentarzy najwyższego poziomu
all_comments.extend(fetch_comments(kid_id, depth=0, max_depth=2))
time.sleep(0.1)
for c in all_comments[:15]:
indent = " " * c["depth"]
preview = c["text"][:80].replace("\n", " ") if c["text"] else "[brak tekstu]"
print(f"{indent}[{c['author']}] {preview}...")
To rekurencyjne podejście jest znacznie prostsze niż parsowanie zagnieżdżonych wątków komentarzy w HTML. Jeśli potrzebujesz pełnych drzew komentarzy, API będzie najlepszym wyborem.
Krok 4: uruchom i zobacz wyniki
python scrape_hn_api.py
Zobaczysz uporządkowane dane o historiach, a potem zagnieżdżony podgląd komentarzy. Dane są czystsze, dostęp do komentarzy jest banalny, a ryzyko, że scraper przestanie działać, bo HN zmieni nazwę klasy CSS, praktycznie znika.
Dalej niż pierwsza strona: paginacja i dane historyczne
Większość tutoriali o scrapowaniu HN kończy się na pierwszej stronie — 30 historiach. To wystarczy do szybkiej demonstracji, ale w realnych zastosowaniach często potrzebujesz więcej.
Scrapowanie wielu stron z BeautifulSoup
Paginacja HN opiera się na prostym wzorcu URL: ?p=2, ?p=3 itd. Każda strona zwraca 30 historii, a serwis obsługuje mniej więcej do 20. strony (czyli około 600 historii łącznie). Powyżej tego pojawiają się puste strony.
import time
def scrape_hn_pages(num_pages=5):
"""Scrapuje wiele stron z front page Hacker News."""
all_stories = []
for page in range(1, num_pages + 1):
url = f"https://news.ycombinator.com/news?p={page}"
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")
story_rows = soup.select("tr.athing")
if not story_rows:
print(f"Page {page}: no stories found, stopping.")
break
for row in story_rows:
title_tag = row.select_one("span.titleline > a")
if not title_tag:
continue
meta_row = row.find_next_sibling("tr")
score = 0
if meta_row and (score_tag := meta_row.select_one("span.score")):
score = int(score_tag.get_text().replace(" points", ""))
all_stories.append({
"title": title_tag.get_text(),
"url": title_tag.get("href", ""),
"score": score,
})
print(f"Page {page}: scraped {len(story_rows)} stories")
# Szanuj crawl-delay 30 sekund z robots.txt
if page < num_pages:
time.sleep(30)
return all_stories
stories = scrape_hn_pages(5)
print(f"\nTotal stories scraped: {len(stories)}")
time.sleep(30) jest tu ważne. Plik robots.txt HN wyraźnie prosi o 30-sekundowe opóźnienie crawl. Zignorujesz to i możesz dostać limitowanie żądań (HTTP 429) albo tymczasową blokadę. Pięć stron w odstępach 30 sekund zajmuje około 2,5 minuty — nie jest to natychmiastowe, ale jest to zgodne z zasadami.
Dla osób, które nie chcą zarządzać kodem paginacji, Thunderbit automatycznie obsługuje paginację klikaniem i infinite scroll. Kliknie przycisk „More” na dole stron HN bez żadnej konfiguracji.
Scrapuj strony Hacker News z pomocą AI
Dostęp do historycznych danych Hacker News przez Algolia API
Firebase API daje dane bieżące. Do analizy historycznej — „Jakie były najlepsze historie o Pythonie w 2023 roku?” albo „Jak zmieniało się pokrycie tematu AI przez ostatnie 5 lat?” — potrzebujesz HN Algolia Search API.
import requests
ALGOLIA_BASE = "https://hn.algolia.com/api/v1"
def search_hn(query, tags="story", page=0, hits_per_page=20):
"""Wyszukuje w HN przez API Algolia."""
params = {
"query": query,
"tags": tags,
"page": page,
"hitsPerPage": hits_per_page,
}
resp = requests.get(f"{ALGOLIA_BASE}/search", params=params)
return resp.json()
# Przykład: znajdź historie o scrapowaniu w Pythonie z 10+ punktami od stycznia 2024
results = search_hn(
query="python scraping",
tags="story",
)
print(f"Found {results['nbHits']} total results")
for hit in results["hits"][:5]:
print(f" [{hit.get('points', 0)} pts] {hit['title']}")
Do zapytań z filtrem daty użyj numericFilters:
import calendar, datetime
# Historie od 1 stycznia 2024
start_date = datetime.datetime(2024, 1, 1)
start_ts = int(calendar.timegm(start_date.timetuple()))
params = {
"query": "python web scraping",
"tags": "story",
"numericFilters": f"created_at_i>{start_ts},points>10",
"hitsPerPage": 50,
}
resp = requests.get(f"{ALGOLIA_BASE}/search_by_date", params=params)
data = resp.json()
print(f"Found {data['nbHits']} stories about Python web scraping since 2024 with >10 points")
Algolia API jest szybkie (5–9 ms czasu przetwarzania po stronie serwera), nie wymaga klucza API i obsługuje paginację do 500 stron. Do masowej analizy historycznej to najlepsza dostępna opcja.
Eksport scraped danych z Hacker News do CSV, Excela i Google Sheets
Każdy poradnik o scrapowaniu HN, jaki widziałem, kończy się wynikiem pprint() w terminalu. To świetne do debugowania, ale jeśli budujesz codzienny digest albo analizujesz trendy, dane muszą trafić do pliku. Oto jak to zrobić.
Eksport do CSV w Pythonie
import csv
def export_to_csv(stories, filename="hn_stories.csv"):
"""Zapisuje zebrane historie do pliku CSV."""
fieldnames = ["title", "url", "score", "author", "comments"]
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(stories)
print(f"Saved {len(stories)} stories to {filename}")
export_to_csv(stories)
Eksport do Excela w Pythonie
import pandas as pd
def export_to_excel(stories, filename="hn_stories.xlsx"):
"""Zapisuje zebrane historie do pliku Excel."""
df = pd.DataFrame(stories)
df.to_excel(filename, index=False, engine="openpyxl")
print(f"Saved {len(stories)} stories to {filename}")
export_to_excel(stories)
Upewnij się, że openpyxl jest zainstalowany — pandas używa go jako silnika Excela. Jeśli go brakuje, pojawi się ImportError.
Wysyłka do Google Sheets (opcjonalnie)
W automatycznych workflowach możesz chcieć wysyłać dane bezpośrednio do Google Sheets przy użyciu biblioteki gspread. Wymaga to jednorazowego skonfigurowania konta usługi Google Cloud:
import gspread
gc = gspread.service_account(filename="service_account.json")
sh = gc.open("HN Daily Digest")
worksheet = sh.sheet1
# Zamieniamy historie na wiersze
header = list(stories[0].keys())
rows = [list(s.values()) for s in stories]
worksheet.clear()
worksheet.update([header] + rows)
print("Pushed to Google Sheets")
Alternatywa bez kodu dla eksportu
Jeśli konfiguracja kont serwisowych i pisanie kodu eksportowego brzmi jak więcej pracy niż samo scrapowanie, rozumiem to. W Thunderbit zbudowaliśmy darmowy eksport danych, który pozwala wysyłać zeskrobane dane bezpośrednio do Excela, Google Sheets, Airtable lub Notion — bez kodu, bez poświadczeń, bez utrzymywania pipeline’u. Przy jednorazowym pobraniu danych to naprawdę szybsze rozwiązanie. O tym niżej.
Jak przygotować scraper do pracy produkcyjnej: obsługa błędów, cache i harmonogram
Jeśli uruchamiasz scraper raz dla zabawy, powyższy kod wystarczy. Jeśli ma działać codziennie jako część workflow, potrzebujesz kilku dodatkowych elementów.
Obsługa błędów i logika ponawiania
Sieć bywa zawodna. Serwery ograniczają ruch. Pojedyncze nieudane żądanie nie powinno wywracać całego procesu. Oto funkcja retry z wykładniczym backoffem:
from tenacity import retry, stop_after_attempt, wait_exponential_jitter
import requests
@retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=1, max=60))
def fetch_with_retry(url):
"""Pobiera URL z automatycznymi ponowieniami i wykładniczym backoffem."""
response = requests.get(url, timeout=10)
response.raise_for_status()
return response
# Użycie:
try:
resp = fetch_with_retry("https://hacker-news.firebaseio.com/v0/topstories.json")
story_ids = resp.json()
except Exception as e:
print(f"Failed after retries: {e}")
Biblioteka tenacity elegancko obsługuje retry. Spróbuje ponownie do 5 razy z losowanym wykładniczym backoffem — start od 1 sekundy, maksymalnie 60 sekund. To dobrze radzi sobie z HTTP 429 (limit), 503 (service unavailable) i przejściowymi błędami sieci.
Cache odpowiedzi, żeby nie scrapować w kółko
Podczas desenvolpmentu wielokrotnie uruchamiasz scraper, poprawiając logikę parsowania. Bez cache każdorazowo trafiasz ponownie na serwery HN z tymi samymi danymi. Biblioteka requests-cache rozwiązuje to w dwóch liniach:
import requests_cache
requests_cache.install_cache("hn_cache", expire_after=3600) # Cache na 1 godzinę
Po dodaniu tych linii na początku skryptu wszystkie wywołania requests.get() są automatycznie cachowane w lokalnej bazie SQLite. Uruchom skrypt 10 razy w ciągu godziny, a tylko pierwsze uruchomienie faktycznie połączy się z siecią. To narzędzie użytkownicy forów często polecają i nie bez powodu.
Rozdzielenie pobierania od parsowania
Jednym z patternów, na który przysięgają doświadczeni scraperzy, jest najpierw pobrać surowe dane, a dopiero potem je parsować. Dzięki temu, jeśli logika parsowania ma błąd, poprawiasz go i parsujesz ponownie bez ponownego pobierania.
import os, json
def crawl_and_save(story_ids, output_dir="raw_data"):
"""Pobiera dane historii i zapisuje surowy JSON na dysku."""
os.makedirs(output_dir, exist_ok=True)
for sid in story_ids:
filepath = os.path.join(output_dir, f"{sid}.json")
if os.path.exists(filepath):
continue # Pomijamy już pobrane elementy
resp = fetch_with_retry(f"{API_BASE}/item/{sid}.json")
with open(filepath, "w") as f:
json.dump(resp.json(), f)
def parse_saved_data(input_dir="raw_data"):
"""Parsuje zapisane pliki JSON do uporządkowanej listy historii."""
stories = []
for filename in os.listdir(input_dir):
with open(os.path.join(input_dir, filename)) as f:
item = json.load(f)
if item and item.get("type") == "story":
stories.append({
"title": item.get("title", ""),
"url": item.get("url", ""),
"score": item.get("score", 0),
"author": item.get("by", ""),
"comments": item.get("descendants", 0),
})
return stories
To podejście dwuetapowe jest szczególnie przydatne, gdy scrapujesz setki elementów i chcesz szybko iterować nad sposobem przetwarzania danych.
Automatyzacja scrapera według harmonogramu
Do codziennego digestu HN scraper musi działać automatycznie. Dwie popularne opcje:
Opcja 1: cron (Linux/Mac)
# Uruchamiaj codziennie o 8:30 UTC
30 8 * * * /usr/bin/python3 /home/user/scrape_hn.py >> /home/user/scrape.log 2>&1
Opcja 2: GitHub Actions (darmowe, bez własnego serwera)
name: Scrape Hacker News
on:
schedule:
- cron: '30 8 * * *' # Codziennie o 8:30 UTC
workflow_dispatch: # Przycisk do ręcznego uruchomienia
jobs:
scrape:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: pip install requests beautifulsoup4 pandas openpyxl
- run: python scrape_hn.py
- run: |
git config user.name "GitHub Actions Bot"
git config user.email "actions@github.com"
git add -A
git diff --staged --quiet || git commit -m "Update HN data $(date -u +%Y-%m-%dT%H:%M:%SZ)"
git push
Kilka pułapek przy harmonogramach w GitHub Actions: wszystkie czasy cron są w UTC, opóźnienia 15–60 minut są dość częste (warto używać nietypowych minut, np. :30 zamiast :00), a GitHub może wyłączyć zaplanowane workflowy w repozytoriach bez aktywności przez 60 dni. Zawsze dodawaj workflow_dispatch, żeby móc uruchamiać workflow ręcznie podczas testów.
Prostsza opcja: funkcja Scheduled Scraper w Thunderbit pozwala opisać harmonogram zwykłym językiem — np. „scrape every morning at 8am” — bez serwera i bez konfiguracji crona.
Gdy Python to przesada: sposób bez kodu na scrapowanie Hacker News
Będę szczery — mimo że uwielbiam Pythona i mój zespół tworzy narzędzia dla developerów. Jeśli potrzebujesz po prostu dzisiejszych 100 najlepszych historii z HN w arkuszu kalkulacyjnym — teraz, jednorazowo — pisanie, debugowanie i uruchamianie skryptu Pythona to zbędny narzut. Sama konfiguracja (wirtualne środowisko, instalacja pakietów, ogarnianie selektorów) trwa dłużej niż samo zebranie danych.
Tutaj pojawia się Thunderbit. Workflow wygląda tak:
- Otwórz
news.ycombinator.comw Chrome - Kliknij ikonę rozszerzenia Thunderbit, a potem „AI Suggest Fields”
- AI odczytuje stronę i proponuje kolumny: Title, URL, Score, Author, Comment Count, Time Posted
- W razie potrzeby dopasuj pola (zmień nazwę, usuń lub dodaj własne — możesz nawet dodać prompt AI typu „Categorize as AI/DevTools/Web/Other”)
- Kliknij „Scrape” — dane pojawiają się w uporządkowanej tabeli
- Wyeksportuj do Excela, Google Sheets, Airtable lub Notion
Dwa kliknięcia do danych strukturalnych. Bez selektorów, bez kodu, bez utrzymania.
Jedna ważna przewaga: AI w Thunderbit automatycznie dostosowuje się do zmian w układzie strony. Tradycyjne scrapery oparte na selektorach CSS psują się, gdy serwis zmienia markup — a choć HTML HN jest dość stabilny, to jednak się zmieniał (klasa class="athing submission" została zaktualizowana, span.titleline zastąpił starszy a.storylink). Scraper oparty na AI czyta stronę „na świeżo” za każdym razem, więc zmiany nazw klas go nie obchodzą.

Thunderbit obsługuje też paginację (automatycznie klikając przycisk „More” na HN) oraz scrapowanie podstron (odwiedzanie strony komentarzy każdej historii, aby pobrać dyskusję). W przypadku wzbogacania komentarzy to odpowiednik rekurencyjnego kodu z Metody 2 — tylko bez pisania ani jednej linii.
Wady i zalety są proste: Python to lepszy wybór, gdy potrzebujesz własnej logiki, złożonych transformacji danych, planowanych pipeline’ów automatyzacji albo po prostu uczysz się programować. Thunderbit sprawdzi się lepiej, gdy potrzebujesz danych szybko, nie chcesz utrzymywać kodu albo nie jesteś developerem. Wybierz narzędzie dopasowane do sytuacji.
Python vs API vs no-code: którą metodę wybrać?
Oto pełna macierz decyzyjna:
| Kryterium | BeautifulSoup (HTML) | Firebase API | Algolia API | Thunderbit (bez kodu) |
|---|---|---|---|---|
| Wymagane umiejętności techniczne | Python średniozaawansowany | Python dla początkujących | Python dla początkujących | Brak |
| Czas konfiguracji | 10–15 min | 5–10 min | 5–10 min | 2 min |
| Koszt utrzymania | Średni (selektory się psują) | Niski (stabilny JSON) | Niski (stabilny JSON) | Brak |
| Głębokość danych | Tylko strona główna | Dowolny element, użytkownicy | Wyszukiwanie + historia | Strona główna + podstrony |
| Komentarze | Trudne | Łatwe (rekurencyjnie) | Łatwe (zagnieżdżone drzewo) | Scrapowanie podstron |
| Dane historyczne | Nie | Nie | Tak (pełne archiwum) | Nie |
| Opcje eksportu | Samodzielnie w kodzie | Samodzielnie w kodzie | Samodzielnie w kodzie | Wbudowane (Excel, Sheets itd.) |
| Harmonogram | cron / GitHub Actions | cron / GitHub Actions | cron / GitHub Actions | Wbudowany scheduler |
| Najlepsze do | Nauki scrapowania | Niezawodnych pipeline’ów | Researchu i analizy | Szybkiego pobierania danych |
Jeśli uczysz się Pythona albo budujesz coś własnego, wybierz metodę 1 lub 2. Jeśli potrzebujesz analizy historycznej, dodaj API Algolia. A jeśli po prostu chcesz dostać dane bez kodu, wypróbuj Thunderbit.
Wypróbuj Thunderbit do scrapowania Hacker News
Podsumowanie i najważniejsze wnioski
Teraz masz w arsenale:
- Dwie kompletne metody w Pythonie do scrapowania Hacker News — BeautifulSoup do parsowania HTML i Firebase API do czystych danych JSON
- Techniki paginacji do wyjścia poza pierwszą stronę, w tym API Algolia do danych historycznych sięgających 2007 roku
- Kod eksportu do CSV, Excela i Google Sheets — bo dane w terminalu nie pomagają nikomu poza Tobą
- Wzorce produkcyjne — retry logic, cache, rozdzielenie crawl/parsing i automatyzacja według harmonogramu przez cron lub GitHub Actions
- Alternatywę bez kodu na momenty, gdy Python jest bardziej narzędziem niż potrzebujesz
Moja rekomendacja: zacznij od Firebase API (Metoda 2) w większości zastosowań. Jest czyściej, bardziej niezawodnie i daje dostęp do komentarzy bez bólu parsowania zagnieżdżonego HTML. API Algolia dołóż wtedy, gdy potrzebujesz danych historycznych. A Thunderbit trzymaj w zakładkach na momenty, gdy potrzebujesz po prostu szybkiego arkusza i nie chcesz uruchamiać całego projektu Python.
Jeśli chcesz pójść głębiej, spróbuj scrapować komentarze HN do analizy sentymentu, zbuduj codzienny pipeline digestu z GitHub Actions albo przejrzyj API Algolia, by zobaczyć, jak zmieniały się trendy technologiczne w ostatniej dekadzie.
Wypróbuj Thunderbit do szybkiego scrapowania Hacker News Get Started Free
FAQ
Czy scrapowanie Hacker News jest legalne?
Dane HN są publicznie dostępne, a Y Combinator udostępnia oficjalne API do programowego dostępu. Plik robots.txt serwisu zezwala na scrapowanie treści tylko do odczytu (strona główna, strony elementów, strony użytkowników), ale prosi o 30-sekundowe opóźnienie crawl. Przestrzegaj tej przerwy, nie scrapuj interaktywnych endpointów (głosowanie, logowanie) i jesteś po bezpiecznej stronie. Więcej o etyce scrapowania przeczytasz w naszym poradniku o prawnych aspektach web scrapingu.
Czy Hacker News ma oficjalne API?
Tak. HN Firebase API pod adresem hacker-news.firebaseio.com/v0/ jest darmowe, nie wymaga uwierzytelniania i zapewnia dostęp do historii, komentarzy, profili użytkowników oraz wszystkich typów feedów (top, new, best, ask, show, jobs). Zwraca czysty JSON i nie ma oficjalnie podanego limitu, choć uprzejme tempo wysyłania żądań jest zawsze wskazane.
Jak scrapować komentarze z Hacker News w Pythonie?
Korzystając z API Firebase, pobierz element historii, aby dostać pole kids (tablicę ID komentarzy najwyższego poziomu). Każdy komentarz jest osobnym elementem z własnym kids dla odpowiedzi. Przechodź po drzewie rekurencyjnie funkcją, która pobiera każdy komentarz i jego dzieci. Pełny kod znajdziesz w sekcji „Scrapowanie komentarzy (rekurencyjne przejście po drzewie)” powyżej. Alternatywnie endpoint /items/<id> w API Algolia zwraca pełne zagnieżdżone drzewo komentarzy w jednym żądaniu — znacznie szybciej przy wątkach z dużą liczbą komentarzy.
Czy mogę scrapować Hacker News bez pisania kodu?
Tak. AI web scraper Thunderbit działa jako rozszerzenie Chrome — otwierasz HN, klikasz „AI Suggest Fields”, a narzędzie automatycznie rozpoznaje kolumny takie jak tytuł, URL, score i autor. Klikasz „Scrape” i eksportujesz dane bezpośrednio do Excela, Google Sheets, Airtable lub Notion. Obsługuje paginację i potrafi też odwiedzać podstrony, aby pobrać komentarze. Bez Pythona, bez selektorów, bez utrzymania.
Jak zdobyć historyczne dane z Hacker News?
Najlepszym narzędziem do tego jest HN Algolia Search API. Użyj endpointu search_by_date z numericFilters=created_at_i>TIMESTAMP, aby filtrować po zakresie dat. Możesz wyszukiwać po słowach kluczowych, filtrować po typie historii i przechodzić przez nawet 500 stron wyników. Do analizy historycznej na dużą skalę dostępne są też publiczne zbiory danych w Google BigQuery (pełne archiwum), ClickHouse (28 milionów rekordów) oraz Hugging Face (4 miliony historii).
Dowiedz się więcej


