YouTube Search Scraper
Pricing
from $0.50 / 1,000 results
YouTube Search Scraper
Search YouTube videos by keyword and export titles, channels, public view counts, durations and URLs. Use query-level coverage reports and repeatable snapshots for content research. JSON, CSV and Excel output; no YouTube API key required.
Pricing
from $0.50 / 1,000 results
Rating
0.0
(0)
Developer
ninhothedev
Maintained by CommunityActor stats
0
Bookmarked
829
Total users
13
Monthly active users
18 days ago
Last modified
Categories
Security
Limited permissionsShare
Find YouTube videos with a search phrase and export clean rows for research, content planning or channel discovery. No YouTube API key is required.
Quick start
Enter What do you want to find?, choose your total video limit and click Start. The form prefills a 20-video example so the first run stays small.
{"query": "python tutorial", "maxResults": 20}
Open Videos for results or Query coverage for the SUMMARY report. Export the dataset as JSON, CSV or Excel. API callers can send the same JSON above.
Try a ready-made research workflow
Research Shopify video topics starts with two queries and up to ten results each. Duplicate the example, replace the keywords with your niche and save your own task. The code below adds a global 20-row cap explicitly.
Input and compatibility
query: one search phrase; the simplest input.queries: optional legacy list (or API string). When nonempty, it takes precedence overquery. Blank phrases are removed and duplicate phrases run once.maxItems: maximum results per query, unchanged default 100, range 1–1000.maxResults: optional total row cap across queries. It has no API default; without it, the legacy per-query limit applies. Queries run in input order.- Empty or unusable query input currently falls back to a small
python tutorialdemo; always provide explicit keywords in production workflows. - Legacy
mode: "search"is still accepted and needs no configuration.
{"queries": ["python tutorial", "javascript tutorial"], "maxItems": 10, "maxResults": 15}
This returns at most 10 rows for the first query and at most 5 for the second. A video matching multiple queries can appear once per query, preserving attribution. Within each query, duplicate video IDs are removed.
Output
Existing fields remain: query, video_id, title, channel, channel_url, duration, views, views_text, published_time, thumbnail, url, source, scraped_at.
Additional fields make exports easier to use:
| Field | Meaning |
|---|---|
rank | 1-based order among returned videos for this query |
duration_seconds | Numeric duration when a standard duration is available; otherwise null |
description | Search-result description snippet when supplied by YouTube; otherwise null |
channel_id | Stable YouTube channel ID when supplied; otherwise null |
Views are best-effort numeric conversions; published_time remains YouTube's relative text, not an invented publication timestamp. Search snippets are not full descriptions. Live streams and unavailable metadata can have null fields. Search uses English/US presentation for more consistent numeric parsing; results can still vary by location and time.
Coverage and failures
SUMMARY records total rows and each query's count/status. Failed queries or interrupted pagination mark partial: true; queries skipped because the global cap was reached are recorded explicitly. Review this report before treating a run as complete. If every query produces zero rows, the run fails rather than reporting a successful empty scrape. YouTube consent pages, blocks and upstream changes can prevent results; no universal success guarantee is made.
How much does YouTube Search Scraper cost?
Current base charges are $0.50 per 1,000 saved query/video rows, plus $0.00005 per start at the configured 512 MB. A video appearing for two different queries is two attributed rows. Optional video enrichment, transcripts and exact watch-page statistics are not included features of this Actor; it returns the search fields described above.
| Bounded workload | Rows and starts | Actor event charges |
|---|---|---|
| Try one topic | 20 rows, one start | $0.01005 |
| Research several topics | 100 rows, one start | $0.05005 |
| Larger search snapshot | 1,000 rows, one start | $0.50005 |
| 20-row snapshot daily for 30 days | 600 rows, 30 starts | $0.30150 |
| 1,000-row snapshot daily for 30 days | 30,000 rows, 30 starts | $15.00150 |
These calculations assume the stated number of rows is actually saved. They are usage examples, not monthly subscriptions or a guarantee of available results. Each new scheduled run is billed separately; there is no charge just for keeping a task saved. Platform usage is included in the configured Actor event price, while your Apify subscription and other services remain separate. The live Pricing tab is authoritative for applicable prices and discounts.
Set Maximum total charge in run options as well as your result limits. For the small example, choose a $0.02 maximum charge; this is a ceiling, not a minimum purchase. maxResults limits rows across all queries, while maxItems limits each query. Always inspect query coverage if a cap stops the run before all queries are processed.
For an issue, provide the run ID and a minimal reproducible input in the Actor Issues tab. Never include credentials.
Repeatable keyword research with Python
Use repeated snapshots to see which videos appear in your bounded searches and how their public view counters change. This is a content-research workflow, not a complete ranking tracker, publication feed or historical YouTube analytics API.
- Create a working folder on your machine or server. Install the tested client with
python -m pip install apify-client==3.2.0. - Set your own
APIFY_TOKENenvironment variable through your secret manager or local environment. Never paste it into this file or a shared notebook. - Save the code below as
compare_search_snapshots.py, then runpython compare_search_snapshots.py. Each invocation starts one paid Actor run, capped at 20 rows and $0.05 in Actor event charges. Your subscription/platform costs may differ. - Open
youtube-search-changes.csv. The first execution creates a baseline. Later executions compare againstyoutube-search-last.jsonin the same folder. The JSON contains data only, not your token. - To repeat daily, schedule this script on your own machine/server with a persistent working folder and no overlapping executions. An Apify schedule alone runs the scraper; it does not execute this local comparison script. Alternatively, compare exported rows in your own Make/n8n/database workflow.
first_seen means absent from the previous snapshot for that query; it does not mean recently published. A video can appear separately for both queries. views_change is the difference between two best-effort public counters, not verified views earned during the interval; rounding, corrections and missing values affect it. Missing videos are not evidence of deletion. Changes to keywords or limits require a new baseline. Keep timestamped copies if you need more than the last comparison.
The script refuses incomplete/failed/skipped query coverage and preserves the last good baseline. It does not send messages or create schedules. CSV text that could be interpreted as a spreadsheet formula is prefixed with an apostrophe.
"""Run manually or on your scheduler. Requires: pip install apify-client==3.2.0.Set APIFY_TOKEN in the environment. Files stay in the current working directory."""from datetime import timedeltafrom decimal import Decimalimport csvimport jsonimport osfrom pathlib import PathINPUT = {"queries": ["Shopify SEO", "Shopify conversion optimization"],"maxItems": 10, "maxResults": 20}SNAPSHOT = Path("youtube-search-last.json")REPORT = Path("youtube-search-changes.csv")def compare(rows, summary, previous=None):"""Reject incomplete/mismatched exports before replacing the saved baseline."""expected = set(INPUT["queries"])statuses = summary.get("queries", [])if (summary.get("partial") is not Falseor {s.get("query") for s in statuses} != expectedor any(s.get("status") != "complete" for s in statuses)or summary.get("totalResults") != len(rows)):raise ValueError("Incomplete query coverage; keep the previous snapshot and inspect SUMMARY.")if previous is not None and previous.get("input") != INPUT:raise ValueError("Input changed. Archive the old snapshot before starting a new comparison.")current = {}for row in rows:key = (row.get("query"), row.get("video_id"))if key[0] not in expected or not key[1] or key in current:raise ValueError("Unexpected query, missing ID or duplicate query/video pair.")current[key] = rowfor status in statuses:if status.get("count") != sum(key[0] == status["query"] for key in current):raise ValueError("Query counts do not match exported rows.")old = {(r["query"], r["video_id"]): r for r in (previous or {}).get("rows", [])}report = []for key, row in current.items():prior = old.get(key)before, now = (prior or {}).get("views"), row.get("views")delta = now - before if type(now) is int and type(before) is int else Nonereport.append({"query": key[0], "video_id": key[1], "title": row.get("title"),"url": row.get("url"), "observed_at": row.get("scraped_at"),"state": "baseline" if previous is None else "seen_again" if prior else "first_seen","views": now, "previous_views": before, "views_change": delta})return reportdef spreadsheet_cell(value):if isinstance(value, str) and (value.startswith(("\t", "\r", "\n")) or value.lstrip().startswith(("=", "+", "-", "@"))):return "'" + valuereturn valuedef main():from apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("ninhothedev/youtube-search-scraper").call(run_input=INPUT, build=os.environ.get("APIFY_BUILD", "latest"), memory_mbytes=512, run_timeout=timedelta(seconds=180), max_total_charge_usd=Decimal("0.05"))if run is None or run.status != "SUCCEEDED":raise RuntimeError("Run did not succeed; previous snapshot was not changed.")summary = client.key_value_store(run.default_key_value_store_id).get_record("SUMMARY")if not summary:raise RuntimeError("SUMMARY missing; previous snapshot was not changed.")rows = list(client.dataset(run.default_dataset_id).iterate_items())previous = json.loads(SNAPSHOT.read_text()) if SNAPSHOT.exists() else Nonereport = compare(rows, summary["value"], previous)if not report:raise ValueError("No comparable rows; previous snapshot was not changed.")with REPORT.open("w", newline="", encoding="utf-8") as handle:writer = csv.DictWriter(handle, fieldnames=list(report[0]))writer.writeheader()writer.writerows({key: spreadsheet_cell(value) for key, value in row.items()} for row in report)temporary = SNAPSHOT.with_suffix(".tmp")temporary.write_text(json.dumps({"input": INPUT, "runId": run.id, "rows": rows},ensure_ascii=False, indent=2), encoding="utf-8")temporary.replace(SNAPSHOT)print(f"Wrote {len(report)} comparisons to {REPORT}")if __name__ == "__main__":main()
For client API details, see the official Python Actor client reference.