Загрузка CSV, разбор строк и сохранение каждой строки в MongoDB (с использованием Mongoose) на сервере Express

Это результат нескольких дней проб и ошибок. Я понятия не имел о потоках и прочем, поэтому это заняло так много времени 😀

Вам понадобятся следующие библиотеки:

  • multer
  • @fast-csv/parse
  • streamifier

Создайте форму для загрузки CSV файлов — multipart/form-data.

Загрузка файлов должна осуществляться через multipart/form-data. Это то, с чем я познакомился недавно, и, возможно, это будет предметом другого поста. Пока что я пропущу это.

multer

multer возьмет файл и поместит его в req.file. Не надейтесь найти файл в req.body — он будет содержать только текстовые данные полей формы. 90% учебников по multer объясняют, как сохранить входящий файл в директории. Меня это не волнует, потому что файл будет храниться на сервере, где у меня нет прав на запись, поэтому я хочу, чтобы файл хранился в памяти.

const multer = require("multer");

const parseCsv = multer().single("whatever-name-you-gave-to-the-input-field-in-your-form");

module.exports = {parseCsv}
Вход в полноэкранный режим Выход из полноэкранного режима

Это промежуточное ПО, которое поместит файл в req.file.

fast-csv и streamifier

req.file будет иметь свойство buffer, но оно не читаемо для createReadStream node. Если вы попробуете fs.createReadStream(buffer), то, скорее всего, получите ошибку, говорящую, что это не файл, или что-то подобное. Хотя Node createReadStream принимает экземпляр Buffer (а наш buffer это именно экземпляр), этот экземпляр не читается createReadStream. Я узнал об этом в этом ответе SO. Решение, которое я нашел? streamifier, о котором я впервые узнал здесь. Если вы посмотрите на его исходный код, он делает некоторую магию для преобразования буфера в req.file в читаемый буфер, который передается в createReadStream. Я был рад, что нашел эту библиотеку.

Итак, вы создаете поток следующим образом

const { buffer } = req.file;

streamifier.createReadStream(buffer)
Вход в полноэкранный режим Выйти из полноэкранного режима

@fast-csv/parse

@fast-csv/parse берет поток с данными из csv и вызывает пару событий для разбора содержимого файла. Он вызывает .on('data', data => callback) для каждой строки, так что вы можете делать с ней все, что захотите. Когда все строки будут разобраны, вызывается .on('end', rowCount => callback). Есть событие .on('error', callback), которое, как я полагаю, связано с возможностями валидации, но я его еще не пробовал.

Вы можете импортировать fast-csv как csv и затем вызвать .pipe(csv.parse()) (см. пример ниже). Также вы можете передать опции в csv.parse(), те, которые я использовал до сих пор: headers: true (пропускает строку заголовка из файла csv, см. документацию здесь) и ignoreEmpty: true (игнорирует пустые строки, см. документацию здесь).

Моя первая итерация заключалась в том, чтобы поместить создание документа при разборе каждой строки. Ошибка из-за асинхронного характера сохранения данных в БД и синхронного характера разбора CSV. Я обнаружил, что событие 'end' срабатывает до сохранения первого документа, и это испортило мою стратегию и ответы сервера.

Я провел небольшое исследование и нашел стратегию, которая хорошо работает: добавьте разобранный ряд (который возвращается как объект) в массив в памяти, и вы вызываете Model.create([ARRAY_OF_OBJECTS]) на 'end' событие. Вам нужно сделать это async и определить ответ сервера клиенту. Как и в этом случае, мне кажется, что это работает хорошо:

const csv = require("@fast-csv/parse");
const streamifier = require("streamifier");

// somewhere below

router.post("/endpoint", [multerMiddlewareExplainedAbove], (req, res) => {
  const { buffer } = req.file;

  const dataFromRows = [];

  streamifier
    .createReadStream(buffer)
    .pipe(csv.parse({ headers: true, ignoreEmpty: true })) // <== this is @fast-csv/parse!!
    .on("data", (row) => {
      dataFromRows .push(row);
    })
    .on("end", async (rowCount) => {
      try {
        const data = await MyModelName.create(dataFromRows );
        res.status(200).json({ rowCount, data });
      } catch (error) {
        res.status(400).json({ error});
      }
    });
});
Войти в полноэкранный режим Выйти из полноэкранного режима

Надеюсь, это имеет смысл. Я буду добавлять материал по мере его обнаружения. Спасибо, что прочитали (:

Оцените статью
Procodings.ru
Добавить комментарий