Мы используем сентябрь для обработки файлов 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"
.
Я сомневаюсь, что это лучший способ, но можете ли вы показать несколько строк из CSV-файла с проблемой, чтобы мы могли точно понять, о чем вы говорите?
@JonathanWood Я обновил пост, добавив ссылку на репозиторий Github, воспроизводящий сбой, а также примеры данных.
@Ry-: Я точно не знаю. Пока мы видели только \"
. Это определенно проблема на стороне поставщика (их пользовательский интерфейс имеет косую черту в одном из полей и экспортируется как есть), но их исправление не имеет ETA.
Рассматривали ли вы возможность предварительной обработки файлов этого поставщика перед их загрузкой в вашу систему? Может быть, sed или awk смогут продезинфицировать?
@Fildor, я рассмотрел возможность предварительной обработки файлов от поставщика. Вот о чем мой вопрос: я бы хотел предварительно обработать их на лету, передав Stream или TextReader в Sep вместо имени файла напрямую. Я не знаю эффективного способа удалить этих персонажей на месте.
Есть ли причина, по которой вы хотите, чтобы это происходило на лету, а не весь файл вне процесса? Что бы вы ни делали на лету, это всегда влияет на производительность.
Файл загружается в процессе на предыдущем шаге. Нам пришлось бы остановить обработку, обратиться к другой утилите, а затем возобновить импорт. Этот процесс также выполняется на хосте Windows, поэтому нам придется использовать WSL или MinGW, чтобы получить доступ к sed/awk.
Переписывание его во время загрузки, вероятно, частично облегчит нагрузку по оптимизации, поскольку легче написать простой код, который будет идти в ногу с сетью, чем поддерживать хорошо оптимизированную библиотеку.
Я не могу поделиться нашим кодом достаточно, чтобы показать, почему это невозможно. Мне не нужно убегать от Сепа, мне не нужно выставлять себя дураком при взаимодействии с ним. Можете ли вы помочь мне перефразировать вопрос, чтобы я мог получить реальный ответ на вопрос: «Каков высокопроизводительный способ изменения потока во время его обработки»? Видимо, я не могу этого сообщить.
Я писал ответ, но, думаю, мог бы просто упомянуть заранее, что речь идет о реализации Stream
. Однако не совсем ясно, пробовали ли вы уже реализовать TextReader
столь же эффективно.
Прочитать «символы» из ввода; запись в поток памяти при преобразовании «плохих» символов; затем прочитайте из потока памяти с помощью «Sep Reader».
Возможно, используйте CsvHelper, который позволяет использовать произвольные escape-последовательности.
Вы можете реализовать 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
Отфильтрованные и нефильтрованные результаты в действительном файле также хэшируются с одним и тем же значением, показывая, что, если недопустимая последовательность символов не обнаружена, поток полностью не изменяется. Файлы считывались один раз до каких-либо результатов синхронизации, поэтому при всех запусках использовались файлы, кэшированные в оперативной памяти.
Это сработало фантастически. Вы не возражаете, если я дополню ваш пост результатами своих анализов?
@BryanBoettcher: Конечно.
они сейчас опубликованы. Отличная работа, серьезно. Он достаточно быстр, чтобы не вызывать беспокойства, и выглядит достаточно модифицируемым, если в конечном итоге становится узким местом (это не так, в конечном итоге это БД).
Нуждаются ли в обработке другие escape-слеши, особенно
\\
?