Browse Source

Initial commit

master
FedorSarafanov 3 months ago
commit
12275e82ea
  1. 99
      index.html
  2. 498
      npz-reader.js

99
index.html

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Чтение .npz в браузере</title>
<script src="npz-reader.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
#input_file {
margin-bottom: 20px;
padding: 10px;
font-size: 16px;
}
#output {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.info {
margin-bottom: 10px;
color: #666;
}
</style>
</head>
<body>
<h1>Чтение NPZ файлов в браузере</h1>
<p class="info">Выберите .npz файл для чтения (без использования внешних библиотек)</p>
<input type="file" id="input_file" accept=".npz">
<pre id="output">Ожидание файла...</pre>
<script>
// Получаем ссылки на DOM-элементы
const input_file = document.getElementById('input_file');
const output_pre = document.getElementById('output');
// Обработчик выбора файла
input_file.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (!file) {
output_pre.textContent = 'Файл не выбран.';
return;
}
output_pre.textContent = 'Чтение файла...\nРазмер файла: ' + (file.size / 1024 / 1024).toFixed(2) + ' МБ';
try {
// Чтение файлового объекта как ArrayBuffer
console.log('Начало чтения файла...');
const array_buffer = await file.arrayBuffer();
console.log('Файл прочитан, размер:', array_buffer.byteLength, 'байт');
// Используем наш NPZReader для чтения файла
console.log('Начало парсинга NPZ...');
output_pre.textContent = 'Распаковка ZIP архива...';
const result = await NPZReader.readNPZ(array_buffer);
console.log('NPZ распарсен, количество массивов:', Object.keys(result).length);
// Форматируем вывод для лучшей читаемости
const formattedResult = {};
for (const [key, value] of Object.entries(result)) {
// Конвертируем данные в обычный массив, обрабатывая BigInt
let dataArray;
if (value.data instanceof BigInt64Array || value.data instanceof BigUint64Array) {
// Для BigInt конвертируем в строки
dataArray = Array.from(value.data).slice(0, 100).map(x => x.toString());
} else {
dataArray = Array.from(value.data).slice(0, 100);
}
formattedResult[key] = {
shape: value.shape,
dtype: value.dtype,
data: dataArray, // Показываем первые 100 элементов
totalElements: value.data.length,
fortran_order: value.fortran_order
};
}
// Выводим результаты (с обработкой BigInt)
output_pre.textContent = JSON.stringify(formattedResult, null, 2);
} catch (err) {
output_pre.textContent = 'Ошибка: ' + err.message + '\n\n' + err.stack;
console.error(err);
}
});
</script>
</body>
</html>

498
npz-reader.js

