Парсинг изображений в Node.js: почему файлы битые и как это исправить

    При разработке парсера на Node.js для сбора товаров с сайта производителя вы можете столкнуться с проблемой: часть скачанных изображений оказывается битой, с артефактами или не открывается вовсе. Чаще всего это связано с асинхронной природой JavaScript и неправильной обработкой потоков данных. В этой статье мы разберём причины возникновения битых файлов и предложим надёжное решение на основе промисов и корректного закрытия потока записи.

    Почему изображения скачиваются повреждёнными?

    Ваш текущий код использует https.get и записывает данные через createWriteStream с флагом a+. Основная проблема - событие data может вызываться несколько раз, а вы каждый раз открываете новый поток, что приводит к перезаписи или наложению данных. Кроме того, вы не дожидаетесь завершения загрузки перед проверкой существования файла - функция exists может вернуть true даже если файл ещё не дописан до конца. В результате на диске остаются неполные или повреждённые файлы.

    Как правильно скачивать файлы в Node.js?

    Для надёжного скачивания используйте промисы и дождитесь события end или finish. Вот исправленная версия функции, которая гарантирует целостность изображения:

    const fs = require('fs');
    const https = require('https');
    const path = require('path');
    
    function downloadFile(url, dir = 'images', prefix = '') {
      return new Promise((resolve, reject) => {
        const extension = url.slice(url.lastIndexOf('.') + 1);
        const targetDir = `./upload/${dir}`;
        if (!fs.existsSync(targetDir)) {
          fs.mkdirSync(targetDir, { recursive: true });
        }
        const fileName = `${prefix}_${Date.now()}.${extension}`;
        const filePath = path.join(targetDir, fileName);
    
        const fileStream = fs.createWriteStream(filePath);
        https.get(url, (response) => {
          // Проверка HTTP-статуса
          if (response.statusCode !== 200) {
            fileStream.close();
            fs.unlinkSync(filePath); // удаляем битый файл
            reject(new Error(`HTTP ${response.statusCode} for ${url}`));
            return;
          }
          response.pipe(fileStream);
          fileStream.on('finish', () => {
            fileStream.close();
            resolve(filePath);
          });
        }).on('error', (err) => {
          fileStream.close();
          fs.unlinkSync(filePath);
          reject(err);
        });
      });
    }

    Пошаговое объяснение решения

    Использование pipe и одного потока

    Вместо обработки события data мы направляем ответ сервера напрямую в файловый поток через response.pipe(fileStream). Это гарантирует последовательную запись без разрывов.

    Ожидание завершения записи

    Событие finish на потоке записи срабатывает только после того, как все данные записаны и поток закрыт. Только после этого мы резолвим промис с путём к файлу.

    Обработка ошибок и HTTP-статусов

    Если сервер вернул ошибку (например, 404), мы удаляем недозаписанный файл и отклоняем промис. Это предотвращает появление битых или пустых файлов на диске.

    Как проверить целостность скачанного изображения?

    Даже после корректной загрузки файл может быть повреждён, если исходное изображение на сервере было битым. Для проверки используйте пакет jimp или sharp:

    const Jimp = require('jimp');
    
    async function isImageValid(filePath) {
      try {
        await Jimp.read(filePath);
        return true;
      } catch {
        return false;
      }
    }

    Если проверка не пройдена - удалите файл и повторите попытку или запишите ошибку в лог.

    Заключение

    Проблема битых изображений при парсинге в Node.js решается заменой асинхронной записи на потоковую с использованием pipe и промисов. Добавьте проверку HTTP-статуса и валидацию целостности через сторонние библиотеки. Это обеспечит 100% сохранность корректных файлов и избавит от артефактов.

    Часто задаваемые вопросы