Как читать и записывать CSV-файлы в Node.js с использованием Node-CSV

Автор выбрал Общество женщин-инженеров для получения пожертвования в рамках программы Написать за Пожертвования.

Введение

A CSV is a plain text file format for storing tabular data. The CSV file uses a comma delimiter to separate values in table cells, and a new line delineates where rows begin and end. Most spreadsheet programs and databases can export and import CSV files. Because CSV is a plain-text file, any programming language can parse and write to a CSV file. Node.js has many modules that can work with CSV files, such as node-csv, fast-csv, and papaparse.

В этом руководстве вы будете использовать модуль node-csv для чтения CSV-файла с использованием потоков Node.js, что позволяет читать большие наборы данных без большого расхода памяти. Вы измените программу, чтобы переместить данные, разобранные из CSV-файла, в базу данных SQLite. Вы также извлечете данные из базы данных, разберете их с помощью node-csv и используете потоки Node.js для записи их в CSV-файл порциями.

Развертывайте ваши приложения Node из GitHub с использованием Платформы приложений DigitalOcean. Позвольте DigitalOcean заниматься масштабированием вашего приложения.

Предварительные требования

Чтобы следовать этому руководству, вам понадобится:

Шаг 1 — Настройка каталога проекта

В этом разделе вы создадите каталог проекта и загрузите пакеты для вашего приложения. Вы также загрузите набор данных CSV от Stats NZ, который содержит данные о международной миграции в Новой Зеландии.

Чтобы начать, создайте каталог с именем csv_demo и перейдите в него:

  1. mkdir csv_demo
  2. cd csv_demo

Затем инициализируйте каталог как проект npm, используя команду npm init:

  1. npm init -y

Опция -y уведомляет npm init ответить “да” на все запросы. Эта команда создает package.json с значениями по умолчанию, которые вы можете изменить в любое время.

После инициализации каталога как проекта npm, теперь вы можете установить необходимые зависимости: node-csv и node-sqlite3.

Введите следующую команду для установки node-csv:

  1. npm install csv

Модуль node-csv – это коллекция модулей, которая позволяет вам разбирать и записывать данные в файл CSV. Эта команда устанавливает все четыре модуля, которые входят в пакет node-csv: csv-generate, csv-parse, csv-stringify и stream-transform. Вы будете использовать модуль csv-parse для разбора CSV-файла и модуль csv-stringify для записи данных в CSV-файл.

Затем установите модуль node-sqlite3:

  1. npm install sqlite3

Модуль node-sqlite3 позволяет вашему приложению взаимодействовать с базой данных SQLite.

После установки пакетов в ваш проект загрузите файл миграции Новой Зеландии в формате CSV с помощью команды wget:

  1. wget https://www.stats.govt.nz/assets/Uploads/International-migration/International-migration-September-2021-Infoshare-tables/Download-data/international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv

Скачанный вами CSV-файл имеет длинное имя. Чтобы было удобнее с ним работать, переименуйте его в более короткое имя с помощью команды mv:

  1. mv international-migration-September-2021-estimated-migration-by-age-and-sex-csv.csv migration_data.csv

Новое имя CSV-файла, migration_data.csv, короче и удобнее для работы.

С помощью nano или вашего любимого текстового редактора откройте файл:

  1. nano migration_data.csv

После открытия вы увидите содержимое, подобное этому:

demo_csv/migration_data.csv
year_month,month_of_release,passenger_type,direction,sex,age,estimate,standard_error,status
2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344,0,Final
2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341,0,Final
...

Первая строка содержит названия столбцов, а все последующие строки содержат данные, соответствующие каждому столбцу. Запятая разделяет каждый элемент данных. Этот символ известен как разделитель, потому что он отделяет поля. Вы не ограничены использованием запятых. Другие популярные разделители включают двоеточия (:), точки с запятой (;) и табуляции (\td). Вам нужно знать, какой разделитель используется в файле, так как большинство модулей требуют его для разбора файлов.

После просмотра файла и определения разделителя закройте ваш файл migration_data.csv, используя CTRL+X.

Теперь у вас установлены необходимые зависимости для вашего проекта. В следующем разделе вы будете читать CSV-файл.

Шаг 2 — Чтение CSV-файлов

