PythonでHacker Newsをスクレイピングする方法【2つの手法・完全コード付き】

最終更新日 June 3, 2026
PythonでHacker Newsをスクレイピングする方法【2つの手法・完全コード付き】

毎朝20分。これが、Hacker News のダイジェストを手作業で続けていたときに私が失っていた時間です。きっかけは、Thunderbit のチーム用に注目記事のまとめを配りたいという思いつきでした。最初は気楽に考えていて、サイトをブックマークして朝に流し読みすればいい、と。ところが見出しを拾ってリンクをスプレッドシートへ貼る、それだけの繰り返しが、たった3日で立派なルーチンワークと化していたわけです。

数字で見ると、Hacker News の存在感がよくわかります。月間の訪問数はおよそ 1,300万 件。新規投稿は1日に約1,300件、コメントは1万3,000件前後が毎日積み上がっていきます。ネット上でも屈指の、濃いテック情報が集まる場所です。だからこそ、技術トレンドの追跡、自社名の言及チェック、「Who's Hiring」スレッドからの採用リード収集、開発者の関心の把握——こうした目的で覗きたくなる。でも、それを人力でやり続けるのはかなりこたえます。

幸い、解決策はシンプルです。Python を使えば、Hacker News のスクレイピングは拍子抜けするほど簡単に書けます。この記事では、BeautifulSoup による HTML スクレイピングと、公式の HN Firebase API という2つの実用的なルートを取り上げます。ページネーション、データの書き出し、本番運用の作り込み、さらには「Python を持ち出すまでもない」場面向けのノーコード手段まで、ひと通り押さえていきます。

なぜ Python で Hacker News をスクレイピングするのか?

Hacker News を単なるリンク集だと思っていると、本質を見誤ります。実態は、投票と議論によって面白い記事が自然に浮き上がってくる、コミュニティ主導の厳選フィードです。読者層はテック系の職種にぐっと寄っていて(おおよそ 男性76%、中心年齢25〜34歳)、しかも流入の66%が直接アクセス。つまり、偶然たどり着いた通りすがりではなく、毎日習慣的に通う濃い読者が大半を占めているということです。

このデータを取りたくなる理由は、いくらでも挙げられます。

ユースケース得られるもの
毎日のテックダイジェスト上位記事、スコア、リンクをメールや Slack で配信
ブランド/競合モニタリング自社や製品名が言及されたときにアラート
トレンド分析注目される技術、言語、トピックの推移を追跡
採用「Who’s Hiring」スレッドから求人、技術スタック、給与の手がかりを抽出
コンテンツ調査記事や投稿ネタとして伸びやすいテーマを発見
感情分析製品、リリース、業界動向に対するコミュニティの反応を把握

価値の大きさを物語る例もあります。合計で時価4,000億ドルを超える Stripe、Dropbox、Airbnb——これらの企業がいずれも、初期の重要なフィードバックやユーザーを Hacker News から得たと振り返っているのです。Dropbox の Drew Houston にいたっては、2007年4月に HN へデモを投稿して1位を取り、ベータ待機リストが一晩で5,000人から7万5,000人へ跳ね上がりました。HN のデータは「読み物として面白い」だけでは終わらず、ビジネスの種にもなるわけです。

データそのものは公開されています。問題は、サイトの構造ゆえに手作業の収集が地味に厄介だという点。ここを Python で自動化してしまおう、というのがこの記事の狙いです。

Python で Hacker News をスクレイピングする2つの方法

この記事で扱うのは、いずれもそのまま動かせる2つのアプローチです。

  1. requests + BeautifulSoup を使った HTML スクレイピング — news.ycombinator.com の生の HTML を取得し、解析して記事データを抜き出します。スクレイピングの基礎を学ぶのに最適で、ページ上に表示されている情報をそのまま取り出せます。
  2. 公式 Hacker News Firebase API — JSON エンドポイントを直接叩く方法です。HTML の解析は不要で、より信頼性の高いデータパイプライン、コメント取得、過去データの活用に向いています。

