Python으로 Hacker News를 스크래핑하는 방법: 2가지 방법과 전체 코드

최종 업데이트: June 29, 2026
Python으로 Hacker News를 스크래핑하는 방법: 2가지 방법과 전체 코드

한때 팀에서 보려고 Hacker News 주요 글을 매일 정리하는 다이제스트를 만들려 했어요. 사이트를 즐겨찾기에 넣고 아침마다 훑으면 되겠지, 했죠. 그런데 사흘을 못 갔어요. 헤드라인 읽고 링크를 스프레드시트에 옮기는 데만 매일 20분을 쓰고 있더라고요.

Hacker News는 인터넷에서 기술 인사이트 밀도가 가장 높은 곳에 속해요. 월 방문이 약 1,300만, 새 글이 하루 1,300개, 댓글은 1만 3천 개씩 쌓여요. 기술 트렌드를 좇든, 브랜드 언급을 살피든, "Who's Hiring" 스레드로 채용 파이프라인을 짜든, 개발자들이 뭘 보는지 알고 싶든 — 이걸 손으로 따라가는 건 처음부터 진 싸움이에요.

다행히 Python으로 긁는 건 의외로 쉬워요. 이 글에서는 두 가지 방법을 끝까지 다뤄요. BeautifulSoup로 HTML을 파싱하는 법, 그리고 공식 HN Firebase API를 쓰는 법이에요. 여기에 페이지네이션, 데이터 내보내기, 운영 환경용 패턴, 그리고 Python이 과하게 느껴질 때 쓸 노코드 대안까지 덧붙였어요.

왜 Python으로 Hacker News를 스크래핑할까?

Hacker News는 그냥 링크 모음이 아니에요. 커뮤니티가 직접 골라내는 피드라, 흥미로운 기술 뉴스가 업보트와 토론을 타고 위로 올라와요. 이용자도 기술 직군이 많고요(남성 약 76%, 25~34세가 주축), 직접 유입이 66%라는 건 우연히 들르는 곳이 아니라 매일 찾아오는 단골이 많다는 뜻이에요.

긁는 목적은 사람마다 달라요.

활용 사례얻을 수 있는 것
데일리 기술 다이제스트주요 스토리, 점수, 링크를 이메일이나 Slack으로 전달
브랜드/경쟁사 모니터링회사나 제품이 언급될 때 알림 수신
트렌드 분석어떤 기술, 언어, 주제가 점점 주목받는지 추적
채용"Who's Hiring" 스레드에서 채용 공고, 기술 스택, 연봉 신호 파악
콘텐츠 리서치글감이 될 만한 성과 좋은 주제 찾기
감성 분석제품, 출시, 업계 변화에 대한 커뮤니티 반응 파악

가치는 단순한 흥미를 넘어서요. 합치면 4,000억 달러가 넘는 Stripe, Dropbox, Airbnb도 여기서 초기 피드백과 사용자를 얻었어요. Drew Houston은 2007년 4월 HN에 Dropbox 데모를 올렸고, 1위에 오르자 베타 대기자가 하루 만에 5천 명에서 7만 5천 명으로 뛰었죠.

데이터는 공개돼 있어요. 다만 사이트 구조상 손으로 모으긴 번거로워요. 그래서 Python 자동화가 가장 현실적이에요.

Python으로 Hacker News를 스크래핑하는 2가지 방법: 개요

바로 돌릴 수 있는 두 가지 완성형 접근을 다뤄요.

  1. requests + BeautifulSoup HTML 스크래핑 — news.ycombinator.com의 원본 HTML을 받아 파싱하고 스토리를 뽑아요. 기초를 익히기 좋고, 화면에 보이는 정보를 그대로 가져올 때 적합해요.
  2. 공식 Hacker News Firebase API — HTML 파싱 없이 JSON 엔드포인트를 직접 호출해요. 안정적인 데이터 파이프라인, 그리고 댓글이나 과거 데이터에 강해요.

어떤 게 맞는지 표로 비교해 볼게요.

