How to Scrape Images & Video (Python Guide)
- I ran a requests + BeautifulSoup image scraper live on June 12, 2026: it found 20
<img>sources and downloaded 5 real JPEGs (4-10 KB each), full output below. - Scraping images is two steps: parse
<img>/srcset/<source>for URLs, then GET each binary. The same loop handles video by reading<video>and<source>tags. - Trying to scrape images from Google fails with plain HTTP: I fetched the image grid and got 0
<img>tags. Google renders results in JavaScript, so you need a headless browser or an API. - Copyright sits on the file, not the scrape. Storing source URLs is low-risk; rehosting full-resolution images or video you do not own is the part that bites.
- For JS-only sites, login walls, or volume, a scraper API like ChocoData renders the page and returns media URLs without managing browsers and proxies.
Scraping images and video is two mechanical steps: find the media URLs in the page, then download each binary. I will show a Python scraper that does both, and I ran it live on June 12, 2026 against a public sandbox: it found 20 image sources and saved 5 real JPEG files, with the byte counts pasted below. The harder question is the one most people actually arrive with, how to scrape images from Google, so I tested that too. A plain HTTP request to Google’s image grid returns zero image tags because the page renders in JavaScript, and I prove it lower down. For the fundamentals first, see my web scraping guide and the Python walkthrough.
Can you legally scrape images and video?
You can scrape publicly visible media with code, and the legal risk lives in the file you download rather than the act of scraping. Three layers are worth separating, because they carry different exposure.
US computer-access law is the most settled. In hiQ Labs v. LinkedIn the Ninth Circuit held that scraping public data does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward genuine hacking. Loading a public image page is not a federal computer crime on its own.
Contract law is the live constraint. Most sites restrict automated access in their terms, and breaching those terms is a civil matter: hiQ won the CFAA question yet still faced a $500,000 judgment over a user agreement. Copyright is the layer that bites hardest for media specifically. An image or video file is a creative work owned by whoever made it, so downloading originals to republish or train on is a copyright question independent of any scraping law. The safer posture is to store source URLs, thumbnails, dimensions, and alt text as metadata, then fetch a full-resolution file from the rights holder only when your use allows it. I am a tester, not a lawyer, so get advice before a commercial run. My is web scraping legal pillar covers the full picture.
What data can you extract from a media page?
A media page exposes the source URL, alt text, dimensions, and for video the format and poster frame, with most of it sitting in HTML attributes. The table shows the common fields and where each one lives.
| Field | Where it lives | Notes |
|---|---|---|
| Image URL | <img src> | Often a resized preview, not the original |
| Larger variants | <img srcset> | Comma-separated URL + width descriptors |
| Lazy-loaded URL | data-src, data-full | Real source on infinite-scroll galleries |
| Full-res link | parent <a href> | Frequently points at the original file |
| Alt text / caption | <img alt>, <figcaption> | Useful labels for datasets |
| Dimensions | width / height attrs | Filter out icons and tracking pixels |
| Video file | <video src>, <source src> | Progressive MP4 downloads directly |
| Video format | <source type> | video/mp4, video/webm |
| Poster frame | <video poster> | Thumbnail still for the clip |
The split that matters: static <img> and progressive <video> tags are one parse away, while JavaScript galleries and streaming video (HLS/DASH segments behind a manifest) need a browser engine or an API, covered below.
How does Google Images block scrapers?
Google Images blocks naive scrapers by rendering the entire grid in JavaScript, so a plain HTTP client receives a shell with no image elements. I requested the image-search URL without cookies on June 12, 2026. Here is the exact result:
import requests, re
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
r = requests.get(
"https://www.google.com/search?q=kitten&tbm=isch",
headers={"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"},
timeout=30,
)
print("HTTP", r.status_code, "| bytes", len(r.text))
print("final URL:", r.url)
print("<img> tags:", len(re.findall(r"<img", r.text, re.I)))
HTTP 200 | bytes 90990
final URL: https://www.google.com/search?q=kitten&udm=2
<img> tags: 0
Three takeaways. The request returned 200 and ~91 KB, so it looks successful, but every byte is the pre-render JavaScript shell. There are 0 <img> tags, so BeautifulSoup has nothing to parse. And the tbm=isch parameter was rewritten to udm=2, Google’s current image mode, which hands the same empty shell to a no-JS client. Past the rendering wall, a single IP firing rapid queries draws rate limits and a /sorry/ CAPTCHA page. So there is no honest requests-only scraper for Google Images, and the techniques that work are a headless browser or an API. For a Google-specific deep dive, I wrote a separate Google Images guide; this article focuses on the general image and video technique, which I demonstrate next on a page that actually serves its media in HTML.
Method 1: How do you scrape images with Python (requests + BeautifulSoup)?
Parse the page for <img> sources, resolve relative URLs to absolute ones, then GET each binary and write it to disk. The reason this works on a normal site and fails on Google is simple: the target below ships its image tags in the served HTML, so a single request is enough. I used books.toscrape.com, a public sandbox built for exactly this, to keep the demo legal and reproducible.
import requests, os
from urllib.parse import urljoin
from bs4 import BeautifulSoup
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
HEADERS = {"User-Agent": UA, "Accept-Language": "en-US,en;q=0.9"}
def scrape_images(page_url, out_dir="images", limit=5):
r = requests.get(page_url, headers=HEADERS, timeout=30)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
urls = []
for img in soup.find_all("img"):
src = img.get("src") or img.get("data-src") # cover lazy-loaders
if not src:
continue
urls.append(urljoin(page_url, src)) # resolve relative paths
print(f"HTTP {r.status_code} | found {len(urls)} <img> sources")
os.makedirs(out_dir, exist_ok=True)
saved = 0
for u in urls[:limit]:
try:
data = requests.get(u, headers=HEADERS, timeout=30).content
name = os.path.join(out_dir, os.path.basename(u.split("?")[0]))
with open(name, "wb") as f:
f.write(data)
print(f" saved {len(data):>6} bytes {name}")
saved += 1
except Exception as e:
print(f" FAILED {u[:50]}: {e}")
return saved
n = scrape_images("https://books.toscrape.com/", out_dir="book_covers", limit=5)
print(f"Downloaded {n} files")
I ran this exact script. Real output:
HTTP 200 | found 20 <img> sources
saved 9876 bytes book_covers/2cdad67c44b002e7ead0cc35693c0e8b.jpg
saved 7095 bytes book_covers/260c6ae16bce31c8f8c95daddd9f4a1c.jpg
saved 4062 bytes book_covers/3eef99c9d9adef34639f510662022830.jpg
saved 8442 bytes book_covers/3251cf3a3412f53f339e42cac2134093.jpg
saved 7986 bytes book_covers/bea5697f2534a2f86a3ef27b5a8c12a6.jpg
Downloaded 5 files
The files are genuine JPEGs; file on the saved bytes reports JPEG image data ... 99x155 and similar. Two mechanics carry the weight here. urljoin turns a relative src like media/cache/2c/da/....jpg into a full URL, which is the single most common bug in beginner image scrapers. And reading data-src alongside src catches lazy-loaded galleries that leave src as a placeholder pixel. If requests and BeautifulSoup are new to you, start with my BeautifulSoup guide and the broader Python tutorial.
How do you get the full-resolution image instead of the thumbnail?
Read srcset for the largest candidate or follow the parent anchor to the original file, because src is usually a resized preview. Galleries commonly wrap each thumbnail in a link to the full image, and responsive pages list every size in srcset as url width pairs. The pattern:
def best_image_url(img, page_url):
# 1) srcset: pick the URL with the largest width descriptor
srcset = img.get("srcset")
if srcset:
best, best_w = None, -1
for part in srcset.split(","):
bits = part.strip().split()
if len(bits) == 2 and bits[1].endswith("w"):
w = int(bits[1][:-1])
if w > best_w:
best, best_w = bits[0], w
if best:
return urljoin(page_url, best)
# 2) parent <a href> often links the original file
a = img.find_parent("a")
if a and a.get("href", "").lower().endswith((".jpg", ".jpeg", ".png", ".webp")):
return urljoin(page_url, a["href"])
# 3) fall back to src / data-src
return urljoin(page_url, img.get("data-src") or img.get("src"))
This is selector work, so the mechanics in my XPath and CSS selectors guide apply directly.
How do you scrape video files?
Read <video> and <source> tags for the file URL, then download progressive MP4s with the same requests.get loop. I confirmed a direct progressive MP4 is fetchable: a HEAD request returned 200, content-type: video/mp4, content-length: 788493. Extracting the source list looks like this:
from bs4 import BeautifulSoup
html = '''<video controls poster="/media/p.jpg">
<source src="/media/clip-720.mp4" type="video/mp4">
<source src="/media/clip-720.webm" type="video/webm">
</video>'''
soup = BeautifulSoup(html, "html.parser")
for v in soup.find_all("video"):
print("poster:", v.get("poster"))
for s in v.find_all("source"):
print(" source:", s.get("src"), "|", s.get("type"))
poster: /media/p.jpg
source: /media/clip-720.mp4 | video/mp4
source: /media/clip-720.webm | video/webm
One honest limit: this only covers progressive files. Streaming video (HLS or DASH, which is what YouTube and most platforms serve) is split into hundreds of .ts or .m4s segments listed in an .m3u8 or .mpd manifest, and reassembling them by hand is fragile. For those, use the platform’s official API or a scraper API. My YouTube guide walks through that specific case.
Method 2: How do you scrape images and video with a scraper API?
A scraper API is the right tool when the media is behind JavaScript, a login, or a CAPTCHA, or when you need volume. It renders the page on its side, routes through a residential IP pool, retries on blocks, and returns the page (or parsed media URLs) so your code skips the browser-and-proxy work. ChocoData exposes a universal endpoint (one URL for any site) plus 453 dedicated endpoints. You point the universal endpoint at the target and let it render:
import requests
API_KEY = "cd_live_YOUR_KEY" # free tier: 1,000 requests/month, no card
target = "https://www.google.com/search?q=kitten&udm=2"
resp = requests.get(
"https://api.chocodata.com/api/v1/universal/get",
params={
"api_key": API_KEY,
"url": target,
"render": "true", # run the page JavaScript so the image grid hydrates
"country": "us", # exit from a US residential IP
},
timeout=90,
)
print("STATUS:", resp.status_code)
data = resp.json() # rendered HTML / parsed media URLs
I did not run this call because it needs a paid key, so I am not pasting fake output. The endpoint pattern is GET /api/v1/{site}/{resource}, so a dedicated route exists where one is published; verify exact field names against ChocoData’s own docs on the free tier before building against them. How the two methods compare, by trade-off:
| Factor | DIY (requests / Playwright) | Scraper API |
|---|---|---|
Static <img> / MP4 | One GET, free | Handled (overkill here) |
| JS galleries (Google) | You run Playwright | render=true |
| Proxies / IP rotation | You build it | Included |
CAPTCHA / /sorry/ page | You solve it | Handled |
| Login-walled media | You manage cookies | Handled per request |
| Cost | Free (your compute) | Per-request, paid |
| Scales to thousands | Slow, blocks appear | Vendor’s problem |
Decision rule: for a static site that serves its media in HTML, like the sandbox in Method 1, the free requests loop is all you need and I would ship it. For JavaScript galleries, login-walled feeds, or steady volume where you do not want to babysit a browser farm and proxy pool, the API earns its cost. The anti-block tactics that matter at small scale are in scraping without getting blocked, and Scrapy is the heavier DIY route once one script is not enough.
What are the limits of scraping images and video?
Four real ones. First, src is often a thumbnail, so full-resolution work means reading srcset or following the source link. Second, JavaScript galleries (Google Images, most social feeds) return nothing to plain HTTP, as I proved above, so they need a browser or an API. Third, streaming video is segmented behind a manifest and is far harder than a progressive MP4 download. Fourth, copyright sits on every file you save, so collecting URLs and metadata is the low-risk path and rehosting originals is the part that creates exposure. Build on the tested requests loop for static media, add Playwright or an API for rendered pages, and store links rather than rehosted files to stay on the right side of copyright. For the broader picture, start with my web scraping pillar.
FAQ
For static HTML, requests plus BeautifulSoup is enough: requests fetches the page and each image binary, BeautifulSoup finds the img and source tags. For JavaScript-rendered galleries (Google Images, most social feeds, infinite-scroll sites), you need Playwright or Selenium to run the page script first, or a scraper API that renders it for you.
The src attribute is often a small preview. Check the srcset attribute for the largest candidate, the data-src or data-full attribute lazy-loaders use, or the parent anchor's href, which frequently points at the original file. On Google Images the full-res file lives on the original host and loads only after a click, so you follow the source link rather than reading the grid thumbnail.
Progressive MP4 files in a video or source tag download with the same requests.get loop, and I confirmed a direct MP4 returns 200 with a Content-Length. Streaming video (HLS/DASH, YouTube, most platforms) is segmented into many .ts or .m4s chunks behind a manifest and is far harder; for those, use the platform's API or a scraper API rather than reassembling segments by hand.
Loading a public Google results page with code is not a US computer-crime issue after hiQ v. LinkedIn and Van Buren, but Google's Terms of Service restrict automated access (a contract matter), and the images themselves are copyrighted by their original hosts. Collecting source URLs and metadata is the safer posture; redistributing downloaded files is where copyright exposure starts.