Парсинг CSV

CSV (Comma-Separated Values) остаётся одним из наиболее распространённых форматов для хранения табличных данных, особенно в задачах геоинформационных систем. В контексте веб-картографических приложений CSV часто используется для описания наборов точек с координатами, атрибутами объектов и метаданными, которые затем отображаются на карте через слои маркеров, кластеризацию или преобразование в GeoJSON.

Интеграция CSV с Leaflet требует двух ключевых этапов: парсинга текстового формата и трансформации полученных данных в структуры, пригодные для визуализации на карте.


Структура CSV в геоданных

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

name,latitude,longitude,type,population
City A,48.8566,2.3522,capital,2148000
City B,51.5074,-0.1278,capital,8982000
City C,52.5200,13.4050,capital,3769000

Ключевые особенности:

  • координаты чаще всего представлены в виде latitude и longitude
  • первая строка обычно содержит заголовки
  • дополнительные поля используются для стилизации и фильтрации объектов
  • возможны разделители ,, ;, \t

Базовый подход к парсингу CSV в JavaScript

Ручной разбор CSV возможен при простых структурах:

function parseCSV(text) {
  const lines = text.trim().split('\n');
  const headers = lines[0].split(',');

  return lines.slice(1).map(line => {
    const values = line.split(',');
    const obj = {};

    headers.forEach((h, i) => {
      obj[h.trim()] = values[i].trim();
    });

    return obj;
  });
}

Ограничения такого подхода:

  • некорректная обработка кавычек
  • невозможность работы с запятыми внутри значений
  • отсутствие поддержки экранирования
  • слабая устойчивость к «грязным» данным

Для реальных проектов применяется специализированный парсер.


Использование PapaParse для CSV

Одним из наиболее распространённых решений является библиотека PapaParse:

import Papa from 'papaparse';

Papa.parse(csvText, {
  header: true,
  dynamicTyping: true,
  skipEmptyLines: true,
  complete: function(results) {
    console.log(results.data);
  }
});

Преимущества:

  • корректная обработка кавычек и спецсимволов
  • автоматическое определение типов
  • потоковая обработка больших файлов
  • поддержка загрузки через URL

Загрузка CSV напрямую:

Papa.parse("data/points.csv", {
  download: true,
  header: true,
  complete: function(results) {
    console.log(results.data);
  }
});

Преобразование CSV в маркеры Leaflet

После парсинга данные обычно преобразуются в набор маркеров.

const map = L.map('map').setView([48.8566, 2.3522], 5);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '© OpenStreetMap contributors'
}).addTo(map);

function addPoints(data) {
  data.forEach(item => {
    const lat = parseFloat(item.latitude);
    const lng = parseFloat(item.longitude);

    if (!isNaN(lat) && !isNaN(lng)) {
      L.marker([lat, lng])
        .bindPopup(`
          <strong>${item.name}</strong><br/>
          Тип: ${item.type}<br/>
          Население: ${item.population}
        `)
        .addTo(map);
    }
  });
}

Ключевая операция — строгая валидация координат перед созданием маркера.


Обработка ошибок и некорректных данных

CSV-источники часто содержат:

  • пустые строки
  • пропущенные координаты
  • неверные разделители
  • строки с текстовыми значениями вместо чисел

Фильтрация выполняется до добавления на карту:

function sanitizeRow(row) {
  const lat = Number(row.latitude);
  const lng = Number(row.longitude);

  if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
    return null;
  }

  return {
    ...row,
    latitude: lat,
    longitude: lng
  };
}

Использование:

const cleaned = data
  .map(sanitizeRow)
  .filter(Boolean);

Преобразование CSV в GeoJSON

GeoJSON является предпочтительным форматом для многих возможностей Leaflet, включая слои, стилизацию и взаимодействие.

Преобразование:

function csvToGeoJSON(data) {
  return {
    type: "FeatureCollection",
    features: data.map(item => ({
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [
          parseFloat(item.longitude),
          parseFloat(item.latitude)
        ]
      },
      properties: { ...item }
    }))
  };
}

Отображение:

L.geoJSON(geojson, {
  onEachFeature: function(feature, layer) {
    layer.bindPopup(feature.properties.name);
  }
}).addTo(map);

Загрузка CSV по сети