기준HTML 스크래핑 (requests + BS4)HN Firebase APIThunderbit (노코드)
설정 복잡도중간 (HTML 선택자 파싱 필요)낮음 (JSON 엔드포인트)없음 (2번 클릭하는 Chrome 확장 프로그램)
데이터 최신성실시간 프론트페이지실시간 (ID만 알면 어떤 항목이든)실시간
속도 제한 위험중간 (robots.txt에 30초 크롤링 지연 명시)낮음 (공식, 관대한 편)Thunderbit가 관리
댓글 접근어려움 (중첩 HTML)쉬움 (재귀적으로 아이템 ID 추적)서브페이지 스크래핑 기능
히스토리 데이터제한적Algolia Search API로 가능해당 없음
최적 용도스크래핑 기초 학습안정적인 데이터 파이프라인비개발자, 빠른 내보내기

두 방법 모두 바로 실행되는 전체 코드를 함께 담았어요. 코드 없이 데이터만 필요한 분을 위한 길도 준비했고요.

시작하기 전에

  • 난이도: 초급~중급
  • 소요 시간: 방법당 약 15~20분
  • 준비물:
    • Python 3.11+ 설치
    • 터미널 또는 코드 편집기
    • Chrome 브라우저(HN의 HTML을 직접 확인하거나 노코드 옵션을 쓰려면 필요)
    • Thunderbit Chrome Extension (선택 사항, 노코드용)

scrape-hacker-news-methods.webp

Python 환경 설정하기

데이터를 만지기 전에 환경부터 잡아요. 의존성을 깔끔히 분리하려면 가상 환경을 권해요.

# 가상 환경 생성 및 활성화
python3 -m venv hn-scraper
# macOS/Linux:
source hn-scraper/bin/activate
# Windows:
hn-scraper\Scripts\activate

# 두 방법 모두에 필요한 패키지 설치
pip install requests==2.33.1 beautifulsoup4==4.14.3 pandas==3.0.2 openpyxl==3.1.5

나중에 캐싱과 재시도 같은 운영 패턴을 붙일 거라면 아래도 미리 깔아두면 편해요.

pip install requests-cache==1.3.1 tenacity==9.1.4

API 키도, 인증 토큰도 안 써요. HN 데이터는 공개니까요.

방법 1: BeautifulSoup로 Python에서 Hacker News 스크래핑하기

가장 클래식한 방식이에요. HTML 받아서 파싱하고, 원하는 데이터만 뽑아요. 웹 스크래핑을 처음 배울 때 다들 거치는 길이고, HN의 단순한 테이블 구조는 연습용으로 딱이에요.

1단계: Hacker News 프론트페이지 가져오기

편집기에서 scrape_hn_bs4.py를 만들어요. 출발은 이렇게요.

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")

돌려보면 Status: 200과 함께 페이지 길이가 4만~5만 자쯤 나와요. HN 프론트페이지의 원본 HTML이 메모리에 올라온 거예요. 이제 파싱만 남았어요.

2단계: HTML 구조 이해하기

HN은 요즘 쓰는 CSS grid나 flexbox가 아니라 테이블 레이아웃이에요. 스토리 하나는 보통 <tr> 두 줄로 이뤄져요.

  • 스토리 행 (<tr class="athing submission">): 순위, 제목, 링크
  • 메타데이터 행(바로 다음 <tr>): 점수, 작성자, 시간, 댓글 수

눈여겨볼 선택자는 이래요.

  • span.titleline > a — 스토리 제목과 URL
  • span.score — 점수(예: "118 points")
  • a.hnuser — 작성자 이름
  • span.age — 게시 시간
  • .subtext 안에서 텍스트에 "comment"가 들어간 마지막 <a> — 댓글 수

Chrome에서 스토리 제목을 오른쪽 클릭하고 "검사"를 누르면 이런 구조가 보여요.

<span class="titleline">
  <a href="https://darkbloom.dev">Darkbloom – Private inference on idle Macs</a>
</span>

아래 메타데이터 행은 이렇게 생겼고요.

<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&nbsp;comments</a>

이 구조를 파악해 두면 좋아요. HN이 마크업을 바꾸면 선택자도 손봐야 하니까요. (힌트: API 방법을 쓰면 이 골치를 통째로 피해요.)