自分にどちらが向くか、比較表で見比べてみてください。

比較項目HTML スクレイピング(requests + BS4)HN Firebase APIThunderbit(ノーコード)
セットアップの難易度中(HTML セレクタの解析が必要)低(JSON エンドポイント)なし(Chrome 拡張を2クリック)
データの鮮度リアルタイムのトップページリアルタイム(ID 指定で任意アイテム取得)リアルタイム
レート制限リスク中(robots.txt で30秒のクロール間隔を要求)低(公式で比較的寛容)Thunderbit 側で管理
コメント取得難しい(入れ子の HTML)簡単(再帰的に item ID を辿る)サブページスクレイピング機能
過去データ限定的Algolia Search API 経由なし
向いている用途スクレイピングの基礎学習安定したデータパイプライン非エンジニア、素早いエクスポート

どちらの方法も、動かせる Python コードをそのまま載せています。加えて、コードを一切書かずにデータだけ手に入れたい場合の手段も後半で紹介します。

始める前に

  • 難易度: 初級〜中級
  • 所要時間: 各手法15〜20分程度
  • 必要なもの:
    • Python 3.11 以上
    • ターミナルまたはコードエディタ
    • Chrome ブラウザ(HN の HTML を確認したり、ノーコード手法を試したりする場合)
    • Thunderbit Chrome 拡張(任意、ノーコード手法用)

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 スクレイピングする

王道から始めましょう。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 ではなく、古典的なテーブルレイアウトを使っています。1記事はだいたい2つの <tr> 行で構成されます。

  • 記事行 (<tr class="athing submission">): ランク、タイトル、リンクを含む
  • メタデータ行(その次の <tr>): ポイント、投稿者、投稿時間、コメント数を含む

押さえるべきセレクタは次のとおり。

  • span.titleline > a — 記事タイトルと URL
  • span.score — 投票数(例: "118 points")
  • a.hnuser — 投稿者名
  • span.age — 投稿時間
  • .subtext 内の最後の <a> で、テキストに "comment" を含むもの — コメント数

Chrome で適当なタイトルを右クリックして「検証」を開くと、たとえばこんな HTML が確認できます。

<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 では「コメント」の手前に \xa0(ノーブレークスペース)が入るので、それを区切りにして数値を取り出しています。
  • 「Ask HN」など 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 は、HN のデータを取るために正式に用意された窓口です。認証もキーも 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現在の最大 item 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 が並びます。各コメントもまた1つの item で、さらに子コメントを抱えることがある——これがコメントツリーの正体です。

ステップ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")
# Output: Got 500 top story IDs

1回叩けば500件の ID が返ります。解析もセレクタも不要、ただの JSON 配列です。

ステップ2: ID を使って記事詳細を取得する

次は中身の取得ですが、ここで「大量取得」のコストが顔を出します。500件の記事が欲しければ、API 呼び出しも500回。私の計測では1件あたり約1.2秒かかるので、順番に回すと500件でおよそ10分です。

もっとも、500件すべてを必要とする場面はそう多くありません。上位30件に絞るなら、こうなります。

def fetch_story(story_id):
    """HN API から1件の記事詳細を取得する"""
    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 の真価が出るのはここです。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 パターンです。1ページにつき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)や一時的なブロックを食らう恐れがあります。5ページを30秒間隔で取ると2分半ほどかかりますが、これがルールに沿った正攻法です。

ページネーションのコードを自前で抱えたくないなら、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程度)、キーも不要、最大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

# stories を行に変換
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 へそのまま流せる無料エクスポートが備わっています。コードも認証情報もパイプラインの保守も不要。一度きりのデータ取得なら、こちらのほうが確実に速いです。詳しくは後ほど。

スクレイパーを本番運用向けにする: エラーハンドリング、キャッシュ、スケジューリング