В этом разделе вы будете использовать node-csv для чтения CSV-файла и записи его содержимого в консоль. Вы будете использовать метод createReadStream() модуля fs для чтения данных из CSV-файла и создания читаемого потока. Затем вы перенаправите поток в другой поток, инициализированный модулем csv-parse, чтобы разбирать куски данных. После того как куски данных будут разобраны, вы можете вывести их в консоль.

Создайте и откройте файл readCSV.js в вашем предпочтительном редакторе:

  1. nano readCSV.js

В вашем файле readCSV.js импортируйте модули fs и csv-parse, добавив следующие строки:

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

В первой строке вы определяете переменную fs и присваиваете ей объект fs, который возвращает метод require() Node.js при импорте модуля.

На второй строке вы извлекаете метод parse из объекта, возвращаемого методом require(), в переменную parse, используя синтаксис деструктуризации.

Добавьте следующие строки для чтения CSV-файла:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })

Метод createReadStream() из модуля fs принимает аргумент с именем файла, который вы хотите прочитать, здесь это migration_data.csv. Затем он создает читаемый поток, который берет большой файл и разбивает его на более мелкие куски. Читаемый поток позволяет вам только читать данные из него, а не записывать в него.

После создания читаемого потока метод pipe() Node перенаправляет куски данных из читаемого потока в другой поток. Второй поток создается, когда внутри метода pipe() вызывается метод parse() модуля csv-parse. Модуль csv-parse реализует поток трансформации (читаемый и записываемый поток), который принимает кусок данных и преобразует его в другую форму. Например, когда он получает кусок данных, подобный 2001-01,2020-09,Мигрант долгосрочный,Прибытия,Женский,0-4 года,344, метод parse() преобразует его в массив.

Метод parse() принимает объект, который принимает свойства. Затем этот объект конфигурирует и предоставляет дополнительную информацию о данных, которые метод будет разбирать. Объект принимает следующие свойства:

  • разделитель определяет символ, который разделяет каждое поле в строке. Значение , сообщает парсеру, что запятые отделяют поля.

  • from_line определяет строку, с которой парсер должен начать анализ строк. Со значением 2 парсер пропустит строку 1 и начнет с строки 2. Поскольку вы будете вставлять данные в базу данных позже, эта функция помогает избежать вставки имен столбцов в первую строку базы данных.

Затем вы присоединяете событие потоковой передачи данных, используя метод on() в Node.js. Событие потоковой передачи позволяет методу потреблять фрагмент данных, если возникает определенное событие. Событие data срабатывает, когда данные, преобразованные из метода parse(), готовы к потреблению. Чтобы получить доступ к данным, вы передаете обратный вызов методу on(), который принимает параметр с именем row. Параметр row является фрагментом данных, преобразованным в массив. Внутри обратного вызова вы регистрируете данные в консоли, используя метод console.log().

Перед запуском файла вы добавите больше событий потока. Эти события потока обрабатывают ошибки и записывают сообщение об успешном выполнении в консоль, когда все данные в CSV-файле будут обработаны.

По-прежнему в вашем файле readCSV.js добавьте выделенный код:

demo_csv/readCSV.js
...
fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

Событие end генерируется, когда все данные в CSV-файле прочитаны. Когда это происходит, вызывается обратный вызов и регистрируется сообщение о завершении.

Если происходит ошибка при чтении и разборе данных CSV, генерируется событие error, которое вызывает обратный вызов и регистрирует сообщение об ошибке в консоли.

Ваш полный файл теперь должен выглядеть следующим образом:

demo_csv/readCSV.js
const fs = require("fs");
const { parse } = require("csv-parse");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    console.log(row);
  })
  .on("end", function () {
    console.log("finished");
  })
  .on("error", function (error) {
    console.log(error.message);
  });

Сохраните и выйдите из файла readCSV.js, используя CTRL+X.

Затем запустите файл с помощью команды node:

  1. node readCSV.js

Вывод будет выглядеть примерно так (отредактировано для краткости):

Output
[ '2001-01', '2020-09', 'Long-term migrant', 'Arrivals', 'Female', '0-4 years', '344', '0', 'Final' ] ... [ '2021-09', ... '70', 'Provisional' ] finished

Все строки в CSV-файле были преобразованы в массивы с использованием потока преобразования csv-parse. Поскольку регистрация происходит каждый раз, когда часть данных получена из потока, данные выглядят так, будто они загружаются, а не отображаются сразу.