3단계: 제목, 링크, 점수 추출하기

이제 본격적으로 뽑아봐요. 스토리 행을 돌면서 제목과 링크를 가져오고, 바로 아래 메타데이터 행에서 점수를 챙겨요.

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:
    # 스토리 행에서 제목과 URL 추출
    title_tag = row.select_one("span.titleline > a")
    if not title_tag:
        continue
    title = title_tag.get_text()
    link = title_tag.get("href", "")

    # 바로 다음 행에서 메타데이터 추출
    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()
        # 댓글 수: 텍스트에 "comment"가 들어간 마지막 <a>
        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,
    })

# 50점 이상 스토리만 필터링하고 점수 높은 순으로 정렬
top_stories = sorted(
    [s for s in stories if s["score"] >= 50],
    key=lambda x: x["score"],
    reverse=True,
)

pprint(top_stories[:10])

몇 가지 짚어둘게요.

  • 월러스 연산자(:=)는 Python 3.8+에서 써요. span.score처럼 매 행에 꼭 있지는 않은 요소를 한 줄에서 할당과 조건 검사로 같이 처리할 때 편해요.
  • HN은 숫자와 "comments" 사이에 \xa0(줄바꿈 없는 공백)을 넣어요. 그걸 기준으로 잘라요.
  • "Ask HN" 같은 내부 링크는 item?id= 형태의 상대 URL이에요. 필요하면 앞에 https://news.ycombinator.com/를 붙이세요.

4단계: 실행하고 결과 확인하기

저장하고 실행해요.

python scrape_hn_bs4.py

이런 출력이 나와요.

[{'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/...'},
 ...]

이게 1페이지 30개 스토리예요. HN에는 항상 이보다 훨씬 많은 활성 스토리가 있고요. 페이지네이션은 뒤에서 다뤄요.

방법 2: 공식 API로 Hacker News를 스크래핑하기

HN Firebase API는 데이터를 공식적으로 받는 길이에요. 인증도, API 키도, HTML 파싱도 없어요. 깔끔한 JSON만 돌아와요. 운영에서 안정적으로 돌려야 하는 작업엔 저도 이 방법을 써요.

꼭 알아야 할 핵심 API 엔드포인트

기본 URL은 https://hacker-news.firebaseio.com/v0/예요. 주요 엔드포인트는 이래요.

엔드포인트반환값예시
/v0/topstories.json최대 500개의 상위 스토리 ID 배열[47788542, 47787901, ...]
/v0/newstories.json최대 500개의 최신 스토리 ID같은 형식
/v0/beststories.json최대 500개의 베스트 스토리 ID같은 형식
/v0/askstories.json최대 200개의 "Ask HN" 스토리 ID같은 형식
/v0/showstories.json최대 200개의 "Show HN" 스토리 ID같은 형식
/v0/jobstories.json최대 200개의 구인 스토리 ID같은 형식
/v0/item/{id}.json특정 아이템(스토리, 댓글, 투표)의 전체 세부 정보JSON 객체
/v0/user/{username}.json사용자 프로필JSON 객체
/v0/maxitem.json현재 최대 아이템 ID정수(예: 47789427)

스토리 아이템은 대략 이런 모양이에요.

{
  "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"
}

kids에는 직속 자식 댓글의 ID가 들어 있어요. 댓글도 각각 하나의 아이템이고, 자기만의 kids를 가질 수 있죠. 이게 댓글 트리를 만들어요.

1단계: 상위 스토리 ID 가져오기

scrape_hn_api.py를 만들어요.

import requests
import time
from pprint import pprint

API_BASE = "https://hacker-news.firebaseio.com/v0"

# 상위 스토리 ID 가져오기
response = requests.get(f"{API_BASE}/topstories.json")
story_ids = response.json()

print(f"Got {len(story_ids)} top story IDs")
# 출력 예시: Got 500 top story IDs

요청 한 번에 스토리 ID 500개가 와요. 파싱도, 선택자도 필요 없어요. 그냥 JSON 배열이에요.

