Другое

Как объединить данные при парсинге без ID в JavaScript

Полное руководство по объединению разрозненных данных при парсинге сайтов без общего идентификатора в JavaScript. Узнайте о нечетком сопоставлении, библиотеках Fuse.js и алгоритмах Jaro-Winkler.

Как объединить разрозненные данные при парсинге сайта без общего идентификатора в JavaScript?

Доброго времени суток. Я собираю данные с сайта для своего приложения и столкнулся с проблемой объединения информации из разных источников. Мне удобнее использовать JavaScript для парсинга внешних страниц, но с внутренними страницами, содержащими характеристики, работать сложнее.

Проблема: У меня есть два массива данных, которые нужно объединить, но они не связаны через общий ID:

  1. Данные с внешних страниц (с присвоенными ID):
javascript
[
    {
        'id':1,
        "name": "Intel Core i3-12100F OEM",
        "img": "img",
        "price": "6099"
    },
    // ...
]
  1. Данные с внутренних страниц (с детальной информацией):
javascript
{
  "data": {
    "code": "5444958",
    "name": "Процессор Intel Core i3-14100F OEM",
    "specs": "[LGA 1700, 4 x 3.5 ГГц, L2 - 5 МБ, L3 - 12 МБ, 2 х DDR4, DDR5-4800 МГц, TDP 110 Вт]",
    "description": "4-ядерный процессор...",
    "price": 8299,
    "bonus": 0,
    "imageUrl": "img",
    "characteristics": {
      "Общие параметры": [
        {
          "title": "Модель",
          "value": "Intel Core i3-14100F"
        },
        // ...
      ]
    },
    // ...
  }
}

Вопросы:

  • Какие есть подходы для сопоставления записей на основе названия или других полей?
  • Стоит ли использовать циклы или библиотеки вроде pandas-js для объединения данных?
  • Как эффективно объединить разрозненные данные в JavaScript при парсинге сайта?

Для объединения разрозненных данных при парсинге сайта без общего идентификатора в JavaScript можно использовать несколько подходов, включая нечеткое сопоставление (fuzzy matching), индексирование синонимов и весовые поля. Основные методы включают библиотеки для нечеткого поиска, такие как Fuse.js, а также алгоритмы сравнения строк, такие как Jaro-Winkler.

Содержание

Основные подходы к объединению данных

При отсутствии общего идентификатора для объединения данных из разных источников можно использовать следующие стратегии:

1. Нечеткое сопоставление на основе названий

Основной подход - использовать алгоритмы нечеткого поиска для сопоставления названий продуктов. Это наиболее эффективный метод, когда названия в разных источниках незначительно различаются.

2. Многофакторное сопоставление

Использовать комбинацию полей для повышения точности:

  • Название продукта
  • Цена (с допустимым диапазоном)
  • Изображение (сравнение хешей)
  • Другие уникальные характеристики

3. Индексирование синонимов

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

Важно: Согласно исследованиям DataScienceCentral, при работе с нечетким сопоставлением необходимо учитывать два ключевых аспекта: как взвешивать поля-прокси и как измерять ошибки первого и второго рода.


Библиотеки для нечеткого поиска в JavaScript

Fuse.js

Fuse.js - это легковесная библиотека для нечеткого поиска на JavaScript, которая предоставляет различные алгоритмы для сопоставления строк.

javascript
const Fuse = require('fuse.js');

const options = {
  keys: ['name'],
  threshold: 0.3, // Порог схожести (0-1)
  distance: 100, // Максимальное расстояние редактирования
  includeScore: true
};

const fuse = new Fuse(externalData, options);

// Поиск совпадений
const result = fuse.search(internalData.name);

FuzzySet.js

FuzzySet.js предоставляет структуру данных для выполнения полнотекстового поиска с определением вероятных опечаток.

javascript
const FuzzySet = require('fuzzyset.js');

const fuzzySet = FuzzySet();

// Добавление названий из внешних данных
externalData.forEach(item => {
  fuzzySet.add(item.name);
});

// Поиск совпадений
const matches = fuzzySet.get(internalData.name);

Сравнение библиотек

Библиотека Особенности Скорость Точность
Fuse.js Гибкая конфигурация, поддерживает массивы полей Средняя Высокая
FuzzySet.js Простота использования, хорошая для опечаток Высокая Средняя
Custom Jaro-Winkler Полный контроль над алгоритмом Низкая Высокая

Алгоритмы сопоставления данных

Алгоритм Jaro-Winkler