На этом этапе вы читаете данные в CSV-файле и преобразуете их в массивы. Далее вы вставите данные из CSV-файла в базу данных.

Шаг 3 — Вставка данных в базу данных

Вставка данных из файла CSV в базу данных с использованием Node.js дает вам доступ к обширной библиотеке модулей, которые вы можете использовать для обработки, очистки или улучшения данных перед их вставкой в базу данных.

В этом разделе вы установите соединение с базой данных SQLite, используя модуль node-sqlite3. Затем вы создадите таблицу в базе данных, скопируете файл readCSV.js и измените его для вставки всех данных, считанных из файла CSV, в базу данных.

Создайте и откройте файл db.js в вашем редакторе:

  1. nano db.js

В вашем файле db.js добавьте следующие строки для импорта модулей fs и node-sqlite3:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";
...

На третьей строке вы определяете путь к базе данных SQLite и сохраняете его в переменной filepath. Файл базы данных пока не существует, но он будет нужен для установления соединения с базой данных с помощью node-sqlite3.

В том же файле добавьте следующие строки для подключения Node.js к базе данных SQLite:

demo_csv/db.js
...
function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

Здесь вы определяете функцию с именем connectToDatabase() для установления соединения с базой данных. Внутри функции вы вызываете метод existsSync() модуля fs в операторе if, который проверяет, существует ли файл базы данных в каталоге проекта. Если условие if оценивается как true, вы создаете экземпляр класса Database() модуля SQLite из node-sqlite3 с путем к базе данных. После установления соединения функция возвращает объект соединения и завершает выполнение.

Однако, если условие if оценивается как false (если файл базы данных не существует), выполнение перейдет к блоку else. В блоке else вы создаете экземпляр класса Database() с двумя аргументами: путем к файлу базы данных и функцией обратного вызова.

Первый аргумент – это путь к файлу базы данных SQLite, который равен ./population.db. Второй аргумент – это функция обратного вызова, которая будет вызвана автоматически, когда соединение с базой данных будет установлено успешно или если произойдет ошибка. Функция обратного вызова принимает объект error в качестве параметра, который равен null, если соединение установлено успешно. Внутри обратного вызова оператор if проверяет, установлен ли объект error. Если это оценивается как true, то функция обратного вызова записывает сообщение об ошибке и завершает выполнение. Если оценивается как false, то вы записываете сообщение об успешном установлении соединения.

В данный момент блоки if и else устанавливают объект подключения. Вы передаете обратный вызов при вызове класса Database в блоке else, чтобы создать таблицу в базе данных, только если файл базы данных не существует. Если файл базы данных уже существует, функция выполнит блок if, подключится к базе данных и вернет объект подключения.

Чтобы создать таблицу, если файл базы данных не существует, добавьте выделенный код:

demo_csv/db.js
const fs = require("fs");
const sqlite3 = require("sqlite3").verbose();
const filepath = "./population.db";

function connectToDatabase() {
  if (fs.existsSync(filepath)) {
    return new sqlite3.Database(filepath);
  } else {
    const db = new sqlite3.Database(filepath, (error) => {
      if (error) {
        return console.error(error.message);
      }
      createTable(db);
      console.log("Connected to the database successfully");
    });
    return db;
  }
}

function createTable(db) {
  db.exec(`
  CREATE TABLE migration
  (
    year_month       VARCHAR(10),
    month_of_release VARCHAR(10),
    passenger_type   VARCHAR(50),
    direction        VARCHAR(20),
    sex              VARCHAR(10),
    age              VARCHAR(50),
    estimate         INT
  )
`);
}

module.exports = connectToDatabase();

Теперь функция connectToDatabase() вызывает функцию createTable(), которая принимает объект подключения, сохраненный в переменной db, в качестве аргумента.

Вне функции connectToDatabase() определите функцию createTable(), которая принимает объект подключения db в качестве параметра. Вызовите метод exec() на объекте подключения db, который принимает оператор SQL в качестве аргумента. Оператор SQL создает таблицу с именем migration с 7 столбцами. Названия столбцов совпадают с заголовками в файле migration_data.csv.

Наконец, вызовите функцию connectToDatabase() и экспортируйте объект подключения, возвращенный функцией, чтобы его можно было использовать в других файлах.