2단계: ID로 스토리 상세 정보 가져오기

이제 실제 데이터가 필요해요. 여기서 팬아웃이 생겨요. 500개면 호출도 500번이니까요. 제 벤치마크로는 아이템 하나 요청에 순차로 약 1.2초, 500개면 거의 10분이에요.

대부분은 500개가 다 필요하진 않아요. 아래는 상위 30개만 받는 예시예요.

def fetch_story(story_id):
    """HN API에서 단일 스토리의 상세 정보를 가져옵니다."""
    resp = requests.get(f"{API_BASE}/item/{story_id}.json")
    return resp.json()

# 상위 30개 스토리 상세 가져오기
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)  # 요청 사이에 짧은 지연을 두어 예의 있게 처리

# 점수 기준 정렬 후 상위 10개 표시
top = sorted(stories, key=lambda x: x["score"], reverse=True)[:10]
pprint(top)

time.sleep(0.1)은 작은 배려예요. Firebase API에 명시된 제한은 없지만, 어떤 API든 쉴 새 없이 두드리는 건 좋은 습관이 아니죠.

3단계: 댓글 스크래핑하기(재귀 트리 탐색)

여기서 API가 HTML보다 확 빛나요. HN 댓글은 깊게 중첩돼요. 답글의 답글의 답글이 이어지죠. HTML이면 복잡한 중첩 테이블을 파싱해야 해요. 반면 API는 각 댓글의 kids만 보면 자식 ID를 알 수 있어서, 트리를 재귀로 타고 내려가면 끝이에요.

def fetch_comments(item_id, depth=0, max_depth=3):
    """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

# 예시: 상위 스토리의 댓글 가져오기
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]:  # 상위 레벨 댓글 5개만
            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 "[no text]"
            print(f"{indent}[{c['author']}] {preview}...")

이 재귀 방식은 중첩 스레드를 HTML로 우격다짐 파싱하는 것보다 훨씬 편해요. 댓글 트리 전체가 필요하면 API가 정답이에요.

4단계: 실행하고 결과 확인하기

python scrape_hn_api.py

구조화된 스토리에 중첩 댓글 미리보기까지 나와요. 데이터는 더 깔끔하고, 댓글 접근도 쉽고, HN이 CSS 클래스 이름을 바꿔도 스크래퍼가 깨질 일이 없어요.

1페이지를 넘어: 페이지네이션과 히스토리 데이터

HN 스크래핑 튜토리얼은 대부분 1페이지, 즉 30개에서 멈춰요. 데모엔 충분하지만, 실전에선 더 깊은 데이터가 필요할 때가 많아요.

BeautifulSoup로 여러 페이지 스크래핑하기

HN 페이지네이션은 ?p=2, ?p=3 식의 단순한 URL 패턴이에요. 페이지당 30개, 약 20페이지(총 약 600개)까지 제공하고, 그 뒤엔 빈 페이지가 나와요.

import time

def scrape_hn_pages(num_pages=5):
    """HN 프론트페이지의 여러 페이지를 스크래핑합니다."""
    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")

        # robots.txt의 30초 크롤링 지연을 준수
        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)은 중요해요. HN의 robots.txt가 30초 크롤링 지연을 대놓고 요청하거든요. 무시하면 속도 제한(HTTP 429)이나 일시 차단을 당할 수 있어요. 30초 간격으로 5페이지면 약 2.5분이 걸리지만, 그만큼 매너 있는 접근이에요.

페이지네이션 코드를 직접 관리하기 싫다면 Thunderbit가 클릭 기반 페이지네이션과 무한 스크롤을 알아서 처리해요. HN 하단의 "More" 버튼도 설정 없이 눌러줘요.

AI로 Hacker News 페이지 스크래핑하기

Algolia API로 과거 Hacker News 데이터 접근하기

Firebase API는 현재 데이터엔 좋아요. 그런데 "2023년에 Python 관련 상위 글은 뭐였지?", "지난 5년간 AI 보도는 어떻게 바뀌었지?" 같은 과거 분석엔 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):
    """Algolia API로 HN을 검색합니다."""
    params = {
        "query": query,
        "tags": tags,
        "page": page,
        "hitsPerPage": hits_per_page,
    }
    resp = requests.get(f"{ALGOLIA_BASE}/search", params=params)
    return resp.json()

# 예시: 2024년 1월 이후 10점 이상인 Python 스크래핑 글 찾기
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']}")

날짜 조건 검색엔 numericFilters를 써요.

import calendar, datetime

# 2024년 1월 1일 이후의 글
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는 빠르고(서버 처리 5~9ms), API 키도 안 쓰고, 최대 500페이지까지 페이지네이션돼요. 대량의 과거 데이터를 볼 땐 최선이에요.

스크래핑한 Hacker News 데이터를 CSV, Excel, Google Sheets로 내보내기

제가 본 HN 튜토리얼은 거의 다 터미널에 pprint() 찍는 데서 끝나요. 디버깅엔 괜찮지만, 데일리 다이제스트나 트렌드 분석을 하려면 파일로 남겨야 해요. 방법을 볼게요.

Python으로 CSV로 내보내기

import csv

def export_to_csv(stories, filename="hn_stories.csv"):
    """스크래핑한 스토리를 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)

