Преобразование XLSX в Json с помощью NodeJs и пакета XLSX

Вот мой полный сценарий: У меня есть статический сайт, размещенный на S3. Это просто интерфейс для загрузки файла. Загрузка выполняется с использованием шлюза API в качестве конечной точки, которая запускает лямбда-функцию.

Эта лямбда-функция прочитает файл XLSX, обработает его и загрузит json в корзину S3. Событие PUT в сегменте запускает другую лямбда-функцию, которая будет читать этот json и отправлять сообщения в очередь SQS. Затем, как только приходит сообщение, он запускает экземпляр EC2 для обработки.

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

Вот пример моего вывода:

API-шлюз настроен правильно, я в этом почти уверен:

  1. Я установил тип двоичного носителя как application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
  2. Добавлены шаблоны отображения тела.

При тестировании отправки файла через Postman работает без проблем. Исходя из этого, я полагаю, что API настроен нормально, а также функция, это что-то происходит между браузером и вызовом лямбда-функции \ API.

Вот что показывает мой журнал лямбды:

2024-09-03T22:13:31.337Z 2024-09-03T22:13:31.337Z a107932e-0bde-4b4c-ad8d-478625019503 ИНФОРМАЦИЯ Обработана данные: [ [ { "numCpfCnpj": "Content-Disposition: данные формы", "numAcordo": " name="file"" }

], null, ]....много двоичных данных

Эта часть специально:

"numCpfCnpj": "Content-Disposition: form-data",
"numAcordo": " name=\"file\"" }

Привлек мое внимание, потому что похоже что-то не так с декодированием, так как эти заголовки присутствуют там, где должны быть данные.

Это код, который я использую для чтения файла XLSX:

import XLSX from 'xlsx';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3Client = new S3Client({ region: process.env.AWS_REGION });

const processXlsxFile = (fileContent) => { // Corrected arrow function syntax and added closing brace
    const workbook = XLSX.read(fileContent, { type: 'buffer' });
    const sheet = workbook.Sheets[workbook.SheetNames[0]];

    // Convert all rows to JSON, treating every cell as text to preserve leading zeros
    const data = XLSX.utils.sheet_to_json(sheet, {
        header: 1, // Use the first row as the header
        raw: false // Ensure that all cells are treated as strings
    }).slice(1); // Skip the header row

    // Process the data and pad CPF/CNPJ numbers as needed
    return data.reduce((acc, row, index) => { // Corrected arrow function syntax
        // Skip empty or invalid rows
        if (!row[0] || !row[1]) return acc;

        // Determine if the number is CPF (11 digits) or CNPJ (14 digits)
        const numCpfCnpj = String(row[0]).length <= 11 
            ? String(row[0]).padStart(11, '0')  // Pad CPF to 11 digits
            : String(row[0]).padStart(14, '0'); // Pad CNPJ to 14 digits

        const groupIndex = Math.floor(index / 2);
        if (!acc[groupIndex]) acc[groupIndex] = [];
        acc[groupIndex].push({
            numCpfCnpj,
            numAcordo: String(row[1])
        });
        return acc;
    }, []); 
};

export const handler = async (event) => { // Corrected arrow function syntax and added closing brace
    try {
        console.info("Received event:", JSON.stringify(event, null, 2));
        
        if (event.body) {
            // Decode the base64-encoded file content
            const fileContent = Buffer.from(event.body, 'base64');
            console.info("File content received");

            // Process the Excel file content
            const processedData = processXlsxFile(fileContent);
            console.info("Processed data:", JSON.stringify(processedData, null, 2));

            // Flatten the processed data to a single array
            const flatData = processedData.flat();
            console.info("Flattened data:", JSON.stringify(flatData, null, 2));

            // Convert the result to a JSON string
            const jsonString = JSON.stringify(flatData);
            const jsonFileName = `converted_data_${Date.now()}.json`;
            const bucketName = process.env.BUCKET_NAME;

            // Upload the JSON to S3
            const putObjectParams = {
                Bucket: bucketName,
                Key: jsonFileName,
                Body: jsonString,
                ContentType: 'application/json'
            };

            const command = new PutObjectCommand(putObjectParams);
            await s3Client.send(command);

            console.info(`JSON file uploaded successfully to S3: ${jsonFileName}`);

            return {
                statusCode: 200,
                body: JSON.stringify({ message: 'File processed and JSON uploaded to S3 successfully.' }),
                headers: {
                    'Content-Type': 'application/json'
                }
            };
        } else {
            return {
                statusCode: 400,
                body: JSON.stringify({ message: 'No file uploaded' }),
                headers: {
                    'Content-Type': 'application/json'
                }
            };
        }
    } catch (error) {
        console.error("Error processing file:", error);
        return {
            statusCode: 500,
            body: JSON.stringify({
                message: 'Internal Server Error',
                error: error.message
            }),
            headers: {
                'Content-Type': 'application/json'
            }
        };
    }
};

Любой вклад приветствуется.

Кроме того, если у кого-нибудь возникнут вопросы, я тоже буду рад ответить.

Обновлено: HTML-код

<!DOCTYPE html>
<html lang = "en">
<head>
    <meta charset = "UTF-8">
    <meta name = "viewport" content = "width=device-width, initial-scale=1.0">
    <title>File Processor</title>
    <link rel = "stylesheet" href = "styles/styles.css"> <!-- Link to external CSS file -->