@ -0,0 +1,498 @@ @@ -0,0 +1,498 @@
/**
* Чтение NPZ файлов в браузере без внешних библиотек
* NPZ файл - это ZIP архив, содержащий .npy файлы
*/
// Проверка поддержки форматов сжатия
const SUPPORTED_DEFLATE_FORMATS = [];
if (typeof DecompressionStream !== 'undefined') {
// Проверяем поддержку форматов
// Примечание: создание DecompressionStream может не выбрасывать ошибку сразу,
// поэтому проверяем при использовании
// Приоритет: 'deflate-raw' для ZIP (raw deflate), затем 'deflate' (zlib формат)
SUPPORTED_DEFLATE_FORMATS.push('deflate-raw', 'deflate');
}
class NPZReader {
/**
* Читает NPZ файл из ArrayBuffer
* @param {ArrayBuffer} buffer - ArrayBuffer с содержимым NPZ файла
* @returns {Promise<Object>} Объект с ключами - именами массивов, значениями - массивами данных
*/
static async readNPZ(buffer) {
const zipFiles = await this.parseZIP(buffer);
console.log(`Найдено файлов в ZIP: ${Object.keys(zipFiles).length}`);
console.log('Файлы:', Object.keys(zipFiles));
const result = {};
for (const [filename, fileData] of Object.entries(zipFiles)) {
if (filename.endsWith('.npy')) {
const arrayName = filename.replace('.npy', '');
console.log(`Парсинг NPY файла: ${filename} -> ${arrayName}`);
try {
result[arrayName] = this.parseNPY(fileData);
console.log(`NPY файл ${filename} успешно распарсен`);
} catch (err) {
console.error(`Ошибка парсинга NPY файла ${filename}:`, err);
throw err;
}
} else {
console.log(`Пропущен файл (не .npy): ${filename}`);
}
}
console.log(`Итого распарсено массивов: ${Object.keys(result).length}`);
return result;
}
/**
* Парсит ZIP архив (упрощённая реализация для NPZ файлов)
* @param {ArrayBuffer} buffer - ArrayBuffer с ZIP архивом
* @returns {Promise<Object>} Объект с именами файлов и их содержимым
*/
static async parseZIP(buffer) {
const view = new DataView(buffer);
const files = {};
let offset = 0;
// Ищем центральный каталог в конце файла
// ZIP файл имеет структуру: [локальные файлы] [центральный каталог] [конец центрального каталога]
// Ищем сигнатуру конца центрального каталога (0x06054b50)
let eocdOffset = -1;
for (let i = buffer.byteLength - 22; i >= 0; i--) {
if (view.getUint32(i, true) === 0x06054b50) {
eocdOffset = i;
break;
}
}
if (eocdOffset === -1) {
throw new Error('Неверный формат ZIP файла');
}
// Читаем информацию о центральном каталоге
const cdOffset = view.getUint32(eocdOffset + 16, true);
const cdSize = view.getUint32(eocdOffset + 12, true);
const totalEntries = view.getUint16(eocdOffset + 10, true);
// Читаем записи центрального каталога
let currentOffset = cdOffset;
for (let i = 0; i < totalEntries; i++) {
// Сигнатура центрального файла (0x02014b50)
if (view.getUint32(currentOffset, true) !== 0x02014b50) {
throw new Error('Ошибка чтения центрального каталога');
}
// Флаги сжатия
const flags = view.getUint16(currentOffset + 8, true);
const method = view.getUint16(currentOffset + 10, true);
// Размеры
const compressedSize = view.getUint32(currentOffset + 20, true);
const uncompressedSize = view.getUint32(currentOffset + 24, true);
// Длина имени файла
const filenameLength = view.getUint16(currentOffset + 28, true);
const extraLength = view.getUint16(currentOffset + 30, true);
const commentLength = view.getUint16(currentOffset + 32, true);
// Смещение локального заголовка
const localHeaderOffset = view.getUint32(currentOffset + 42, true);
// Читаем имя файла
const filenameBytes = new Uint8Array(buffer, currentOffset + 46, filenameLength);
const filename = new TextDecoder('utf-8').decode(filenameBytes);
// Отладочная информация
console.log(`Файл: ${filename}, метод сжатия: ${method}, размер: ${compressedSize}/${uncompressedSize}`);
// Переходим к следующей записи
currentOffset += 46 + filenameLength + extraLength + commentLength;
// Читаем локальный заголовок файла
if (view.getUint32(localHeaderOffset, true) !== 0x04034b50) {
throw new Error('Ошибка чтения локального заголовка');
}
const localFilenameLength = view.getUint16(localHeaderOffset + 26, true);
const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
const dataOffset = localHeaderOffset + 30 + localFilenameLength + localExtraLength;
// Читаем данные файла
if (method === 0) {
// Без сжатия (store)
console.log(`Чтение несжатого файла ${filename} (размер: ${(uncompressedSize / 1024).toFixed(2)} КБ)...`);
const fileData = new Uint8Array(buffer, dataOffset, uncompressedSize);
// Создаём копию данных, чтобы иметь независимый ArrayBuffer
const fileDataCopy = new Uint8Array(fileData);
files[filename] = fileDataCopy.buffer;
console.log(`Файл ${filename} прочитан (${fileDataCopy.length} байт)`);
} else if (method === 8) {
// Deflate сжатие - используем встроенный API браузера
// ZIP использует "raw deflate" (без zlib заголовка)
console.log(`Начало распаковки файла ${filename} (${(compressedSize / 1024 / 1024).toFixed(2)} МБ -> ${(uncompressedSize / 1024 / 1024).toFixed(2)} МБ)...`);
const compressedData = new Uint8Array(buffer, dataOffset, compressedSize);
try {
const startTime = Date.now();
const decompressedData = await this.inflate(compressedData);
const endTime = Date.now();
console.log(`Файл ${filename} распакован за ${((endTime - startTime) / 1000).toFixed(2)} секунд`);
files[filename] = decompressedData.buffer;
} catch (err) {
throw new Error(`Ошибка распаковки файла ${filename}: ${err.message}`);
}
} else {
throw new Error(`Неподдерживаемый метод сжатия: ${method} для файла ${filename}`);
}
}
return files;
}
/**
* Распаковывает данные, сжатые методом Deflate
* @param {Uint8Array} data - Сжатые данные
* @returns {Promise<Uint8Array>} Распакованные данные
*/
static async inflate(data) {
// Используем встроенный API DecompressionStream (доступен в современных браузерах)
if (typeof DecompressionStream === 'undefined') {
throw new Error('DecompressionStream не поддерживается. Используйте современный браузер (Chrome 80+, Firefox 113+, Safari 16.4+).');
}
// ZIP использует "raw deflate" (без zlib заголовка и контрольной суммы)
// Используем поддерживаемые форматы
const formats = SUPPORTED_DEFLATE_FORMATS.length > 0
? SUPPORTED_DEFLATE_FORMATS
: ['deflate-raw', 'deflate']; // Fallback на стандартные форматы
let lastError = null;
for (const format of formats) {
try {
console.log(`Попытка распаковки с форматом: ${format}`);
// Создаём новый stream для каждой попытки
const stream = new DecompressionStream(format);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
// Запускаем чтение параллельно с записью
const readPromise = (async () => {
const chunks = [];
let done = false;
let totalRead = 0;
while (!done) {
const { done: readerDone, value } = await reader.read();
done = readerDone;
if (value) {
chunks.push(value);
totalRead += value.length;
// Логируем прогресс каждые 10 МБ
if (totalRead % (10 * 1024 * 1024) < value.length) {
console.log(`Распаковано: ${(totalRead / 1024 / 1024).toFixed(2)} МБ`);
}
}
}
console.log(`Распаковка завершена, всего: ${(totalRead / 1024 / 1024).toFixed(2)} МБ`);
return { chunks, totalRead };
})();
// Записываем данные частями, чтобы не блокировать поток
console.log('Запись данных в stream...');
const chunkSize = 1024 * 1024; // 1 МБ за раз
let written = 0;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, Math.min(i + chunkSize, data.length));
await writer.write(chunk);
written += chunk.length;
if (written % (1024 * 1024 * 5) < chunkSize) { // Логируем каждые 5 МБ
console.log(`Записано: ${(written / 1024 / 1024).toFixed(2)} МБ из ${(data.length / 1024 / 1024).toFixed(2)} МБ`);
}
}
await writer.close();
console.log('Данные записаны, ожидание завершения распаковки...');
// Ждём завершения чтения
const { chunks, totalRead } = await readPromise;
// Объединяем все чанки в один массив
if (chunks.length === 0) {
throw new Error('Нет данных после распаковки');
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
} catch (err) {
lastError = err;
// Пробуем следующий формат
console.log(`Формат ${format} не сработал:`, err.message);
continue;
}
}
// Если ни один формат не сработал
throw new Error(`Ошибка распаковки: ${lastError?.message || 'неизвестная ошибка'}. Попробованы форматы: ${formats.join(', ')}. ZIP использует raw deflate, который может не поддерживаться в вашем браузере.`);
}
/**
* Парсит NPY файл (формат NumPy)
* @param {ArrayBuffer} buffer - ArrayBuffer с содержимым NPY файла
* @returns {Object} Объект с данными массива {data, shape, dtype}
*/
static parseNPY(buffer) {
const view = new DataView(buffer);
let offset = 0;
// Проверяем магический заголовок (0x93 'NUMPY')
if (view.getUint8(offset) !== 0x93) {
throw new Error('Неверный формат NPY файла: неверный магический байт');
}
offset++;
const magic = String.fromCharCode(
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++)
);
if (magic !== 'NUMPY') {
throw new Error('Неверный формат NPY файла: неверная магическая строка');
}
// Читаем версию формата
const majorVersion = view.getUint8(offset++);
const minorVersion = view.getUint8(offset++);
// Читаем длину заголовка
let headerLength;
if (majorVersion === 1) {
headerLength = view.getUint16(offset, true);
offset += 2;
} else if (majorVersion === 2 || majorVersion === 3) {
headerLength = view.getUint32(offset, true);
offset += 4;
} else {
throw new Error(`Неподдерживаемая версия NPY формата: ${majorVersion}.${minorVersion}`);
}
// Читаем заголовок (Python dict в виде строки)
const headerBytes = new Uint8Array(buffer, offset, headerLength);
const headerStr = new TextDecoder('utf-8').decode(headerBytes);
offset += headerLength;
// Парсим заголовок (упрощённый парсинг Python dict)
console.log('Заголовок NPY:', headerStr);
const header = this.parseHeader(headerStr);
console.log('Распарсенный заголовок:', header);
// Читаем данные
const dataOffset = offset;
const dataLength = buffer.byteLength - dataOffset;
const data = new Uint8Array(buffer, dataOffset, dataLength);
console.log(`Данные NPY: offset=${dataOffset}, length=${dataLength}, shape=${JSON.stringify(header.shape)}, dtype=${header.descr}`);
// Проверяем, что shape и descr найдены
if (!header.descr) {
throw new Error('Не найден dtype (descr) в заголовке NPY');
}
if (!header.shape || header.shape.length === 0) {
throw new Error('Не найден shape в заголовке NPY');
}
// Конвертируем данные в нужный тип
const typedArray = this.convertToTypedArray(data, header.descr, header.shape);
console.log(`Конвертировано: ${typedArray.length} элементов`);
return {
data: typedArray,
shape: header.shape,
dtype: header.descr,
fortran_order: header.fortran_order || false
};
}
/**
* Парсит заголовок NPY файла (Python dict)
* @param {string} headerStr - Строка заголовка
* @returns {Object} Распарсенный заголовок
*/
static parseHeader(headerStr) {
// Упрощённый парсер Python dict
// Формат: {'descr': '<f8', 'fortran_order': False, 'shape': (2, 3), }
const result = {};
// Извлекаем dtype (descr)
const descrMatch = headerStr.match(/descr['"]\s*:\s*['"]([^'"]+)['"]/);
if (descrMatch) {
result.descr = descrMatch[1];
} else {
console.warn('Не найден descr в заголовке:', headerStr);
}
// Извлекаем fortran_order
const fortranMatch = headerStr.match(/fortran_order['"]\s*:\s*(True|False)/);
if (fortranMatch) {
result.fortran_order = fortranMatch[1] === 'True';
}
// Извлекаем shape
// Формат может быть: 'shape': (3,) или 'shape': (3, 4) или 'shape': (3,)
const shapeMatch = headerStr.match(/shape['"]\s*:\s*\(([^)]*)\)/);
if (shapeMatch) {
const shapeStr = shapeMatch[1].trim();
if (shapeStr) {
// Разбиваем по запятой и парсим числа
result.shape = shapeStr.split(',').map(s => {
const trimmed = s.trim();
return trimmed ? parseInt(trimmed, 10) : null;
}).filter(x => x !== null); // Убираем null значения
} else {
result.shape = []; // Скаляр
}
} else {
console.warn('Не найден shape в заголовке:', headerStr);
result.shape = [];
}
return result;
}
/**
* Конвертирует байты в типизированный массив
* @param {Uint8Array} data - Сырые данные
* @param {string} dtype - Описание типа данных (например, '<f8', '<i4')
* @param {Array<number>} shape - Форма массива
* @returns {TypedArray} Типизированный массив
*/
static convertToTypedArray(data, dtype, shape) {
// Парсим dtype: '<f8' -> little-endian, float64
// Формат: [endian][type][size]
// endian: '<' (little), '>' (big), '=' (native), '|' (not applicable, single byte)
// type: 'f' (float), 'i' (int), 'u' (uint), 'c' (complex), 'b' (bool)
// size: количество байт (1, 2, 4, 8)
console.log(`convertToTypedArray: dtype=${dtype}, shape=${JSON.stringify(shape)}, data.length=${data.length}`);
const endian = dtype[0];
const type = dtype[1];
const size = parseInt(dtype.substring(2), 10);
// Проверяем, что size валидный
if (isNaN(size) || size <= 0) {
throw new Error(`Неверный размер dtype: ${dtype}`);
}
const isLittleEndian = endian === '<' || (endian === '=' && this.isLittleEndian());
// Вычисляем общее количество элементов
const totalElements = shape.length === 0 ? 1 : shape.reduce((a, b) => a * b, 1);
let typedArray;
switch (type) {
case 'f': // float
if (size === 4) {
typedArray = new Float32Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 8) {
typedArray = new Float64Array(data.buffer, data.byteOffset, totalElements);
} else {
throw new Error(`Неподдерживаемый размер float: ${size}`);
}
break;
case 'i': // int (signed)
if (size === 1) {
typedArray = new Int8Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 2) {
typedArray = new Int16Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 4) {
typedArray = new Int32Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 8) {
// Int64Array не существует, используем BigInt64Array
typedArray = new BigInt64Array(data.buffer, data.byteOffset, totalElements);
} else {
throw new Error(`Неподдерживаемый размер int: ${size}`);
}
break;
case 'u': // uint (unsigned)
if (size === 1) {
typedArray = new Uint8Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 2) {
typedArray = new Uint16Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 4) {
typedArray = new Uint32Array(data.buffer, data.byteOffset, totalElements);
} else if (size === 8) {
// Uint64Array не существует, используем BigUint64Array
typedArray = new BigUint64Array(data.buffer, data.byteOffset, totalElements);
} else {
throw new Error(`Неподдерживаемый размер uint: ${size}`);
}
break;
case 'b': // bool
typedArray = new Uint8Array(data.buffer, data.byteOffset, totalElements);
break;
case 'c': // complex
// Комплексные числа занимают в 2 раза больше места
if (size === 8) {
// complex64 = 2 * float32
typedArray = new Float32Array(data.buffer, data.byteOffset, totalElements * 2);
} else if (size === 16) {
// complex128 = 2 * float64
typedArray = new Float64Array(data.buffer, data.byteOffset, totalElements * 2);
} else {
throw new Error(`Неподдерживаемый размер complex: ${size}`);
}
break;
default:
throw new Error(`Неподдерживаемый тип данных: ${type}`);
}
// Если порядок байт не совпадает, нужно переставить байты
if (!isLittleEndian && size > 1) {
const view = new DataView(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
for (let i = 0; i < typedArray.length; i++) {
const offset = i * size;
// Переставляем байты для каждого элемента
const bytes = new Uint8Array(size);
for (let j = 0; j < size; j++) {
bytes[j] = view.getUint8(offset + j);
}
bytes.reverse();
for (let j = 0; j < size; j++) {
view.setUint8(offset + j, bytes[j]);
}
}
}
return typedArray;
}
/**
* Проверяет, является ли система little-endian
* @returns {boolean}
*/
static isLittleEndian() {
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, 0x0001, true);
return view.getUint8(0) === 1;
}
}
Loading…
Cancel
Save