🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.πŸ‘‰Try Now
Back to Blog

Image Scraper: How to Batch Scrape Images from Websites

Isabella Garcia
Isabella Garcia

Web Data Collection Specialist

30-Jun-2026

TL;DR:

  • The src attribute is the least reliable place to find an image. Modern pages put the real URL in srcset, data-src, or only set it after lazy-load fires β€” read currentSrc and the data attributes, not just src.
  • Lazy-loaded images don't exist until you scroll to them. Auto-scroll the page first so the loader swaps placeholders for real URLs, then collect.
  • Resolve every URL to absolute and keep the metadata. new URL(src, location.href) fixes relative and protocol-relative paths; alt, naturalWidth, and naturalHeight are worth capturing alongside the URL.
  • Download through the page, not a bare HTTP client. A same-origin fetch() inside the rendered session carries the cookies and referer the CDN expects, so hotlink-protected images come back instead of a 403.
  • It runs on a real browser, which is the whole point. Scrapeless Scraping Browser renders the JavaScript that builds the gallery and supplies residential egress, so the images load the way they do for a visitor.
  • Free to start. New Scrapeless accounts include free Scraping Browser runtime β€” sign up at app.scrapeless.com.

Introduction: why "just grab the img tags" doesn't work

Scraping images sounds like a one-liner β€” select every <img>, read src, done. On a real site it falls apart immediately. Galleries lazy-load, so most images have a placeholder in src and the real URL in data-src until you scroll them into view. Responsive images put several candidates in srcset and the browser picks one at runtime. URLs are relative or protocol-relative. And when you do have a URL, the CDN often rejects a plain HTTP download because it expects the cookies and referer of an actual page view.

So a reliable image scraper is really four steps: render the page in a real browser, scroll to force lazy images to load, read the resolved image URL (not the raw attribute) plus its metadata, and download the bytes through the rendered session.

This guide runs all four on Scrapeless Scraping Browser β€” an anti-detection cloud browser connected to Puppeteer β€” and every snippet below was run against a live image-heavy page. Public images only.


What You Can Do With It

  • Batch-collect every image on a page with its resolved URL, alt text, and dimensions.
  • Handle lazy-loaded and infinite-scroll galleries by scrolling before you collect.
  • Pull responsive images by reading currentSrc (the candidate the browser actually chose).
  • Download in bulk through the page so hotlink-protected CDNs serve the file.
  • Filter by size using natural dimensions to skip icons, spacers, and tracking pixels.

Why Scrapeless Scraping Browser

Scrapeless Scraping Browser is a customizable, anti-detection cloud browser designed for web crawlers and AI agents. For image scraping specifically, it brings:

  • Self-developed Chromium β€” renders the JS that builds galleries and fires the lazy-loader, so the real URLs appear.
  • Anti-detection fingerprinting β€” the session reads as a real browser, so image-heavy pages and their CDNs serve normally.
  • Residential proxies in 195+ countries β€” egress from an IP the page and its image CDN trust.
  • Session persistence β€” keep cookies warm so in-page downloads carry the right credentials.
  • A standard Puppeteer connection β€” Puppeteer.connect() returns a normal Browser; your collection code is plain Puppeteer.

Get your API key on the free plan at app.scrapeless.com.


Prerequisites

  • Node.js 18 or newer
  • A Scrapeless account and API key β€” sign up at app.scrapeless.com
  • Basic familiarity with Puppeteer

Install

bash Copy
npm install @scrapeless-ai/sdk puppeteer-core
bash Copy
export SCRAPELESS_API_KEY="your_api_token_here"

Step 1 β€” Connect and load the page

javascript Copy
import { Puppeteer } from '@scrapeless-ai/sdk';

const browser = await Puppeteer.connect({
  apiKey: process.env.SCRAPELESS_API_KEY,
  sessionName: 'image-scraper',
  proxyCountry: 'US',
  sessionTTL: 300,
});

const page = await browser.newPage();
await page.goto('https://books.toscrape.com/', {
  waitUntil: 'networkidle2',
  timeout: 60000,
});

networkidle2 waits until the initial image requests settle, so the above-the-fold images are already in the DOM.


Step 2 β€” Scroll to trigger lazy-loaded images

Below-the-fold images often stay as placeholders until they scroll into view. Walk the page down in steps, pausing so the loader can swap in real URLs:

javascript Copy
await page.evaluate(async () => {
  for (let y = 0; y < document.body.scrollHeight; y += 600) {
    window.scrollBy(0, 600);
    await new Promise((r) => setTimeout(r, 300));
  }
});

For infinite-scroll galleries, loop this until document.body.scrollHeight stops growing β€” that's the signal there are no more images to load.

Get your API key on the free plan: app.scrapeless.com


Step 3 β€” Collect resolved URLs and metadata

Read the URL the browser actually resolved, falling back through the common lazy-load attributes, and make every URL absolute:

