Guides · YouTube Channel Scraper · Published July 23, 2026

Export a YouTube channel's videos to CSV with Python and pandas

Pull a channel's video list into a pandas DataFrame and out to CSV: @handle, URL, or channel ID in, one row per long-form video with title, duration, and the view_count display field (known exception on collaboration videos). Batching up to 50 channels, cost math ($0.50 per 1,000 videos), and the failure rows to filter first — a dead handle costs $0.

By slvDev · Updated and technically verified July 23, 2026


The short answer: the YouTube Channel Scraper takes an @handle, channel URL, or UC… ID, returns one JSON record per long-form video — title, URL, the view_count display field (with a known exception on collaboration videos, handled below), duration, thumbnail — and pandas turns that into a CSV in three lines. No YouTube Data API key or quota. $0.50 per 1,000 videos; a misspelled handle returns a free typed row instead of an error. Full script below. (If you already run a Google Cloud project, the official uploads-playlist route is genuinely quota-cheap and returns exact integers — 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.

@handle → DataFrame → CSV

import os
import pandas as pd
from apify_client import ApifyClient

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

run = client.actor("apihq/youtube-channel-scraper").call(
    run_input={
        "channelUrls": ["@mkbhd", "@veritasium"],   # URL, @handle, or UC… ID, mixed
        "maxResults": 500,                           # newest-first, per channel
    }
)

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

videos = [v for v in items if v["success"]]         # billed: $0.0005 each
misses = [v for v in items if not v["success"]]     # free, typed

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

df = pd.DataFrame(videos)
if not df.empty:
    # flag rows whose view_count is not a "… views" string — YouTube
    # collaboration videos currently surface a collaborator credit
    # there instead (disclosed actor defect)
    df["view_count_ok"] = df["view_count"].fillna("").str.endswith("views")
df.to_csv("channel_videos.csv", index=False)
print(f"{len(df)} videos -> channel_videos.csv")

Filtering on success first matters: a misspelled handle, a pasted video URL, or a channel with no long-form uploads comes back as a typed success: false row (CHANNEL_NOT_FOUND, NO_VIDEOS — the registry has all of them), not as an exception — and not on your bill. Batch up to 50 channels in one run by adding more entries to channelUrls; one dead handle never blocks the others.

Two lines of analysis to start from

# videos per channel in the batch
print(df["channel_title"].value_counts())

# the longest uploads (duration is an integer, in seconds)
longest = df.sort_values("duration", ascending=False).head(10)

duration is the analysis-friendly field — an integer of seconds, absent on live streams and unparseable lengths. view_count and publishedare display strings as YouTube renders them ("55M views", "2 weeks ago"), fine for eyeballing and sorting out of scope for exact arithmetic — the comparison covers when the official API's exact integers are worth the Google Cloud setup. The most common next step is feeding video_id into the Transcript Scraper or Comments Scraper — there's an importable n8n template that chains channel → transcripts in one workflow.

Cost math

  • If both channels deliver all 500 rows, as in the script: $0.50 (maxResults is a ceiling — you pay per delivered video, so a smaller channel costs less).
  • 50 channels at up to 100 newest videos each: at most $2.50 per run.
  • A weekly "newest 25" sweep of 10 channels: at most $0.125 per sweep — four full weekly sweeps cost at most $0.50; a five-run calendar month costs at most $0.625.

Honest limits

  • Long-form uploads only. It reads the Videos tab, newest first; Shorts have their own actor, and there is no sort by popularity or oldest.
  • Display strings, not integers. view_count ("55M views") and published("2 weeks ago") come as YouTube renders them. For exact integer view counts and ISO timestamps, the official uploads-playlist route is the stronger dataset — compared here.
  • Collaboration videos are currently mis-parsed. On YouTube collab videos, view_count can carry a collaborator credit instead of a view count, with publishedempty — a disclosed defect tracked in the actor repository (a 2026-07-22 first-hand run hit 2 such rows in 100). The script's view_count_ok mask flags them for review.
  • Video rows, not channel statistics. Subscriber counts and lifetime channel views are out of scope — the official channels.list covers those.

Deploying this?

Run one input on the YouTube Channel Scraper. It comes back as either a billed video 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.