Поиск строк в кавычках с экранированными кавычками в C# с использованием регулярного выражения

Я пытаюсь найти весь цитируемый текст в одной строке.

Пример:

"Some Text"
"Some more Text"
"Even more text about \"this text\""

Мне нужно получить:

  • "Some Text"
  • "Some more Text"
  • "Even more text about \"this text\""

\"[^\"\r]*\" дает мне все, кроме последнего, из-за экранированных кавычек.

Я читал о том, что \"[^\"\\]*(?:\\.[^\"\\]*)*\" работает, но во время выполнения получаю сообщение об ошибке:

parsing ""[^"\]*(?:\.[^"\]*)*"" - Unterminated [] set.

Как я могу это исправить?


person Joshua Lowry    schedule 27.01.2010    source источник


Ответы (11)


То, что у вас есть, является примером техники «развернутого цикла» Фридла, но вы, кажется, немного путаетесь в том, как выразить ее в виде строкового литерала. Вот как это должно выглядеть для компилятора регулярных выражений:

"[^"\\]*(?:\\.[^"\\]*)*"

Начальный "[^"\\]* соответствует кавычке, за которой следует ноль или более любых символов, кроме кавычек или обратной косой черты. Только эта часть вместе с последним " будет соответствовать простой строке в кавычках без встроенных escape-последовательностей, таких как "this" или "".

Если он обнаруживает обратную косую черту, \\. использует обратную косую черту и все, что следует за ней, а [^"\\]* (опять же) использует все до следующей обратной косой черты или кавычки. Эта часть повторяется столько раз, сколько необходимо, пока не появится неэкранированная кавычка (или она не достигнет конца строки и попытка сопоставления не удастся).

Обратите внимание, что это будет соответствовать "foo\"- в \"foo\"-"bar". Это может показаться недостатком в регулярном выражении, но это не так; это ввод недействителен. Цель состояла в том, чтобы сопоставить строки в кавычках, необязательно содержащие экранированные кавычки с обратной косой чертой, встроенные в другой текст - почему должны быть экранированные кавычки за пределами строк в кавычках? Если вам действительно нужно это поддерживать, у вас гораздо более сложная проблема, требующая совсем другого подхода.

Как я уже сказал, это то, как регулярное выражение должно выглядеть для компилятора регулярных выражений. Но вы пишете его в виде строкового литерала, а они, как правило, специально обрабатывают определенные символы, т. е. обратную косую черту и кавычки. К счастью, дословные строки C# избавляют вас от хлопот, связанных с двойным экранированием обратной косой черты; вам просто нужно экранировать каждую кавычку другой кавычкой:

Regex r = new Regex(@"""[^""\\]*(?:\\.[^""\\]*)*""");

Таким образом, правило заключается в двойных кавычках для компилятора C# и двойных обратных косых чертах для компилятора регулярных выражений — красиво и просто. Это конкретное регулярное выражение может выглядеть немного неуклюжим, с тремя кавычками на каждом конце, но рассмотрите альтернативу:

Regex r = new Regex("\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"");

В Java вы всегда должны писать их именно так. :-(

person Alan Moore    schedule 28.01.2010
comment
Мне больше всего нравится это объяснение. - person Joshua Lowry; 28.01.2010
comment
Круиз по некоторым ответам, которые сделали вас знаменитыми ... Голосую за этот ответ за такое четкое объяснение из худшего супа с обратной косой чертой! :) - person zx81; 06.05.2014
comment
То же самое можно сделать с помощью одинарной кавычки (') - person Kalpesh Rajai; 05.02.2016
comment
@KalpeshRajai: Конечно, просто замените двойные кавычки на одинарные в моем первом регулярном выражении. Вам даже не нужно экранировать их (если только вы не используете строковый литерал в одинарных кавычках, который C# не поддерживает). - person Alan Moore; 05.02.2016
comment
@AlanMoore: Спасибо - person Kalpesh Rajai; 05.02.2016
comment
Я бы проголосовал за это 100 раз, если бы мог! Ваше объяснение, наконец, помогло мне выполнить то, что я пытался сделать. Ваше дословное регулярное выражение работает так, как рекламируется для традиционных escape-последовательностей, то есть обратной косой черты, за которой следует символ (включая кавычку). Однако он не обрабатывает управляющую последовательность для кавычек, используемых в дословных строках, т. е. двойную кавычку. Но опять же, ваше объяснение помогло мне увидеть ответ с использованием чередования, например, @"""[^""\\]*(?:(?:\\.|"""")[^""\\]*)*""". Большое спасибо! - person Matt Davis; 10.07.2021

Регулярное выражение для захвата строк (с \ для экранирования символов), для движка .NET:

(?>(?(STR)(?(ESC).(?<-ESC>)|\\(?<ESC>))|(?!))|(?(STR)"(?<-STR>)|"(?<STR>))|(?(STR).|(?!)))+   

Вот, "дружеский" вариант:

(?>                            | especify nonbacktracking
   (?(STR)                     | if (STRING MODE) then
         (?(ESC)               |     if (ESCAPE MODE) then
               .(?<-ESC>)      |          match any char and exits escape mode (pop ESC)
               |               |     else
               \\(?<ESC>)      |          match '\' and enters escape mode (push ESC)
         )                     |     endif
         |                     | else
         (?!)                  |     do nothing (NOP)
   )                           | endif
   |                           | -- OR
   (?(STR)                     | if (STRING MODE) then
         "(?<-STR>)            |     match '"' and exits string mode (pop STR)
         |                     | else
         "(?<STR>)             |     match '"' and enters string mode (push STR)
   )                           | endif
   |                           | -- OR
   (?(STR)                     | if (STRING MODE) then
         .                     |     matches any character
         |                     | else
         (?!)                  |     do nothing (NOP)  
   )                           | endif
)+                             | REPEATS FOR EVERY CHARACTER

На основе http://tomkaminski.com/conditional-constructs-net-regular-expressions примеры. Он основан на балансировке котировок. Я использую его с большим успехом. Используйте его с флагом Singleline.

Чтобы поэкспериментировать с регулярными выражениями, я рекомендую Rad Software Regular Expression Designer, который имеет хороший "Язык Вкладка «Элементы» с быстрым доступом к некоторым основным инструкциям. Он основан на механизме регулярных выражений .NET.

person Ricardo Nolde    schedule 10.09.2010

"(\\"|\\\\|[^"\\])*"

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

In C#:

StringCollection resultList = new StringCollection();
Regex regexObj = new Regex(@"""(\\""|\\\\|[^""\\])*""");
Match matchResult = regexObj.Match(subjectString);
while (matchResult.Success) {
    resultList.Add(matchResult.Value);
    matchResult = matchResult.NextMatch();
} 

Изменить: в список добавлена ​​экранированная обратная косая черта для правильной обработки "This is a test\\".

Объяснение:

Сначала найдите символ кавычки.

Затем альтернативы оцениваются слева направо. Движок сначала пытается сопоставить экранированную цитату. Если это не совпадает, он пытается использовать экранированную обратную косую черту. Таким образом, он может различать "Hello \" string continues" и "String ends here \\".

Если какой-либо из них не совпадает, то допускается все остальное, кроме кавычек или символов обратной косой черты. Затем повторите.

Наконец, сопоставьте закрывающую цитату.

person Tim Pietzcker    schedule 27.01.2010
comment
Извините за столь сильное редактирование этого поста. Но теперь я думаю, что у меня получилось достаточно элегантно. И правильно тоже. Надеюсь. - person Tim Pietzcker; 27.01.2010
comment
Это регулярное выражение не работает с этим текстом: \Some Text\ Some Text Some Text, and Some more Text and Even more text about \this text\ - person Kamarey; 27.01.2010
comment
Это отлично! Я думаю, что часть проблемы заключалась в том, что я не использовал @, который добавлял сложности из-за необходимости косой черты повсюду. - person Joshua Lowry; 27.01.2010
comment
Ну, тексты, заключенные в экранированные кавычки, не были частью вопроса; ни один из них не удваивался как еще один способ избежать кавычек. - person Tim Pietzcker; 28.01.2010
comment
Прости, Тим, но "(\\"|\\\\|[^"])*" никуда не годится. Да, он очень хорошо соответствует допустимым строкам в кавычках, но отклоняется в сторону катастрофического поиска с возвратом при представлении несоответствующей строки, такой как: "\\\\\\\\\\\\\\\\\\\\\\\ (параметры в группе чередования должны быть взаимоисключающими, если вы применяете к ней * или +) Это регулярное выражение может соответствовать обратной косой черте более чем одним способом. - person ridgerunner; 09.04.2011
comment
@ridgerunner: Вы правы, спасибо. Я исправил регулярное выражение (включив обратную косую черту в отрицательный класс символов). Теперь ваша патологическая струна выходит из строя через 85 вместо 750 000 шагов. - person Tim Pietzcker; 10.04.2011
comment
Еще раз извините, но: "(\\"|\\\\|[^"\\])*" не соответствует: "\n" или "\t". Здесь нужен шаблон: "([^"\\]|\\.)*", который соответствует правильно (или еще лучше: "([^"\\]++|\\.)*", если доступен притяжательный квантификатор). Но развернутая версия этого выражения Фридля намного быстрее. Смотрите ответ Алана. Вы читали MRE3 еще нет? Если нет, я знаю, что вам это очень понравится (если вы используете регулярные выражения, а я думаю, что вы). - person ridgerunner; 10.04.2011
comment
Тем не менее, я склонен говорить, что ([^\]|\\.)* - лучший ответ здесь. Самая натуральная и полностью рабочая струна, когда развёрнутая Фридла примерно такая же, с оптимизацией (но избыточностью) - person 131; 25.09.2012

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

Что касается вашей проблемы, я бы попробовал четыре / вместо двух:

\"[^\"\\\\]*(?:\\.[^\"\\\\]*)*\"
person Jason    schedule 27.01.2010
comment
Одним из преимуществ RegexBuddy является то, что он может автоматически преобразовывать регулярное выражение в исходный код на любом языке, который вы укажете. В этом случае он преобразует необработанное регулярное выражение "[^"\\]*(?:\\.[^"\\]*)*" в @"""[^""\\]*(?:\\.[^""\\]*)*""". - person Alan Moore; 28.01.2010

Регулярное выражение

(?<!\\)".*?(?<!\\)"

также будет обрабатывать текст, начинающийся с экранированной кавычки:

\"Some Text\" Some Text "Some Text", and "Some more Text" an""d "Even more text about \"this text\""
person Kamarey    schedule 27.01.2010
comment
Есть ли способ, которым это может работать для нескольких строк в кавычках? - person Joshua Lowry; 28.01.2010
comment
Это не обрабатывает экранированные обратные косые черты в конце строк: "Hello\\". - person Tim Pietzcker; 28.01.2010

Что ж, ответ Алана Мура хорош, но я бы немного изменил его, чтобы сделать его более компактным. Для компилятора регулярных выражений:

"([^"\\]*(\\.)*)*"

Сравните с выражением Алана Мура:

"[^"\\]*(\\.[^"\\]*)*"

Объяснение очень похоже на объяснение Алана Мура:

Первая часть " соответствует кавычкам.

Вторая часть [^"\\]* соответствует нулю или более любых символов, кроме кавычек или обратной косой черты.

И последняя часть (\\.)* соответствует обратной косой черте и любому отдельному символу, следующему за ней. Обратите внимание на *, говорящий о том, что эта группа необязательна.

Описанные части вместе с конечным " (т.е. "[^"\\]*(\\.)*") будут соответствовать: "Некоторый текст" и "Еще больше текста\"", но не будут соответствовать: "Еще больше текста об \"этом тексте\"".

Чтобы сделать это возможным, нам нужна часть: [^"\\]*(\\.)* повторяется столько раз, сколько необходимо, пока не появится неэкранированная кавычка (или она не достигнет конца строки и попытка сопоставления не удастся). Поэтому я заключил эту часть в скобки и добавил звездочку. Теперь он соответствует: "Некоторый текст", "Еще больше текста\"", "Еще больше текста об \"этом тексте\"" и "Здравствуйте\\".

В коде C# это будет выглядеть так:

var r = new Regex("\"([^\"\\\\]*(\\\\.)*)*\"");

Кстати, порядок двух основных частей: [^"\\]* и (\\.)* не имеет значения. Ты можешь написать:

"([^"\\]*(\\.)*)*"

or

"((\\.)*[^"\\]*)*"

Результат будет таким же.

Теперь нам нужно решить еще одну задачу: \"foo\"-"bar". Текущее выражение будет соответствовать "foo\"-", но мы хотим сопоставить его с "bar". Я не знаю

почему должны быть экранированные кавычки снаружи строк в кавычках

но мы можем легко реализовать это, добавив в начало следующую часть: (\G|[^\\]). В нем говорится, что мы хотим, чтобы совпадение начиналось с того места, где закончилось предыдущее совпадение, или после любого символа, кроме обратной косой черты. Зачем нам \G? Это для следующего случая, например: "a""b".

Обратите внимание, что (\G|[^\\])"([^"\\]*(\\.)*)*" соответствует -"bar" в \"foo\"-"bar". Итак, чтобы получить только "bar", нам нужно указать группу и при желании дать ей имя, например «MyGroup». Тогда код С# будет выглядеть так:

[TestMethod]
public void RegExTest()
{
    //Regex compiler: (?:\G|[^\\])(?<MyGroup>"(?:[^"\\]*(?:\.)*)*")
    string pattern = "(?:\\G|[^\\\\])(?<MyGroup>\"(?:[^\"\\\\]*(?:\\\\.)*)*\")";
    var r = new Regex(pattern, RegexOptions.IgnoreCase);

    //Human readable form:       "Some Text"  and  "Even more Text\""     "Even more text about  \"this text\""      "Hello\\"      \"foo\"  - "bar"  "a"   "b" c "d"
    string inputWithQuotedText = "\"Some Text\" and \"Even more Text\\\"\" \"Even more text about \\\"this text\\\"\" \"Hello\\\\\" \\\"foo\\\"-\"bar\" \"a\"\"b\"c\"d\"";
    var quotedList = new List<string>();
    for (Match m = r.Match(inputWithQuotedText); m.Success; m = m.NextMatch())
        quotedList.Add(m.Groups["MyGroup"].Value);

    Assert.AreEqual(8, quotedList.Count);
    Assert.AreEqual("\"Some Text\"", quotedList[0]);
    Assert.AreEqual("\"Even more Text\\\"\"", quotedList[1]);
    Assert.AreEqual("\"Even more text about \\\"this text\\\"\"", quotedList[2]);
    Assert.AreEqual("\"Hello\\\\\"", quotedList[3]);
    Assert.AreEqual("\"bar\"", quotedList[4]);
    Assert.AreEqual("\"a\"", quotedList[5]);
    Assert.AreEqual("\"b\"", quotedList[6]);
    Assert.AreEqual("\"d\"", quotedList[7]);
}
person Alex    schedule 02.08.2013
comment
Хотя я ценю усилия, это регулярное выражение заметно медленнее, чем оригинал Алана. - person wp78de; 11.06.2018

Я знаю, что это не самый чистый метод, но в вашем примере я бы проверил символ перед ", чтобы увидеть, является ли он \. Если это так, я бы проигнорировал цитату.

person Krill    schedule 27.01.2010

Подобно RegexBuddy, опубликованному @Blankasaurus, RegexMagic тоже помогает.

person Emre    schedule 09.04.2011

Простой ответ без использования ?:

"([^\\"]*(\\")*)*\"

или, как дословная строка

@"^""([^\\""]*(\\"")*(\\[^""])*)*"""

Это просто означает:

  • найди первую "
  • найти любое количество символов, которые не являются \ или "
  • найти любое количество экранированных кавычек \"
  • найти любое количество экранированных символов, которые не являются кавычками
  • повторяйте последние три команды, пока не найдете "

Я считаю, что это работает так же хорошо, как ответ @Alan Moore, но для меня его легче понять. Он также принимает несбалансированные («несбалансированные») котировки.

person Piotr Zierhoffer    schedule 18.07.2013
comment
Я вижу, что этот ответ по какой-то причине немного ошибочен. См. stackoverflow.com/questions/20196740/ - person Piotr Zierhoffer; 25.11.2013

Любой шанс, который вам нужно сделать: \"[^\"\\\\]*(?:\\.[^\"\\\\]*)*\"

person Fried Hoeben    schedule 27.01.2010
comment
Это дает мне: Некоторый текст; Еще немного текста; - person Joshua Lowry; 27.01.2010

Если вы можете определить начало и конец, должно работать следующее:

new Regex(@"^(""(.*)*"")$")
person Babu James    schedule 20.12.2019