Kurz gesagt: Ein Puppeteer-Workflow zum Herunterladen von Dateien lässt sich auf vier Arten gut umsetzen: Klicken Sie auf eine Schaltfläche und lassen Sie Chrome in einen von Ihnen verwalteten Ordner schreiben, führen Sie fetch() innerhalb der Seite ausführen und Base64 zurück an Node leiten, das Chrome DevTools Protocol mit Download-Fortschrittsereignissen steuern oder den Browser überspringen und die URL mit Axios unter Verwendung von Cookies abrufen, die aus der Puppeteer-Sitzung gesammelt wurden. Wähle nach Dateigröße, Authentifizierung und der Art und Weise, wie die Website den Link bereitstellt.Einleitung
Wenn Sie schon einmal versucht haben, einen Puppeteer-Workflow zum Herunterladen von Dateien auf einer echten Produktionswebsite zu skripten, kennen Sie bereits den Moment der Wahrheit: Das Skript klickt auf den Download-Button, die Headless-Chrome-Instanz meldet Erfolg, und die Festplatte bleibt leer. Das passiert, weil Chromium automatisierte Downloads im Headless-Modus standardmäßig blockiert, und die Lösung liegt nicht in der High-Level-API von Puppeteer. Sie befindet sich eine Ebene tiefer, im Chrome DevTools Protocol.
Dieser Leitfaden richtet sich an fortgeschrittene Node.js-Entwickler, QA-Ingenieure und Scraping-Praktiker, die bereits wissen, wie man einen Browser startet, auf einer Seite navigiert und ein Element auswählt, und nun die tatsächlichen Bytes erfassen müssen. Wir werden vier abgegrenzte Methoden durchgehen, jede mit vollständigem Code, und wir werden ehrlich sagen, welche in welcher Situation angebracht ist.
Sie werden sehen, dass überall dieselbe Basis-Harness wiederverwendet wird: ein Download-Ordner, erstellt mit fs.mkdirSync, ein realistischer User-Agent, ein Desktop-Viewport und ein Muster, um zu warten, bis die Datei tatsächlich auf der Festplatte ist und nicht mehr geschrieben wird. Am Ende verfügen Sie über ein Puppeteer-Rezept für Dateidownloads bei klickgesteuerten Downloads, authentifizierungsgeschützten Downloads, großen binären Payloads und bekannten URLs sowie über eine Entscheidungshilfe zur Auswahl zwischen diesen und eine Checkliste zur Absicherung für die Produktion.
Warum das Herunterladen von Dateien mit Puppeteer kniffliger ist, als es aussieht
Wenn Sie page.click() auf eine Schaltfläche „CSV herunterladen“ in Chrome mit Headers klickst, landet die Datei in deinem Download-Ordner und du machst mit deinem Tag weiter. Führe dasselbe Skript mit headless: 'new' und nichts passiert. Der Klick wird ausgelöst, die Netzwerkanfrage wird gesendet, und Ihr Dateisystem bleibt leer. Das ist kein Puppeteer-Fehler. Chromium behandelt automatisierte Downloads absichtlich als verdächtig, und die Lösung liegt im Chrome DevTools Protocol und nicht in der Oberflächen-API von Puppeteer. Solange Sie diesen Schalter nicht umlegen, wird kein Puppeteer-Download-Ablauffluss jemals ein Byte auf der Festplatte hinterlassen.
Es gibt keinen einzigen besten Weg, damit umzugehen. Der richtige Ansatz hängt davon ab, wie die Website die Datei bereitstellt, wie streng ihre Authentifizierung ist, wie groß die Nutzlast ist und wie viel Zuverlässigkeit Sie benötigen. Vier Muster decken fast jeden Fall ab:
- Klicken plus
setDownloadBehavior. Konfigurieren Sie das Download-Verzeichnis des Browsers über CDP, klicken Sie auf die Schaltfläche und warten Sie auf den Abschluss. Am besten geeignet, wenn der Download durch JavaScript ausgelöst wird und Sie die zugrunde liegende URL nicht haben oder nicht nachverfolgen möchten. - In-Page
fetch()plus base64. Führen Siefetch()innerhalbpage.evaluate(), codieren Sie die Antwort und senden Sie sie als Base64 zurück an Node. Ideal für SPAs, Blob-URLs und Downloads, die durch Cookies geschützt sind, die nur im Browserkontext existieren. - Reines CDP mit Download-Ereignissen. Öffne eine CDP-Sitzung, rufe
Browser.setDownloadBehaviorund aufBrowser.downloadWillBeginundBrowser.downloadProgress. Ideal, wenn Sie Echtzeit-Fortschrittsanzeige, GUID-zu-Dateinamen-Zuordnung oder detaillierte Fehlererkennung benötigen. - Übergeben Sie die URL an Axios oder
https. Verwenden Sie Puppeteer, um die Seite zu rendern und die tatsächliche Datei-URL zu extrahieren, und laden Sie sie dann über Node mit den Cookies und Headern herunter, die Sie aus der Puppeteer-Sitzung gewonnen haben. Am besten geeignet für große Dateien, parallele Jobs und immer dann, wenn der Browser nur im Weg ist.
Der Rest dieses Leitfadens besteht aus einem Abschnitt pro Methode sowie einer Entscheidungshilfe, einer Checkliste zur Absicherung und einem Realitätscheck zu Puppeteer versus Playwright am Ende.
Voraussetzungen und Projekteinrichtung
Bevor wir uns mit den einzelnen Methoden befassen, benötigen wir ein Projekt, das alle vier gemeinsam nutzen können. Das Setup ist hier bewusst einfach gehalten: ein Ordner, ein package.json, ein Download-Verzeichnis und eine einzelne launch.js Datei, die wir in jedem Beispiel wiederverwenden werden. Wenn Sie das Grundgerüst konsistent halten, können Sie eine Methode gegen eine andere austauschen, ohne den Rest Ihres Codes anzutasten, und es macht den Unterschied zwischen den Methoden sehr deutlich, wenn Sie sie nebeneinander vergleichen.
Die Einrichtungshinweise beziehen sich zum Zeitpunkt der Erstellung auf Node.js 20 oder neuer; überprüfen Sie die aktuellen Puppeteer-Release-Notes, wenn Sie eine ältere Laufzeitumgebung verwenden, da sich die minimal unterstützte Node.js-Version mit jedem größeren Puppeteer-Release ändert.
Installation von Puppeteer, Node.js-Grundlagen und Ordnerstruktur
Erstellen Sie ein Projekt, initialisieren Sie npm und installieren Sie Puppeteer:
mkdir puppeteer-downloads
cd puppeteer-downloads
npm init -y
npm install puppeteerÖffnen package.json und fügen Sie "type": "module" hinzu, damit wir import Syntax in den Beispielen verwenden können. Fügen Sie dabei gleich ein paar nützliche Entwicklungshilfen hinzu:
{
"type": "module",
"scripts": {
"method1": "node method1.js",
"method2": "node method2.js",
"method3": "node method3.js",
"method4": "node method4.js"
}
}Puppeteer wird mit Chrome for Testing ausgeliefert und lädt dieses bei der Installation auf den meisten Plattformen herunter, was für alle Schritte in dieser Anleitung ausreicht. Wenn Sie in einem abgespeckten Container arbeiten, überprüfen Sie das Installationsverhalten in den Puppeteer-Release-Notes für die von Ihnen festgelegte Version, da sich das Verhalten des mitgelieferten Chrome über die verschiedenen Releases hinweg geändert hat.
Ordnerstruktur:
puppeteer-downloads/
downloads/ # files end up here
launch.js # shared harness
method1.js
method2.js
method3.js
method4.jsErstellen Sie den downloads/ Ordner jetzt (mkdir downloads) oder lassen Sie ihn beim ersten Ausführen vom Startskript erstellen.
Ein Basis-Startskript mit Download-Pfad, User-Agent und Viewport
Jede Methode in dieser Anleitung geht von demselben Grundgerüst aus. Fügen Sie dies in launch.js:
// launch.js
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const DOWNLOAD_DIR = path.resolve(__dirname, 'downloads');
export async function launchBrowser({ headless = 'new' } = {}) {
// setDownloadBehavior requires an absolute path. Relative paths silently fail.
if (!fs.existsSync(DOWNLOAD_DIR)) {
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
}
const browser = await puppeteer.launch({
headless,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
],
});
return browser;
}
export async function newPage(browser) {
const page = await browser.newPage();
// Realistic desktop fingerprint. Some sites hide download buttons on mobile.
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
);
await page.setViewport({ width: 1366, height: 900 });
return page;
}Drei Dinge sind zu beachten. Erstens setDownloadBehavior erfordert einen absoluten Pfad; wenn Sie einen relativen Pfad übergeben, ignoriert Chrome diesen stillschweigend und schreibt nichts. Zweitens erzwingen wir einen Desktop-User-Agent und einen Viewport, da einige Websites Download-Links hinter einem mobilen Layout verbergen und ein automatisierter Client ohne User-Agent oft einen erhält, den Chrome als nicht vertrauenswürdig einstuft. Drittens verwenden wir headless: 'new' anstelle von headless: 'shell'. Das Download-Verhalten kann im shell Modus, insbesondere bei vom Browser verwalteten Downloads, daher bleiben wir bei der Standardeinstellung.
Sie können headless zum false zum Debuggen umschalten. Das Beobachten des Klickvorgangs im echten Chrome ist oft der schnellste Weg, um zu diagnostizieren, warum ein Puppeteer-Datei-Download-Ablauf stillschweigend fehlschlägt. Sobald es im Head-Modus funktioniert, aber nicht im Headless-Modus, wissen Sie, dass das Problem in der Download-Richtlinie liegt und nicht in Ihrem Selektor.
Zwei kleine Ergänzungen sind sinnvoll, bevor du dieses Test-Harness überall wiederverwendest. Erstens: Lege ein Standard-Navigations-Timeout fest: page.setDefaultNavigationTimeout(60_000) bei leeren Caches erspart Ihnen viele unzuverlässige CI-Läufe. Zweitens: Installieren Sie ein einfaches console und pageerror Listener, damit alle Fehler auf der Seite während des Download-Klicks in Ihren Node-Logs angezeigt werden, anstatt vom Browser verschluckt zu werden. Beides sind Einzeiler, die sich schon beim ersten Fehlschlag eines Deploys um 2 Uhr morgens bezahlt machen.
Dies ist auch ein naheliegender Ort, um auf einen ausführlicheren Puppeteer-Scraping-Leitfaden zu verweisen, falls du die umfassenderen Navigations-, Selektor- und Warte-Muster benötigst, von denen dieser Artikel ausgeht, dass du sie bereits beherrschst.
Methode 1: Klicken Sie auf den Download-Button und warten Sie auf die Datei
Methode 1 kommt dem, „was ein Mensch tun würde“, am nächsten. Navigieren Sie zur Seite, klicken Sie auf den Download-Button und lassen Sie Chrome die Datei in einen von Ihnen gewählten Ordner schreiben. Der Haken dabei ist, dass Headless Chrome standardmäßig nirgendwo schreibt; Sie müssen ihm mithilfe eines Chrome DevTools-Protokollaufrufs explizit mitteilen, wo Downloads erlaubt sind und wohin sie gehen sollen. Sobald das eingerichtet ist, besteht der Rest der Arbeit darin, zu erkennen, wann die Datei tatsächlich fertig ist, da page.click() die Rückgabe erfolgt lange bevor die Bytes auf der Festplatte landen.
Diese Methode ist die richtige Wahl, wenn:
- Der Download durch JavaScript ausgelöst wird, nicht durch einen einfachen
<a href>Link, sodass Sie die URL nicht einfach extrahieren können. - Sie keinen Echtzeit-Fortschrittsbericht benötigen (sondern nur wissen wollen: „Ist es schon fertig?“).
- Die Datei klein genug ist, sodass eine Zwischenspeicherung auf der Festplatte ausreichend ist (typischerweise unter einigen hundert MB).
Sie ist die falsche Wahl, wenn:
- Die Website komplexe Authentifizierung und Cookies erfordert, die erst nach mehreren SPA-Interaktionen vorhanden sind (Methode 2 ist eleganter).
- Sie Fortschrittsereignisse oder eine Unterbrechungserkennung benötigen (Methode 3).
- Die Datei ist riesig und Sie möchten direkt auf S3 oder einen anderen Speicherort streamen (Methode 4).
Im Folgenden legen wir den Download-Ordner fest, klicken auf die Schaltfläche und prüfen mithilfe eines .crdownload Sentinel und einer stabilen Dateigrößenprüfung auf den Abschluss ab, sodass eine teilweise geschriebene Datei niemals als fertiggestellt zurückgegeben wird.
Konfigurieren des Download-Ordners mit setDownloadBehavior
Es gibt zwei CDP-Aufrufe, die Sie in der Praxis antreffen werden. Der ältere ist Page.setDownloadBehavior, der auf eine einzelne Seite beschränkt ist:
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR, // absolute path
});Dies funktioniert in vielen Konfigurationen noch, ist jedoch offiziell veraltet, und neuere Chrome-Versionen leiten Downloads mittlerweile über das CDP-Ziel auf Browserebene weiter. In diesem Fall gibt Ihr Page.setDownloadBehavior Aufruf erfolgreich zurück und die Datei landet immer noch in ~/Downloads (oder nirgendwo), da die Seitensitzung nicht mehr für Downloads zuständig ist. Wenn Sie schon einmal einen Nachmittag damit verbracht haben, auf ein „funktionierendes“ Skript zu starren, das nach einem automatischen Chrome-Update plötzlich keine Dateien mehr schrieb, ist dies meist der Grund dafür.
Der zukunftsfähige Aufruf ist Browser.setDownloadBehavior, dessen Geltungsbereich auf den Browser beschränkt ist:
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true, // required for Method 3 progress events
});Browser.setDownloadBehavior gilt für jede Seite im Browser, nicht nur für die, auf der Sie die Sitzung geöffnet haben, was genau das ist, was Sie für einen Download-Workflow mit mehreren Tabs benötigen. Außerdem können Sie sich mit eventsEnabled: true, was Methode 3 intensiv nutzen wird. Das Chrome DevTools-Team dokumentiert beide Aufrufe, und die Chrome DevTools-Protokollreferenz ist die maßgebliche Quelle, wenn sich das Verhalten zwischen Chrome-Versionen ändert.
Praktischer Rat: Verwenden Sie Browser.setDownloadBehavior für neuen Code. Verwenden Sie Page.setDownloadBehavior nur als Fallback für sehr alte Chrome-Versionen, die du nicht aktualisieren kannst. Und übergebe immer einen absoluten Pfad; relative Pfade sind nicht nur riskant, sie schlagen auch stillschweigend fehl.
Auslösen des Klicks und Abfragen des Abschlusses
Der Aufruf von await page.click(selector) gibt den Moment zurück, in dem das Klick-Ereignis ausgelöst wird, was weit vor dem Zeitpunkt liegt, an dem die Bytes übertragen werden. Um zu wissen, wann der Download tatsächlich abgeschlossen ist, benötigen wir einen Helfer, der den Download-Ordner überwacht und Chromes temporäre Dateien ignoriert. Chrome schreibt in something.pdf.crdownload während der Download läuft und benennt die Datei dann in ihren endgültigen Namen um, sobald die Bytes festgeschrieben sind. Unser Hilfsprogramm wartet sowohl auf die Umbenennung als auch auf ein Zeitfenster mit stabiler Dateigröße, was vor unvollständigen Dateien bei langsamen Verbindungen und seltsamen Dateisystemen schützt.
// waitForRealFile.js
import fs from 'fs/promises';
import path from 'path';
export async function waitForRealFile(dir, knownBefore, {
timeoutMs = 90_000,
stableChecks = 3,
intervalMs = 250,
} = {}) {
const deadline = Date.now() + timeoutMs;
let lastSize = -1;
let stable = 0;
let candidate = null;
while (Date.now() < deadline) {
const entries = await fs.readdir(dir);
const fresh = entries.filter(
(n) => !knownBefore.has(n) && !n.endsWith('.crdownload'),
);
if (fresh.length) {
candidate = path.join(dir, fresh[0]);
const { size } = await fs.stat(candidate);
if (size === lastSize && size > 0) {
if (++stable >= stableChecks) return candidate;
} else {
stable = 0;
lastSize = size;
}
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Download did not finish within ${timeoutMs}ms`);
}Die Standardeinstellungen – ein Timeout von 90 Sekunden, drei Überprüfungen der stabilen Größe und ein Abfrageintervall von 250 ms – sind ein vernünftiger Ausgangspunkt für Dateien im Bereich von mehreren zehn MB. Erhöhen Sie das Timeout für größere Downloads und verringern Sie es für schnelle Endpunkte, bei denen Sie lieber schnell scheitern möchten.
Der Ablauf auf der aufrufenden Seite sieht wie folgt aus:
const before = new Set(await fs.readdir(DOWNLOAD_DIR));
await page.click('[data-testid="download-button"]');
const finalPath = await waitForRealFile(DOWNLOAD_DIR, before);
console.log('Downloaded:', finalPath);Ein Hinweis zur Integrität: waitForRealFile ist heuristisch. Chrome kann in seltenen Fällen eine Datei umbenennen, bevor sie vollständig geschrieben wurde, insbesondere bei Netzwerk-Dateisystemen. Wenn Sie stärkere Garantien benötigen, kombinieren Sie diesen Helper mit dem CDP Browser.downloadProgress aus Methode 3, wo das state: 'completed' Signal verlässlicher ist (wenn auch, wie wir sehen werden, immer noch nicht absolut).
Vollständiges Skript für Methode 1 und häufige Fehlerfälle
Zusammenfassung method1.js:
// method1.js
import fs from 'fs/promises';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
import { waitForRealFile } from './waitForRealFile.js';
const TARGET_URL = 'https://example.com/reports';
const DOWNLOAD_SELECTOR = '[data-testid="download-report"]';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: false,
});
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
const before = new Set(await fs.readdir(DOWNLOAD_DIR));
await page.click(DOWNLOAD_SELECTOR);
const finalPath = await waitForRealFile(DOWNLOAD_DIR, before);
console.log('Saved to:', finalPath);
await browser.close();
})();Ein paar Dinge, die dieses Skript richtig macht, die die meisten Tutorials jedoch auslassen:
- Es verwendet
networkidle2, sodass der Download-Button im DOM vorhanden und gebunden ist, bevor wir klicken. Klicken Sie zu früh, lösen Sie den Klick aus, bevor das JavaScript, das ihn verarbeitet, geladen wurde. - Es erstellt vor dem Klicken einen Snapshot des Verzeichnisses, sodass eine übrig gebliebene Datei aus einem früheren Durchlauf nicht als neuer Download gemeldet wird.
- Es schließt den Browser explizit; andernfalls kann der Node-Prozess an einem noch geöffneten Chrome hängen bleiben.
Häufige Fehler und was zu überprüfen ist:
- Es wird überhaupt nichts heruntergeladen. Stelle sicher,
Browser.setDownloadBehaviorvor der Navigation ausgeführt wurde und dassdownloadPathabsolut ist. Ein relativer Pfad ist der häufigste stille Fehler. - Der Selektor wird angeklickt, aber es passiert nichts. Der „Download“ könnte eher eine Navigation als ein Download sein. Beobachten Sie die Seite im Head-Modus; wenn sich die URL ändert, anstatt einen Speicherdialog auszulösen, wechseln Sie zu Methode 2 oder Methode 4, um die Bytes direkt zu erfassen.
- Der Download bleibt
.crdownload. Entweder ist der Server abgestürzt, Ihr Timeout ist zu kurz, oder die Seite wurde geschlossen, bevor der Download abgeschlossen war. Erhöhen SietimeoutMsund stellen Sie sicher, dass Siebrowser.close(), biswaitForRealFileaufgelöst ist. - Headless funktioniert lokal, aber nicht in CI. Container-Chromes werden manchmal ohne Schreibrechte für den Download-Pfad oder mit strengeren Sandbox-Richtlinien ausgeliefert. Erstelle den Ordner vorab und übergebe
--no-sandboxnur, wenn du die sicherheitstechnischen Auswirkungen verstehst.
Ein weiterer Fehler, der leicht übersehen wird: Ein Skript nach Methode 1, das beim ersten Mal funktioniert und beim zweiten Durchlauf fehlschlägt, weil der vorherige Durchlauf eine report.pdf.crdownload im Ordner zurückgelassen hat und der neue Klick nun blockiert wird oder die Datei in report (1).pdf. Löschen Sie *.crdownload und alle übrig gebliebenen Ausgabedateien zu Beginn jedes Laufs, damit der Verzeichnis-Snapshot sauber ist, bevor du klickst. Die before Einstellung in waitForRealFile schützt Sie nur vor Dateien, die zum Zeitpunkt des Snapshots bereits existierten, nicht vor solchen, die Chrome für Sie mit einem deduplizierten Dateinamen generiert hat, den Sie nicht erwartet haben.
Methode 2: Die Datei innerhalb der Seite abrufen und an Node.js weiterleiten
Methode 1 funktioniert, solange Chrome bereit ist, den Download für Sie zu übernehmen. Manche Websites sind nicht so entgegenkommend. Sie generieren die Datei-URL in JavaScript, sperren sie hinter Cookies, die erst nach einer mehrstufigen SPA-Anmeldung existieren, oder geben Ihnen eine blob: URL, die Chrome selbst erstellt hat und die kein externer HTTP-Client auflösen kann. In all diesen Fällen ist der einzige Ort, an dem die Datei abgerufen werden kann, die Seite selbst, da die Seite bereits über die richtige Sitzung verfügt.
Methode 2 läuft fetch() innerhalb page.evaluate(), liest den Antworttext im Browser und sendet die Bytes über die Serialisierungsschicht von Puppeteer zurück an Node. Da page.evaluate() nur JSON-serialisierbare Werte zurückgeben kann, müssen Binärdaten kodiert werden, und die universelle Lösung ist Base64. Node dekodiert sie, schreibt einen Buffer auf die Festplatte, und schon hast du deine Datei.
Diese Methode eignet sich besonders für:
- authentifizierte SPAs, bei denen Cookies und Header innerhalb der Seite leichter „ausgeliehen“ werden können, als sie zu erfassen und erneut abzuspielen.
- Dateien, die über Blob-URLs, Objekt-URLs oder durch In-Memory-Generierung bereitgestellt werden (PDF-Berichte, die in JavaScript erstellt werden, sind ein klassisches Beispiel).
- CORS-freundliche Endpunkte, bei denen die Seite selbst die Datei herunterladen darf.
Sie hat Schwierigkeiten bei:
- Sehr große Dateien, da Base64 die Nutzlast um ca. 33 % vergrößert und die Übertragung über V8 CPU- und speicherintensiv ist.
- Nicht-CORS-Endpunkte, die die Seite nicht abrufen darf (es gelten weiterhin die Browser-Regeln).
Im Folgenden behandeln wir zunächst das Muster für kleine bis mittelgroße Dateien, dann eine Chunked-Variante, die den Fall von mehreren hundert MB bewältigt, ohne Ihren Node-Prozess zum Absturz zu bringen.
Verwendung von `page.evaluate` mit `fetch`, um die Antwort als Blob zu lesen
Innerhalb von page.evaluate(), fetch() verhält sich genau wie ein normaler Browser-Fetch. Es berücksichtigt Cookies für Same-Origin-Anfragen, folgt Weiterleitungen und beachtet CORS. Das macht es hier so leistungsstark: Wenn die Seite die Datei sehen kann, kann Ihr Skript das auch.
const base64 = await page.evaluate(async (fileUrl) => {
const res = await fetch(fileUrl, { credentials: 'include' });
if (!res.ok) {
throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
}
const buf = await res.arrayBuffer();
// Convert ArrayBuffer to base64 inside the browser.
let binary = '';
const bytes = new Uint8Array(buf);
const chunkSize = 0x8000; // 32 KB stride to avoid stack issues
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + chunkSize),
);
}
return btoa(binary);
}, fileUrl);Zwei Implementierungsdetails, die es zu beachten gilt. Erstens: String.fromCharCode.apply(null, bigArray) sprengt den Aufrufstapel, wenn Sie Dutzende von Megabyte auf einmal übergeben, weshalb wir den Puffer in 32-KB-Schritten durchlaufen, bevor wir btoa. Zweitens credentials: 'include' macht dies überhaupt erst zu einem „Puppeteer-Fetch-Download“-Muster; ohne diese Funktion gehen die Session-Cookies verloren und die Anfrage ist nicht mehr authentifiziert.
Du kannst dasselbe Muster für einen Puppeteer-PDF-Download-Anwendungsfall anpassen, bei dem die URL dynamisch in der SPA aufgebaut wird: Extrahiere die URL aus dem data- eines Buttons oder eines JS-Callbacks extrahieren, übergeben Sie sie an page.evaluate()und lassen Sie die Seite den Abruf durchführen. Die zurückgegebenen Bytes sind einfach nur Bytes; das Quellformat spielt für Node keine Rolle.
Wenn fetch() mit einem CORS-Fehler fehlschlägt, teilt Ihnen der Browser damit mit, dass die Seite den Antworttext nicht lesen darf. Sie haben zwei Möglichkeiten: Wechseln Sie zu Methode 1 und lassen Sie Chrome den Download steuern (CORS gilt nicht für Navigationen oder Downloads) oder wechseln Sie zu Methode 4 und wiederholen Sie die Anfrage von Node aus, wo die Same-Origin-Policy nicht gilt.
Rückgabe von Base64 an Node und Schreiben des Puffers auf die Festplatte
Sobald base64 wieder in Node ist, ist der Rest einfach. Buffer.from(base64, 'base64') dekodiert es, fs.writeFile speichert es auf der Festplatte und Buffer.byteLength ermöglicht es dir, die Größe anhand eines Content-Length , die Sie zuvor abgerufen haben:
import fs from 'fs/promises';
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
const TARGET_URL = 'https://example.com/report-page';
const FILE_URL_SELECTOR = 'a#download-link';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
const fileUrl = await page.$eval(FILE_URL_SELECTOR, (a) => a.href);
const base64 = await page.evaluate(async (url) => {
const res = await fetch(url, { credentials: 'include' });
const buf = await res.arrayBuffer();
let binary = '';
const bytes = new Uint8Array(buf);
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + 0x8000),
);
}
return btoa(binary);
}, fileUrl);
const buffer = Buffer.from(base64, 'base64');
console.log('Bytes from page.evaluate:', buffer.byteLength);
const outPath = path.join(DOWNLOAD_DIR, 'report.pdf');
await fs.writeFile(outPath, buffer);
console.log('Saved to:', outPath);
await browser.close();
})();Bei einem echten Durchlauf mit einer kleinen PDF-Datei protokolliert dieses Skript etwa Folgendes Bytes from page.evaluate: 3672808 und schreibt die Datei dann in einem einzigen fs.writeFile. Die Byteanzahl ist ein nützlicher Indikator: Wenn Sie 5 MB erwartet haben und 80 KB erhalten haben, haben Sie mit ziemlicher Sicherheit eine HTML-Fehlerseite anstelle eines PDFs zurückerhalten, und Sie sollten die ersten paar Bytes des Puffers überprüfen, um dies vor dem Speichern zu bestätigen.
Dieses Muster funktioniert bis zu etwa 50 MB. Darüber hinaus beginnt die Base64-Zeichenkette selbst, den Heap von Node zu dominieren (jedes Zeichen ist in V8 zwei Bytes groß), und es kommt zu JavaScript heap out of memory Fehler auftreten. Das ist es, was der nächste Unterabschnitt löst.
Streaming großer Dateien mit chunked base64
Bei Dateien mit mehreren hundert MB ist die Rückgabe einer einzigen Base64-Zeichenkette von page.evaluate() ist ein sicheres Rezept für einen Absturz wegen Speichermangels. Die Lösung besteht darin, die Antwort als Stream im Browser zu lesen, sie in etwa 1 MB große Chunks zu zerlegen, jeden Chunk als Base64 zu kodieren und sie nacheinander an Node zurückzusenden. Auf der Node-Seite dekodieren Sie jeden Chunk in einen Buffer und hängen ihn an einen Schreib-Stream an, sodass die gesamte Datei niemals im RAM gespeichert wird.
Das Muster verwendet expose function , um dem Browser eine Möglichkeit zu geben, Node zurückzurufen, sowie ReadableStream.getReader() um den Antworttext Stück für Stück zu durchlaufen:
import fs from 'fs';
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
const FILE_URL = 'https://example.com/big-archive.zip';
const OUT_PATH = path.join(DOWNLOAD_DIR, 'big-archive.zip');
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const out = fs.createWriteStream(OUT_PATH);
let written = 0;
await page.exposeFunction('onChunk', async (b64) => {
const buf = Buffer.from(b64, 'base64');
written += buf.byteLength;
if (!out.write(buf)) {
// Apply backpressure if the write stream is saturated.
await new Promise((r) => out.once('drain', r));
}
});
await page.exposeFunction('onDone', () => {
out.end();
console.log('Total bytes:', written);
});
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.evaluate(async (url) => {
const res = await fetch(url, { credentials: 'include' });
const reader = res.body.getReader();
const CHUNK = 1 << 20; // 1 MB target
let pending = new Uint8Array(0);
const flush = (bytes) => {
let binary = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
binary += String.fromCharCode.apply(
null,
bytes.subarray(i, i + 0x8000),
);
}
return window.onChunk(btoa(binary));
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
const merged = new Uint8Array(pending.length + value.length);
merged.set(pending, 0);
merged.set(value, pending.length);
pending = merged;
while (pending.length >= CHUNK) {
await flush(pending.subarray(0, CHUNK));
pending = pending.subarray(CHUNK);
}
}
if (pending.length) await flush(pending);
await window.onDone();
}, FILE_URL);
await browser.close();
})();Ein paar Dinge, die man verinnerlichen sollte. page.exposeFunction fügt der Seite eine globale Variable hinzu, die bei Aufruf auf einen Handler auf der Node-Seite wartet. Wir nutzen sie, um Base64-Blöcke direkt in einen Schreib-Stream zu schieben, sodass sich die Bytes nie im V8-Speicher ansammeln. Wir berücksichtigen auch den Backpressure: Wenn out.write() returns false, warten wir auf 'drain' , bevor wir fortfahren. Ohne diese Vorgehensweise würden ein schnelles Netzwerk und eine langsame Festplatte die gesamte Datei letztendlich ohnehin in Node zwischenspeichern, was den Sinn zunichte machen würde.
Die Chunk-Größe von 1 MB ist ein Kompromiss. Kleinere Chunks bedeuten mehr Roundtrips zwischen der Seite und Node sowie mehr Base64-Overhead pro Aufruf. Größere Chunks verringern den Overhead, belegen aber mehr Speicher im Browser. Ein MB ist ein vernünftiger Ausgangspunkt; passe ihn an deine Arbeitslast an.
Wann In-Page-Fetch die richtige Wahl ist (Authentifizierung, SPA, Blob-URLs)
Methode 2 ist die richtige Wahl, wenn die Datei nur innerhalb der Browsersitzung „existiert“ und Methode 1 sie aus einem von drei Gründen nicht erreichen kann.
Der erste Grund ist eine Cookie- oder Token-basierte Authentifizierung, die gegen Replay-Angriffe geschützt ist. Einige Websites binden die Sitzung an Fingerabdrücke (User-Agent plus IP plus ein CSRF-Token in einem Nicht-Cookie-Speicher), und die Reproduktion dieser Daten außerhalb des Browsers ist schwierig. Das In-Page-Fetch umgeht dies vollständig, da die Anfrage von der Seite stammt, die die Sitzung besitzt.
Der zweite Grund sind SPA-generierte Downloads. Ein Klick auf eine Schaltfläche führt JavaScript aus, das eine Bloberstellt, an URL.createObjectURLund löst über einen synthetischen <a download> Klick auslöst. Die URL sieht etwa so aus blob:https://app.example.com/abc-123 und kann nur von der Ursprungsseite aufgelöst werden. Methode 1 könnte den resultierenden Download erfassen, wenn setDownloadBehavior vorhanden ist, aber Methode 2 ist deterministischer: Erstellen Sie denselben Abruf selbst nach, kodieren Sie das Ergebnis und umgehen Sie den Download-Ablauf von Chrome vollständig.
Die dritte Möglichkeit sind dynamische Export-Endpunkte. APIs, die eine JSON-Nutzlast entgegennehmen, spontan eine CSV- oder PDF-Datei generieren und diese inline zurückgeben, lassen sich leicht mit page.evaluate() , da man JSON.stringify die Nutzlast, einen POST senden und die Antwort als Stream lesen.
Wann der In-Page-Fetch ungeeignet ist: sehr große Dateien (siehe oben), Dateien hinter CORS, auf die die Seite keinen Zugriff hat, und alle Fälle, in denen eine einfache Axios-Anfrage von Node einfach funktionieren würde. Verwenden Sie das einfachste Tool, das die Bytes liefert.
Methode 3: Downloads mit dem Chrome DevTools Protocol steuern
Methode 1 nutzt CDP im Hintergrund, behandelt es jedoch als Vorbereitungsschritt. Bei Methode 3 steht CDP im Mittelpunkt. Wenn Sie einen Echtzeit-Fortschrittsbalken benötigen, wenn Sie parallele Downloads ausführen und jeden einzelnen dem Klick zuordnen müssen, der ihn ausgelöst hat, oder wenn Sie Unterbrechungen frühzeitig erkennen möchten, benötigen Sie die CDP-Ereignisse auf Browserebene: Browser.downloadWillBegin und Browser.downloadProgress. Sie liefern dir eine GUID pro Download, den vorgeschlagenen Dateinamen, die Gesamtbytezahl (sofern bekannt), die bisher empfangenen Bytes und eine Zustandsmaschine von inProgress, completedund canceled.
Dies ist dasselbe Protokoll, das auch das DevTools-Panel von Chrome verwendet, und es kommt einer „echten“ Download-API näher als alles, was Puppeteer nativ bereitstellt. Der Haken ist, dass es eine Ebene unter page.click()liegt, sodass Sie es explizit einbinden und auf Ereignisse in der CDP-Sitzung warten müssen, anstatt auf ein Puppeteer-Promise zu warten.
Wann Sie Methode 3 wählen sollten:
- Sie müssen den Fortschritt einem Benutzer anzeigen oder ihn an eine Job-Warteschlange übergeben.
- Sie führen parallele Puppeteer-Download-Jobs aus und müssen Dateinamen dem Kontext zuordnen.
- Sie möchten ein eindeutiges Signal „Dieser Download wurde abgebrochen“ erhalten, anstatt dies anhand des Dateisystems zu erraten.
- Sie möchten eine zuverlässige Headless-Download-Lösung mit Puppeteer, die nicht von der Legacy-Lösung abhängt
Page.setDownloadBehavior.
Wann Sie diese Methode überspringen sollten:
- Sie benötigen jeweils nur eine Datei und Methode 1 reicht aus.
- Sie können die URL abrufen und Axios verwenden; die CDP-Integration lohnt sich in diesem Fall selten angesichts der Komplexität.
Öffnen einer CDP-Sitzung mit page.createCDPSession
In Puppeteer stehen zwei CDP-Sitzungen zur Auswahl: die seitenbezogene und die browserbezogene. Für Methode 3 benötigen wir die browserbezogene Sitzung, da Download-Ereignisse auf Browserebene ausgelöst werden und Browser.setDownloadBehavior es sich um eine Methode auf Browserebene handelt.
const session = await browser.target().createCDPSession();Vergleichen Sie dies mit await page.createCDPSession(), das seitenbezogen ist. Seitensitzungen funktionieren weiterhin für Navigations-, Netzwerk- und Laufzeitaufrufe, die auf eine Seite beschränkt sind, erkennen jedoch keine Downloads auf Browserebene, wenn Chrome diese über das Browser-Ziel weiterleitet (was in neueren Versionen der Trend ist).
Ein nützliches mentales Modell: Eine CDP-Sitzung ist ein typisierter WebSocket zu einem Ziel. browser.target() ist das Browser-Ziel, page.target() ist ein Seitenziel, und beide empfangen unterschiedliche Ereignisse. Eine Verwechslung dieser beiden ist eine häufige Ursache für „Mein Listener wird nie ausgelöst“-Fehler in Methode 3. Wenn Ihr Browser.downloadProgress Listener nicht reagiert, überprüfen Sie noch einmal, ob Sie die Sitzung auf browser.target()und nicht auf der Seite geöffnet haben.
Sie können mehrere CDP-Sitzungen gleichzeitig offen haben, darunter eine pro Seite sowie eine auf der Browser-Ebene. Für Download-Aufgaben reicht eine einzige Sitzung auf Browser-Ebene aus.
Browser.setDownloadBehavior und Abhören von downloadWillBegin / downloadProgress
Konfigurieren Sie mit der Browser-Sitzung das Download-Verhalten und abonnieren Sie Ereignisse:
const downloads = new Map(); // guid -> { filename, totalBytes, received, state }
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true, // turn on downloadWillBegin / downloadProgress
});
session.on('Browser.downloadWillBegin', (event) => {
// event: { guid, url, suggestedFilename, frameId }
downloads.set(event.guid, {
filename: event.suggestedFilename,
received: 0,
totalBytes: 0,
state: 'inProgress',
});
console.log(`Starting download: ${event.suggestedFilename}`);
});
session.on('Browser.downloadProgress', (event) => {
// event: { guid, totalBytes, receivedBytes, state }
const entry = downloads.get(event.guid);
if (!entry) return;
entry.totalBytes = event.totalBytes;
entry.received = event.receivedBytes;
entry.state = event.state;
if (event.totalBytes > 0) {
const pct = ((event.receivedBytes / event.totalBytes) * 100).toFixed(1);
process.stdout.write(` ${entry.filename}: ${pct}%\r`);
}
if (event.state === 'completed') {
console.log(`\nFinished: ${entry.filename}`);
} else if (event.state === 'canceled') {
console.warn(`\nCanceled: ${entry.filename}`);
}
});Einige Muster, die es sich zu verinnerlichen lohnt:
- Das
guidFeld ist Ihr Schlüssel zur Verfolgung paralleler Downloads. Chrome weist jedem Download eine neue GUID zu, und dassuggestedFilenameist der Name, unter dem die Datei auf der Festplatte gespeichert wird (vorbehaltlich von Kollisionen, bei denen Chrome(1),(2), usw. an). totalByteskann0, wenn der Server keinContent-Length. In diesem Fall können Sie keinen Prozentsatz anzeigen, sondern nur eine laufende Byte-Zählung. Planen Sie Ihre Benutzeroberfläche entsprechend.state: 'completed'ist ein starkes Signal dafür, dass der Download abgeschlossen ist, aber es ist keine absolute Garantie dafür, dass die Datei vollständig auf die Festplatte geschrieben wurde. Chrome meldet den Abschluss möglicherweise kurz vor der Umbenennung oder dem endgültigen Schreiben, daher ist eine kurze Überprüfung der stabilen Größe zusätzlich zu dem Ereignis immer noch eine gute Idee.state: 'canceled'umfasst vom Benutzer abgebrochene Downloads (selten bei Headless) und abgebrochene Downloads (Netzwerkausfall, Server-Absturz). Behandle beide gleich: Wiederholen oder deutlich als Fehler melden.
Wenn Sie eventsEnabled: true, erhalten Sie den Download, aber keine Ereignisse, was Sie wieder in den Bereich von Methode 1 (Polling) zurückversetzt. Entscheiden Sie sich immer für Methode 3.
Für eine strengere Überprüfung, ob „die Datei wirklich auf der Festplatte ist“, kombinieren Sie das 'completed' Ereignis mit einem kurzen waitForFileStable Helper, ähnlich dem in Methode 1, aber strenger (Timeout 30 Sekunden, drei Stabilitätsprüfungen):
async function waitForFileStable(filePath, {
timeoutMs = 30_000,
stableChecks = 3,
intervalMs = 200,
} = {}) {
const deadline = Date.now() + timeoutMs;
let last = -1, stable = 0;
while (Date.now() < deadline) {
try {
const { size } = await fs.stat(filePath);
if (size === last && size > 0) {
if (++stable >= stableChecks) return size;
} else {
stable = 0; last = size;
}
} catch {}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`File never stabilized: ${filePath}`);
}Jetzt hast du beide Signale: CDP sagt „fertig“, und das Dateisystem stimmt zu.
Vollständiges Skript für Methode 3 mit Fortschrittsprotokollierung
// method3.js
import path from 'path';
import { launchBrowser, newPage, DOWNLOAD_DIR } from './launch.js';
import { waitForFileStable } from './waitForFileStable.js';
const TARGET_URL = 'https://example.com/reports';
const SELECTOR = '[data-testid="download-report"]';
(async () => {
const browser = await launchBrowser();
const page = await newPage(browser);
const session = await browser.target().createCDPSession();
await session.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: DOWNLOAD_DIR,
eventsEnabled: true,
});
let resolveDone, rejectDone;
const done = new Promise((r, j) => { resolveDone = r; rejectDone = j; });
let lastFilename = null;
session.on('Browser.downloadWillBegin', (e) => {
lastFilename = e.suggestedFilename;
console.log('Begin:', e.guid, '->', e.suggestedFilename);
});
session.on('Browser.downloadProgress', async (e) => {
if (e.state === 'completed') {
const finalPath = path.join(DOWNLOAD_DIR, lastFilename);
try {
await waitForFileStable(finalPath);
resolveDone(finalPath);
} catch (err) { rejectDone(err); }
} else if (e.state === 'canceled') {
rejectDone(new Error('Download canceled'));
}
});
await page.goto(TARGET_URL, { waitUntil: 'networkidle2' });
await page.click(SELECTOR);
const finalPath = await done;
console.log('Saved to:', finalPath);
await browser.close();
})();Was dieses Skript Ihnen gegenüber Methode 1 bietet: deterministische Fertigstellung (Sie wissen über Ereignisse genau, wann der Download beginnt und endet, statt zu raten), Fortschritt in Echtzeit (der downloadProgress Handler wird alle paar hundert KB ausgelöst) und explizite Abbruchbehandlung. Es lässt sich auch sauber auf N parallele Downloads verallgemeinern: Halten Sie eine Map<guid, Promise>, löse jedes Promise innerhalb des Handlers auf und Promise.all das Ganze.
In der Produktion solltest du das Skript normalerweise done in ein Timeout ein, damit ein hängender Download Ihren Worker nicht auf Dauer blockiert. Eine Obergrenze von 5 bis 10 Minuten ist für typische Dateien angemessen. Wenn Sie diese überschreiten, protokollieren Sie die GUID, beenden Sie den Download und versuchen Sie es erneut. CDP bietet Ihnen die Transparenz, diese Entscheidung zu treffen; das Dateisystem allein tut dies nicht.
Ein zweites Muster, das man für Methode 3 kennen sollte: Versprechen pro Download. Anstelle eines einzigen done Promise sollten Sie Map<guid, { resolve, reject }> und erstellen Sie einen Eintrag innerhalb Browser.downloadWillBegin. Der Browser.downloadProgress Handler ruft dann resolve oder reject für den Eintrag, der dem Ereignis entspricht guid. Sobald dies eingerichtet ist, können Sie N Klicks nacheinander auslösen, N Promises sammeln und Promise.all sie ausführen. Der gleiche Handler-Code funktioniert für eine Datei oder fünfzig, und Sie erhalten eine übersichtliche Fehleranzeige pro Datei anstelle eines einzigen globalen Timeouts, das verschleiert, welcher Download tatsächlich fehlgeschlagen ist.
Methode 4: Überspringe den Browser, übergebe die URL an Axios oder https
Manchmal besteht die beste Strategie zum Herunterladen von Dateien mit Puppeteer darin, Puppeteer fast gar nicht zu verwenden. Wenn die Website eine echte, stabile URL für die Datei bereitstellt (selbst wenn du die Seite rendern und herumklicken musst, um sie zu finden), kannst du mit Puppeteer gerade so lange rendern, bis du diese URL sowie den Authentifizierungsstatus extrahiert hast, und dann mit axios oder Nodes integrierter httpsherunterladen. Das Ergebnis ist schneller als Methode 1, speicherschonender als Methode 2 und auf eine Weise trivial parallelisierbar, wie es die Ausführung von N Chrom-Instanzen nicht ist.
Dies ist auch die „langweiligste“ Methode – im positiven Sinne. Sobald die URL vorliegt, ist der Download nur noch ein HTTP-GET. Es gibt keine Regression im Headless-Modus, die man verfolgen muss, keine CDP-Versionsabweichung und keinen .crdownload Sentinel, das abgefragt werden muss. Man übergibt die URL und ein paar Header an Axios, leitet die Antwort an einen Schreibstrom weiter, und schon befindet sich die Datei auf der Festplatte.
Wählen Sie Methode 4, wenn:
- Die Zieldatei unter einer stabilen URL liegt, die Sie aus dem DOM, einer Netzwerkantwort oder einer JS-Variablen extrahieren können.
- Die Datei ist groß und du möchtest echtes Streaming auf die Festplatte ohne Pufferung über V8.
- Sie viele Downloads gleichzeitig ausführen müssen. Ein Pool von Axios-Anfragen ist weitaus kostengünstiger als ein Pool von Headless-Chromes.
Verzichten Sie auf Methode 4, wenn:
- Die Download-URL ist nur einmalig verwendbar, signiert oder auf eine Weise an die Browsersitzung gebunden, die eine Wiederholung unmöglich macht.
- Die Website erzwingt JavaScript-Challenges oder Fingerabdruck-Prüfungen, die Axios ohne erheblichen Aufwand nicht bestehen kann.
Im zweiten Fall tauschen Sie Axios in der Regel gegen eine Anforderungsschicht aus, die diese Prüfungen übernimmt, wobei sich die Struktur des Skripts jedoch nicht ändert.
Extrahieren von Cookies und Headern aus Puppeteer zur Authentifizierung der Anfrage
Der Sinn eines hybriden Ablaufs besteht darin, die Sitzung von Puppeteer zu übernehmen. Sie führen die SPA-Anmeldung oder das von der Website geforderte Verfahren durch und übergeben dann die Cookies und einige wichtige Header an Axios.
async function buildAxiosHeaders(page) {
const cookies = await page.cookies(); // current page's cookies
const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
const userAgent = await page.evaluate(() => navigator.userAgent);
const referer = page.url();
return {
Cookie: cookieHeader,
'User-Agent': userAgent,
Referer: referer,
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
};
}Die vier oben genannten Header decken den Großteil der CDN- und WAF-Prüfungen ab. Cookie trägt die Sitzung, User-Agent stimmt mit dem überein, was die Seite bereits nachgewiesen hat, Referer stimmt mit dem überein, was der Browser beim Klicken auf den Download-Link senden würde, und Accept-Language ist ein kleiner Hinweis darauf, dass gerade ein echter Browser da war. Wenn die Website Sec-Ch-Ua oder andere Client-Hinweise, kopieren Sie diese ebenfalls mit page.evaluate(() => navigator.userAgentData).
Zwei Fallstricke. Erstens page.cookies() gibt standardmäßig Cookies für die aktuelle URL zurück. Wenn die Datei auf einer anderen Subdomain gehostet wird, übergebe diese URL explizit: page.cookies(fileUrl). Andernfalls werden die von Ihnen übermittelten Cookies nicht gesendet. Zweitens setzen einige Websites HttpOnly oder Secure Flags, die Axios problemlos berücksichtigt, aber Cookies mit Pfad-Gültigkeit (Path=/api) ignoriert, es sei denn, du behältst sie beim Erstellen des Headers bei. Die einfachste Lösung besteht darin, Cookies für genau den Ursprung abzurufen, den du aufrufen wirst, und nur Cookies zusammenzufügen, deren path ein Präfix des Pfads der Datei-URL ist.
Wenn du dies nicht manuell umsetzen möchtest, gibt es ausgereifte „axios-cookiejar“-Adapter, die die Cookies von Puppeteer übernehmen und Axios diese pro Anfrage verwalten lassen. Für den gängigen Fall reicht ein einzeiliger Cookie aus. Für tiefergehende Informationen zur Absicherung von Axios-Aufrufen gegen Erkennung bietet sich ein interner axios-headers-Leitfaden als Ergänzung zu diesem Abschnitt an.
Streaming der Antwort mit axios responseType: stream
Der Download selbst ist unkompliziert, wenn Sie responseType: 'stream'. Axios gibt den Antworttext als Node-Stream zurück, und du leitest ihn an einen Schreib-Stream weiter. Die gesamte Datei wird nie im RAM gehalten:
import axios from 'axios';
import fs from 'fs';
import { pipeline } from 'stream/promises';
async function downloadToFile(url, outPath, headers) {
const res = await axios.get(url, {
headers,
responseType: 'stream',
timeout: 30_000,
maxRedirects: 5,
validateStatus: (s) => s >= 200 && s < 400,
});
await pipeline(res.data, fs.createWriteStream(outPath));
}stream.pipeline (oder dessen Promise-Version, die hier verwendet wird) ist die richtige Primitive, da sie Fehler von beiden Seiten weiterleitet und die Streams bei einem Fehler ordnungsgemäß bereinigt. Ein naiver res.data.pipe(write) schluckt Write-Stream-Fehler, was dazu führt, dass man am Ende eine halbgeschriebene Datei und keine Ausnahme hat.
Ein paar produktionsreife Einstellmöglichkeiten:
- Timeouts.
timeout: 30_000ist ein Timeout für den Aufbau der Verbindung. Bei langen Downloads sollten Sie die Pipeline zusätzlich in einen Watchdog einbinden, damit ein langsamer Datenfluss nicht endlos hängt. - Wiederholungsversuche. Hüllen Sie den Aufruf in einen kleinen Wiederholungshelfer mit exponentiellem Backoff, begrenzt auf drei Versuche. Die meisten vorübergehenden Fehler (504, ECONNRESET) werden durch einen Wiederholungsversuch behoben.
- Vermeiden Sie gleichzeitige Schreibvorgänge auf denselben Pfad. Zwei parallele Jobs, die
report.pdf, sind ein stiller Korruptionsfehler. Verwenden Sie einen temporären Dateinamen plus Umbenennung oder verwenden Sie eindeutige Dateinamen pro Job.
Für Parallelität ist ein kleiner Pool die sicherste Standardeinstellung. Drei bis fünf gleichzeitige Axios-Downloads sind eine angemessene Obergrenze, und eine sequenzielle for...of await Schleife ist die sicherste Basis, wenn Sie sich über serverseitige Ratenbeschränkungen nicht sicher sind. Bei mehr als fünf gleichzeitigen Jobs sollten Sie messen, anstatt zu raten.
Reine URL-Downloads ohne Puppeteer
Sobald Sie das URL-Muster herausgefunden haben, können Sie Puppeteer oft ganz weglassen. Ein typischer Hybrid-Lauf nutzt Puppeteer, um ein Suchergebnisraster zu scrapen, pro Ergebnis eine Detailseiten-URL zu extrahieren und dann entweder jede Detailseite aufzurufen, um die Datei-URL zu erfassen, oder – falls das URL-Muster vorhersehbar ist – diese direkt aus der Auflistung abzuleiten.
Ein repräsentativer End-to-End-Ablauf, der fünf Bilddateien herunterlädt, sieht wie folgt aus:
import axios from 'axios';
import fs from 'fs';
import path from 'path';
async function downloadAll(items, headers, outDir) {
for (let i = 0; i < items.length; i++) {
const url = items[i].downloadUrl;
const out = path.join(outDir, `image-${String(i + 1).padStart(3, '0')}.jpg`);
await downloadToFile(url, out, headers);
console.log('Saved', out);
}
}Führen Sie das mit einer Liste von fünf extrahierten URLs durch, und Sie erhalten image-001.jpg durch image-005.jpg auf der Festplatte, ohne dass ein Chrome-Prozess für die eigentliche Übertragung erforderlich ist. Wenn die URLs öffentlich und nicht signiert sind, können Sie Puppeteer bei nachfolgenden Durchläufen komplett überspringen und die URLs einfach direkt aufrufen. Das ist oft der richtige Schritt für tägliche Aktualisierungen eines bekannten Datensatzes; Sie zahlen die Kosten für Puppeteer nur beim ersten Mal, während Sie die URL-Struktur ermitteln.
Die wichtigere Erkenntnis: Betrachten Sie Puppeteer als ein Tool zur Erkennung und Authentifizierung, nicht als Download-Tool. Die Aufgabe des Browsers besteht darin, herauszufinden, wo sich die Daten befinden, und die richtige Sitzung zu bestätigen; der Download selbst kann fast immer von einem kleineren, schnelleren Client durchgeführt werden.
Zwei Betriebsmuster erweitern dies. Erstens: Speichere das erkannte URL-Muster in einer kleinen JSON-Datei oder Datenbank, sortiert nach Website, und führe den Puppeteer-Erkennungsschritt nur dann erneut aus, wenn ein Axios-Abruf einen 404-Fehler oder unerwarteten HTML-Code zurückgibt. Die Datei-URLs der meisten Websites folgen einer stabilen Vorlage (/exports/{id}/{filename}.csv), und sobald man die Vorlage hat, ist für tägliche Aktualisierungen überhaupt kein Browser mehr erforderlich. Zweitens: Wenn die URL signiert ist, die Signaturlogik aber reproduzierbar ist (z. B. HMAC auf einer Request-Nutzlast), sollte man die Signatur einmal rückentwickeln und Puppeteer für dieses Ziel dauerhaft überspringen. Der Ansatz mit der Puppeteer-Download-Datei macht sich beim ersten Kontakt bezahlt; alles danach ist reines HTTP.
Die Wahl der richtigen Puppeteer-Download-Datei-Methode: eine Entscheidungsrubrik
Vier Methoden sind mehr, als die SERP normalerweise anzeigt, und genau das ist der Punkt: Jede hat ihre Nische. Hier ist eine Entscheidungshilfe, die anhand einiger Ja/Nein-Fragen die richtige Methode ermittelt, sowie eine Vergleichstabelle, die Sie während des Lesens dieses Leitfadens geöffnet lassen können.
Beginnen Sie mit den Fragen:
- Haben Sie eine stabile, wiederholbare Datei-URL? Wenn ja, springen Sie zu Frage 2. Wenn nein (die URL ist einmalig, von JS generiert oder nur innerhalb der Seitensitzung gültig), befinden Sie sich im Bereich von Methode 1 oder Methode 2.
- Befindet sich die Datei hinter einer Authentifizierung, die außerhalb des Browsers bestehen bleibt? Wenn Sie Cookies löschen und die Anfrage wiederholen können, ist Methode 4 fast immer die richtige Wahl. Wenn die Authentifizierung browsergebunden ist (CSRF-Token im JS-Speicher gespeichert, Sitzungs-Fingerprint), verwenden Sie Methode 2.
- Ist die Datei sehr groß (mehr als ~100 MB) oder führen Sie viele parallel aus? Dann ist Methode 4 die beste Wahl. Das Streamen mit Axios ist kostengünstiger als das Ausführen von N Chrome-Instanzen, und die Base64-Roundtrips in Methode 2 sind nicht skalierbar.
- Benötigen Sie Fortschrittsereignisse oder ein eindeutiges Abbruchsignal? Methode 3 ist die einzige, die Ihnen beides direkt aus Chrome liefert.
- Wird der Download durch einen Klick ausgelöst, dessen URL Sie nicht ohne Weiteres überprüfen können? Methode 1 ist die einfachste Lösung und reicht in der Regel aus.
|
Methode |
Am besten geeignet für |
Zu vermeiden bei |
Speicherprofil |
Auth-Modell |
|---|---|---|---|---|
|
JS-ausgelöste Downloads, unbekannte URLs |
Sehr große Dateien, Fortschrittsanzeige |
Niedrig (Chrome streamt auf die Festplatte) |
Was auch immer der Klick sieht |
|
SPAs, Blob-URLs, browsergebundene Authentifizierung |
Dateien mit mehreren hundert MB |
Hoch ohne Chunking |
Browser-Cookies, automatische |
|
Parallele Jobs, Fortschritt, Abbruch |
Einmalige kleine Dateien |
Niedrig (Chrome streamt auf die Festplatte) |
Was auch immer der Klick sieht |
|
Große Dateien, parallele Pipelines, bekannte URLs |
Einmalig verwendbare signierte URLs |
Niedrig (echtes Streaming) |
Wiederverwendete Cookies + Header |
Eine allgemeine Regel: Bevorzuge die Methode, die am wenigsten Puppeteer nutzt und trotzdem funktioniert. Methode 4 ist die Standardmethode, wenn die URL bekannt ist. Methode 1 ist die Standardmethode, wenn sie nicht bekannt ist. Methode 3 ist das, was Methode 1 hätte sein sollen, wenn du Parallelität oder Fortschrittsanzeige benötigst. Methode 2 ist der Ausweg für alles andere.
Im Zweifelsfall erst einmal Methode 4 prototypisieren. Wenn sie funktioniert, wirst du froh sein, dass du nicht für jede Datei einen Chrome-Prozess gestartet hast. Wenn nicht, weißt du innerhalb von Minuten, ob die Authentifizierung das Problem ist (Methode 2) oder die URL (Methode 1).
Produktionsoptimierung: Timeouts, Wiederholungsversuche und Integritätsprüfungen
Ein Puppeteer-Skript zum Herunterladen von Dateien, das auf Ihrem Laptop funktioniert und in der Produktion abstürzt, scheitert fast immer aus einem von vier Gründen: einem Timeout, das Sie vergessen haben einzustellen, einem Wiederholungsversuch, den Sie vergessen haben zu schreiben, einem .crdownload Sentinel, den Sie vergessen haben zu bereinigen, oder eine unvollständige Datei, die Sie als vollständig behandelt haben. Hier ist die Checkliste, die wir mit Skripten durchgehen, bevor sie live gehen.
Timeouts auf jeder Ebene. Setzen timeout auf page.goto (Standardwert ist 30 s, oft zu knapp bei kalten Caches) ein explizites Timeout in deinem waitForRealFile Helper, ein Axios timeout für Methode 4 und eine zeitliche Obergrenze für den gesamten Job. CI-Hänger sind in der Regel auf das Fehlen einer dieser Maßnahmen zurückzuführen, nicht auf das Vorhandensein eines echten Fehlers.
Wiederholungsversuche mit Backoff. Hüllen Sie den netzwerkbezogenen Aufruf in einen Retry-Helper mit exponentiellem Backoff, begrenzt auf drei Versuche, mit einem endgültigen Hard-Fail. Versuchen Sie es erneut bei ECONNRESET, ETIMEDOUT, 5xx-Antworten und bei allem, was nach einem vorübergehenden Problem aussieht. Versuchen Sie es nicht erneut bei 401, 403 oder 404, da diese Fehler in Ihrem Code signalisieren.
Bereinigen Sie .crdownload Dateien zwischen den Durchläufen. Chrome lässt diese zurück, wenn ein Download abgebrochen wird oder der Prozess vorzeitig beendet wird. Wenn Sie das Skript erneut ausführen, waitForRealFile den veralteten Sentinel möglicherweise auf und meldet die falsche Datei als neu. Löschen Sie .crdownload, .tmpund Ihre eigenen Arbeitsdateien zu Beginn jedes Durchlaufs.
Überprüfen Sie die Integrität, nicht nur die Existenz. Bei wichtigen Payloads sind drei Überprüfungsebenen sinnvoll: Datei existiert, Dateigröße stimmt mit der erwarteten Content-Length (sofern der Server eine bereitstellt) und eine Prüfsumme, falls die Quelle eine veröffentlicht. Ein schneller MD5- oder SHA-256-Vergleich crypto.createHash('sha256') ist bei Dateien mit mehreren GB schnell und erkennt Beschneidungen, die eine naive Existenzprüfung übersieht.
Begrenzen Sie die Parallelität, parallelisieren Sie nicht einfach. Drei bis fünf gleichzeitige Downloads sind ein vernünftiger Standardwert; darüber hinaus konkurrieren Sie mit sich selbst um Festplattenspeicher und Bandbreite, und viele Websites verschärfen ihre Ratenbegrenzungen. Ein p-limit Pool-Ansatz plus Parallelitätsbeschränkungen pro Host ist ein kleiner Code-Aufwand, der viele Störungsmeldungen verhindert.
Protokollieren Sie GUID-zu-Dateinamen-Zuordnungen (Methode 3) oder URL-zu-Ausgabe-Zuordnungen (Methode 4). Wenn um 3 Uhr morgens etwas schiefgeht, rettet Sie ein strukturiertes Protokoll mit dem Eintrag „Diese URL hat diese Datei mit dieser Byteanzahl und diesem Status erzeugt“. Bewahren Sie die Protokolle auf.
Quarantänieren Sie unvollständige Dateien. Wenn ein Download mitten im Prozess fehlschlägt, sind die unvollständigen Bytes „radioaktiv“. Verschieben Sie sie in ein partial/ Verzeichnis, lassen Sie sie nicht dort, wo die nächste Stufe Ihrer Pipeline sie lesen kann, als wären sie vollständig. Eine unvollständige Datei, die vollständig aussieht, ist die teuerste Fehlerklasse in der Download-Automatisierung.
Blockierungen bei automatisierten Downloads vermeiden
Selbst wenn Ihr Puppeteer-Download-Dateifluss auf der Ebene der Dateiverarbeitung absolut sicher ist, kann die Anfrage selbst blockiert werden, bevor sie überhaupt Bytes erzeugt. CDNs, WAFs und Anti-Bot-Anbieter prüfen dieselben Fingerabdrücke, egal ob Sie HTML scrapen oder eine 200-MB-CSV-Datei herunterladen, daher gelten dieselben Abwehrmaßnahmen.
Die kostengünstigste und effektivste Absicherung besteht aus drei Headern und einer IP-Entscheidung:
- Realistischer User-Agent. Verwenden Sie einen aktuellen Chrome-Desktop-UA, der mit der gebündelten Chrome-for-Testing-Version übereinstimmt, nicht den Puppeteer-Standard. Einige Hosts blockieren den Standard-UA sofort.
- Passender Viewport. Ein Viewport von 1366x900 entspricht einer echten Desktop-Sitzung. Ein Viewport von 800x600 schreit geradezu nach „Automatisierung“.
- Referer. Setzen Sie
Refererauf die Seite, die auf die Datei verlinkt hat. WAFs geben häufig einen 403-Fehler bei direkten Zugriffen auf Assets ohne Referer aus, insbesondere bei PDFs und Bildern. - Sinnvolle IP. Rechenzentrums-IPs gängiger Cloud-Anbieter werden von den meisten Anti-Bot-Anbietern vorab markiert. Wenn Ihre Downloads in echten Browsern 403-Fehler erhalten, aber erfolgreich sind, wenn Sie über ein VPN eine private Verbindung nutzen, haben Sie ein IP-Problem, kein Skriptproblem.
Ein paar zusätzliche Maßnahmen helfen in hartnäckigen Fällen. Fügen Sie eine kurze slowMo (50 bis 200 ms) ein, um die Klicks zu streuen. Verwenden Sie page.waitForTimeout nach goto , damit sich JavaScript-basierte Bot-Prüfungen beruhigen können. Verteilen Sie Aufträge mit mehreren Dateien, damit Sie nicht N Zugriffe in derselben Sekunde ausführen.
Wenn Sie alle oben genannten Schritte durchgeführt haben und die Website Sie immer noch blockiert, ist es sinnvoller, die Anforderungsschicht auszulagern, anstatt weiter an den Headern zu feilen. Tools wie unser scraping-freundliches Residential-Proxy-Netzwerk oder unser Scraper-API-Endpunkt bei WebScrapingAPI kümmern sich hinter einer einzigen Anfrage um Proxy-Rotation, IP-Reputation und die schwierigeren Fingerprinting-Prüfungen, sodass sich Ihr Puppeteer-Code ganz auf das Aufrufen der Seite konzentrieren kann. Das ist auch die richtige Anlaufstelle, wenn Sie länderspezifische Downloads benötigen oder hinter Challenge-Seiten scrapen müssen.
Dies ist auch ein guter Zeitpunkt, um darüber nachzudenken, ob Sie überhaupt einen vollständigen Headless-Browser benötigen. Die an anderer Stelle auf der Website verlinkte Übersicht über Headless-Browser ist lesenswert, wenn Sie sich noch zwischen einem selbst erstellten Puppeteer-Harness und einer gehosteten Alternative entscheiden müssen.
Puppeteer vs. Playwright für Dateidownloads
Ehrliche Antwort: Playwright hat eine schönere API für Downloads, Puppeteer hat einen direkteren Zugang zu den Interna von Chrome, und beide sind in der Produktion gut geeignet.
Playwright stellt page.waitForEvent('download'), das ein Download Objekt mit Helfern wie download.path(), download.saveAs(path)und download.suggestedFilename(). Für den Basisfall musst du CDP nicht anfassen. Das ist tatsächlich kürzer als die entsprechende Puppeteer-Konfiguration und funktioniert in Chromium, Firefox und WebKit gleich, was bei browserübergreifenden Testsuiten der größere Vorteil ist. Wenn du bei Null anfängst und dein Stack noch nicht auf Puppeteer setzt, ist ein Playwright-Download-Workflow etwa halb so lang.
Die Stärke von Puppeteer liegt darin, dass es näher am Chrome DevTools Protocol ist. Wenn Sie rohe CDP-Ereignisse, benutzerdefinierte Protokollaufrufe oder Verhaltensweisen benötigen, die noch nicht in eine übergeordnete API verpackt wurden, erreicht Puppeteer dies mit einer Vermittlungsschicht weniger. Methode 3 in diesem Leitfaden ist ein gutes Beispiel. Das gleiche Muster funktioniert auch in Playwright (Playwright stellt eine CDP-Sitzung bereit), aber die Idiome von Puppeteer fühlen sich nativ an, da die gesamte Bibliothek auf CDP ausgerichtet ist.
Für eine bereits laufende Puppeteer-Download-Datei-Pipeline ist nichts davon ein Grund für eine Migration. Methode 1 plus Browser.setDownloadBehavior entspricht in Bezug auf die Funktionen waitForEvent('download') in Bezug auf die Funktionen fast genau; man schreibt lediglich ein paar Zeilen mehr. Migrieren Sie zu Playwright, wenn die browserübergreifende Kompatibilität der eigentliche Vorteil ist, nicht nur wegen der Downloads. Wir haben einen ausführlicheren Playwright-Web-Scraping-Leitfaden auf der Website, falls Sie den vollständigen Vergleich wünschen.
Wichtige Erkenntnisse
- Es gibt keine einzige beste Methode für Puppeteer-Download-Dateien. Passen Sie die Methode an die Einschränkung an, die am meisten stört: unbekannte URL (Methode 1), browsergebundene Authentifizierung (Methode 2), parallele Jobs mit Fortschrittsanzeige (Methode 3) oder bekannte URL mit wiederverwendbaren Cookies (Methode 4).
setDownloadBehaviorist nicht verhandelbar. Headless Chrome blockiert Downloads standardmäßig. Verwenden Sie die Methode auf BrowserebeneBrowser.setDownloadBehaviormit einem absoluten Pfad; der Aufruf auf Seitenebene ist veraltet und führt zu unvorhersehbaren Fehlern.- Warten Sie auf tatsächliche Dateien, nicht auf Klickereignisse. Erstellen Sie einen Snapshot des Download-Ordners, ignorieren Sie
.crdownloadund warte auf ein Zeitfenster mit stabiler Dateigröße, bevor du den Erfolg meldest. - Umgehen Sie den Browser, wann immer möglich. Eine Kombination aus Puppeteer und Axios ist schneller, schlanker und leichter zu skalieren als der Einsatz von N Chrome-Instanzen für parallele Downloads.
- Sichern Sie die Anforderungsschicht separat vom Skript ab. Realistische User-Agents, passende Viewports, Referer, private IP-Adressen und begrenzte Parallelität verhindern die meisten „mysteriösen 403“-Fehler.
Häufig gestellte Fragen
Ein paar Fragen tauchen in jedem Puppeteer-Download-Datei-Projekt auf, meist nachdem das erste Skript im Headed-Modus mehr oder weniger funktioniert und in der CI abstürzt. Die folgenden Antworten überspringen die Zusammenfassung der vier Methoden – diese finden sich weiter oben – und konzentrieren sich auf operative Entscheidungen: Wie wählt man schnell aus, wenn man nicht alle vier prototypisieren kann? Was tun, wenn Dateien sich weigern, den Download abzuschließen? Wie sieht der sauberere, browserunabhängige Weg in der Praxis aus? Wo steht Playwright im Vergleich zu Puppeteer beim Herunterladen? Und wie geht man mit sitzungsgebundener Authentifizierung um, ohne ein ganzes Wochenende dafür zu opfern?
Wie wähle ich die beste Methode zum Herunterladen einer Datei mit Puppeteer aus?
Arbeite eine kurze Liste ab. Wenn du eine stabile URL extrahieren kannst und die Authentifizierung wiederholbar ist, verwende Axios mit Cookies, die aus der Puppeteer-Sitzung gesammelt wurden. Wenn die URL JavaScript-generiert oder nur innerhalb der Seite gültig ist, führe fetch() innerhalb page.evaluate() und sende Base64 zurück. Wenn du nur ein Klickziel hast und eine einfache Ausführung benötigst, konfiguriere Browser.setDownloadBehavior und klicken Sie. Wenn Sie Fortschrittsanzeige oder parallele Sicherheit benötigen, lassen Sie alles über CDP-Ereignisse laufen. Passen Sie die Methode an die Einschränkung an, die am meisten stört.
Warum bleibt mein Puppeteer-Download bei einer .crdownload-Datei hängen oder wird nie abgeschlossen?
Die häufigste Ursache ist, dass das Skript beendet wird, bevor Chrome die Datei ausgibt; schließen Sie den Browser daher immer erst, nachdem ein Polling-Helper bestätigt hat, dass der endgültige Dateiname mit stabiler Größe existiert. Weitere mögliche Ursachen: ein relativer downloadPath (muss absolut sein), der Klick löst eine Navigation statt eines Downloads aus oder ein Server-Hänger, den Chrome als abgebrochen meldet. Beobachten Sie den Lauf einmal im Headed-Modus, und die Ursache wird meist innerhalb von Sekunden offensichtlich.
Kann ich Dateien herunterladen, ohne Chrome überhaupt zu starten?
Ja, und das ist oft die richtige Entscheidung. Wenn die Datei-URL öffentlich ist oder die zum Abrufen benötigten Cookies und Header von einem HTTP-Client wiedergegeben werden können, überspringe den Browser und verwende axios oder Nodes integrierte https mit einem Streaming-Write. Einen Browser benötigen Sie nur dann, wenn JavaScript die URL aufbaut, wenn die Authentifizierung auf eine Weise an die Browsersitzung gebunden ist, die Sie nicht reproduzieren können, oder wenn eine Bot-Erkennungsschicht Nicht-Browser-Clients auf dieser URL gezielt blockiert.
Wie schneidet Puppeteer im Vergleich zu Playwright beim Herunterladen von Dateien ab?
Playwright verpackt Downloads in eine hochrangige Ereignis-API (page.waitForEvent('download')), die ein Download Objekt mit saveAs() und path() Helfern, was kürzer ist als die entsprechende Konfiguration aus Puppeteer und CDP. Bei Puppeteer müssen Sie Browser.setDownloadBehavior und entweder das Dateisystem abfragen oder auf CDP-Ereignisse warten. Beide sind in der Produktion zuverlässig. Entscheiden Sie sich für die Bibliothek, die Ihr Stack bereits verwendet, und nicht allein aufgrund der Download-API.
Wie kann ich Dateien hinter einem Login oder einem Session-Cookie herunterladen?
Zwei saubere Optionen. Entweder führen Sie die Anmeldung in Puppeteer durch, speichern die Cookies mit page.cookies()und die Dateianfrage von Axios mit einem Cookie Header sowie einem passenden User-Agent und Referer. Oder führen Sie den Dateiruf innerhalb von page.evaluate() aus, sodass die Anfrage die Sitzung automatisch übernimmt. Die erste Variante ist schneller und leichter zu skalieren; die zweite ist robuster, wenn die Authentifizierung an In-Memory-Token oder Fingerabdrücke gebunden ist, die eine Wiederholung nicht überstehen.
Zusammenfassung und nächste Schritte
Bei einem zuverlässigen Puppeteer-Workflow zum Herunterladen von Dateien geht es weniger um Puppeteer selbst als vielmehr darum, zu entscheiden, wohin die Bytes tatsächlich übertragen werden. Verwenden Sie Methode 1, wenn Ihnen nur ein Klick zur Verfügung steht. Greifen Sie auf Methode 2 zurück, wenn die Seitensitzung das Einzige ist, das die Datei abrufen kann. Setzen Sie auf Methode 3, wenn Sie Fortschrittsanzeigen, Parallelität oder saubere Abbruchsignale benötigen. Verwenden Sie standardmäßig Methode 4, sobald Sie die URL wiederholen können, und betrachten Sie Puppeteer eher als Erkundungstool denn als Download-Tool.
Statten Sie jedes Skript mit den Grundlagen für produktionsreife Sicherheit aus: absolute Download-Pfade, mehrstufige Timeouts, Wiederholungsversuche mit Backoff, Integritätsprüfungen über die bloße Existenz hinaus und begrenzte Parallelität. Erkennen Sie .crdownload Wächter, bereinigen Sie diese zwischen den Durchläufen und lassen Sie niemals eine unvollständige Datei so weiterfließen, als wäre sie vollständig.
Wenn Ihre Downloads blockiert werden, anstatt zu scheitern, liegt das Problem nicht mehr in Ihrem Skript, sondern in der Anforderungsschicht. Genau hier macht sich eine verwaltete Scraping-Infrastruktur bezahlt. Die WebScrapingAPI Browser-API bietet Ihnen vollständig gehostete Cloud-Browser, die Sie mit demselben Puppeteer- (oder Playwright-)Code steuern können, sowie ein Residential-Proxy-Netzwerk und integrierte Entsperrungsfunktionen für die schwierigeren Ziele. So können Sie das oben genannte Vier-Methoden-Playbook beibehalten und lediglich den Ursprungsort der Anfragen austauschen. Von da an ist die Skalierung einer Puppeteer-Download-Dateipipeline eher eine Konfigurationsänderung als eine Neugestaltung der Architektur.
Wählen Sie die richtige Methode für die heutige Datei, optimieren Sie sie einmal und machen Sie weiter.




