如何批量取得 YouTube 頻道及播放清單的逐字稿(文字或 JSON),無需 YouTube Data API
How to get YouTube transcripts in bulk (whole channels and playlists) as text or JSON, without the YouTube Data API
批量取得 YouTube 頻道或播放清單逐字稿,可用 Apify 上的 YouTube Transcript & Subtitles Scraper 一次處理;工具會列出影片、擷取字幕,並按影片返回 JSON。
You want the transcripts of a whole YouTube channel: to feed a RAG pipeline, summarise talks with an LLM, or count how often a topic comes up. The obvious first stop is the official YouTube Data API. It won't get you there. This post covers why, what the free DIY route looks like, and how to do it in bulk with one API call.
Disclosure: I built the YouTube Transcript & Subtitles Scraper on Apify that the last part uses. The numbers below come from real runs of it.
Why the YouTube Data API doesn't work for this
The Data API has a captions resource, so it looks like the right tool. But the captions.download reference says the method needs OAuth with the youtube.force-ssl or youtubepartner scope, and the user has to have permission to edit the video. If you call it on someone else's video, you get a 403 forbidden error: "The permissions associated with the request are not sufficient to download the caption track." Each call also costs 200 quota units, out of a default budget of 10,000 units a day.
So captions.download works for your own videos. Most transcript projects need other people's videos, and for those it doesn't work at all.
The DIY route: youtube-transcript-api
The Python library youtube-transcript-api reads the same captions the YouTube web player shows. It needs no API key and no OAuth:
# pip install youtube-transcript-api
from youtube_transcript_api import YouTubeTranscriptApi
ytt = YouTubeTranscriptApi()
fetched = ytt.fetch("zjkBMFhNj_g", languages=["en"])
text = " ".join(s.text for s in fetched)
print(len(text.split()), "words")
Here are the pros and cons.
Pros:
- It's free and open source, and it's good. My actor uses it under the hood.
- It handles auto-generated vs. manual captions, language selection and YouTube's built-in translations.
- For a few videos on your own machine, it's all you need.
Cons:
-
IP blocks. YouTube blocks many cloud IP ranges (AWS, GCP, Azure and others). Code that works on your laptop often fails on a server with
RequestBlocked/IpBlocked. The library's README recommends rotating residential proxies to get around this, which means a proxy subscription and more code to maintain. -
One video at a time. It takes video IDs. To get a whole channel or playlist, you first have to list the video IDs with something like
yt-dlpor the Data API'splaylistItemsendpoint, which uses up quota. - Upkeep. Retries, rate limits, bookkeeping for videos without captions, and fixes whenever YouTube changes something are all on you.
If you need ten videos from your laptop, stop here. If you need 500 videos from a cron job on a cloud server, read on.
Bulk: whole channels and playlists in one call
The actor takes any mix of video URLs, Shorts, @channel URLs, playlist URLs and search queries. It lists the videos with yt-dlp (metadata only, nothing is downloaded), fetches the transcripts, and returns one JSON item per video. It starts with cheap datacenter proxies and retries blocked requests through residential proxies automatically.
With curl (the call waits for the run to finish and returns the dataset):
curl -X POST \
"https://api.apify.com/v2/acts/fast_data_asap~youtube-transcript-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startUrls": [
"https://www.youtube.com/@aiDotEngineer/videos",
"https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID"
],
"maxVideosPerSource": 50,
"languages": ["en"],
"includeSegments": true,
"chunkSizeChars": 2000
}'
With Python (pip install apify-client):
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("fast_data_asap/youtube-transcript-scraper").call(run_input={
"startUrls": ["https://www.youtube.com/@aiDotEngineer/videos"],
"maxVideosPerSource": 12,
"languages": ["en"],
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["status"] == "ok":
print(item["title"], item["wordCount"], item["isGenerated"])
Each item has these fields:
-
videoId,url,title,channel,durationSeconds -
status:ok,transcripts_disabled,no_transcript_in_requested_languages,video_unavailable,age_restrictedorfailed -
language,languageCode,isGenerated,isTranslated,availableTranscripts -
text: the full transcript as one string -
segments:[{start, duration, text}]with timestamps in seconds -
chunks: optional[{start, end, text}]blocks for embeddings wordCount
Real numbers. Andrej Karpathy's "[1hr Talk] Intro to Large Language Models" runs 59:49. It came back as 12,151 words in 1,704 timestamped segments, for $0.002. A run on the AI Engineer channel (maxVideosPerSource: 12) returned 12 transcripts, all status: ok, 58,654 words in total, for $0.024.
Pricing: $2 per 1,000 transcripts, dropping to $1.70 and then $1.50 per 1,000 on higher Apify plans. You aren't charged for videos without a transcript; they still show up in the output with their status. Apify's $5 of free monthly credit covers about 2,500 transcripts.
Tip: auto-captions misspell product names
Almost every transcript you'll get is auto-generated (isGenerated: true). All 12 AI Engineer talks above were. Speech recognition is bad with proper nouns, which quietly breaks keyword counts and search. In those 12 talks:
| Spoken | Transcribed as | Wrong |
|---|---|---|
| Kimi | "kimmy" | 19 of 19 |
| DeepSeek | "deep seek" | 7 of 14 |
| Claude Code | "cloud code" | 3 of 7 |
| ChatGPT | "chat gpt" / "jgpt" | 2 of 7 |
| Anthropic | "entropic" | 1 of 15 |
(Small sample: n=12 talks from one channel, and Kimi and DeepSeek both come from a single workshop. The pattern is still real.)
Normalise the text before you count or embed it:
import re
ALIASES = {
r"\bkimmy\b": "Kimi",
r"\bdeep seek\b": "DeepSeek",
r"\bcloud code\b": "Claude Code",
r"\bchat gpt\b|\bjgpt\b": "ChatGPT",
r"\bentropic\b": "Anthropic",
}
def fix_names(text: str) -> str:
for pattern, canonical in ALIASES.items():
text = re.sub(pattern, canonical, text, flags=re.IGNORECASE)
return text
for item in items: # items = the dataset items from the run above
if item["status"] == "ok" and item["isGenerated"]:
item["text"] = fix_names(item["text"])
Only apply rules like cloud code → Claude Code to isGenerated transcripts on a topic where that's what the speaker meant. In a talk about cloud infrastructure, "cloud code" may be correct. Build your own alias list from the names that matter in your niche.
Summary
-
Your own videos: the YouTube Data API's
captions.downloadworks with OAuth. -
A handful of other people's videos, run locally: use
youtube-transcript-api. It's free and solid. - Whole channels, playlists or searches, run on a server or schedule: use a hosted scraper that handles listing videos, proxies and retries. Here's mine, at $2 per 1,000 transcripts, with no charge for videos that have none.
Whichever route you take, check isGenerated and clean up proper nouns before you trust any counts. Transcripts belong to their creators, so make sure your use follows copyright law and YouTube's terms.
來源:DEV Community · dev.to