Apify 的 YouTube Comments Scraper 回覆數上限如何影響評論樣本
Ten YouTube Comments, One Commenter: What a Reply Budget Does to Your Sample
作者實測發現,Apify 的 YouTube Comments Scraper 開啟 includeReplies 時,回覆與頂層評論共用同一資料列上限,因此 10 列樣本可能只有 1 條頂層評論和 9 條回覆。
I run a visa agency in Bali. The part of my week that has nothing to do with visas goes into Apify Actors, and one of them pulls YouTube comments — one video, a channel, a playlist, or the top results of a search, one row per comment with likes, reply count, the pinned and hearted flags and the commenter's channel.
People use comment data for sentiment: pull the last twenty videos of a competitor, feed the text to a model, read what their audience actually complains about. That works, and the scraping part is not the hard bit. The hard bit is that two of the three obvious assumptions about a comment sample are wrong, and both of them are wrong quietly — you get a full dataset, the right number of rows, no errors, and a conclusion that isn't about the audience.
Here is the measurement, from runs on 11–14 October 2026, build 0.1.39, no proxy.
One flag, two completely different samples
Same video — jNQXAC9IVRw, "Me at the zoo", the first video on YouTube, 20 years of comments on it. Same limit: 10 comments. Sorted by top. The only difference between the two runs is includeReplies.
With replies off, ten comments from ten different people:
"We're so honored that the first ever YouTube video was filmed here!" 4,800,000 likes
"So we're all here again on October 14th 2026 right" 746
"Hello" 303,000
"Is anyone else watching this on a random day in October?" 1,000
"I'm glad this pops up on my feed every couple years" 467
"This started it all...... bruh 😳" 248,000
"this video lived long enough to see gifs come true" 459
'Seeing "20 years ago" on a YouTube video makes me feel a certain way' 211,000
'This guys is the only person who has the rights to say "first"' 418,000
"First word on Youtube ever:\nAlright" 115,000
With replies on — the default — and the same maxCommentsPerVideo: 10:
isReply text likes
false "We're so honored that the first ever YouTube video was…" 4,800,000
true "San Diego Zoo That's so awesome! I'm so glad the first…" 206,000
true "Very cool!" 71,000
true "cool" 57,000
true "." 54,000
true "How did I randomly go to this video and see this comment…" 57,000
true "Im here before this blows up" 32,000
true " @Quetzal_ lol what" 17,000
true "yoo here before thousands of likes" 18,000
true "Awesome!" 14,000
One top-level comment and nine replies to it. Ten rows, one conversation, and four of them are a single word or a full stop. Run that through a sentiment model and you have measured one thread on a video with 2.4 million comments.
This is not a bug, and the input field says so in six words — "replies count too when included". It is the interaction that bites. The collector walks the comment list depth-first: take a top-level comment, immediately follow its replies, then move to the next one. Both compete for the same budget:
if (keep(c)) { await pushComment(c, base, null); kept += 1; }
if (includeReplies && c.replyCount && page.replyTokens.has(c.commentId) && kept < wanted) {
// …up to maxRepliesPerComment replies, each one incrementing the same `kept`
}
The comment at the top of "Me at the zoo" has 985 replies and maxRepliesPerComment defaults to 20. So the first comment plus its reply cap is 21 rows — more than the sample I asked for. The depth-first order is the right default (a thread stays together, and a keyword match inside a thread is still a match), but it means the limit you set is a row budget, not a voice budget.
The arithmetic to set it deliberately:
rows ≈ topLevelComments × (1 + maxRepliesPerComment)
Twenty distinct opinions with their threads at the default reply cap is 420 rows, not 20. If you want breadth, includeReplies: false is the whole fix; the reply count stays on every row, so you still know which comments started something. If you want threads, raise the comment limit by a factor of 21 and expect to pay for it — the Actor bills per comment row, so replies are rows you buy.
Then I ran the identical input four days running
Four runs, 11 to 14 October, same JSON every time: that video, maxCommentsPerVideo: 50, replies on, sort: top. Four datasets of exactly 50 rows.
| Rows | Distinct comments shared with the 14 Oct run | |
|---|---|---|
| 11 Oct | 50 | 21 |
| 12 Oct | 50 | 21 |
| 13 Oct | 50 | 21 |
| 14 Oct | 50 | — |
Across the four runs there are 121 distinct comments, and only 21 appear in all four. Every run returned the same three top-level comments' worth of structure — 3 top-level, 47 replies, the first thread always the 4.8-million-like one — but comments two and three changed almost every day:
11 Oct: "Randomly got recommended this at 11:16 PM…" 3,300 likes
12 Oct: "You will never know why this comment got…" 24,000
13 Oct: 'Seeing "20 years ago" on a YouTube video…' 211,000
14 Oct: "This started it all...... bruh 😳" 247,000
"Top comments" is a ranking YouTube recomputes, not a stable list, and on a video this size the tail of the top-50 reshuffles daily. The same comment also came back with 417,000 likes on one day and 418,000 on the next, which is the honest behaviour — counts move — but it means a comment is identified by commentId, never by its text plus its like count. If you are tracking a video over time, dedupe on commentId and expect the set to drift under you. If you are sampling once for sentiment, know that you sampled a top-50, not the top-50.
And the dates are arithmetic, not data
YouTube never shows a comment date. It shows an age: "6 years ago", "9 hours ago", "2 days ago". The Actor keeps that string in publishedAgo and also subtracts it from the run clock into publishedAt, because a date column is what every spreadsheet downstream wants. Here is what that does to one comment — the same commentId, five runs:
| Run started (UTC) | publishedAgo |
publishedAt |
|---|---|---|
| 11 Oct 00:33 | 6 years ago | 2020-10-10 |
| 12 Oct 21:28 | 6 years ago | 2020-10-12 |
| 13 Oct 00:28 | 6 years ago | 2020-10-12 |
| 14 Oct 00:45 | 6 years ago | 2020-10-13 |
| 14 Oct 21:05 | 6 years ago | 2020-10-14 |
Two runs on the same calendar day, two different dates, because the subtraction uses the run's clock and a year is 365.25 days. The mapping is not even one-to-one in either direction: the 12 Oct and 13 Oct runs, three hours apart, both landed on 2020-10-12, while the two 14 Oct runs landed a day apart.
The drift is the small problem. The real one is the bucket: "6 years ago" is YouTube rounding down, so the comment's true age is somewhere in [6 years, 7 years). publishedAt: 2020-10-13 is the newest date the comment could have; it could genuinely be from October 2019. For fresh comments the label is tight — "9 hours ago" pins the day — and for anything older than a month it is a floor, not a date. So: filter and group on publishedAgo buckets when the comments are old, use publishedAt for recency windows on fresh comments, and never diff two runs' publishedAt columns and conclude anything.
One more small trap in the same field: an edited comment is labelled "6 years ago (edited)", suffix and all. Across those four runs there were 22 such rows and four distinct label strings — 6 years ago (edited), 5 years ago (edited), 5 months ago (edited), 1 month ago (edited). Matching publishedAgo == "6 years ago" silently drops them. The date parser strips the suffix before doing its arithmetic; a GROUP BY in your sheet does not.
Calling it
One request, and the run-sync endpoint hands back the rows directly — this is the exact call that produced the ten-distinct-commenters sample above:
import requests
rows = requests.post(
"https://api.apify.com/v2/acts/lergassy~youtube-comments-scraper/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"videoUrls": ["https://www.youtube.com/watch?v=jNQXAC9IVRw"],
"maxCommentsPerVideo": 10,
"includeReplies": False, # breadth, not one thread
"sort": "top",
"language": "en",
},
).json()
Swap videoUrls for channelUrls, playlistUrls or searchQueries and maxVideosPerSource decides how many videos each source contributes; every row carries source so you know where it came from. The Actor is here: YouTube Comments Scraper.
When not to reach for it. If you need exact comment timestamps, nothing scraped from the watch page will give them to you — the official Data API returns real publishedAt values, at the cost of a key and a daily quota. If you need all comments of a viral video, the arithmetic is unkind: twenty comments per request means 2.4 million comments is 120,000 round trips. And if you want to post, reply or moderate, this is read-only; that is the API's job.
The lesson I keep relearning on other people's data: a limit is only meaningful once you know what the thing being limited is. Ten rows looked like ten opinions right up until I printed the isReply column.
來源:DEV Community · dev.to