Python으로 Excel로 내보내기

import pandas as pd

def export_to_excel(stories, filename="hn_stories.xlsx"):
    """스크래핑한 스토리를 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)

openpyxl이 깔려 있어야 해요. pandas가 Excel 엔진으로 그걸 쓰거든요. 없으면 ImportError가 나요.

Google Sheets로 보내기(선택 사항)

자동화 워크플로를 만든다면 gspread로 데이터를 Google Sheets에 바로 밀어 넣을 수 있어요. 단, Google Cloud 서비스 계정 설정이 한 번 필요해요.

import gspread

gc = gspread.service_account(filename="service_account.json")
sh = gc.open("HN Daily Digest")
worksheet = sh.sheet1

# 스토리를 행 데이터로 변환
header = list(stories[0].keys())
rows = [list(s.values()) for s in stories]

worksheet.clear()
worksheet.update([header] + rows)
print("Pushed to Google Sheets")

노코드 내보내기 대안

서비스 계정 설정과 내보내기 코드가 정작 스크래핑보다 번거롭게 느껴진다면, 충분히 공감해요. Thunderbit은 스크래핑한 데이터를 Excel, Google Sheets, Airtable, Notion으로 바로 보내는 무료 내보내기를 갖췄어요. 코드도, 자격 증명도, 유지할 파이프라인도 없죠. 일회성 추출이면 훨씬 빨라요. 아래에서 더 다뤄요.

스크래퍼를 프로덕션 수준으로 만들기: 오류 처리, 캐싱, 스케줄링

재미로 한 번 돌리는 거면 위 코드로 충분해요. 하지만 매일 워크플로에 끼워 돌린다면 몇 가지를 더 챙겨야 해요.

오류 처리와 재시도 로직

네트워크는 끊기고, 서버는 요청을 제한해요. 요청 하나 잘못됐다고 전체 스크래핑이 무너지면 안 되죠. 지수 백오프를 넣은 재시도는 이렇게요.

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):
    """자동 재시도와 지수 백오프로 URL을 가져옵니다."""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response

# 사용 예시:
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}")

tenacity가 재시도를 깔끔히 맡아줘요. 최대 5번, 초기 1초에서 시작해 최대 60초까지 점점 늘려가며 다시 시도해요. HTTP 429(속도 제한), 503(서비스 불가), 일시적 네트워크 오류를 부드럽게 넘겨요.

다시 크롤링하지 않도록 응답 캐싱하기

개발 중엔 파싱 로직을 손보며 스크래퍼를 여러 번 돌려요. 캐싱이 없으면 매번 같은 데이터를 HN 서버에서 새로 받아오죠. requests-cache면 두 줄로 끝나요.

import requests_cache

requests_cache.install_cache("hn_cache", expire_after=3600)  # 1시간 캐시

이 줄을 스크립트 맨 위에 두면 requests.get() 호출이 로컬 SQLite에 자동 캐시돼요. 한 시간 안에 10번 돌려도 실제 네트워크 요청은 첫 번만 나가요. 포럼에서 자주 추천되는 데는 이유가 있어요.

크롤링과 파싱 분리하기

경험 많은 스크래퍼들이 중요하게 보는 패턴이 있어요. 원본을 먼저 내려받고, 파싱은 그다음에 하는 거예요. 이러면 파싱에 버그가 있어도 다시 받지 않고 파싱만 고치면 돼요.

import os, json

def crawl_and_save(story_ids, output_dir="raw_data"):
    """스토리 데이터를 가져와 원본 JSON을 디스크에 저장합니다."""
    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  # 이미 가져온 항목은 건너뜀
        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"):
    """저장된 JSON 파일을 구조화된 스토리 목록으로 파싱합니다."""
    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

이 2단계 방식은 수백 개를 긁으면서 데이터 처리 방식을 빠르게 다듬고 싶을 때 특히 유용해요.

스케줄에 따라 스크래퍼 자동 실행하기

데일리 다이제스트를 만들려면 스크래퍼가 알아서 돌아야 해요. 흔한 방법 두 가지예요.

옵션 1: cron(Linux/Mac)

# 매일 UTC 오전 8시 30분 실행
30 8 * * * /usr/bin/python3 /home/user/scrape_hn.py >> /home/user/scrape.log 2>&1

옵션 2: GitHub Actions(무료, 별도 서버 불필요)

name: Scrape Hacker News

on:
  schedule:
    - cron: '30 8 * * *'  # 매일 UTC 오전 8시 30분
  workflow_dispatch:        # 수동 실행 버튼

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

GitHub Actions 스케줄링엔 알아둘 점이 있어요. cron 시간은 전부 UTC 기준이고, 15~60분쯤 늦게 도는 일이 잦아서 :00보다 :30 같은 애매한 시각이 나아요. 또 60일간 활동이 없는 저장소는 예약 워크플로가 꺼질 수 있으니, 테스트용으로 workflow_dispatch를 꼭 넣어 수동 실행이 가능하게 하세요.

더 간단하게 가고 싶다면, Thunderbit의 Scheduled Scraper로 "매일 아침 8시에 스크래핑"처럼 자연어로 일정을 잡을 수 있어요. 서버도, cron 설정도 없어요.

Python이 과할 때: Hacker News를 노코드로 스크래핑하는 방법

저는 Python을 좋아하고, 우리 팀은 개발자 도구를 만들어요. 그래도 솔직히, 오늘 HN 상위 100개만 스프레드시트로 뽑고 싶은 거라면 Python 스크립트는 과해요. 가상 환경 만들고, 패키지 깔고, 선택자 파악하는 준비가 실제 수집보다 더 오래 걸릴 수도 있죠.

이럴 때 Thunderbit가 잘 맞아요. 흐름은 이래요.

  1. Chrome에서 news.ycombinator.com을 열어요.
  2. Thunderbit 확장 프로그램 아이콘을 클릭한 뒤 "AI Suggest Fields"를 선택해요.
  3. AI가 페이지를 읽고 Title, URL, Score, Author, Comment Count, Time Posted 같은 열을 제안해요.
  4. 필요하면 필드를 조정합니다(이름 바꾸기, 삭제, 추가 가능 — "AI/DevTools/Web/Other로 분류" 같은 AI 프롬프트도 넣을 수 있어요).
  5. "Scrape"를 클릭하면 데이터가 구조화된 표로 나타나요.
  6. Excel, Google Sheets, Airtable, Notion으로 내보내요.

구조화된 데이터까지 클릭 두 번이면 끝이에요. 선택자도, 코드도, 유지보수도 없어요.

진짜 장점은 Thunderbit의 AI가 레이아웃 변화에 자동으로 적응한다는 거예요. CSS 선택자 기반 스크래퍼는 마크업이 바뀌면 깨지죠. HN의 HTML은 비교적 안정적이지만 바뀐 전례가 있어요(class="athing submission"이 바뀌었고, 옛 a.storylinkspan.titleline으로 교체됐어요). AI 스크래퍼는 매번 페이지를 새로 읽어서 클래스 이름 변화에 덜 흔들려요.

python-vs-thunderbit-comparison.webp

Thunderbit는 페이지네이션도 처리하고(HN "More" 버튼 자동 클릭) 서브페이지 스크래핑도 지원해요(각 스토리의 댓글 페이지를 방문해 토론 데이터를 가져옴). 댓글 보강에서는 2번 방법의 재귀 API 코드와 비슷한 일을 해주는데, 한 줄도 안 짜도 돼요.

결국 선택은 단순해요. Python은 커스텀 로직, 복잡한 변환, 예약 자동화 파이프라인, 혹은 코딩 학습이 필요할 때 좋아요. Thunderbit은 데이터를 빨리 얻고 싶거나, 코드 유지보수가 싫거나, 개발자가 아닐 때 맞아요. 상황에 맞춰 고르세요.

Python vs. API vs. 노코드: 어떤 방법을 선택해야 할까?

전체 의사결정 프레임은 이래요.

기준BeautifulSoup(HTML)Firebase APIAlgolia APIThunderbit (노코드)
필요한 기술 수준중급 Python초급 Python초급 Python없음
설정 시간10–15분5–10분5–10분2분
유지보수 부담중간(선택자 깨짐 가능)낮음(안정적인 JSON)낮음(안정적인 JSON)없음
데이터 깊이프론트페이지만어떤 아이템이든, 사용자도 가능검색 + 히스토리프론트페이지 + 서브페이지
댓글어려움쉬움(재귀)쉬움(중첩 트리)서브페이지 스크래핑
히스토리 데이터아니오아니오예(전체 아카이브)아니오
내보내기 옵션직접 코딩직접 코딩직접 코딩내장(Excel, Sheets 등)
스케줄링cron / GitHub Actionscron / GitHub Actionscron / GitHub Actions내장 스케줄러
최적 용도스크래핑 학습안정적인 파이프라인리서치 & 분석빠른 데이터 추출

Python을 익히는 중이거나 맞춤형을 만든다면 1번이나 2번을 고르세요. 과거 데이터가 필요하면 Algolia API를 더하고요. 코드 없이 데이터만 원한다면 Thunderbit를 사용해보세요.

Hacker News 스크래핑에 Thunderbit 사용해보기

결론과 핵심 요약

이제 여러분 손엔 이런 도구가 들어왔어요.

  • Hacker News를 스크래핑하는 두 가지 완전한 Python 방법 — HTML 파싱용 BeautifulSoup와 깔끔한 JSON 데이터를 위한 Firebase API
  • 1페이지를 넘어가는 페이지네이션 기술 — 2007년까지 거슬러 올라가는 히스토리 데이터용 Algolia API 포함
  • CSV, Excel, Google Sheets로 내보내는 코드 — 터미널의 데이터는 팀원 누구에게도 쓸모가 없기 때문이에요
  • 프로덕션 패턴 — 재시도 로직, 캐싱, 크롤링/파싱 분리, cron 또는 GitHub Actions를 통한 예약 자동화
  • Python이 과할 때 쓸 수 있는 노코드 대안

제 추천은 이래요. 대부분은 2번 Firebase API부터 시작하세요. 더 깔끔하고, 더 안정적이며, 중첩 HTML을 헤집지 않고도 댓글에 닿을 수 있어요. 과거 데이터가 필요하면 Algolia API를 더하고요. 그리고 그냥 빠른 스프레드시트가 필요한데 Python 프로젝트를 새로 띄우긴 싫은 순간을 위해 Thunderbit는 즐겨찾기에 넣어두세요.

더 파고들고 싶다면 HN 댓글을 감성 분석용으로 긁어보거나, GitHub Actions로 데일리 다이제스트 파이프라인을 만들거나, Algolia API로 지난 10년간 기술 트렌드가 어떻게 변했는지 추적해보세요.

빠른 Hacker News 스크래핑을 위해 Thunderbit 사용해보기 Get Started Free

자주 묻는 질문

Hacker News를 스크래핑하는 건 합법인가요?

HN 데이터는 공개돼 있고, Y Combinator는 프로그램 방식 접근용 공식 API도 줘요. 사이트의 robots.txt는 읽기 전용 콘텐츠(프론트페이지, 아이템 페이지, 사용자 페이지)의 스크래핑을 허용하되 30초 크롤링 지연을 요청해요. 이 지연을 지키고, 상호작용이 필요한 엔드포인트(투표, 로그인)는 건드리지 않으면 문제없어요. 스크래핑 윤리는 웹 스크래핑 법적 쟁점 가이드를 참고하세요.

Hacker News에 공식 API가 있나요?

네. hacker-news.firebaseio.com/v0/HN Firebase API는 무료에 인증도 필요 없고, 스토리, 댓글, 사용자 프로필, 모든 피드 타입(top, new, best, ask, show, jobs)을 줘요. 깔끔한 JSON을 반환하고 명시된 제한은 없지만, 요청 빈도는 예의 있게 조절하는 게 좋아요.

Python으로 Hacker News 댓글을 어떻게 스크래핑하나요?

Firebase API로 스토리 아이템을 받으면 kids 필드(최상위 댓글 ID 배열)가 나와요. 각 댓글도 자기 kids를 가진 아이템이고 답글을 가리키죠. 각 댓글과 그 자식을 가져오는 함수로 트리를 재귀로 타고 내려가면 돼요. 전체 코드는 위 "댓글 스크래핑하기(재귀 트리 탐색)" 섹션을 보세요. 또는 Algolia API의 /items/<id> 엔드포인트는 중첩 댓글 트리 전체를 한 번에 돌려줘서, 댓글 많은 스토리엔 훨씬 빨라요.

코드를 작성하지 않고 Hacker News를 스크래핑할 수 있나요?

네. Thunderbit의 AI 웹 스크래퍼는 Chrome 확장 프로그램으로 돌아가요. HN을 열고 "AI Suggest Fields"를 누르면 제목, URL, 점수, 작성자 같은 열을 자동으로 잡아요. "Scrape"를 누르면 Excel, Google Sheets, Airtable, Notion으로 바로 내보낼 수 있고요. 페이지네이션도 처리하고, 서브페이지를 방문해 댓글 데이터도 가져와요. Python도, 선택자도, 유지보수도 없어요.

Hacker News의 과거 데이터를 어떻게 얻나요?

HN Algolia Search API가 가장 좋아요. search_by_date 엔드포인트와 numericFilters=created_at_i>TIMESTAMP로 날짜 범위를 거르세요. 키워드로 검색하고, 스토리 유형으로 거르고, 최대 500페이지까지 페이지네이션할 수 있어요. 대규모 히스토리 분석용 공개 데이터셋도 Google BigQuery(전체 아카이브), ClickHouse(2,800만 레코드), Hugging Face(400만 개 스토리)에서 쓸 수 있어요.

더 알아보기

Shuai Guan
Shuai Guan
Thunderbit CEO | AI 데이터 자동화 전문가 Shuai Guan은 Thunderbit의 CEO이자 University of Michigan 공학과 출신입니다. 10년 가까운 기술 및 SaaS 아키텍처 경험을 바탕으로, 복잡한 AI 모델을 누구나 바로 활용할 수 있는 노코드 데이터 추출 도구로 바꾸는 데 전문성을 갖고 있습니다. 이 블로그에서는 웹 스크래핑과 자동화 전략에 대한 필터링 없는 실전 경험과 검증된 인사이트를 공유하며, 더 똑똑하고 데이터 중심적인 워크플로를 만드는 데 도움을 드립니다. 데이터 워크플로를 최적화하지 않을 때는, 같은 꼼꼼함으로 사진이라는 취미에 몰두합니다.
목차

말 한마디로 웹페이지를 스크래핑하세요

원하는 걸 평범한 영어로 말하면 됩니다. 아니면 아무 말도 안 해도 괜찮아요.

Thunderbit 사용해보기 무료
AI로 데이터 추출하기
Google Sheets, Airtable, Notion으로 데이터를 손쉽게 منتقل하세요
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week