Этот алгоритм хорошо подходит для сравнения коротких строк, таких как названия продуктов.

javascript
function jaroWinkler(s1, s2) {
  if (s1 === s2) return 1.0;
  
  const m1 = s1.length;
  const m2 = s2.length;
  if (m1 === 0 || m2 === 0) return 0.0;
  
  const matchDistance = Math.floor(Math.max(m1, m2) / 2) - 1;
  const s1Matches = new Array(m1).fill(false);
  const s2Matches = new Array(m2).fill(false);
  let matches = 0;
  let transpositions = 0;
  
  // Поиск совпадений
  for (let i = 0; i < m1; i++) {
    const start = Math.max(0, i - matchDistance);
    const end = Math.min(i + matchDistance + 1, m2);
    
    for (let j = start; j < end; j++) {
      if (!s2Matches[j] && s1[i] === s2[j]) {
        s1Matches[i] = true;
        s2Matches[j] = true;
        matches++;
        break;
      }
    }
  }
  
  if (matches === 0) return 0.0;
  
  // Подсчет транспозиций
  let k = 0;
  for (let i = 0; i < m1; i++) {
    if (s1Matches[i]) {
      while (!s2Matches[k]) k++;
      if (s1[i] !== s2[k]) transpositions++;
      k++;
    }
  }
  
  const jaro = (matches / m1 + matches / m2 + (matches - transpositions / 2) / matches) / 3;
  const prefixLength = 0;
  
  return jaro + prefixLength * 0.1 * (1 - jaro);
}

Комбинирование TF-IDF с Jaro-Winkler

Как упоминается в Stack Overflow, можно заменить точные совпадения токенов в TF-IDF на приближенные совпадения на основе схемы Jaro-Winkler.

Взвешивание полей

Для повышения точности сопоставления можно использовать взвешенные поля:

javascript
function calculateMatchScore(external, internal) {
  let score = 0;
  let maxScore = 0;
  
  // Название (вес 40%)
  const nameScore = jaroWinkler(external.name, internal.name);
  score += nameScore * 0.4;
  maxScore += 0.4;
  
  // Цена (вес 30%)
  const priceDiff = Math.abs(parseFloat(external.price) - parseFloat(internal.price));
  const priceScore = priceDiff < 1000 ? 1 - (priceDiff / 1000) : 0;
  score += priceScore * 0.3;
  maxScore += 0.3;
  
  // Изображение (вес 20%)
  const imageScore = external.img === internal.imageUrl ? 1 : 0;
  score += imageScore * 0.2;
  maxScore += 0.2;
  
  // Характеристики (вес 10%)
  const specsScore = jaroWinkler(external.specs || '', JSON.stringify(internal.specs)) * 0.1;
  score += specsScore;
  maxScore += 0.1;
  
  return score / maxScore;
}

Практическая реализация объединения данных

Шаг 1: Подготовка данных

Сначала необходимо нормализовать данные для улучшения качества сопоставления:

javascript
function normalizeString(str) {
  return str.toLowerCase()
    .replace(/[^\w\sа-яё]/g, '') // Удаление спецсимволов
    .replace(/\s+/g, ' ')       // Нормализация пробелов
    .trim();
}

function prepareData(data) {
  return data.map(item => ({
    ...item,
    normalizedName: normalizeString(item.name),
    normalizedSpecs: normalizeString(item.specs || '')
  }));
}

Шаг 2: Создание индекса

Для оптимизации поиска создайте индекс внешних данных:

javascript
function createIndex(externalData) {
  const index = {};
  
  externalData.forEach(item => {
    const key = item.normalizedName.split(' ')[0]; // Первое слово как ключ
    if (!index[key]) {
      index[key] = [];
    }
    index[key].push(item);
  });
  
  return index;
}

Шаг 3: Основной алгоритм объединения

javascript
function mergeDatasets(externalData, internalData, threshold = 0.7) {
  const preparedExternal = prepareData(externalData);
  const preparedInternal = prepareData(internalData);
  const index = createIndex(preparedExternal);
  
  const result = [];
  
  preparedInternal.forEach(internal => {
    const firstWord = internal.normalizedName.split(' ')[0];
    const candidates = index[firstWord] || [];
    
    let bestMatch = null;
    let bestScore = 0;
    
    candidates.forEach(external => {
      const score = calculateMatchScore(external, internal);
      if (score > bestScore) {
        bestScore = score;
        bestMatch = external;
      }
    });
    
    if (bestScore >= threshold) {
      result.push({
        ...bestMatch,
        ...internal,
        confidence: bestScore,
        matchedFields: getMatchedFields(bestMatch, internal)
      });
    } else {
      // Нет достаточно хорошего совпадения
      result.push({
        id: null,
        confidence: 0,
        ...internal,
        matchedFields: []
      });
    }
  });
  
  return result;
}

