С# - Разделение на трубу со сбежавшей трубой в данных?

У меня есть файл с разделителями каналов, который я хотел бы разделить (я использую С#). Например:

This|is|a|test

Однако некоторые данные могут содержать в себе канал. Если это так, он будет экранирован обратной косой чертой:

This|is|a|pip\|ed|test (this is a pip|ed test)

Мне интересно, есть ли регулярное выражение или какой-либо другой метод, чтобы разделить это только на «чистые» каналы (то есть каналы, перед которыми нет обратной косой черты). Мой текущий метод состоит в том, чтобы заменить экранированные каналы пользовательским фрагментом текста, разделить на каналы, а затем заменить мой пользовательский текст на канал. Не очень элегантно, и я не могу не думать, что есть лучший способ. Спасибо за любую помощь.


person Frijoles    schedule 28.04.2011    source источник
comment
Вы видели эту (чудовищную) ветку. Не прямой ответ, но, надеюсь, толчок в правильном направлении.   -  person dawebber    schedule 28.04.2011
comment
Что делать, если вам нужна буквальная обратная косая черта в конце одной из частей?   -  person Random832    schedule 28.04.2011


Ответы (6)


Просто используйте String.IndexOf(), чтобы найти следующую трубу. Если предыдущий символ не является обратной косой чертой, используйте String.Substring() для извлечения слова. В качестве альтернативы вы можете использовать String.IndexOfAny(), чтобы найти следующее вхождение вертикальной черты или обратной косой черты.

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

ИЗМЕНИТЬ

На самом деле, может быть, что-то вроде этого. Было бы интересно посмотреть, как это сравнивается по производительности с решением RegEx.

public List<string> ParseWords(string s)
{
    List<string> words = new List<string>();

    int pos = 0;
    while (pos < s.Length)
    {
        // Get word start
        int start = pos;

        // Get word end
        pos = s.IndexOf('|', pos);
        while (pos > 0 && s[pos - 1] == '\\')
        {
            pos++;
            pos = s.IndexOf('|', pos);
        }

        // Adjust for pipe not found
        if (pos < 0)
            pos = s.Length;

        // Extract this word
        words.Add(s.Substring(start, pos - start));

        // Skip over pipe
        if (pos < s.Length)
            pos++;
    }
    return words;
}
person Jonathan Wood    schedule 28.04.2011
comment
Да, лучше парсить string по-своему, чем использовать regex. Это работает быстрее. +1 - person KaeL; 28.04.2011
comment
Если вы не добавите слова в List<string> и не вернете его, метод ручного анализа будет работать примерно в 5 раз быстрее, чем метод регулярного выражения. Если вы добавите накладные расходы на управление List<string>, это будет примерно в 3 раза быстрее, во всяком случае, на моей машине. - person Cᴏʀʏ; 28.04.2011
comment
Смотрите мое обновление... Я изменил свой тест и замедлил реализацию регулярных выражений примерно в 1,6 раза, но вы все равно выиграли! - person Cᴏʀʏ; 28.04.2011
comment
Я думаю, что это проблема, если последнее слово пустое/пустое. У меня есть файл с 37 именами столбцов заголовков, но последний элемент каждой строки пуст, поэтому строки заканчиваются символом | но без дополнительного пробела; слова в этом случае возвращают только 36 - person Adam; 26.04.2019
comment
Я думаю, что это также может привести к проблемам, когда в конце поля есть скрытая обратная косая черта.. например, данные\\|больше данных|.. решение этой головной боли от клиентских данных ›.‹ - person Adam; 26.04.2019
comment
@Adam: Это восемь лет назад, но можно утверждать, что если поле пусто, его не следует учитывать. Так что я просто говорю, что то, как обрабатывается этот случай, зависит от требований. Не должно быть сложно изменить код, чтобы обрабатывать его по-другому. (И я был бы рад настроить его под другие требования, но это было бы платным консультантом. - person Jonathan Wood; 26.04.2019
comment
спасибо @JonathanWood :) Я смог немного изменить его, чтобы он соответствовал моим потребностям - очень признателен! - person Adam; 16.05.2019

Это должно сделать это:

string test = @"This|is|a|pip\|ed|test (this is a pip|ed test)";
string[] parts = Regex.Split(test, @"(?<!(?<!\\)*\\)\|");

Регулярное выражение в основном говорит: разделить на каналы, которым не предшествует escape-символ. Однако я не должен брать на себя ответственность за это, я просто взломал регулярное выражение из этого поста и упростил его.

ИЗМЕНИТЬ

С точки зрения производительности, по сравнению с методом ручного синтаксического анализа, представленным в этом потоке, я обнаружил, что эта реализация Regex в 3-5 раз медленнее, чем реализация Джонатона Вуда с использованием более длинной тестовой строки, предоставленной OP.

С учетом сказанного, если вы не создадите экземпляр или не добавите слова в List<string> и вместо этого вернете void, метод Джона будет выполняться примерно в 5 раз быстрее, чем метод Regex.Split() (0,01 мс против 0,002 мс) для простого разделения строки. Если вы добавите накладные расходы на управление и возврат List<string>, это будет примерно в 3,6 раза быстрее (0,01 мс против 0,00275 мс), в среднем за несколько наборов из миллиона итераций. Я не использовал статический Regex.Split() для этого теста, вместо этого я создал новый экземпляр Regex с приведенным выше выражением вне моего тестового цикла, а затем вызвал его метод Split.

ОБНОВЛЕНИЕ

Использование статической функции Regex.Split() на самом деле намного быстрее, чем повторное использование экземпляра выражения. В этой реализации использование регулярных выражений всего примерно в 1,6 раза медленнее, чем реализация Джона (0,0043 мс против 0,00275 мс).

Результаты были такими же, используя расширенное регулярное выражение из поста, на который я ссылался.

person Cᴏʀʏ    schedule 28.04.2011
comment
Предполагая, что обратную косую черту также можно экранировать (например, "This|is|a|pip\\|ed|test (this is a pip|ed test)"), это не работает. Вам нужно будет использовать полную версию из упомянутого поста. - person porges; 28.04.2011
comment
@Ты прав, Порджес. Это первое, о чем я подумал, когда решил написать код об этом :) - person Oscar Mederos; 28.04.2011

