مقدمه
در وب اسکریپینگ، دانلود خودکار تصاویر یکی از کارهای پرکاربرد است: جمعآوری دیتاست برای یادگیری ماشین، آینهسازی گالریهای آنلاین، یا بکاپگیری و آرشیو تصاویر. در این مقاله قدمبهقدم با چند رویکرد محبوب در Node.js آشنا میشویم، تفاوتها، نکات عملکردی و امنیتی را بررسی میکنیم و مثالهای عملی و قابل اجرا میدهیم. مخاطب این راهنما توسعهدهندهای است که با پایتون آشناست؛ بنابراین گاهی مقایسهی کوتاهی با معادلهای پایتونی (مثل requests یا aiohttp) خواهم داشت تا انتقال مفاهیم راحتتر باشد.
ابزارها و انتخاب درست برای دانلود تصاویر
چهار خانواده ابزار را بررسی میکنیم: کتابخانههای سطح بالا مانند Axios، رابط شبیه مرورگر node-fetch، کتابخانه قدیمی request (غیرفعالشده) و ماژولهای داخلی http/https. انتخاب بستگی به موارد زیر دارد:
- ساده بودن استفاده (developer ergonomics)
- پشتیبانی از استریم برای فایلهای بزرگ
- کارایی و مصرف حافظه
- نیاز به کنترل دقیق روی header/redirects/timeout
خلاصه: برای اغلب مواقع Axios و node-fetch مناسباند؛ برای فایلهای خیلی بزرگ یا نیاز به حداکثر کارایی از http/https استفاده کنید.
نمونه عملی: دانلود با Axios
ایده کلی: با 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);
}
}
توضیح خطبهخط:
- ورودی: url و filename.
- با axios.get(url, { responseType: 'arraybuffer' }) دادهٔ باینری گرفته میشود.
- با fs.writeFileSync بافر روی دیسک نوشته میشود.
- در صورت خطا پیام در کنسول لاگ میشود (شبکه، timeout یا خطای سیستم فایل).
نکتهٔ performance: برای فایلهای بزرگ از responseType: 'stream' استفاده کنید تا داده را به صورت stream به فایل pipe کنید و حافظهٔ کمتری مصرف شود. مثال استریم با Axios در بخش "مدیریت فایلهای بزرگ" آمده است.
نمونه: دانلود با node-fetch
اگر به 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
برای کمترین وابستگی و حداکثر کنترل، میتوان از 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 را پیاده کنید. الگوی معمول:
- حداکثر تعداد تلاشها (مثلاً 3)
- backoff نمایی یا خطی بین تلاشها
- تشخیص خطاهای غیرقابل بازیابی (مثلاً 4xx خاص)
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 بهتر است تلاش مجدد انجام نشود.
همزمانی، throttling و مدیریت فایلها
اگر هزاران عکس دارید، اجرا به صورت همزمان (مثلاً با Promise.all) ممکن است سرور را بمباران کند یا حافظه را پر کند. الگوهای عملی:
- استفاده از batching و اجرای دستهای (مثلاً 5 یا 10 همزمان)
- استفاده از صف (queue) یا کتابخانههایی مثل p-limit برای محدود کردن concurrency
- کنترل نامگذاری برای جلوگیری از overwrite و مدیریت ساختار دایرکتوری
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 بهره ببرید.
مدیریت فایلهای بزرگ: استریم و write streams
برای تصاویر خیلی بزرگ از استریم استفاده کنید تا حافظه مصرفی کم باشد. نمونه با 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 را اعتبارسنجی کنید؛ ترجیحاً فقط HTTPS بپذیرید.
- قبل از ذخیره، Content-Type را بررسی کنید تا مطمئن شوید محتوای دریافتی تصویر است (مثلاً شروع با "image/").
- از sandbox یا کانتینر برای پردازش فایلهای دانلودشده استفاده کنید اگر قرار است آنها را پردازش (مثلاً resize) کنید.
- مطالب حقوقی: پیش از استفاده از تصاویر، مجوزها و قوانین استفاده را بررسی کنید.
// مثال کوتاه: اعتبارسنجی 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:
- آدرس جستوجو را بسازید و HTML صفحه را بگیرید.
- با cheerio یا مشابه آن DOM را پارس کنید و URL تصاویر را از srcset، data-src یا src استخراج کنید.
- لیست یکتا از URLها بسازید (استفاده از Set) و سپس آنها را با الگوی throttled و retry دانلود کنید.
نکات عملی: 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) آماده کنم و برای شما قابل اجرای محلی بفرستم.




