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 pandasThe 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 (
maxResultsis 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") andpublished("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_countcan carry a collaborator credit instead of a view count, withpublishedempty — a disclosed defect tracked in the actor repository (a 2026-07-22 first-hand run hit 2 such rows in 100). The script'sview_count_okmask flags them for review. - Video rows, not channel statistics. Subscriber counts and lifetime channel views are out of scope — the official
channels.listcovers those.