Шаг 4: Обработка результатов

javascript
function getMatchedFields(external, internal) {
  const matchedFields = [];
  
  if (jaroWinkler(external.name, internal.name) > 0.8) {
    matchedFields.push('name');
  }
  
  if (Math.abs(parseFloat(external.price) - parseFloat(internal.price)) < 100) {
    matchedFields.push('price');
  }
  
  if (external.img === internal.imageUrl) {
    matchedFields.push('image');
  }
  
  return matchedFields;
}

Оптимизация производительности

Кэширование результатов

javascript
const mergeCache = new Map();

function cachedMerge(externalData, internalData) {
  const cacheKey = JSON.stringify({
    externalHash: hashData(externalData),
    internalHash: hashData(internalData)
  });
  
  if (mergeCache.has(cacheKey)) {
    return mergeCache.get(cacheKey);
  }
  
  const result = mergeDatasets(externalData, internalData);
  mergeCache.set(cacheKey, result);
  return result;
}

Параллельная обработка

javascript
const { Worker } = require('worker_threads');

function parallelMerge(externalData, internalData, chunkSize = 100) {
  return new Promise((resolve) => {
    const chunks = [];
    for (let i = 0; i < internalData.length; i += chunkSize) {
      chunks.push(internalData.slice(i, i + chunkSize));
    }
    
    const workers = [];
    const results = [];
    
    chunks.forEach((chunk, index) => {
      const worker = new Worker('./mergeWorker.js', {
        workerData: {
          externalData,
          internalData: chunk
        }
      });
      
      worker.on('message', (result) => {
        results[index] = result;
        if (results.filter(r => r !== undefined).length === chunks.length) {
          resolve(results.flat());
        }
      });
      
      workers.push(worker);
    });
  });
}

Пример кода для объединения данных

javascript
// Основной скрипт объединения данных
const Fuse = require('fuse.js');

async function mergeProductData() {
  // Данные с внешних страниц
  const externalData = [
    {
      'id': 1,
      "name": "Intel Core i3-12100F OEM",
      "img": "img1",
      "price": "6099"
    },
    {
      'id': 2,
      "name": "AMD Ryzen 5 5600G",
      "img": "img2", 
      "price": "8299"
    }
  ];

  // Данные с внутренних страниц
  const internalData = [
    {
      "data": {
        "code": "5444958",
        "name": "Процессор Intel Core i3-14100F OEM",
        "specs": "[LGA 1700, 4 x 3.5 ГГц, L2 - 5 МБ, L3 - 12 МБ, 2 х DDR4, DDR5-4800 МГц, TDP 110 Вт]",
        "description": "4-ядерный процессор...",
        "price": 8299,
        "bonus": 0,
        "imageUrl": "img1",
        "characteristics": {
          "Общие параметры": [
            {
              "title": "Модель",
              "value": "Intel Core i3-14100F"
            }
          ]
        }
      }
    }
  ];

  // Конфигурация Fuse.js
  const fuseOptions = {
    keys: [
      { name: 'name', weight: 0.4 },
      { name: 'price', weight: 0.3 },
      { name: 'img', weight: 0.2 },
      { name: 'specs', weight: 0.1 }
    ],
    threshold: 0.4,
    distance: 100,
    includeScore: true,
    minMatchCharLength: 3
  };

  // Создание экземпляра Fuse
  const fuse = new Fuse(externalData, fuseOptions);

  // Объединение данных
  const mergedData = internalData.map(internal => {
    const searchResult = fuse.search(internal.data.name);
    
    if (searchResult.length > 0) {
      const bestMatch = searchResult[0];
      return {
        ...bestMatch.item,
        ...internal.data,
        confidence: bestMatch.score,
        matchedFields: Object.keys(bestMatch.item).filter(key => 
          bestMatch.item[key] === internal.data[key]
        )
      };
    }
    
    return {
      id: null,
      confidence: 0,
      ...internal.data,
      matchedFields: []
    };
  });

  console.log('Объединенные данные:', mergedData);
  return mergedData;
}

// Выполнение
mergeProductData().catch(console.error);

Рекомендации и лучшие практики

1. Выбор подходящего алгоритма

  • Для точных названий: Fuse.js с низким порогом
  • Для опечаток: Jaro-Winkler
  • Для больших датасетов: Индексирование + кэширование