Сохраните и закройте ваш файл db.js.

После установления соединения с базой данных скопируйте и измените файл readCSV.js, чтобы вставить строки, которые модуль csv-parse разобрал, в базу данных.

Скопируйте и переименуйте файл следующей командой:

  1. cp readCSV.js insertData.js

Откройте файл insertData.js в вашем редакторе:

  1. nano insertData.js

Добавьте выделенный код:

demo_csv/insertData.js
const fs = require("fs");
const { parse } = require("csv-parse");
const db = require("./db");

fs.createReadStream("./migration_data.csv")
  .pipe(parse({ delimiter: ",", from_line: 2 }))
  .on("data", function (row) {
    db.serialize(function () {
      db.run(
        `INSERT INTO migration VALUES (?, ?, ? , ?, ?, ?, ?)`,
        [row[0], row[1], row[2], row[3], row[4], row[5], row[6]],
        function (error) {
          if (error) {
            return console.log(error.message);
          }
          console.log(`Inserted a row with the id: ${this.lastID}`);
        }
      );
    });
  });

На третьей строке вы импортируете объект подключения из файла db.js и сохраняете его в переменной db.

Внутри обратного вызова события data, присоединенного к потоку модуля fs, вы вызываете метод serialize() на объекте подключения. Метод обеспечивает завершение выполнения оператора SQL перед началом выполнения другого, что может помочь предотвратить гонку баз данных, когда система выполняет конкурирующие операции одновременно.

Метод serialize() принимает обратный вызов. Внутри обратного вызова вы вызываете метод run на объекте подключения db. Метод принимает три аргумента:

  • Первый аргумент – это оператор SQL, который будет передан и выполнен в базе данных SQLite. Метод run() принимает только операторы SQL, которые не возвращают результатов. Оператор INSERT INTO migration VALUES (?, ..., ?) вставляет строку в таблицу migration, а ? – это заполнители, которые позже заменяются значениями во втором аргументе метода run().

  • Второй аргумент представляет собой массив [row[0], ... row[5], row[6]]. В предыдущем разделе метод parse() получает фрагмент данных из потока чтения и преобразует его в массив. Поскольку данные получаются в виде массива, для получения значения каждого поля необходимо использовать индексы массива для доступа к ним, как [row[1], ..., row[6]] и т. д.

  • Третий аргумент представляет собой обратный вызов, который выполняется, когда данные были вставлены или если произошла ошибка. Обратный вызов проверяет, произошла ли ошибка, и регистрирует сообщение об ошибке. Если ошибок нет, функция регистрирует сообщение об успешном выполнении в консоли с использованием метода console.log(), уведомляя вас о том, что строка была вставлена вместе с идентификатором.

Наконец, удалите события end и error из вашего файла. В связи с асинхронным характером методов node-sqlite3, события end и error выполняются до того, как данные будут вставлены в базу данных, поэтому они больше не требуются.

Сохраните и закройте ваш файл.

Запустите файл insertData.js с помощью node:

  1. node insertData.js

В зависимости от вашей системы это может занять некоторое время, но node должен вернуть следующий вывод:

Output
Connected to the database successfully Inserted a row with the id: 1 Inserted a row with the id: 2 ... Inserted a row with the id: 44308 Inserted a row with the id: 44309 Inserted a row with the id: 44310

Это сообщение, особенно идентификаторы, подтверждает, что строка из CSV-файла была сохранена в базе данных.

Теперь вы можете читать CSV-файл и вставлять его содержимое в базу данных. Далее вы будете писать CSV-файл.

Шаг 4 — Запись CSV-файлов

В этом разделе вы извлечете данные из базы данных и запишете их в CSV-файл с использованием потоков.

Создайте и откройте в вашем редакторе файл writeCSV.js:

  1. nano writeCSV.js

В вашем файле writeCSV.js добавьте следующие строки для импорта модулей fs и csv-stringify и объекта подключения к базе данных из db.js:

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");

Модуль csv-stringify преобразует данные из объекта или массива в текстовый формат CSV.

Затем добавьте следующие строки для определения переменной, содержащей имя файла CSV, в который вы хотите записать данные, и записываемого потока, в который вы будете записывать данные:

demo_csv/writeCSV.js
...
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

