Guides · YouTube Comments Scraper · Published July 22, 2026

Export YouTube comments to CSV with Python and pandas

Pull a video's YouTube comments into a pandas DataFrame and out to CSV: video URL or ID in, one row per comment with author, text, and reply structure. Replies opt-in, cost math ($0.40 per 1,000), and the failure rows to filter first — comments-off videos cost $0.

By slvDev · Updated and technically verified July 22, 2026


The short answer: the YouTube Comments Scraper takes a video URL or bare 11-character ID, returns one JSON record per comment — text, author, reply structure, pinned flag — and pandas turns that into a CSV in three lines. No YouTube Data API key or quota. $0.40 per 1,000 comments; a video with comments turned off returns a free typed row instead of an error. Full script below. (If you need exact integer like counts and have a Google Cloud project, the official commentThreads API is genuinely strong here — our comparison lays out both sides.)

Setup

pip install apify-client pandas

The token comes from Apify Console → Settings → API tokens (free tier works). No Google account or API key.

Video URL → DataFrame → CSV

import os
import pandas as pd
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run = client.actor("apihq/youtube-comments-scraper").call(
    run_input={
        "videoUrls": ["dQw4w9WgXcQ"],   # watch URL, youtu.be, Shorts, or bare ID
        "maxComments": 2000,
        "sort": "top",                  # top | newest
        "includeReplies": True,         # replies count toward maxComments
    }
)

items = list(client.dataset(run["defaultDatasetId"]).iterate_items())

comments = [c for c in items if c["success"]]        # billed: $0.0004 each
misses   = [c for c in items if not c["success"]]    # free, typed

for m in misses:
    print(m.get("video_url"), "->", m["code"])        # e.g. COMMENTS_DISABLED

df = pd.DataFrame(comments)
if not df.empty:
    # like_count is a display string; zero-like comments currently carry
    # whitespace instead of an empty value (disclosed actor defect)
    df["like_count"] = df["like_count"].fillna("").str.strip()
df.to_csv("youtube_comments.csv", index=False)
print(f"{len(df)} comments -> youtube_comments.csv")

Filtering on success first matters: a deleted or private video, a comments-off video, or a bad ID comes back as a typed success: false row (VIDEO_NOT_FOUND, COMMENTS_DISABLED — the registry has all of them), not as an exception — and not on your bill. Batch up to 50 videos in one run by adding more entries to videoUrls; one comments-off video never blocks the others.

How replies land in the DataFrame

With includeReplies: true, reply rows arrive in the same dataset carrying reply_level (0 = top-level, 1+ = reply) and parent_comment_id, so threads reconstruct with a groupby instead of follow-up API calls. Two honest mechanics to know: replies count toward maxComments, so a huge thread can be cut off mid-replies — raise the cap if you want more of the thread — and the split is easy to check:

top_level = df[df["reply_level"] == 0]
replies   = df[df["reply_level"] > 0]

# the busiest threads, by the integer reply counter
busiest = top_level.sort_values("reply_count", ascending=False).head(10)

Two lines of analysis to start from

# who comments most (author_name is the public display name, often an @handle)
print(df["author_name"].value_counts().head(10))

# pinned comment, if the creator pinned one
pinned = df[df["is_pinned"]]

From here the usual moves are sentiment scoring on text, splitting creator-hearted threads out — though see the honest limits below before using that flag — and joining the comment set against video metadata from the Channel Scraper for per-video comparisons. Note that like_countis a display string as YouTube renders it ("264K"), not an integer — sort engagement by the integer reply_count instead, or use the official API when exact like arithmetic is the job.

Cost math

  • If the script delivers all 2,000 rows: $0.80 (maxComments is a ceiling — you pay per delivered row, so a smaller comment section costs less).
  • 10,000 delivered comments across a 50-video batch: $4.00.
  • With maxComments: 100, 30 full daily checks on one video cost at most $1.20 per month. The importable n8n and GitHub Actions templates run this same actor-call pattern, but as shipped they use a manual trigger and 200 comments (n8n sorts newest, Actions sorts top) — switch the trigger to a schedule and set sort and maxComments to make them a daily newest-100 check.

Honest limits

  • Display strings, not integers. like_countcomes as YouTube renders it ("264K") — and on zero-like comments the field is currently a whitespace-only string rather than empty, a disclosed actor defect: normalize with .str.strip() before using it (the script above does). published_timeis relative ("1 year ago"). For exact integers and timestamps, the official commentThreads API is the stronger dataset — compared here.
  • Do not branch on is_hearted yet. A 2026-07-22 first-hand run returned it true on all 200 rows, which is implausible; it is tracked as a known limitation in the actor repository and disclosed on the actor page.
  • Public video comments only: private, deleted, and members-only videos return typed success: false rows; community-post comments and live chat are out of scope.
  • Replies share the cap. maxCommentscounts top-level comments and replies together, so it bounds the run's spend — but a 1,000-reply thread will not arrive complete under a low cap.

Deploying this?

Run one input on the YouTube Comments Scraper. It comes back as either a billed comment row or an unbilled, typed failure row — the contract this guide builds on.

A one-item first run costs under a cent, on your existing Apify account.