2. Настройка весовых коэффициентов

Адаптируйте веса в зависимости от важности каждого поля:

javascript
const weights = {
  name: 0.5,      // Название самое важное
  price: 0.2,     // Цена второстепенна
  image: 0.15,    // Изображение для доп. проверки
  specs: 0.1,     // Характеристики для финальной проверки
  description: 0.05 // Описание редко используется
};

3. Обработка ошибок

javascript
function safeMerge(externalData, internalData) {
  try {
    return mergeDatasets(externalData, internalData);
  } catch (error) {
    console.error('Ошибка при объединении данных:', error);
    return [];
  }
}

4. Валидация результатов

javascript
function validateMergedData(mergedData) {
  return mergedData.filter(item => {
    // Проверка на полноту данных
    const hasRequiredFields = item.id && item.name && item.price;
    
    // Проверка confidence
    const hasGoodConfidence = item.confidence > 0.6;
    
    return hasRequiredFields && hasGoodConfidence;
  });
}

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

Для эффективной интеграции с процессом парсинга можно использовать следующий подход:

javascript
class DataMerger {
  constructor(options = {}) {
    this.options = {
      threshold: 0.7,
      weights: {
        name: 0.4,
        price: 0.3,
        image: 0.2,
        specs: 0.1
      },
      ...options
    };
    
    this.externalData = [];
    this.fuse = null;
  }
  
  addExternalData(data) {
    this.externalData = data;
    this.updateIndex();
  }
  
  updateIndex() {
    this.fuse = new Fuse(this.externalData, {
      keys: Object.keys(this.options.weights).map(key => ({
        name: key,
        weight: this.options.weights[key]
      })),
      threshold: this.options.threshold,
      includeScore: true
    });
  }
  
  mergeInternalData(internalData) {
    return internalData.map(internal => {
      const result = this.fuse.search(internal.name)[0];
      
      if (result && result.score >= this.options.threshold) {
        return {
          ...result.item,
          ...internal,
          confidence: result.score
        };
      }
      
      return {
        id: null,
        confidence: 0,
        ...internal
      };
    });
  }
}

// Использование
const merger = new DataMerger({
  threshold: 0.6,
  weights: {
    name: 0.5,
    price: 0.3,
    image: 0.2
  }
});

merger.addExternalData(externalData);
const merged = merger.mergeInternalData(internalData);

Источники

  1. [Fuzzy Merge - Guides](https://povertyaction.github.io/guides/cleaning/04 Data Aggregation/02 Fuzzy Merge/) - Подробное руководство по нечеткому объединению данных
  2. A Comprehensive Guide to Matching Web-Scraped Data | Crawlbase - Методы сопоставления данных при веб-скрапинге
  3. Detailed Guide to Data Matching - Подробное руководство по сопоставлению данных
  4. JavaScript fuzzy search that makes sense - Stack Overflow - Обсуждение алгоритмов нечеткого поиска
  5. Fuzzy Bootstrap Matching - DataScienceCentral.com - Техники объединения файлов данных без ключевых полей
  6. Fast, accurate and multilingual fuzzy search library for the frontend - Reddit - Библиотеки для нечеткого поиска
  7. How to Implement Fuzzy Search in JavaScript | Codementor - Практическая реализация нечеткого поиска
  8. Fuse.js | Lightweight fuzzy-search library - Официальная документация Fuse.js
  9. Fuzzy Search in JavaScript - GeeksforGeeks - Обзор нечеткого поиска в JavaScript
  10. fuzzyset.js - a fuzzy string set for javascript - GitHub Pages - Реализация FuzzySet.js

Заключение

Для эффективного объединения разрозненных данных при парсинге сайта без общего идентификатора в JavaScript рекомендуется:

  1. Использовать специализированные библиотеки такие как Fuse.js или FuzzySet.js для нечеткого сопоставления строк
  2. Реализовать многофакторную систему оценки с учетом названия, цены, изображения и характеристик
  3. Создавать индексы для оптимизации производительности при работе с большими объемами данных
  4. Настроить пороги и весовые коэффициенты в зависимости от конкретной задачи
  5. Реализовать механизм кэширования и параллельной обработки для повышения производительности

Основная сложность заключается в балансировке между точностью и производительностью. Рекомендуется начинать с простых алгоритмов и постепенно усложнять их по мере необходимости. Важно также реализовать механизмы валидации и обработки ошибок для обеспечения надежности системы.

Авторы
Проверено модерацией
Модерация