Я столкнулся с аналогичным сценарием. Для меня было исправлено количество каналов (не каналов с "\|") . Вот как я справился.

string sPipeSplit = "This|is|a|pip\\|ed|test (this is a pip|ed test)";
string sTempString = sPipeSplit.Replace("\\|", "¬"); //replace \| with non printable character
string[] sSplitString = sTempString.Split('|');
//string sFirstString = sSplitString[0].Replace("¬", "\\|"); //If you have fixed number of fields and you are copying to other field use replace while copying to other field.
/* Or you could use a loop to replace everything at once
foreach (string si in sSplitString)
{
    si.Replace("¬", "\\|");
}
*/
person Akshay    schedule 15.12.2016

Вот еще одно решение.

Одна из самых прекрасных вещей в программировании — это несколько способов решения одной и той же проблемы:

string text = @"This|is|a|pip\|ed|test"; //The original text
string parsed = ""; //Where you will store the parsed string

bool flag = false;
foreach (var x in text.Split('|')) {
    bool endsWithArroba = x.EndsWith(@"\");
    parsed += flag ? "|" + x + " " : endsWithArroba ? x.Substring(0, x.Length-1) : x + " ";
    flag = endsWithArroba;
}
person Oscar Mederos    schedule 28.04.2011
comment
Это довольно удобно, но не очень хороший выбор, если вас беспокоит производительность. - person Jonathan Wood; 28.04.2011
comment
@Jonathan Как я уже сказал, это просто еще один способ сделать это. Нет смысла вставлять код, похожий на тот, который вы предоставили. Я с вами согласен, хотя производительность может и не быть чем-то действительно важным в этой задаче. - person Oscar Mederos; 28.04.2011
comment
Я не критиковал вас за то, что вы это опубликовали. На самом деле, я упоминал, что это было гладко. Я просто комментировал эффективность этого подхода. - person Jonathan Wood; 28.04.2011
comment
Я думаю, что этот и @Jonathan оба довольно гладкие, так что +1 обоим. - person Justin Morgan; 29.04.2011

Решение Кори довольно хорошее. Но если вы предпочитаете не работать с регулярными выражениями, тогда вы можете просто выполнить поиск "\|" и замените его каким-либо другим символом, затем выполните разделение, а затем снова замените его на «\|».

Другой вариант — выполнить разбиение, затем просмотреть все строки и, если последним символом является \, соединить его со следующей строкой.

Конечно, все это игнорирует то, что происходит, если вам нужна экранированная обратная косая черта перед каналом... например, "\\|".

В целом, я склоняюсь к регулярному выражению.

Откровенно говоря, я предпочитаю использовать FileHelpers, потому что, хотя это и не разделяется запятой, по сути это одно и то же. И у них есть отличная история о том, почему вам не следует писать этот материал самостоятельно.

person Erik Funkenbusch    schedule 28.04.2011

Вы можете сделать это с помощью регулярного выражения. Как только вы решите использовать обратную косую черту в качестве escape-символа, вам придется учитывать два случая escape-символа:

  • Побег из трубы: \|
  • Экранирование обратной косой черты, которую вы хотите интерпретировать буквально.

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

/^(?:((?:[^|\\]|(?:\\{2})|\\\|)+)(?:\||$))*/

Запутанно, возможно, но должно работать. Объяснение:

^              #The start of a line
(?:...
    [^|\\]     #A character other than | or \ OR
    (?:\\{2})* #An even number of \ characters OR
    \\\|       #A literal \ followed by a literal |
...)+          #Repeat the preceding at least once
(?:$|\|)       #Either a literal | or the end of a line
person Justin Morgan    schedule 28.04.2011
comment
@Justin почему-то не работает на моем компьютере. Также отсутствует ). - person Oscar Mederos; 28.04.2011
comment
@Oscar - Было так много вложенных скобок, что было трудно уследить. Попробуй это сейчас. - person Justin Morgan; 28.04.2011
comment
@Justin теперь это работает, хотя то же самое происходит с решением @Cory: A\\|b должно стать A\|b вместо A\\ и б. Первый \\ является таким же символом, как и любой другой, а второй экранирует |, поэтому второй будет удален, а предложение останется как есть. - person Oscar Mederos; 28.04.2011
comment
@Oscar - Если вы введете A\\|b, вы избежите самого символа обратной косой черты, поэтому его следует интерпретировать как A` plus b. To get A\|b, you would input A\\\|b. That's how I would expect it to work, myself, and it's consistent with most escape schemes I've seen. In C#, for example, the string \\\n` будет литералом `` и возвратом каретки. - person Justin Morgan; 28.04.2011
comment
@ Джастин, это зависит от того, как ты это воспринимаешь. Когда кто-то говорит вам: I want to parse the string ABC\DE, вы должны предположить, что \ уже экранируется. В противном случае исходный пример не имеет смысла, потому что сам C# выдаст ошибку, если вы напишете \| потому что вы ничего не избежать здесь. Чтобы возобновить, я думаю, что строка для анализа является буквальной (уже экранированной). - person Oscar Mederos; 28.04.2011
comment
@Оскар, я понимаю, к чему ты клонишь. С другой стороны, если вы не сделаете этого таким образом, не будет никакого способа, чтобы ввод заканчивался буквальным обратным слэшем. Если вам нужны A\ и b, ни A\|b, ни A\\|b не подойдут. Объявление \ в качестве escape-символа заставляет пользователя экранировать его по всему тексту, но допускает все возможные вводы. Это может быть даже неприемлемо для ситуации спрашивающего, но я решил пойти с наименее ограничительным вариантом. Кстати, похоже, что мы оба нарушили собственные правила экранирования Stack Overflow. - person Justin Morgan; 28.04.2011