Метод createWriteStream принимает аргумент с именем файла, в который вы хотите записать поток данных, который является именем файла saved_from_db.csv, хранящимся в переменной filename.

На четвертой строке вы определяете переменную columns, которая хранит массив, содержащий имена заголовков для данных CSV. Эти заголовки будут записаны в первой строке файла CSV, когда вы начнете запись данных в файл.

В вашем файле writeCSV.js добавьте следующие строки для извлечения данных из базы данных и записи каждой строки в файл CSV:

demo_csv/writeCSV.js
...
const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

Сначала вызовите метод stringify с объектом в качестве аргумента, который создает поток преобразования. Поток преобразования преобразует данные из объекта в текст CSV. Объект, переданный в метод stringify(), имеет два свойства:

  • header принимает логическое значение и генерирует заголовок, если логическое значение установлено в true.
  • columns принимает массив, содержащий имена столбцов, которые будут записаны в первой строке файла CSV, если опция header установлена в true.

Затем вы вызываете метод each() из объекта подключения db с двумя аргументами. Первый аргумент – это SQL-запрос select * from migration, который извлекает строки по одной из базы данных. Второй аргумент – это обратный вызов, вызываемый каждый раз, когда из базы данных извлекается строка. Обратный вызов принимает два параметра: объект error и объект row, содержащий данные, извлеченные из одной строки в базе данных. Внутри обратного вызова вы проверяете, установлен ли объект error в операторе if. Если условие оценивается как true, сообщение об ошибке записывается в консоль с помощью метода console.log(). Если ошибки нет, вы вызываете метод write() на stringifier, который записывает данные в поток преобразования stringifier.

Когда метод each() завершает итерацию, метод pipe() на потоке stringifier начинает отправлять данные порциями и записывать их в writableStream. Поток для записи будет сохранять каждую порцию данных в файле saved_from_db.csv. После того как все данные будут записаны в файл, console.log() выведет сообщение об успешном выполнении.

Теперь файл будет выглядеть следующим образом:

demo_csv/writeCSV.js
const fs = require("fs");
const { stringify } = require("csv-stringify");
const db = require("./db");
const filename = "saved_from_db.csv";
const writableStream = fs.createWriteStream(filename);

const columns = [
  "year_month",
  "month_of_release",
  "passenger_type",
  "direction",
  "sex",
  "age",
  "estimate",
];

const stringifier = stringify({ header: true, columns: columns });
db.each(`select * from migration`, (error, row) => {
  if (error) {
    return console.log(error.message);
  }
  stringifier.write(row);
});
stringifier.pipe(writableStream);
console.log("Finished writing data");

Сохраните и закройте файл, а затем выполните файл writeCSV.js в терминале:

  1. node writeCSV.js

Вы получите следующий вывод:

Output
Finished writing data

Чтобы подтвердить, что данные были записаны, проверьте содержимое файла, используя команду cat:

  1. cat saved_from_db.csv

cat вернет все строки, записанные в файле (отредактировано для краткости):

Output
year_month,month_of_release,passenger_type,direction,sex,age,estimate 2001-01,2020-09,Long-term migrant,Arrivals,Female,0-4 years,344 2001-01,2020-09,Long-term migrant,Arrivals,Male,0-4 years,341 2001-01,2020-09,Long-term migrant,Arrivals,Female,10-14 years, ...

Теперь вы можете извлекать данные из базы данных и записывать каждую строку в файл CSV, используя потоки.

Вывод

В этой статье вы читали файл CSV и вставляли его данные в базу данных, используя модули node-csv и node-sqlite3. Затем вы извлекали данные из базы данных и записывали их в другой файл CSV.

Теперь вы можете читать и записывать файлы CSV. В качестве следующего шага вы можете работать с большими наборами данных CSV, используя ту же реализацию с потоками, эффективными по памяти, или вы можете изучить пакет типа event-stream, который делает работу с потоками намного проще.

Чтобы узнать больше о node-csv, посетите их документацию Проект CSV – пакет CSV для Node.js. Чтобы узнать больше о node-sqlite3, посетите их документацию на Github. Чтобы продолжить развивать свои навыки в Node.js, ознакомьтесь с серией How To Code in Node.js.

Source:
https://www.digitalocean.com/community/tutorials/how-to-read-and-write-csv-files-in-node-js-using-node-csv