</head>
<body>
    <div class = "container">
        <h1>Process XLSX File</h1>
        <input type = "text" id = "txtInput" readonly placeholder = "No file chosen">
        <input type = "file" id = "fileInput" style = "display:none" accept = ".xlsx">
        <button id = "btnBrowse">Procurar arquivo</button>
        <button id = "btnProcess">Processar</button>
        <div id = "results"></div>
    </div>
    <script src = "scripts/main.js"></script> <!-- Link to external JavaScript file -->
</body>
</html>

JS-код:

document.addEventListener('DOMContentLoaded', () => {
    const fileInput = document.getElementById('fileInput');
    const txtInput = document.getElementById('txtInput');
    const btnBrowse = document.getElementById('btnBrowse');
    const btnProcess = document.getElementById('btnProcess');
    const resultsDiv = document.getElementById('results');

    btnBrowse.addEventListener('click', () => {
        fileInput.click();
    });

    fileInput.addEventListener('change', () => {
        txtInput.value = fileInput.files[0] ? fileInput.files[0].name : 'No file chosen';
    });

    btnProcess.addEventListener('click', async () => {
        if (!fileInput.files.length) {
            alert('Please select a file first.');
            return;
        }

        const formData = new FormData();
        formData.append('file', fileInput.files[0]);

        try {
            const response = await fetch('https://5lvbhojaaf.execute-api.sa-east-1.amazonaws.com/conversion/readFile', {
                method: 'POST',
                body: formData
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();

            // Format JSON output for better readability
            resultsDiv.innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
        } catch (error) {
            console.error('Error processing file:', error);
            resultsDiv.innerHTML = '<p>Error processing file. Check the console for details.</p>';
        }
    });
});

Конечная точка почтальона: https://5lvbhojaaf.execute-api.sa-east-1.amazonaws.com/conversion/readFile

Странные символы появляются после выполнения функции. В журналах CloudWatch я вижу, что данные принимаются в формате Base64, а после выполнения функции они зашифровываются.

🤔 А знаете ли вы, что...
Node.js используется в IoT (интернете вещей) для управления устройствами и сбора данных.


50
1

Ответ:

Решено

Краткое содержание

  • Ваш клиент (html/js) не кодирует файл в base64 перед загрузкой.
  • Ваша лямбда ожидает тело base64

Совет

  • Попробуйте локально и отладьте (или распечатайте) построчно, если ваш файл отправляется так, как ожидает серверная часть.
  • Исправить это непосредственно на aws будет непросто. Проверьте это, чтобы попробовать локально перед развертыванием на aws:
  • Если вы хотите избежать multipart/form-data, преобразуйте файл в base64 на клиентском уровне (html/js), а затем отправьте содержимое как json, чтобы в обработчике лямбда было легко получить содержимое
    • Предупреждение. Если ваш файл имеет тенденцию увеличивать свой размер, преобразование в base64 может стать проблемой (оперативная память, пропускная способность, браузер и т. д.). Вот почему существует тип контента multipart/form-data. Проверьте это и это

# 1 База64

На клиенте (html/js) вы не кодируете файл как base64, поэтому на лямбда-уровне это не имеет смысла:

const fileContent = Buffer.from(event.body, 'base64');

Возможно, используя почтальона, вы отправляете файл как base64, но в своем html/js вы не конвертируете файл в base64. Вы отправляете его как Content-Type: multipart/form-data.

#2 Данные многочастной формы

(1) Получить контент из multipart/form-data непросто по сравнению, если тип контента — Content-Type: application/json

(2) Обычно отправляется имя файла и его содержимое (двоичное).

(3) Обычно этот тип контента имеет такие разделы, как размер, расположение, содержание и т. д.

Практически никто этим непосредственно не занимается (в nodejs), поэтому конечному разработчику помогают библиотеки на всех языках. Например, библиотека multer делает нашу жизнь проще:

app.post('/upload', upload.single('file'), function(req, res) {
  const title = req.body.title;
  const file = req.file;
  //file is ready to use (pdf, xls, zip, images, etc)

Ссылки

#3. Данные многочастной формы с помощью Aws Lambda

event.body нет готового к использованию файла. Это еще одна твоя ошибка

Быстрое исследование не дало мне «простых» способов работы с данными multipart/form с использованием лямбда-выражений aws.

Просматривая некоторые библиотеки, я нашел волшебство, позволяющее получить файл из переменной события лямбда:

module.exports.parse = (event, spotText) => {
    const boundary = getBoundary(event);
    const result = {};
    event.body
        .split(boundary)
        .forEach(item => {
            if (/filename = ".+"/g.test(item)) {
                result[item.match(/name = ".+";/g)[0].slice(6, -2)] = {
                    type: 'file',
                    filename: item.match(/filename = ".+"/g)[0].slice(10, -1),
                    contentType: item.match(/Content-Type:\s.+/g)[0].slice(14),
                    content: spotText? Buffer.from(item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4), 'binary'):
                        item.slice(item.search(/Content-Type:\s.+/g) + item.match(/Content-Type:\s.+/g)[0].length + 4, -4),
                };
            } else if (/name = ".+"/g.test(item)){
                result[item.match(/name = ".+"/g)[0].slice(6, -1)] = item.slice(item.search(/name = ".+"/g) + item.match(/name = ".+"/g)[0].length + 4, -4);
            }
        });
    return result;
};

Это уже реализовано в этих библиотеках:

Выберите один и попробуйте. Я уверен, что файл будет готов к использованию в вашем

const workbook = XLSX.read(file, { type: 'buffer' });