Асинхронная загрузка выполняется через fetch:

async function loadCSV(url) {
  const response = await fetch(url);
  const text = await response.text();
  return text;
}

Интеграция с парсером:

async function init() {
  const csvText = await loadCSV('/data/cities.csv');

  Papa.parse(csvText, {
    header: true,
    complete: function(results) {
      const geojson = csvToGeoJSON(results.data);
      L.geoJSON(geojson).addTo(map);
    }
  });
}

Работа с крупными CSV-файлами

При обработке больших наборов данных возникают ограничения по памяти и производительности.

Потоковая обработка PapaParse

Papa.parse(file, {
  header: true,
  worker: true,
  step: function(results) {
    const row = results.data;

    const lat = parseFloat(row.latitude);
    const lng = parseFloat(row.longitude);

    if (Number.isFinite(lat) && Number.isFinite(lng)) {
      L.circleMarker([lat, lng], { radius: 4 })
        .addTo(map);
    }
  }
});

Преимущество подхода:

  • отсутствие необходимости загружать весь файл в память
  • постепенная отрисовка объектов
  • снижение блокировок UI

Кластеризация CSV-данных

При большом количестве точек используется кластеризация:

const markers = L.markerClusterGroup();

data.forEach(item => {
  const lat = parseFloat(item.latitude);
  const lng = parseFloat(item.longitude);

  if (Number.isFinite(lat) && Number.isFinite(lng)) {
    markers.addLayer(
      L.marker([lat, lng]).bindPopup(item.name)
    );
  }
});

map.addLayer(markers);

Это снижает нагрузку на рендеринг и повышает отзывчивость карты.


Преобразование типов данных

CSV хранит все значения как строки, поэтому требуется явное приведение:

const transform = (row) => ({
  name: row.name,
  latitude: +row.latitude,
  longitude: +row.longitude,
  population: +row.population,
  type: row.type
});

Особое внимание уделяется:

  • числовым полям
  • булевым значениям ("true", "false")
  • отсутствующим значениям ("", "null")

Фильтрация данных перед отображением

Часто требуется отображать только часть данных:

const filtered = data.filter(item =>
  item.type === 'capital' &&
  parseInt(item.population, 10) > 1000000
);

Комбинация фильтрации и гео-визуализации позволяет создавать тематические слои без изменения исходного CSV.


Сопоставление CSV с пользовательскими стилями

Атрибуты CSV могут использоваться для стилизации объектов:

function getColor(type) {
  switch (type) {
    case 'capital': return 'red';
    case 'city': return 'blue';
    default: return 'gray';
  }
}

Применение:

L.circleMarker([lat, lng], {
  radius: 6,
  color: getColor(item.type)
}).addTo(map);

Привязка событий к CSV-объектам

Каждая строка CSV может содержать интерактивные данные:

L.marker([lat, lng])
  .on('click', function() {
    console.log('ID:', item.id);
  })
  .bindPopup(item.name)
  .addTo(map);

Это позволяет связывать географические объекты с внешними API или базами данных.


Объединение CSV с другими источниками данных

CSV часто комбинируется с:

  • REST API
  • GeoJSON слоями
  • WebSocket потоками

Слияние данных:

const merged = csvData.map(row => {
  const extra = apiData.find(d => d.id === row.id);
  return { ...row, ...extra };
});

Оптимизация отображения CSV на карте

Основные принципы:

  • минимизация количества DOM-объектов
  • использование canvas-слоёв
  • кластеризация
  • предварительная фильтрация
  • отказ от тяжёлых popup-структур

Для больших массивов данных предпочтительны circleMarker вместо marker.


Обработка кодировок CSV

CSV-файлы часто содержат разные кодировки (UTF-8, Windows-1251).

При загрузке через fetch возможна необходимость декодирования:

const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(buffer);

Некорректная кодировка приводит к нарушению структуры строк и смещению столбцов.


Нормализация данных перед визуализацией

Перед добавлением в Leaflet выполняется нормализация:

  • удаление пробелов
  • унификация разделителей
  • приведение регистра
  • обработка null-значений
function normalize(row) {
  return Object.fromEntries(
    Object.entries(row).map(([k, v]) => [
      k.trim().toLowerCase(),
      typeof v === 'string' ? v.trim() : v
    ])
  );
}