Каков эффективный способ удаления данных из потока (или TextWriter)?

Мы используем сентябрь для обработки файлов CSV перед массовым импортом в базу данных. Процесс быстрый, но Сен обменивает некоторую коррекцию ошибок на чистую скорость.

Один из наших поставщиков отправляет CSV-файлы с недопустимым escape-символом (с использованием \" внутри строкового поля с правильным экранированием). Sep не справляется с этим корректно, поставщик еще не исправляет свой процесс, поэтому третий вариант, который я вижу, — попытаться исправить эти данные во время приема. API для Sep может читать из Stream, TextReader, byte[] или напрямую брать имя файла и выполнять собственное чтение.

Как правильно реализовать фильтр, подобный этой сигнатуре? (Я знаю, что это вообще недопустимо на C#). Идея состоит в том, что этот фильтр будет находиться между необработанным, потенциально поврежденным файлом на диске и перегрузкой TextReader, из которой Sep может читать. У меня есть идеи, как неэффективно реализовать это, но все, что я делаю с анализом строк, немедленно снижает производительность приложения.

private TextReader FilterData(TextReader raw)
{
  return raw.Replace("\\"", "");
}

Минимальное воспроизведение проблемы здесь: https://github.com/bryanboettcher/SepCrash, но я не пытаюсь сосредоточиться на сентябре. Sep ведет себя правильно.

Сами данные воспроизводятся в репозитории из массового файла данных fakenamegenerator. Строка 3 — это критическая линия, которая очень репрезентативна для наших фактических данных.

Number,Gender,NameSet,Title,GivenName,MiddleInitial,Surname,StreetAddress,City,State,StateFull,ZipCode
1,female,American,Ms.,Danette,J,Bradham,"1385 Foley Street","Fort Lauderdale",FL,Florida,33308
2,female,American,Mrs.,Karla,F,Gandy,"2184 Irish Lane","La Crosse",WI,Wisconsin,54601
3,male,American,Mr.,Kevin,V,Jenkins,"1365\" Rosemont Avenue","Los Angeles",CA,California,90031
4,female,American,Ms.,Vickie,N,Howard,"3847 Java Lane",Columbia,SC,"South Carolina",29201
5,female,American,Ms.,Bonnie,S,Winship,"3389 Shadowmar Drive","New Orleans",LA,Louisiana,70118

Строка 3, адрес "1365\" Rosemont Avenue". Для правильного форматирования CSV это должно быть "1365\"" Rosemont Avenue". Мы можем обработать ошибочную косую черту в кавычках в коде приложения, но только после того, как она будет проанализирована библиотекой. Нарушающее поле на самом деле является адресом и позже будет очищено на этапе проверки адреса. Для этой цели подойдет либо "1365 Rosemont Avenue", либо "1365\"" Rosemont Avenue".

Нуждаются ли в обработке другие escape-слеши, особенно \\?

Ry- 24.07.2024 23:19

Я сомневаюсь, что это лучший способ, но можете ли вы показать несколько строк из CSV-файла с проблемой, чтобы мы могли точно понять, о чем вы говорите?

Jonathan Wood 24.07.2024 23:30

@JonathanWood Я обновил пост, добавив ссылку на репозиторий Github, воспроизводящий сбой, а также примеры данных.

Bryan B 25.07.2024 00:21

@Ry-: Я точно не знаю. Пока мы видели только \". Это определенно проблема на стороне поставщика (их пользовательский интерфейс имеет косую черту в одном из полей и экспортируется как есть), но их исправление не имеет ETA.

Bryan B 25.07.2024 00:22

Рассматривали ли вы возможность предварительной обработки файлов этого поставщика перед их загрузкой в ​​вашу систему? Может быть, sed или awk смогут продезинфицировать?

Fildor 25.07.2024 00:28

@Fildor, я рассмотрел возможность предварительной обработки файлов от поставщика. Вот о чем мой вопрос: я бы хотел предварительно обработать их на лету, передав Stream или TextReader в Sep вместо имени файла напрямую. Я не знаю эффективного способа удалить этих персонажей на месте.

Bryan B 25.07.2024 00:31

Есть ли причина, по которой вы хотите, чтобы это происходило на лету, а не весь файл вне процесса? Что бы вы ни делали на лету, это всегда влияет на производительность.

Fildor 25.07.2024 00:35

Файл загружается в процессе на предыдущем шаге. Нам пришлось бы остановить обработку, обратиться к другой утилите, а затем возобновить импорт. Этот процесс также выполняется на хосте Windows, поэтому нам придется использовать WSL или MinGW, чтобы получить доступ к sed/awk.

Bryan B 25.07.2024 00:46

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

Ry- 25.07.2024 00:48

Я не могу поделиться нашим кодом достаточно, чтобы показать, почему это невозможно. Мне не нужно убегать от Сепа, мне не нужно выставлять себя дураком при взаимодействии с ним. Можете ли вы помочь мне перефразировать вопрос, чтобы я мог получить реальный ответ на вопрос: «Каков высокопроизводительный способ изменения потока во время его обработки»? Видимо, я не могу этого сообщить.

Bryan B 25.07.2024 00:52

Я писал ответ, но, думаю, мог бы просто упомянуть заранее, что речь идет о реализации Stream. Однако не совсем ясно, пробовали ли вы уже реализовать TextReader столь же эффективно.

Ry- 25.07.2024 00:59

Прочитать «символы» из ввода; запись в поток памяти при преобразовании «плохих» символов; затем прочитайте из потока памяти с помощью «Sep Reader».

Gerry Schmitz 25.07.2024 03:13

Возможно, используйте CsvHelper, который позволяет использовать произвольные escape-последовательности.

Charlieface 25.07.2024 14:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
13
73
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете реализовать Stream, который оборачивает входной поток. Он не оптимизирован, как Sep, но его также легко сравнивать с чем-то более оптимизированным, но при этом он не особенно неэффективен (не буферизует весь ввод в память, не добавляет копирования или декодирования).

using System;
using System.Diagnostics;
using static System.IO.Stream;

/// <summary>
/// Replaces all instances of <c>\"</c> with <c>""</c> in an ASCII-compatible stream.
/// </summary>
class ReplacementHack : Stream {
    private Stream Wrapped;

    /// <summary>
    /// Up to one byte of buffered data that's been read from the underlying stream but not read from this stream.
    /// </summary>
    private byte? Buffered = null;

    public override bool CanRead => true;
    public override bool CanWrite => false;
    public override bool CanSeek => false;

    public ReplacementHack(Stream wrapped) {
        this.Wrapped = wrapped;
    }

    public override int Read(byte[] buffer, int offset, int count) =>
        this.Read(new Span<byte>(buffer, offset, count));

    public override int Read(Span<byte> buffer) {
        if (buffer.IsEmpty) {
            return 0;
        }

        int r;
        if (this.Buffered is byte b) {
            this.Buffered = null;
            buffer[0] = b;
            r = checked(1 + this.Wrapped.Read(buffer.Slice(1)));
        } else {
            r = this.Wrapped.Read(buffer);
        }

        this.Transform(buffer.Slice(0, r));

        return r;
    }

    private void Transform(Span<byte> buffer) {
        if (buffer.IsEmpty) {
            return;
        }

        const byte ASCII_BACKSLASH = 0x5c;
        const byte ASCII_DQUOTE = 0x22;

        // Replace all complete instances of \" in the buffer.
        for (int i = 0; i < buffer.Length - 1; i++) {
            if (buffer[i] == ASCII_BACKSLASH && buffer[i + 1] == ASCII_DQUOTE) {
                buffer[i] = ASCII_DQUOTE;
                i++;
            }
        }

        // If the last byte of the buffer could be the start of \", peek one character ahead to resolve it.
        if (buffer[buffer.Length - 1] == ASCII_BACKSLASH) {
            var peek = this.Wrapped.ReadByte();

            if (peek == -1) {
                Debug.Assert(this.Buffered is null);
            } else {
                this.Buffered = (byte)peek;

                if (peek == ASCII_DQUOTE) {
                    buffer[buffer.Length - 1] = ASCII_DQUOTE;
                }
            }
        }
    }

#region Non-seekable stream
    public override long Length => throw new NotSupportedException();
    public override long Position {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }
#endregion

#region Read-only stream
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override void Flush() {}
    public override long Seek(long offset, System.IO.SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long length) => throw new NotSupportedException();
#endregion
}

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


Дополнение ОП с результатами испытаний:

valid: Unfiltered: 396ms  (raw Sep speed)
valid: Filtered: 490ms
broken: Unfiltered: Exception was thrown
broken: Filtered: 478ms

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

Это сработало фантастически. Вы не возражаете, если я дополню ваш пост результатами своих анализов?

Bryan B 25.07.2024 18:30

@BryanBoettcher: Конечно.

Ry- 25.07.2024 20:33

они сейчас опубликованы. Отличная работа, серьезно. Он достаточно быстр, чтобы не вызывать беспокойства, и выглядит достаточно модифицируемым, если в конечном итоге становится узким местом (это не так, в конечном итоге это БД).

Bryan B 25.07.2024 20:38

Другие вопросы по теме