遊びで1回動かすだけなら、ここまでのコードで事足ります。けれど毎日のワークフローに組み込むとなると、いくつか足しておきたいものが出てきます。

エラーハンドリングとリトライ

ネットワークは落ちるし、サーバーは制限をかけてきます。1回の失敗で全部が止まるのは避けたい。指数バックオフ付きのリトライ関数は、こんな形になります。

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 なら、たった2行で片がつきます。

import requests_cache

requests_cache.install_cache("hn_cache", expire_after=3600)  # 1時間キャッシュ

この2行をスクリプト冒頭に置くだけで、すべての requests.get() がローカルの SQLite に自動キャッシュされます。1時間以内に10回流しても、実際にネットへ出るのは最初の1回きり。多くのフォーラムユーザーが推す定番 で、その価値は十分にあります。

クロールと解析を分ける

熟練のスクレイパー書きが大事にする定石が、「まず生データを保存し、解析はあとから」です。この順番なら、解析ロジックにバグがあっても、取り直しなしで直して再解析できます。

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  # すでに取得済みの item はスキップ
        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段構えは、数百件のデータを抱えつつ処理の中身を素早く磨きたい場面で、特に効いてきます。

スクレイパーを定期実行する

毎日の HN ダイジェストを回すなら、自動実行が必須です。代表的な選択肢は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 に出力する

2クリックで構造化データが揃います。セレクタもコードも保守も不要です。

ここで効いてくるのが、Thunderbit の AI がレイアウト変更に勝手に追従してくれる点です。従来の CSS セレクタ型スクレイパーは、サイトのマークアップが変わると途端に壊れます。HN の HTML は比較的安定しているとはいえ、実際に変更はありました(class="athing submission" が更新され、span.titleline がかつての a.storylink を置き換えた経緯があります)。AI ベースのスクレイパーは毎回ページを読み直すので、クラス名の移り変わりを気にせずに済むのです。

python-vs-thunderbit-comparison.webp

Thunderbit はページネーションにも対応し(HN の「More」ボタンを自動クリック)、サブページのスクレイピングもこなします(各記事のコメントページへ移って議論データを取得)。コメントの拡張が要る場面でも、方法2の再帰的 API コードに相当する処理を、1行も書かずに実現できます。

判断はシンプルです。独自ロジック、込み入ったデータ変換、定期実行パイプライン、あるいはプログラミングの学習——こうした目的なら Python。今すぐデータが欲しい、コードを保守したくない、そもそも開発者ではない——なら Thunderbit。場面に応じて選び分けましょう。

Python・API・ノーコードの比較: どの方法を選ぶべき?

迷ったときの判断材料として、全部入りの比較表をまとめました。

比較項目BeautifulSoup(HTML)Firebase APIAlgolia APIThunderbit(ノーコード)
必要な技術レベル中級 Python初級 Python初級 Python不要
セットアップ時間10〜15分5〜10分5〜10分2分
保守負担中(セレクタが壊れる)低(安定した JSON)低(安定した JSON)なし
データの深さトップページのみ任意の item、ユーザー検索 + 過去データトップページ + サブページ
コメント難しい簡単(再帰)簡単(入れ子ツリー)サブページスクレイピング
過去データなしなしあり(完全アーカイブ)なし
出力方法自作が必要自作が必要自作が必要標準搭載(Excel、Sheets など)
定期実行cron / GitHub Actionscron / GitHub Actionscron / GitHub Actions標準スケジューラあり
向いている用途スクレイピング学習安定したパイプライン調査・分析素早いデータ取得

Python の勉強中、あるいは独自処理を組みたいなら、方法1か2が向きます。過去データの分析が必要になったら Algolia API を足しましょう。コードなしでデータだけ欲しいなら、Thunderbit を試してみてください

Hacker News のスクレイピングに Thunderbit を試す

