如何用 Python 下載網站上的所有圖片,並處理延遲載入、srcset 和 CSS 背景圖
How to download all images from a website with Python (lazy-loaded, srcset and CSS backgrounds)
PennyData 分享一段 Python 腳本,用來擷取並下載網站圖片,涵蓋延遲載入、srcset、CSS 背景圖及社交圖片 meta 標籤。
Right-click, "Save image as" does not scale past a handful of pictures. Here is a short Python script that downloads every image from a page, plus the traps that make naive scripts miss images or get you blocked.
The script
Install: pip install requests beautifulsoup4. Run: python grab_images.py https://books.toscrape.com/ images
#!/usr/bin/env python3
"""Download every image from a web page. pip install requests beautifulsoup4"""
import hashlib, os, re, sys, time
from urllib.parse import urljoin, urlparse
from urllib import robotparser
import requests
from bs4 import BeautifulSoup
UA = "image-grabber-tutorial/1.0 (+contact: you@example.com)"
CSS_URL = re.compile(r"url\(\s*['\"]?([^'\")]+)['\"]?\s*\)")
LAZY_ATTRS = ("src", "data-src", "data-lazy-src", "data-original", "data-lazy")
def best_from_srcset(srcset):
"""Pick the largest candidate from 'a.jpg 480w, b.jpg 1080w'."""
best, best_w = None, -1
for part in srcset.split(","):
bits = part.strip().split()
if not bits:
continue
w = 0
if len(bits) > 1 and bits[1][:-1].replace(".", "").isdigit():
w = float(bits[1][:-1]) * (1 if bits[1].endswith("w") else 1000)
if w > best_w:
best, best_w = bits[0], w
return best
def find_images(html, page_url):
soup = BeautifulSoup(html, "html.parser")
found = []
for img in soup.find_all("img"):
for a in ("srcset", "data-srcset"):
if img.get(a):
found.append(best_from_srcset(img[a]))
for a in LAZY_ATTRS:
if img.get(a):
found.append(img[a])
for src in soup.select("picture source[srcset]"):
found.append(best_from_srcset(src["srcset"]))
for m in soup.select("meta[property='og:image'], meta[name='twitter:image']"):
found.append(m.get("content"))
blobs = [t.get_text() for t in soup.find_all("style")]
blobs += [t["style"] for t in soup.find_all(style=True)]
for blob in blobs:
found += CSS_URL.findall(blob)
urls = []
for u in found:
if u and not u.startswith("data:"):
urls.append(urljoin(page_url, u.strip()))
return list(dict.fromkeys(urls)) # dedupe URLs, keep order
def main(page_url, out_dir="images", min_bytes=5000, delay=1.0):
s = requests.Session()
s.headers["User-Agent"] = UA
# fetch robots.txt ourselves: urllib's default UA gets a 403 on some sites,
# and RobotFileParser then treats the whole site as disallowed
rp = robotparser.RobotFileParser()
rr = s.get(urljoin(page_url, "/robots.txt"), timeout=20)
rp.parse(rr.text.splitlines() if rr.status_code == 200 else [])
if not rp.can_fetch(UA, page_url):
sys.exit("robots.txt disallows this page")
html = s.get(page_url, timeout=20).text
urls = find_images(html, page_url)
print(f"{len(urls)} candidate image URLs")
os.makedirs(out_dir, exist_ok=True)
seen, saved = set(), 0
for u in urls:
if not rp.can_fetch(UA, u):
continue
time.sleep(delay) # be polite
try:
r = s.get(u, timeout=30)
except requests.RequestException as e:
print("skip", u, e); continue
ct = r.headers.get("content-type", "")
if r.status_code != 200 or not ct.startswith("image/"):
continue
if len(r.content) < min_bytes: # icons, tracking pixels
continue
digest = hashlib.sha1(r.content).hexdigest()
if digest in seen: # same file under another URL
continue
seen.add(digest)
name = os.path.basename(urlparse(u).path) or digest
name = re.sub(r"[^A-Za-z0-9._-]", "_", name)[:80]
with open(os.path.join(out_dir, f"{digest[:8]}_{name}"), "wb") as f:
f.write(r.content)
saved += 1
print(f"saved {len(r.content):>8} B {u[:90]}")
print(f"done: {saved} unique images in {out_dir}/")
if __name__ == "__main__":
main(sys.argv[1], *(sys.argv[2:3] or ["images"]))
What it handles (and why)
-
Lazy loading. Many sites keep the real URL in
data-src,data-lazy-srcordata-originaland put a 1px placeholder insrc. The script checks all of them. -
srcset and
<picture>. One<img>can list a dozen sizes (a.jpg 480w, b.jpg 1080w).best_from_srcsetkeeps the widest candidate instead of the thumbnail. -
CSS background images.
url(...)inside inlinestyle=attributes and<style>blocks. Not covered: external.cssfiles (fetch those too if you need them). -
Social images.
og:imageandtwitter:imagemeta tags often hold the best-quality version. - Dedupe twice. URLs are deduped first, then files are deduped by SHA-1 of the content, because the same picture is often served under two URLs.
-
Size filter.
min_bytesdrops icons and tracking pixels. Raise it to 30000 for photos only.
Real output on two public sites
books.toscrape.com (a site made for scraping practice):
20 candidate image URLs
saved 9876 B https://books.toscrape.com/media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg
saved 7095 B https://books.toscrape.com/media/cache/26/0c/260c6ae16bce31c8f8c95daddd9f4a1c.jpg
...
done: 17 unique images in out_b/
20 URLs found, 17 unique files saved (the other 3 were dropped by the dedupe or the size filter).
en.wikipedia.org/wiki/Cat (heavy page with srcset everywhere):
112 candidate image URLs
saved 115918 B https://en.wikipedia.org/static/images/icons/enwiki-25.svg
saved 34961 B https://thumb.wikimedia.org/wikipedia/commons/thumb/2/25/Siam_lilacpoint.jpg/330px-Siam_li...
...
skip https://en.wikipedia.org/static/images/footer/wikimedia.svg ... Read timed out
done: 70 unique images in out_wp/
112 candidates, 70 unique files saved, one timeout skipped (the script logs it and carries on). It took about two minutes, because of the 1 second delay between requests, which is deliberate.
Traps
-
robots.txt. The script checks it for the page and every image. A bug I hit while testing: Python's
RobotFileParser.read()uses its own User-Agent, Wikipedia answered 403, and the parser then treated the whole site as disallowed, so the script refused to run. Fetchingrobots.txtyourself withrequestsand your own User-Agent, thenparse(), fixes it. - Rate limits. One request per second is polite. Hammering a site gets your IP blocked and is rude to the site owner. Set a User-Agent with a contact address.
- JavaScript-injected images. This script reads the HTML the server sends. Galleries that build themselves with JavaScript (infinite scroll, carousels fetched via API) need a real browser such as Playwright, or the JSON API the page calls (check the browser's Network tab).
-
Hotlink protection. Some CDNs return 403 unless the
Refererheader matches the page. Adds.headers["Referer"] = page_url. - Copyright. Being able to download an image does not mean you may reuse it. Check the site's terms and the image licence (Wikimedia Commons images carry per-file licences). Use downloads for personal archiving, analysis or with permission.
When one script is not enough
For dozens of sites, same-site crawling, size filters, or a ZIP of the files without maintaining the script, there is a hosted option. I run the Apify actor pennydata/website-image-extractor (plain HTTP, no browser, respects robots.txt). It reads img, srcset, picture, og:image and optionally CSS backgrounds, and returns one row per unique image.
Screen recording of a real run (no audio).
Sample input:
{"mode":"images","startUrls":[{"url":"https://en.wikipedia.org/wiki/Cat"}],"maxImages":10}
Real output rows from run mvWmlc1aaw8J1Bdi8 (2026-10-25, 3 seconds, 10 rows, trimmed to three fields; run on the Wikipedia Cat page):
ok Siam_lilacpoint.jpg image/jpeg 647001 bytes source: og:image
ok 40px-Symbol_support_vote.svg.png image/png 2206 bytes source: img-srcset
ok 330px-Siam_lilacpoint.jpg image/jpeg 34961 bytes source: img-srcset
Same limits as the script: no JavaScript rendering and no login. Price at the time of writing: pay per event, USD 0.002 per image found on the free tier, failed or robots-blocked pages not charged. The actor page is the source of truth for current prices.
Questions or an image source the script misses? Comment below and I will test it.
I publish the Apify actor linked above and earn from its runs. Written with AI assistance (Claude).
來源:DEV Community · dev.to
