# How to Scrape YouTube: Search, Videos, Comments, and Channels > Source: https://scrape.do/blog/youtube-scraping/ Published: 2026-05-11 · Updated: 2026-05-11 · Authors: Serhat Kurtulus · Categories: Scraping Use Cases YouTube takes in [more than 500 hours of video every minute](https://thesocialshepherd.com/blog/youtube-statistics) and serves it back to a base of 2.49 billion monthly active users. It works fine in a browser. The problem starts when we try to fetch a video URL with `requests`: we get back a 200, but the HTML has zero search results, zero comments, and a full-screen consent overlay where the page should be. **That is actually good news.** Once past the consent wall, every piece of data on every YouTube surface lives in a place we can reach with one HTTP call. No headless browser. No JavaScript engine. The win condition is simple: turn the consent wall into structured rows of search results, video metadata, comments, and channel data. We will route every request through Scrape.do with `super=true`, pull `ytInitialData` and `ytInitialPlayerResponse` straight from the page HTML, then page through InnerTube continuation tokens for everything that loads after the first screen. Four clean outputs: a CSV of search results, a JSON record per video, a JSON tree of comments with nested replies, and a JSON file per channel with paginated video lists. [Full working code on GitHub ⚙](https://github.com/scrape-do/scrapedo-scrapers) If you'd rather skip InnerTube parsing entirely, Scrape.do offers a dedicated [**YouTube API**](https://scrape.do/products/ready-api/youtube-scraper/). One HTTP call returns the full search page as structured JSON: videos, channels, playlists, Shorts shelves, and ads, with `sp` tokens for sort, duration, 4K/HD/LIVE/CC, upload date, and result type. Jump to [the Plugin API section](#the-quick-way) for examples. ## Challenges with Scraping YouTube YouTube's anti-bot is aggressive but probabilistic. From a datacenter IP (cloud VMs, VPS, shared hosting), a clean Python request almost always lands on a [bot-detection consent wall](https://scrape.do/blog/prevent-web-scraping/): HTTP 200, HTML that looks like a real page, but with none of the data we wanted. From a residential IP the same request may succeed today and fail tomorrow. This is the kind of target we like. The failure mode is predictable when it fires, the data is already structured once we get past the gate, and Scrape.do absorbs the gate so we can stick with `requests` instead of building our own anti-detection stack. YouTube fingerprints every request against IP reputation, TLS handshake, browser fingerprint, and JavaScript challenge solving. Datacenter IPs fail every check. Residential IPs pass most of them, but any flag we trip (rapid requests, suspicious User-Agent, missing cookie) still lands us on a [200 with a consent wall instead of a 403](https://scrape.do/blog/python-requests-403-forbidden/). The fix is to route every request through Scrape.do with `super=true`, which sends each request from a residential IP with a real browser fingerprint and a challenge solver attached. **Every request needs the parameter.** The initial GET, every continuation POST, every reply continuation POST. Skip it and we are back to rolling dice with YouTube's anti-bot on every call. ### Two Data Sources: Embedded JSON vs. InnerTube API Two JavaScript variables in the page HTML carry almost every field we need. `ytInitialData` holds the rendered page state. `ytInitialPlayerResponse` holds the player-specific fields on watch pages. We pull both out of the HTML the same way: ```python m = re.search(r"var ytInitialData\s*=\s*", html) data = json.JSONDecoder().raw_decode(html, m.end())[0] ``` Nothing fancy. Two lines, both blobs out. We will reuse this pattern under video (twice, once per blob), channel, and comments. Anything that loads after the first page comes from InnerTube, YouTube's internal POST API. The endpoint name changes per surface: `/youtubei/v1/search`, `/youtubei/v1/browse`, `/youtubei/v1/next`. The body shape stays the same on all three: a `context.client` block plus a `continuation` token from the previous page. Two more things to grab from the initial HTML before we can call InnerTube: the embedded API key and the WEB client version. Both are one-line regex matches against the page source. The client version rolls forward roughly once a week with YouTube's frontend deploys, so we read it fresh on every run. ![Annotated DevTools view of a YouTube watch page source showing four highlighted strings on the same page: the var ytInitialData declaration, the var ytInitialPlayerResponse declaration, the embedded INNERTUBE_API_KEY, and the INNERTUBE_CLIENT_VERSION value](/uploads/blog/youtube-scraping-watch-page-source-extraction-targets.png) ## Scraping YouTube Search Results Search results come first because the page exposes every pattern we will lean on later: the embedded blob extract, the WEB client version regex, the InnerTube continuation loop. A search like `web scraping python` returns about 18 organic videos on the first page, plus zero or more "Shorts shelves" interleaved through the results. Each shelf groups 10 to 30 short-form videos in a different schema than regular video results, so we dispatch on item type as we walk the list. ### The Quick Way If you don't want to maintain the InnerTube parsing yourself, the [`/plugin/google/youtube`](https://scrape.do/documentation/youtube-api) endpoint returns the same data as one HTTP call. Each request costs 10 credits. ```bash # Basic search curl "https://api.scrape.do/plugin/google/youtube?search_query=best+laptop+2025&token=" ``` ```json { "search_parameters": { "engine": "google_youtube", "search_query": "best laptop 2025", "hl": "en", "gl": "us" }, "video_results": [ { "position_on_page": 2, "title": "The Best Laptops of 2025", "link": "https://www.youtube.com/watch?v=PKshhTHyoZU", "video_id": "PKshhTHyoZU", "channel": { "name": "Just Josh", "verified": true }, "published_date": "4 months ago", "views": 313829, "length": "12:38", "extensions": ["4K"], "live": false } ], "shorts_results": [ { "position_on_page": 5, "shorts": [ { "title": "Top 3 Laptops", "video_id": "abc123", "views": 2100000 } ] } ], "pagination": { "next_page_token": "EqwDEhBiZXN0..." } } ``` The endpoint takes a `search_query` plus an optional `sp` token. `sp` doubles as a filter and pagination cursor: short values control sort (`CAM==` view count, `CAI==` upload date), result type (`EgIQAg==` channels only, `EgIQAw==` playlists only), duration (`EgIYAg==` over 20min), features (`EgJwAQ==` 4K, `EgJAAQ==` LIVE), and upload date (`EgIIAg==` today, `EgIIAw==` this week). Long values are continuation tokens from the previous response's `pagination.next_page_token`. The plugin routes both automatically. Two clean wins for this use case: **views come back parsed as integers** (`"1.2M views"` → `1200000`), and `channel.verified`, `extensions: ["4K"]`, and `live: true/false` are already extracted on every entry. No `runs` array walking, no view-string regex, no continuation-token bookkeeping. The rest of this article walks through doing it manually: same data, fewer abstractions, helpful when you want to learn the InnerTube model or extract fields the plugin doesn't expose. ### Prerequisites One library: ```bash pip install requests ``` No BeautifulSoup, no headless browser. Every parsing step works against JSON blobs. The other prerequisite is a Scrape.do account. Sign up at [scrape.do](https://scrape.do/?utm_source=blog&utm_medium=article&utm_campaign=youtube-scraping) for a free account that includes 1,000 monthly requests, which is enough to run everything we cover here on multiple targets. The dashboard shows the API token at the top of the home page. We will paste it into a `TOKEN` constant at the top of every code block. ![Scrape.do dashboard showing the API token field with the value highlighted, ready to be copied into the TOKEN constant at the top of each Python code block](/uploads/blog/youtube-scraping-scrape-do-dashboard-token.png) Imports and the config we will tweak per run: ```python import csv import json import re import urllib.parse import requests TOKEN = "" SEARCH_QUERY = "web scraping python" MAX_RESULTS = 60 OUTPUT_CSV = "youtube-search.csv" ``` We need a fetch helper that wraps every Scrape.do call, so one URL pattern handles both GETs and POSTs: ```python def fetch(target_url, method="GET", body=None): api = ( "http://api.scrape.do/?" + urllib.parse.urlencode( {"token": TOKEN, "url": target_url, "super": "true"}, quote_via=urllib.parse.quote, ) ) if method == "POST": api += "&customMethod=POST" r = requests.post(api, json=body, timeout=120) else: r = requests.get(api, timeout=120) if r.status_code != 200: raise SystemExit(f"HTTP {r.status_code} for {target_url}") return r.text ``` The `customMethod=POST` parameter is the one quirk: Scrape.do needs it to forward the request method correctly to YouTube. We will hit this `fetch` helper again when we get to video, comments, and channel. ### Parsing ytInitialData for Videos and Shorts The search-results page response includes a `