

در وب اسکریپینگ، دانلود خودکار تصاویر یکی از کارهای پرکاربرد است: جمعآوری دیتاست برای یادگیری ماشین، آینهسازی گالریهای آنلاین، یا بکاپگیری و آرشیو تصاویر. در این مقاله قدمبهقدم با چند رویکرد محبوب در Node.js آشنا میشویم، تفاوتها، نکات عملکردی و امنیتی را بررسی میکنیم و مثالهای عملی و قابل اجرا میدهیم. مخاطب این راهنما توسعهدهندهای است که با پایتون آشناست؛ بنابراین گاهی مقایسهی کوتاهی با معادلهای پایتونی (مثل requests یا aiohttp) خواهم داشت تا انتقال مفاهیم راحتتر باشد.
چهار خانواده ابزار را بررسی میکنیم: کتابخانههای سطح بالا مانند Axios، رابط شبیه مرورگر node-fetch، کتابخانه قدیمی request (غیرفعالشده) و ماژولهای داخلی http/https. انتخاب بستگی به موارد زیر دارد:
خلاصه: برای اغلب مواقع Axios و node-fetch مناسباند؛ برای فایلهای خیلی بزرگ یا نیاز به حداکثر کارایی از http/https استفاده کنید.
ایده کلی: با Axios تصویر را به صورت باینری بگیریم و با fs در دیسک ذخیره کنیم. این روش برای فایلهای متوسط ساده و خواناست؛ برای فایلهای بزرگ بهتر است از استریم استفاده کنیم.
نصب:
npm install axiosتابع نمونه (ساده و خوانا):
const axios = require('axios');
const fs = require('fs');
const path = require('path');
async function downloadImage(url, filename) {
const savePath = path.resolve(__dirname, 'images', filename);
try {
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 15000 });
fs.writeFileSync(savePath, response.data);
console.log('Image saved as', filename);
} catch (error) {
console.error('Error downloading the image:', error.message);
}
}
توضیح خطبهخط:
نکتهٔ performance: برای فایلهای بزرگ از responseType: 'stream' استفاده کنید تا داده را به صورت stream به فایل pipe کنید و حافظهٔ کمتری مصرف شود. مثال استریم با Axios در بخش "مدیریت فایلهای بزرگ" آمده است.
اگر به API شبیه مرورگر نیاز دارید یا میخواهید footprint سبکتری داشته باشید، node-fetch گزینهٔ خوبی است. در نسخههای جدید این بسته معمولاً ESM استفاده میشود اما میتوان با CommonJS هم کار کرد.
npm install node-fetchنمونهٔ ساده (CommonJS):
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
async function downloadImageFetch(url, filename) {
const savePath = path.resolve(__dirname, 'images', filename);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to fetch image: ${res.status} ${res.statusText}`);
const arrayBuffer = await res.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
fs.writeFileSync(savePath, buffer);
console.log('Image saved as', filename);
} catch (err) {
console.error('Error downloading the image:', err.message);
}
}
توضیح: برای بررسی موفقیت درخواست از res.ok استفاده میکنیم و arrayBuffer() را به Buffer تبدیل و ذخیره میکنیم.
برای کمترین وابستگی و حداکثر کنترل، میتوان از http یا https استفاده کرد. این روش برای سیستمهایی که نمیخواهند پکیج خارجی نصب کنند یا برای دانلود فایلهای حجیم عالی است (چون stream native است).
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
function downloadImageNative(url, filename) {
const savePath = path.resolve(__dirname, 'images', filename);
const client = url.startsWith('https') ? https : http;
client.get(url, (res) => {
if (res.statusCode !== 200) {
console.error('Failed to fetch image:', res.statusCode);
res.resume(); // discard data
return;
}
const fileStream = fs.createWriteStream(savePath);
res.pipe(fileStream);
fileStream.on('finish', () => fileStream.close(() => console.log('Image saved as', filename)));
}).on('error', (err) => {
console.error('Request error:', err.message);
});
}
مزیت این روش: stream نیتیو، بدون بار اضافی روی حافظه. عیب: API سطح پایینتر و نیاز به هندل دستی مواردی مثل redirect، header یا تایماوت.
برای پایداری اسکریپ لازم است retry با backoff، timeout و مدیریت وضعیتهای HTTP را پیاده کنید. الگوی معمول:
async function downloadWithRetry(url, filename, retries = 3) {
try {
await downloadImage(url, filename); // هر کدام از پیادهسازیها
} catch (err) {
if (retries > 0) {
const wait = (4 - retries) * 1000; // ساده: افزایش تاخیر
console.log(`Retrying in ${wait/1000}s...`);
await new Promise(r => setTimeout(r, wait));
return downloadWithRetry(url, filename, retries - 1);
}
console.error('Failed after retries:', err.message);
}
}
نکته: برای retry روی خطاهای موقتی (timeout، 5xx) مناسب است؛ در صورت 4xx بهتر است تلاش مجدد انجام نشود.
اگر هزاران عکس دارید، اجرا به صورت همزمان (مثلاً با Promise.all) ممکن است سرور را بمباران کند یا حافظه را پر کند. الگوهای عملی:
async function throttledDownload(images, limit = 5) {
for (let i = 0; i < images.length; i += limit) {
const batch = images.slice(i, i + limit);
await Promise.all(batch.map((url, idx) => downloadWithRetry(url, `image_${i + idx + 1}.jpg`)));
console.log(`Batch ${Math.floor(i/limit) + 1} completed`);
}
}
مدیریت نام فایل: اگر پسوند مشخص نیست، از Content-Type یا استخراج پسوند از URL استفاده کنید و برای جلوگیری از تداخل از timestamp یا counter بهره ببرید.
برای تصاویر خیلی بزرگ از استریم استفاده کنید تا حافظه مصرفی کم باشد. نمونه با Axios و استریم:
const fs = require('fs');
const axios = require('axios');
async function downloadLargeImage(url, filename) {
const writer = fs.createWriteStream(filename);
const response = await axios({ url, method: 'GET', responseType: 'stream', timeout: 30000 });
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
}
این الگو از پر شدن حافظه جلوگیری میکند و مناسب دانلودهای حجیم یا تعداد زیاد تصاویر است.
هنگام اسکریپ کردن تصاویر باید مراقب امنیت باشید:
// مثال کوتاه: اعتبارسنجی URL و Content-Type
try {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') throw new Error('Only HTTPS allowed');
const res = await axios.get(url, { responseType: 'stream' });
const contentType = res.headers['content-type'];
if (!contentType || !contentType.startsWith('image/')) throw new Error('Not an image');
// سپس ذخیره یا پردازش
} catch (err) {
console.error('Security check failed:', err.message);
}
روند کلی برای صفحات مثل Unsplash:
نکات عملی: Unsplash و سایتهای مشابه ممکن است پیادهسازی lazy-loading یا srcset پیچیده داشته باشند؛ همیشه HTML را با inspector بررسی و selector مناسب را انتخاب کنید.
دانلود تصاویر با Node.js مجموعهای از تصمیمهای مهندسی است: انتخاب کتابخانه (Axios/node-fetch/native)، نحوهٔ مدیریت حافظه (buffer vs stream)، شیوهٔ همزمانی (Promise.all vs throttling)، و پیادهسازی پایداری (retry، timeout). برای دانلودهای کوچک و سریع از Axios یا node-fetch استفاده کنید؛ برای فایلهای بزرگ و سیستمهای با مقیاس بالا از استریم و ماژولهای native بهره ببرید. همواره نکات امنیتی و حقوقی را در نظر داشته باشید و برای اسکریپهای حجیم از batching و retry با backoff استفاده کنید.
اگر مایل باشید میتوانم یک repository نمونه با تمام الگوهای بالا (Axios, node-fetch, native) آماده کنم و برای شما قابل اجرای محلی بفرستم.

