مقدمه
دانلود فایلها در پروژههای وب اسکریپینگ یکی از نیازهای رایج است—از گرفتن PDF و تصاویر تا آرشیو کردن پیوستها. در این مقاله عملی و فنی، از پایه تا تکنیکهای پیشرفته با Playwright و روشهای تکمیلی مثل axios را میبینیم. پس از خواندن این مطلب شما یاد میگیرید که چگونه لینکها را پیدا کنید، دانلود را آغاز و مدیریت کنید، چند فایل را همزمان دانلود کنید، پیشرفت دانلود را پایش کنید و مسائل عملی مثل احراز هویت، تغییر مسیر دانلود و مدیریت خطا را پیادهسازی نمایید.
TLDR: مثال سریع دانلود با Playwright
اینجا یک اسکریپت فشرده که مراحل اصلی را نشان میدهد: ایجاد مرورگر، ایجاد context با اجازه دانلود، کلیک روی لینک، انتظار برای رویداد "download" و ذخیره فایل با نام پیشنهادی.
const playwright = require("playwright");
const path = require("path");
(async () => {
const browser = await playwright.chromium.launch();
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();
await page.goto("https://example.com/page-with-pdf");
// آمادهسازی Promise برای شنیدن رویداد دانلود
const downloadPromise = page.waitForEvent("download");
// کلیک یا هر عملی که دانلود را شروع میکند
await page.locator("a.download-link").click();
// وقتی دانلود رخ داد، شی دانلود را بگیرید و آن را ذخیره کنید
const download = await downloadPromise;
const filePath = path.join(process.cwd(), download.suggestedFilename());
await download.saveAs(filePath);
await browser.close();
})();توضیح سریع:
- ورودی: آدرس صفحهای که لینک دانلود دارد یا خود URL فایل.
- خروجی: فایل ذخیرهشده در مسیر محلی.
- نقش توابع: page.waitForEvent("download") یک Promise میسازد که تا رخداد دانلود منتظر میماند، و download.saveAs() فایل را در دیسک ذخیره میکند.
درک کلی: چرا GET و تفاوت نمایش/دانلود مهم است
وقتی مرورگر «نمایش» یک منبع را انجام میدهد، پاسخ سرور در حافظه مرورگر رندر میشود. در حالت دانلود، همان پاسخ به فایل نوشته میشود. برای دانلود معمولاً از GET استفاده میشود؛ ولی نکته مهم هدرها (مثل content-disposition و content-type) و رفتار سرور است که تعیین میکند مرورگر فایل را دانلود کند یا داخل صفحه نمایش دهد.
دانلود یک فایل با Playwright: گامبهگام
ایده ساده است: پیدا کردن المان دانلود → شروع دانلود (کلیک یا رفتن به URL) → انتظار برای رویداد دانلود → ذخیرهسازی. هر مرحله را با جزئیات ببینیم:
مراحل عملیاتی:
- پیدا کردن لینک/دکمه (با CSS selector، XPath یا locator های Playwright)
- ساختن downloadPromise با page.waitForEvent("download")
- شروع دانلود با click() یا page.goto(url)
- گرفتن شی download و ذخیره آن با saveAs()
تنظیم مسیر و نام فایل سفارشی
برای مرتب نگه داشتن دانلودها معمولاً یک پوشه مخصوص ایجاد میکنیم و نام فایل را کنترل میکنیم. نکات مهم: پیش از ذخیرهسازی وجود مسیر را بررسی کنید و در صورت لزوم بسازید؛ از نام پیشنهادی سرور یا نام دلخواه استفاده کنید.
const fs = require("fs");
const path = require("path");
// مثال: ساخت فولدر دانلود
const downloadsFolder = path.join(process.cwd(), "pdfDownloads");
if (!fs.existsSync(downloadsFolder)) fs.mkdirSync(downloadsFolder);
// هنگام دریافت download object:
const filePath = path.join(downloadsFolder, "MY_CUSTOM_NAME.pdf");
await download.saveAs(filePath);
توضیح: این الگو برای اسکریپهایی مناسب است که نیاز به نامگذاری منظم یا ذخیره در دایرکتوریهای مجزا دارند.
مدیریت مودالها و دیالوگهای دانلود
گاهی قبل از دانلود یک پنجرهٔ پاپآپ یا مودال ظاهر میشود. Playwright با locator و click() اجازه میدهد آنها را ببندید یا تایید کنید و سپس دانلود را ادامه دهید. اگر مودال روی صفحه باشد، ابتدا آن را ببندید و سپس عملیات دانلود را انجام دهید.
دانلود چند فایل: استفاده از توابع و Promise.all
برای دانلود دستهای از فایلها، به جای اینکه هر فایل را با Playwright دریافت کنید، معمولاً آدرس دانلودها را با Playwright اسکریپ میکنیم و خود دانلودها را با یک HTTP client (مثل axios) بهصورت همزمان انجام میدهیم. الگوی رایج:
// آرایهای از لینکهای فایل
const links = await page.$$eval("a[href$='.pdf']", els => els.map(a => a.href));
// تابع کمکی برای دانلود یک فایل با context (یا با axios)
async function downloadFile(context, linkHref) {
const page = await context.newPage();
try {
const [download] = await Promise.all([
page.waitForEvent("download"),
page.goto(linkHref),
]);
const downloadPath = path.join(process.cwd(), download.suggestedFilename());
await download.saveAs(downloadPath);
} catch (err) {
console.error(`Failed to download ${linkHref}:`, err);
} finally {
await page.close();
}
}
// اجرای همزمان
await Promise.all(links.map(l => downloadFile(context, l)));
نکته: اجرای همزمان زیاد میتواند منجر به فشار روی سرور یا بلوکه شدن شود؛ آن را با محدودسازی concurrency (مثلاً با p-limit یا صف ساده) کنترل کنید.
وقتی سرعت مهم است: استفاده از axios برای دانلودهای سنگین یا تعداد بالا
Playwright برای تعامل با رابط کاربری عالی است ولی برای دانلود تعداد زیادی فایل یا فایلهای بزرگ، یک کلاینت HTTP سبک مانند axios اغلب بهتر و سریعتر است. روش معمول: با Playwright آدرسها را استخراج کنید و سپس با axios بهصورت همزمان آنها را دانلود نمایید.
const axios = require("axios");
const fs = require("fs");
async function downloadImage(url, filepath) {
const response = await axios({ url, method: "GET", responseType: "stream" });
const writer = fs.createWriteStream(filepath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
}
// استفادهی همزمان با Promise.all
await Promise.all(imageUrls.map(url => {
const name = path.basename(new URL(url).pathname);
const fp = path.join(downloadDir, `${name}.png`);
return downloadImage(url, fp).then(() => console.log("Downloaded:", name));
}));
مزیت: اتصال مستقیم به سرور بدون سربار یک مرورگر؛ امکان اعمال headerها، retry و مدیریت استریمها آسانتر است.
پایش پیشرفت دانلود
برای نمایش درصد دانلود وقتی از استریم استفاده میکنید، هدر content-length را میخوانیم و با هر chunk دریافتی مقدار پیشرفت را بهروز میکنیم:
const response = await axios({ url, method: "GET", responseType: "stream" });
const total = Number(response.headers['content-length']);
let received = 0;
response.data.on("data", chunk => {
received += chunk.length;
console.log(`Received ${received} of ${total} bytes (${((received/total)*100).toFixed(2)}%)`);
});
// سپس pipe به فایل و بازگرداندن Promise مانند قبل
نکته: بعضی سرورها content-length نمیدهند، در آن صورت نمایش درصد دقیق غیرممکن است و باید از نشانگرهای دیگر (مثلاً ETA مبتنی بر سرعت فعلی) استفاده کنید.
تایید دانلود (مستقل و خودکار)
برای اطمینان از موفقیت دانلود میتوانید بهصورت دستی پوشه را بررسی کنید یا در کد با fs.existsSync(path) و بررسی سایز فایل یا محتوای آن اعتبارسنجی کنید:
const exists = fs.existsSync(downloadPath);
if (exists) console.log(`File found at: ${downloadPath}`);
else console.error(`Failed to find file at: ${downloadPath}`);
احراز هویت برای دانلودهای محافظتشده
برای دانلود از صفحات محافظتشده ابتدا با Playwright لاگین کنید، سپس از همان context برای دسترسی به لینکهای محافظتشده استفاده نمایید—کوکی و session ذخیره شده اجازهٔ دانلود میدهد.
// مثال ساده پر کردن فرم
await page.goto("https://example.com/login");
await page.locator("#username").fill("myuser");
await page.locator("#password").fill("mypassword");
await page.locator("button[type='submit']").click();
// اکنون context احراز هویت شده را برای دانلود استفاده کنید
نکته: برای اجرای بدون UI میتوانید state یا cookies را ذخیره و در اجراهای بعدی بازیابی کنید تا دوباره لاگین نکنید.
خطاها، تایماوتها و بهترینروشها
- استفاده از header مناسب (مثل User-Agent) و مدیریت ریترای هوشمند برای مواجهه با خطاهای موقتی.
- محدودسازی concurrency هنگام دانلود تعداد زیادی فایل تا از بلوکه شدن جلوگیری شود.
- برای فایلهای خیلی بزرگ، به جای نگه داشتن اتصال مرورگر بهتر است URL را به یک دانلود منیجر یا کلاینت چندرشتهای بدهید.
- استفاده از پروکسی و رِیت-لیمیت برای عبور از دفاعهای سادهٔ سرورها—ولی همیشه قوانین و سیاستهای سایت را رعایت کنید.
- اضافه کردن بررسی فرمت فایل (پسوند یا header content-type) تا از اشتباهات جلوگیری شود.
- تنظیم timeoutها: گاهی لازم است page.goto را با { timeout: 60000 } اجرا کنید یا آن را غیرفعال نمایید.
جمعبندی
ترکیب Playwright برای استخراج لینکها و رفتار مبتنی بر مرورگر و استفاده از axios یا سایر کلاینتهای HTTP برای خود دانلود اغلب بهترین ترکیب است: کنترل دقیق DOM، مدیریت احراز هویت و سپس دانلود سریع و مقیاسپذیر. در عمل بهینهسازیهایی مثل مدیریت concurrency، بررسی هدرها، پایش پیشرفت و سیاستهای retry را پیاده کنید تا سیستم دانلود مطمئن و پایدار داشته باشید.
پیشنهاد عملی
- ابتدا اسکریپ جمعآوری لینکها را بنویسید؛ سپس دانلود را با محدودیت concurrency انجام دهید.
- برای پروژههای تولیدی، لاگ کامل، متریکهای موفق/ناموفق و مکانیزم ریتری را اضافه کنید.
- قبل از اسکریپ کردن مقیاس بزرگ، سیاستهای سایت را بررسی کنید و از راهکارهای اخلاقی و قانونی تبعیت نمایید.





