Регулярное выражение не может обрабатывать мошеннические квадратные скобки - PullRequest
3 голосов
/ 28 октября 2011

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

/// <summary>
/// Static class containing common regular expression strings.
/// </summary>
public static class RegularExpressions
{
    /// <summary>
    /// Expression to find all root-level BBCode tags. Use this expression recursively to obtain nested tags.
    /// </summary>
    public static string BBCodeTags
    {
        get
        {
            return @"
                    (?>
                      \[ (?<tag>[^][/=\s]+) \s*
                      (?: = \s* (?<val>[^][]*) \s*)?
                      ]
                    )

                    (?<content>
                      (?>
                        \[(?<innertag>[^][/=\s]+)[^][]*]
                        |
                        \[/(?<-innertag>\k<innertag>)]
                        |
                        [^][]+
                      )*
                      (?(innertag)(?!))
                    )

                    \[/\k<tag>]
                    ";
        }
    }
}

Это регулярное выражение прекрасно работает, рекурсивно сопоставляя все теги. Как это:

[code]  
    some code  
    [b]some text [url=http://www.google.com]some link[/url][/b]  
[/code]

Регулярное выражение делает именно то, что я хочу, и соответствует тегу [code]. Он делится на три группы: тег, необязательное значение и контент. Tag - это имя тега (в данном случае «code»). Необязательное значение, являющееся значением после знака равенства (=), если оно есть. А содержимое - это все, что находится между открывающим и закрывающим тегом.

Регулярное выражение может использоваться рекурсивно для сопоставления вложенных тегов. Поэтому после сопоставления на [code] я бы снова запустил его для группы содержимого, и он совпадал бы с тегом [b]. Если бы я снова запустил его в следующей группе контента, он бы соответствовал тегу [url].

Все это замечательно и вкусно, но с одной проблемой. Он не может обрабатывать мошеннические квадратные скобки.

[code]This is a successful match.[/code]

[code]This is an [ unsuccessful match.[/code]

[code]This is also an [unsuccessful] match.[/code]

Я действительно отстой от регулярных выражений, но если кто-нибудь знает, как я могу настроить это регулярное выражение, чтобы правильно игнорировать неконтролируемые скобки (скобки, которые не составляют открывающий тег и / или не имеют совпадающего закрывающего тега), чтобы он все еще соответствовал окружающие теги, я был бы очень признателен: D

Заранее спасибо!

Редактировать

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

Ответы [ 3 ]

3 голосов
/ 28 октября 2011

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

Реализация - рекурсивный синтаксический анализатор спуска , но если вам нужны какие-то контекстные данные, вы можете поместить все это в класс ParseContext.

Это довольно долго, но я считаю, что это лучше, чем решение на основе регулярных выражений.

Чтобы проверить это, создайте консольное приложение и замените весь код внутри Program.cs следующим кодом:

using System.Collections.Generic;
namespace q7922337
{
    static class Program
    {
        static void Main(string[] args)
        {
            var result1 = Match.ParseList<TagsGroup>("[code]This is a successful match.[/code]");
            var result2 = Match.ParseList<TagsGroup>("[code]This is an [ unsuccessful match.[/code]");
            var result3 = Match.ParseList<TagsGroup>("[code]This is also an [unsuccessful] match.[/code]");
            var result4 = Match.ParseList<TagsGroup>(@"
                        [code]  
                            some code  
                            [b]some text [url=http://www.google.com]some link[/url][/b]  
                        [/code]");
        }
        class ParseContext
        {
            public string Source { get; set; }
            public int Position { get; set; }
        }
        abstract class Match
        {
            public override string ToString()
            {
                return this.Text;
            }
            public string Source { get; set; }
            public int Start { get; set; }
            public int Length { get; set; }
            public string Text { get { return this.Source.Substring(this.Start, this.Length); } }
            protected abstract bool ParseInternal(ParseContext context);
            public bool Parse(ParseContext context)
            {
                var result = this.ParseInternal(context);
                this.Length = context.Position - this.Start;
                return result;
            }
            public bool MarkBeginAndParse(ParseContext context)
            {
                this.Start = context.Position;
                var result = this.ParseInternal(context);
                this.Length = context.Position - this.Start;
                return result;
            }
            public static List<T> ParseList<T>(string source)
                where T : Match, new()
            {
                var context = new ParseContext
                {
                    Position = 0,
                    Source = source
                };
                var result = new List<T>();
                while (true)
                {
                    var item = new T { Source = source, Start = context.Position };
                    if (!item.Parse(context))
                        break;
                    result.Add(item);
                }
                return result;
            }
            public static T ParseSingle<T>(string source)
                where T : Match, new()
            {
                var context = new ParseContext
                {
                    Position = 0,
                    Source = source
                };
                var result = new T { Source = source, Start = context.Position };
                if (result.Parse(context))
                    return result;
                return null;
            }
            protected List<T> ReadList<T>(ParseContext context)
                where T : Match, new()
            {
                var result = new List<T>();
                while (true)
                {
                    var item = new T { Source = this.Source, Start = context.Position };
                    if (!item.Parse(context))
                        break;
                    result.Add(item);
                }
                return result;
            }
            protected T ReadSingle<T>(ParseContext context)
                where T : Match, new()
            {
                var result = new T { Source = this.Source, Start = context.Position };
                if (result.Parse(context))
                    return result;
                return null;
            }
            protected int ReadSpaces(ParseContext context)
            {
                int startPos = context.Position;
                int cnt = 0;
                while (true)
                {
                    if (startPos + cnt >= context.Source.Length)
                        break;
                    if (!char.IsWhiteSpace(context.Source[context.Position + cnt]))
                        break;
                    cnt++;
                }
                context.Position = startPos + cnt;
                return cnt;
            }
            protected bool ReadChar(ParseContext context, char p)
            {
                int startPos = context.Position;
                if (startPos >= context.Source.Length)
                    return false;
                if (context.Source[startPos] == p)
                {
                    context.Position = startPos + 1;
                    return true;
                }
                return false;
            }
        }
        class Tag : Match
        {
            protected override bool ParseInternal(ParseContext context)
            {
                int startPos = context.Position;
                if (!this.ReadChar(context, '['))
                    return false;
                this.ReadSpaces(context);
                if (this.ReadChar(context, '/'))
                    this.IsEndTag = true;
                this.ReadSpaces(context);
                var validName = this.ReadValidName(context);
                if (validName != null)
                    this.Name = validName;
                this.ReadSpaces(context);
                if (this.ReadChar(context, ']'))
                    return true;
                context.Position = startPos;
                return false;
            }
            protected string ReadValidName(ParseContext context)
            {
                int startPos = context.Position;
                int endPos = startPos;
                while (char.IsLetter(context.Source[endPos]))
                    endPos++;
                if (endPos == startPos) return null;
                context.Position = endPos;
                return context.Source.Substring(startPos, endPos - startPos);
            }
            public bool IsEndTag { get; set; }
            public string Name { get; set; }
        }
        class TagsGroup : Match
        {
            public TagsGroup()
            {
            }
            protected TagsGroup(Tag openTag)
            {
                this.Start = openTag.Start;
                this.Source = openTag.Source;
                this.OpenTag = openTag;
            }
            protected override bool ParseInternal(ParseContext context)
            {
                var startPos = context.Position;
                if (this.OpenTag == null)
                {
                    this.ReadSpaces(context);
                    this.OpenTag = this.ReadSingle<Tag>(context);
                }
                if (this.OpenTag != null)
                {
                    int textStart = context.Position;
                    int textLength = 0;
                    while (true)
                    {
                        Tag tag = new Tag { Source = this.Source, Start = context.Position };
                        while (!tag.MarkBeginAndParse(context))
                        {
                            if (context.Position >= context.Source.Length)
                            {
                                context.Position = startPos;
                                return false;
                            }
                            context.Position++;
                            textLength++;
                        }
                        if (!tag.IsEndTag)
                        {
                            var tagGrpStart = context.Position;
                            var tagGrup = new TagsGroup(tag);
                            if (tagGrup.Parse(context))
                            {
                                if (textLength > 0)
                                {
                                    if (this.Contents == null) this.Contents = new List<Match>();
                                    this.Contents.Add(new Text { Source = this.Source, Start = textStart, Length = textLength });
                                    textStart = context.Position;
                                    textLength = 0;
                                }
                                this.Contents.Add(tagGrup);
                            }
                            else
                            {
                                textLength += tag.Length;
                            }
                        }
                        else
                        {
                            if (tag.Name == this.OpenTag.Name)
                            {
                                if (textLength > 0)
                                {
                                    if (this.Contents == null) this.Contents = new List<Match>();
                                    this.Contents.Add(new Text { Source = this.Source, Start = textStart, Length = textLength });
                                    textStart = context.Position;
                                    textLength = 0;
                                }
                                this.CloseTag = tag;
                                return true;
                            }
                            else
                            {
                                textLength += tag.Length;
                            }
                        }
                    }
                }
                context.Position = startPos;
                return false;
            }
            public Tag OpenTag { get; set; }
            public Tag CloseTag { get; set; }
            public List<Match> Contents { get; set; }
        }
        class Text : Match
        {
            protected override bool ParseInternal(ParseContext context)
            {
                return true;
            }
        }
    }
}

Если вы используете этот код и когда-нибудь обнаружите, что вам нужны оптимизации, потому что синтаксический анализатор стал неоднозначным, попробуйте использовать словарь в ParseContext, посмотрите здесь для получения дополнительной информации: http://en.wikipedia.org/wiki/Top-down_parsing в теме Пространственно-временная сложность синтаксического анализа сверху вниз . Я нахожу это очень интересным.

1 голос
/ 28 октября 2011

Первое изменение довольно простое - вы можете получить его, изменив [^][]+, отвечающий за сопоставление свободного текста, на ..Возможно, это кажется немного сумасшедшим, но на самом деле это безопасно, потому что вы используете притяжательную группу (?> ), поэтому все действительные теги будут сопоставлены при первом чередовании - \[(?<innertag>[^][/=\s]+)[^][]*] - и не смогут отследить и сломать теги.1005 * (Не забудьте включить флаг Singleline, поэтому . соответствует символам новой строки)

Второе требование, [unsuccessful], похоже, идет вразрез с вашей целью.Идея с самого начала состоит в том, чтобы не соответствовать этим незамкнутым тегам.Если вы разрешите незакрытые теги, все совпадения формы \[(.*?)\].*?[/\1] станут действительными .Нехорошо.В лучшем случае вы можете попытаться внести в белый список несколько тегов, которые нельзя сопоставлять.

Пример обоих изменений:

(?>
\[ (?<tag>[^][/=\s]+) \s*
(?: = \s* (?<val>[^][]*) \s*)?
\]
)
  (?<content>
    (?>
       \[(?:unsuccessful)\]  # self closing
       |
       \[(?<innertag>[^][/=\s]+)[^][]*]
       |
       \[/(?<-innertag>\k<innertag>)]
       |
       .
    )*
    (?(innertag)(?!))
  )
\[/\k<tag>\]

Рабочий пример для Regex Hero

0 голосов
/ 28 октября 2011

Хорошо. Вот еще одна попытка. Это немного сложнее.
Идея состоит в том, чтобы сопоставить весь текст от начала и до конца и разобрать его в один Match. Несмотря на то, что .Net Balancing Groups редко используются в качестве таковых, вы можете точно настроить свои захваты, запоминая все позиции и захваты именно так, как вы хотите.
Шаблон, который я придумал:

\A
(?<StartContentPosition>)
(?:
    # Open tag
    (?<Content-StartContentPosition>)          # capture the content between tags
    (?<StartTagPosition>)                      # Keep the starting postion of the tag
    (?>\[(?<TagName>[^][/=\s]+)[^\]\[]*\])     # opening tag
    (?<StartContentPosition>)                  # start another content capture
    |
    # Close tag
    (?<Content-StartContentPosition>)          # capture the content in the tag
    \[/\k<TagName>\](?<Tag-StartTagPosition>)  # closing tag, keep the content in the <tag> group
    (?<-TagName>)
    (?<StartContentPosition>)                  # start another content capture
    |
    .           # just match anything. The tags are first, so it should match
                # a few if it can. (?(TagName)(?!)) keeps this in line, so
                # unmatched tags will not mess with the resul
)*
(?<Content-StartContentPosition>)          # capture the content after the last tag
\Z
(?(TagName)(?!))

Помните - балансировочная группа (?<A-B>) захватывает в A весь текст с момента последнего захвата B (и извлекает эту позицию из стека B).

Теперь вы можете разобрать строку, используя:

Match match = Regex.Match(sample, pattern, RegexOptions.Singleline |
                                           RegexOptions.IgnorePatternWhitespace);

Ваши интересные данные будут на match.Groups["Tag"].Captures, который содержит все теги (некоторые из них содержатся в других), и match.Groups["Content"].Captures, который содержит содержимое тега и содержимое между тегами. Например, без пробелов он содержит:

  • some code
  • some text
  • This is also an successful match.
  • This is also an [ unsuccessful match.
  • This is also an [unsuccessful] match.

Это довольно близко к полному проанализированному документу, но вам все равно придется поиграть с индексами и длиной, чтобы выяснить точный порядок и структуру документа (хотя это не сложнее, чем сортировка всех захватов) 1039 *

В этот момент я скажу то, что сказали другие - возможно, сейчас самое время написать парсер, этот шаблон не очень хорош ...

...