Я реализую программу, которая должна получать файлы от пользователя в Angular, отправлять их в серверную часть node.js, где они будут прочитаны и преобразованы в массив объектов.
Я выбрал JavaScript, потому что для меня это был самый быстрый способ реализовать эту программу и протестировать ее.
Если у вас есть какие-либо другие советы по языку, я учту их в своей следующей реализации.
На данный момент мои XML-файлы имеют самый большой размер 110 МБ и самый маленький размер 16 МБ.
Мне нужно иметь возможность анализировать до 3000 файлов, размеры которых могут различаться.
Все эти файлы имеют одинаковую структуру и теги почти в одном и том же порядке.
Пример XML:
<start_xml_tag>
<property1>something</property1>
.
.
<property10>somethingElse</property10>
<Offerte>
<codice>123</codice>
<nome>andrea</nome>
<stato>italia</stato>
</Offerte>
... 1Milion row after
<Offerte>
<codice>123</codice>
<nome>andrea</nome>
<stato>italia</stato>
</Offerte>
...2 milion row after
</start_xml_tag>
Результатом обработки является массив, содержащий объекты, где каждый объект выглядит следующим образом:
{ код: 123, Имя: Андреа Стато: Италия }
Заранее пользователь может вставить фильтр, где, если поле stato равно (например) 'italia', результатом в объектах должны быть только те, у которых есть stato = italia.
Я предоставлю свой код, как только смогу, меня не будет дома до завтра, поэтому у меня его здесь нет.
Можете ли вы мне помочь или сказать, где я ошибаюсь?
Заранее спасибо!
До сих пор мне удавалось добиться этого, загружая файлы по тексту с меньшим количеством файлов, до 400 или даже 600.
Затем я переключился на использование потокового саксофона для чтения файлов по частям, но завершение синтаксического анализа занимало целую вечность, я не уверен, допустил ли я какие-то ошибки, он анализировал все хорошо, но через 9 часов он все еще работал.
Размер фрагмента был установлен на 100 000 байт, выше этого значения я получал ошибку максимального размера стека.
Сейчас я пытаюсь с помощью подхода xpath посмотреть, смогу ли я нацелиться на всех детей «Offerte» с помощью выражения, но я новичок в этом и все еще думаю, что буду вообще не слишком эффективен.
import { Injectable } from '@angular/core';
import { HttpClient, } from '@angular/common/http';
import * as sax from 'sax';
import { targetTags, TargetTags, startTagName, checkUnitReferenceNo, checkStatusCd } from 'src/models/util';
const BATCH_SIZE = 100;
@Injectable({
providedIn: 'root'
})
export class BatchXmlService {
constructor(private http: HttpClient) { }
private result: TargetTags[] = [];
async processFiles(files: FileList, unitReferenceNoFilter: any, statusCdFilters: any) {
let result = [];
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const batch = Array.from(files).slice(i, i + BATCH_SIZE);
result.push(...await this.processBatch(batch, unitReferenceNoFilter, statusCdFilters));
}
return result;
}
async processBatch(batch: File[], unitReferenceNoFilter: any, statusCdFilters: any) {
const promises = batch.map(file => this.parseFile(file, unitReferenceNoFilter, statusCdFilters));
const parsedData = await Promise.all(promises);
return parsedData.flatMap(x=>x);
}
// Function to parse a single XML file
async parseFile(file: File, unitReferenceNoFilter: any, statusCdFilters: any): Promise<any[]> {
const strict = true;
return new Promise((resolve, reject) => {
const saxStream = sax.createStream(strict);
saxStream.on('error', (error) => {
reject(error);
});
const parsedData: any[] = []; // Array to store parsed data
let lastTag = "";
let inTargetSection = false; // Flag to track if we're within the target section
let skipParsing = false;
saxStream.on('opentag', (node) => {
if (node.name === startTagName) {
inTargetSection = true;
parsedData.push({});
}
else if (inTargetSection && !skipParsing) {
lastTag = node.name;
if (targetTags.includes(node.name)){
// Store tag name as key and content as value in the current offer
const currentOffer = parsedData[parsedData.length - 1];
if (currentOffer) {
currentOffer[node.name] = "";
}
}
}
});
saxStream.on('text', (text) => {
// Assuming you only care about text content within target tags
if (text.trim() === "") return;
if (inTargetSection && lastTag?.length > 0 && targetTags.includes(lastTag) && !skipParsing) {
const currentOffer = parsedData[parsedData.length - 1];
if (lastTag){
if (
lastTag === "UNIT_REFERENCE_NO" && !checkUnitReferenceNo(unitReferenceNoFilter, text.trim()) ||
lastTag === "STATUS_CD" && !checkStatusCd(statusCdFilters, text.trim())
){
skipParsing = true;
parsedData.pop();
return;
}
currentOffer[lastTag] = text.trim();
}
}
});
saxStream.on('closetag', (nodeName) => {
if (nodeName === startTagName) {
inTargetSection = false;
skipParsing = false;
}
});
saxStream.on('end', () => {
resolve(parsedData);
});
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
const arrayBuffer = reader.result as ArrayBuffer;
const byteArray = new Uint8Array(arrayBuffer); // Convert to Uint8Array
let remainingData = byteArray;
while (remainingData.length > 0) {
const chunkSize = Math.min(remainingData.length, 100000); // Read in chunks of 50000 bytes
saxStream.write(String.fromCharCode.apply(null, remainingData.slice(0, chunkSize)));
remainingData = remainingData.slice(chunkSize);
}
saxStream.end(); // Call end after processing all data
};
reader.onerror = (error) => {
reject(error); // Handle file read errors
};
});
}
}
<form [formGroup] = "form" class = "d-flex flex-column justify-content-around w-100" (ngSubmit) = "generateExcel()" style = "height: 300px">
<div class = "d-flex flex-column">
<label for = "Filtro_UNIT_REFERENCE_NO">Filtro per UNIT_REFERENCE_NO <sup style = "color: red">*</sup></label>
<div class = "d-flex justify-content-between w-50">
<select class = "form-control" formControlName = "UNIT_REFERENCE_NO" (change) = "setUnitReferenceNoFilter()">
<option *ngFor = "let item of unitReferenceNoOptions" [value] = "item.value">{{item.label}}</option>
</select>
</div>
</div>
<div class = "d-flex flex-column">
<label for = "Filtro_STATUS_CD">Filtro per STATUS_CD</label>
<div class = "d-flex justify-content-between w-50">
<ng-select [items] = "statusCdOptions" class = "w-100"
[multiple] = "true"
placeholder = "Se nessuna scelta e' selezionata allora si estraggono tutte"
formControlName = "STATUS_CD"
bindLabel = "label"
bindValue = "value">
</ng-select>
</div>
</div>
<input #myInput for = "files-xml" (click) = "checkFiltersInserted($event)" formControlName = "files" class = "form-control mx-1" type = "file" class = "file-upload bg-secondary-subtle w-50" (change) = "onUpload($event)" required webkitdirectory multiple />
<button class = "btn btn-primary w-25" type = "submit" [disabled] = "!isUnitReferenceNoFiltered || (loadingData && result !== [])">
<span *ngIf = "loadingData" class = "spinner-border spinner-border-sm" role = "status" aria-hidden = "true"></span>
Genera Excel
</button>
</form>
И моя функция app.ts onUpload такова:
async onUpload(event: any) {
const files = event.target.files;
this.loadingData = true;
try{
this.result = await this.batchXmlService.processFiles(files, this.form.controls['UNIT_REFERENCE_NO'].value, this.form.controls['STATUS_CD'].value);
this.loadingData = false;
}
catch(error){
this.loadingData = false;
}
}
Я постарался очистить код как можно лучше, у меня было еще несколько закомментированных кодов из предыдущих тестов.
Я знаю, что это происходит на Angular.js, а не на Node.js. Сейчас я пытаюсь написать ту же логику в node.js, чтобы делегировать эти вычисления на серверную часть.
Я также забыл, что второй фильтр представляет собой массив вариантов, где в предыдущем примере XML поле «nome» могло иметь разные значения, все из которых содержатся в списке вариантов множественного выбора.
Этот фильтр работает следующим образом: Если в списке есть ['Андреа', 'Франческо', 'Брайан']
Он фильтрует объект так, чтобы все результаты соответствовали одной из этих трех строк.
Accepted XML example:
<Offerte>
<codice>123</codice>
<nome>andrea</nome>
<stato>italia</stato>
</Offerte>
Discarded xml example:
<Offerte>
<codice>123</codice>
<nome>john</nome>
<stato>italia</stato>
</Offerte>
🤔 А знаете ли вы, что...
JavaScript может выполняться как на стороне клиента (в браузере), так и на стороне сервера (с использованием Node.js).
Попробуйте выполнить сценарий PS
using assembly System.Xml.Linq
$xmlFilename = 'c:\temp\test.xml'
$reader = [System.Xml.XmlReader]::Create($xmlFilename)
$table = [System.Collections.ArrayList]::new()
While(-not $reader.EOF)
{
if ($reader.Name -ne 'Offerte')
{
$reader.ReadToFollowing('Offerte') | out-null;
}
if (-not $reader.EOF)
{
$offerte = [System.Xml.Linq.XElement][System.Xml.Linq.XElement]::ReadFrom($reader);
$codice = $offerte.Element('codice').Value
$nome = $offerte.Element('nome').Value
$stato = $offerte.Element('stato').Value
$newRow = [pscustomobject]@{
codice = $codice
nome = $nome
stato = $stato
}
$table.Add($newRow) | out-null
}
}
$table
Мне удалось решить свою проблему, следуя тому, что предложил jdweng с помощью его ps-скрипта, а затем я вернулся к своему первому решению синтаксического анализа, которое похоже на его.
Сначала я решил получить файлы из папки прямо из node.js, а не переносить их с фронтенда на бэкенд, потому что упаковывать и отправлять их будет очень долго.
Затем я попытался прочитать файлы целиком, но те файлы были слишком большими, и моя память терпела сбой, поэтому я реализовал логику чтения по частям.
Итак, для тех, кто борется с чем-то похожим на мой случай, я сделал это:
Достаём файлы из папки и начинаем читать их по одному:
async function getExcelRows(filterNome, filterStato){
let result = [];
const fileNames = await readFolder();
for(let fileName of fileNames){
const response = await readAndParseFile(folderPath+fileName, filterNome, filterStato);
result.push(...response);
}
return JSON.stringify(result);
}
Реализация для чтения папки:
async function readFolder(){
const files = await fsp.readdir(folderPath);
return files.filter(file => file.endsWith('.xml'));
}
Реализация чтения файлов частями:
async function readAndParseFile(filePath, filterNome, filterStato){
return new Promise((resolve, reject) => {
let keeper = {};
let isToErase = true;
let result = [];
const re = new RegExp("<"+objTag+">", 'g');
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
stream.on('data', async (chunk) => {
if (isToErase){
keeper = {};
keeper.remain = chunk;
}
else{
if (keeper.remain.indexOf("<"+objTag+">") != -1)
testFileOfferteNumber--;
keeper.remain +=chunk;
}
testFileOfferteNumber += keeper.remain.match(re).length;
if (keeper.remain.indexOf("<"+objTag+">") !== -1){
keeper = await parseFile(keeper.remain, filterNome, filterStato);
if (keeper?.result.length > 0){
result.push(...keeper.result);
keeper.result = [];
}
if (keeper?.remain != "")
isToErase = false;
else
isToErase = true;
}
});
stream.on('end', () => {
resolve(result);
});
stream.on('error', (error) => {
reject(error);
});
});
}
Переменная testFileOfferteNumber должна была проверять, действительно ли я обрабатывал все теги без ошибок, не более того.
Итак, здесь мы читаем файл частями, и если после анализа у меня все еще остается неполный контент, я возвращаю его обратно в эту функцию и устанавливаю для isToErase значение false, если это так, чтобы следующий фрагмент был добавлен к моему текущему оставшемуся фрагменту. содержание. В противном случае я просто беру новый фрагмент и обрабатываю его.
Реализация логики синтаксического анализа:
// objTag = "Offerte"
// filterNome = ["andrea"]
// filterStato = "italia"
function parseFile(source, filterNome, filterStato){
return new Promise((resolve, reject)=>{
try{
const endTagLen = ("</"+objTag+">").length;
let result = [];
while(source.indexOf("<"+objTag+">") !== -1){
if (source.indexOf("</"+objTag+">") === -1)
return resolve({result: result, remain: source});
testResultOfferteNumber++;
if (
!checkNomeFilter(filterNome, checkField(source, 'nome') ? getField(source, "nome") : "") ||
!checkStato(filterStato, checkField(source, 'stato') ? getField(source, "stato") : "")
){
source = source.slice(source.indexOf("</"+objTag+">")+endTagLen);
continue;
}
const obj = {
codice: checkField(source, 'codice') ? getField(source, "codice") : "",
nome: checkField(source, 'nome') ? getField(source, "nome") : "",
stato: checkField(source, 'stato') ? getField(source, "stato") : "",
}
source = source.slice(source.indexOf("</"+objTag+">")+endTagLen);
result.push(obj);
}
resolve({result: result, remain: source});
}
catch(e){
reject(e);
}
});
}
testResultOfferteNumber — это переменная, которую нужно сравнить с предыдущей, чтобы проверить, правильно ли обработаны все теги.
Итак, здесь я перебираю все теги «Offerte», пока они есть, и в этом «пока» я проверяю, есть ли закрывающий тег, если нет, я возвращаю все, что осталось, в функцию чтения, которая добавит следующий фрагмент текста к этому тексту, при этом он сможет впоследствии проверить это содержимое.
После этого я применяю свои фильтры, чтобы избежать анализа данных, которые не принимаются до их анализа, а затем, поскольку я знаю структуру xml, я просто извлекаю все 3 тега, которые мне нужны, одновременно.
После этого анализа я отсекаю уже разобранную часть файла и перезапускаю цикл с новым содержимым в <Offerte>...</Offerte>
.
Я оставил размер фрагмента по умолчанию, который у меня отлично работал: 3000 файлов были обработаны примерно за 20-30 минут без сбоев.
В моем случае мне нужно было вернуть результат в виде строки, чтобы я мог создать файл Excel.
Я надеюсь, что это решение поможет кому-то, как я, спасибо всем, кто помог мне с этой проблемой, особенно jdweng.