YouTube Search Scraper avatar

YouTube Search Scraper

Pricing

from $0.50 / 1,000 results

Go to Apify Store
YouTube Search Scraper

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

ninhothedev

Maintained by Community

Actor stats

0

Bookmarked

829

Total users

13

Monthly active users

18 days ago

Last modified

Security

Limited permissions

Share

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 over query. 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 tutorial demo; 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:

FieldMeaning
rank1-based order among returned videos for this query
duration_secondsNumeric duration when a standard duration is available; otherwise null
descriptionSearch-result description snippet when supplied by YouTube; otherwise null
channel_idStable 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 workloadRows and startsActor event charges
Try one topic20 rows, one start$0.01005
Research several topics100 rows, one start$0.05005
Larger search snapshot1,000 rows, one start$0.50005
20-row snapshot daily for 30 days600 rows, 30 starts$0.30150
1,000-row snapshot daily for 30 days30,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.

  1. Create a working folder on your machine or server. Install the tested client with python -m pip install apify-client==3.2.0.
  2. Set your own APIFY_TOKEN environment variable through your secret manager or local environment. Never paste it into this file or a shared notebook.
  3. Save the code below as compare_search_snapshots.py, then run python 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.
  4. Open youtube-search-changes.csv. The first execution creates a baseline. Later executions compare against youtube-search-last.json in the same folder. The JSON contains data only, not your token.
  5. 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 timedelta
from decimal import Decimal
import csv
import json
import os
from pathlib import Path
INPUT = {"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 False
or {s.get("query") for s in statuses} != expected
or 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] = row
for 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 None
report.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 report
def spreadsheet_cell(value):
if isinstance(value, str) and (value.startswith(("\t", "\r", "\n")) or value.lstrip().startswith(("=", "+", "-", "@"))):
return "'" + value
return value
def main():
from apify_client import ApifyClient
client = 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 None
report = 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.