javascript Copy
const images = await page.evaluate(() => {
  const abs = (u) => { try { return new URL(u, location.href).href; } catch { return null; } };
  return [...document.querySelectorAll('img')]
    .map((img) => {
      const src =
        img.currentSrc ||           // the candidate the browser chose (handles srcset)
        img.src ||
        img.getAttribute('data-src') ||
        img.getAttribute('data-lazy-src');
      return {
        src: abs(src),
        alt: img.alt || null,
        width: img.naturalWidth,
        height: img.naturalHeight,
      };
    })
    .filter((i) => i.src);
});

console.log(images.length, 'images');
console.log(images[0]);
// 20 images
// {
//   src: 'https://books.toscrape.com/media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg',
//   alt: 'A Light in the Attic',
//   width: 125,
//   height: 155
// }

currentSrc is the key: for a responsive srcset image it gives you the candidate the browser picked at the current viewport, not a guess. Capturing naturalWidth/naturalHeight lets you drop icons and spacers later with a simple size filter.


Step 4 β€” Download the images through the page

Downloading through a bare HTTP client is where hotlink protection bites β€” the CDN sees no referer or cookies and returns a 403. Fetch each image inside the page context instead, so it carries the session's credentials, and decode the base64 locally:

javascript Copy
import { writeFileSync } from 'node:fs';

const url = images[0].src;
const out = await page.evaluate(async (u) => {
  const res = await fetch(u);
  const bytes = new Uint8Array(await res.arrayBuffer());
  let binary = '';
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
  return { status: res.status, type: res.headers.get('content-type'), b64: btoa(binary) };
}, url);

writeFileSync('./image-0.jpg', Buffer.from(out.b64, 'base64'));
console.log({ status: out.status, type: out.type, bytes: Buffer.from(out.b64, 'base64').length });
// { status: 200, type: 'image/jpeg', bytes: 9876 }

Loop this over your collected images to batch-download, keeping a small delay between requests and modest concurrency so the CDN stays happy.


What You Get Back

Each collected image is a flat record β€” resolved URL plus metadata:

json Copy
[
  {
    "src": "https://books.toscrape.com/media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg",
    "alt": "A Light in the Attic",
    "width": 125,
    "height": 155
  }
]
// Real capture from books.toscrape.com (20 images on the homepage). Values move as the page changes.

A few honest observations:

  • Tiny dimensions are usually not content. Filter out anything below a threshold (say 50Γ—50) to drop icons, sprites, and tracking pixels.
  • srcset means multiple resolutions exist. currentSrc gives the viewport-appropriate one; parse the raw srcset if you specifically want the largest.
  • Background images aren't <img> tags. CSS background-image URLs live in computed styles β€” collect those separately if the gallery uses them.
  • Download through the page for any CDN that checks referer; a bare request will 403.

Conclusion: a reliable batch image scraper

A dependable image scraper is render β†’ scroll β†’ read the resolved URL β†’ download through the page. Skip the scroll and you miss the lazy images; read raw src instead of currentSrc and you get placeholders; download with a bare client and hotlink-protected CDNs reject you. Running on Scrapeless Scraping Browser supplies the real browser and residential egress that make the gallery load in the first place. For collecting page URLs rather than images, see the Etsy scraper guide; the Scraping Browser product page and docs cover the full SDK surface. Scroll first, read currentSrc, keep the metadata, and fetch the bytes through the session.


Ready to Build Your AI-Powered Data Pipeline?

Join our community to claim a free plan and connect with developers building image and media pipelines: Discord Β· Telegram.

Sign up at app.scrapeless.com for free Scraping Browser runtime and adapt the patterns above to the galleries and CDNs your workflow needs. See pricing for scale.


FAQ

Q: Is scraping images from a website legal?
Collecting publicly visible images is generally permissible, but copyright and the site's Terms of Service still apply to how you use them. Scrape only public images, respect licensing, review the ToS, and consult counsel for your use case.

Q: Why are my scraped src values blank or tiny placeholders?
Because the images lazy-load. Scroll the page first so the loader swaps the placeholder for the real URL, and read currentSrc/data-src rather than just src.

Q: How do I handle infinite-scroll galleries?
Loop the scroll step until document.body.scrollHeight stops increasing, then collect β€” that's the signal no more images will load.

Q: My downloads return 403 even though the URL works in a browser. Why?
The CDN is checking referer/cookies (hotlink protection). Download with an in-page fetch() so the request carries the rendered session's credentials.

Q: How do I get the highest-resolution version?
Parse the raw srcset and pick the largest candidate, instead of currentSrc which reflects the current viewport.

Q: Do I need a proxy?
For public image pages, often not β€” but pinning proxyCountry gives a consistent residential IP that image-heavy pages and their CDNs treat as a normal visitor.

Q: Can I run this without an AI agent?
Yes. It's plain Puppeteer over the Scrapeless session β€” no agent required.

At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.

Most Popular Articles

Catalogue