まとめ: ここまでで手に入れたもの

この記事を通じて、あなたの手元には次のものが揃いました。

  • Hacker News をスクレイピングする2つの完全な Python 手法 — HTML 解析用の BeautifulSoup と、クリーンな JSON を返す Firebase API
  • ページ1を超えて取得するためのページネーション手法 — 2007年までさかのぼれる Algolia API を含む
  • CSV、Excel、Google Sheets への出力コード — ターミナルの表示だけではチームで共有できないからです
  • 本番運用向けのパターン — リトライ、キャッシュ、クロールと解析の分離、cron や GitHub Actions による定期実行
  • Python が過剰な場面向けのノーコード代替

私のおすすめは、多くの用途でまず Firebase API(方法2)から入ること。よりきれいで信頼性が高く、入れ子の HTML と格闘せずにコメントまで取れます。過去データが必要になったら Algolia API を追加。そして、サッとスプレッドシートが欲しいだけで Python プロジェクトを立ち上げるほどでもないとき用に、Thunderbit はブックマークしておくと重宝します。

もっと先まで踏み込みたいなら、HN コメントを感情分析用に集めたり、GitHub Actions で毎日のダイジェストパイプラインを組んだり、Algolia API で過去10年の技術トレンドの変遷を追ったりしてみてください。

高速な Hacker News スクレイピングに Thunderbit を試す Get Started Free

FAQ

Hacker News のスクレイピングは合法ですか?

HN のデータは公開されており、Y Combinator はプログラムからのアクセスを想定した公式 API も提供しています。サイトの robots.txt では、読み取り専用コンテンツ(トップページ、アイテムページ、ユーザーページ)のスクレイピングは許可されていますが、30秒のクロール間隔を求めています。この間隔を守り、投票やログインなどの対話的なエンドポイントを取得しなければ、問題はありません。スクレイピングの倫理については、web scraping legal implications のガイドも参考にしてください。

Hacker News には公式 API がありますか?

はい。HN Firebase APIhacker-news.firebaseio.com/v0/ にあり、無料で認証不要です。記事、コメント、ユーザープロフィール、すべてのフィードタイプ(top, new, best, ask, show, jobs)にアクセスできます。クリーンな JSON を返し、明示的なレート制限はありませんが、リクエスト頻度を抑えて丁寧に使うのが望ましいです。

Python で Hacker News のコメントをスクレイピングするには?

Firebase API を使って記事 item を取得し、kids フィールド(トップレベルコメント ID の配列)を取り出します。各コメントも item であり、返信用の kids フィールドを持っています。コメントとその子要素を再帰的に取得する関数でツリーをたどりましょう。完全なコードは上の「コメントを取得する(再帰的にツリーをたどる)」セクションを参照してください。あるいは、Algolia API の /items/<id> エンドポイント を使えば、完全なネスト済みコメントツリーを1回のリクエストで取得できます。コメントが多い投稿ではこちらのほうがかなり速いです。

コードを書かずに Hacker News をスクレイピングできますか?

はい。Thunderbit の AI Web Scraper は 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 であり、ミシガン大学工学部の卒業生です。テクノロジーと SaaS アーキテクチャの分野で約10年にわたる経験をもとに、複雑な AI モデルを、実務で使えるノーコードのデータ抽出ツールへと落とし込むことを得意としています。このブログでは、ウェブスクレイピングや自動化戦略について、実践で磨かれた率直な知見を共有し、より賢くデータ主導のワークフローを構築できるよう支援しています。データワークフローの最適化から離れているときは、同じこだわりと観察眼を写真への情熱にも注いでいます。
目次

ただ伝えるだけで、Webページをスクレイピング

必要なことをそのまま英語で伝えるだけ。いや、何も言わなくてもOKです。

Thunderbitを試す 無料
AIでデータを抽出
Google Sheets、Airtable、Notionへ簡単にデータを移行できます
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week