正则表达式不能处理非法的方括号
本文关键字:方括号 非法 处理 不能 正则表达式 | 更新日期: 2023-09-27 18:12:31
多亏了这里过去的聪明才智,我有了这个神奇的递归正则表达式,它帮助我在文本块中转换自定义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]
标记。它将其分为三组:标记、可选值和内容。标记为标记名(在本例中为"代码")。可选值是等号(=
)后面的值(如果有)。内容是介于开始和结束标签之间的所有内容。
正则表达式可以递归地用来匹配嵌套的标签。因此,在匹配[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
提前感谢!
编辑
如果你对我使用这个表达式的方法感兴趣,欢迎查看
我做了一个程序,可以解析您的字符串在一个可调试的,开发人员友好的方式。它不是像那些正则表达式那样的小代码,但它有积极的一面:您可以调试它,并根据需要对其进行微调。
实现是一个下降递归解析器,但如果您需要某种上下文数据,您可以将其全部放在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在主题中自上而下解析的时间和空间复杂性。我觉得它很有趣。
第一个更改非常简单—您可以通过将负责匹配自由文本的[^][]+
更改为.
来获得它。这看起来有点疯狂,也许,但它实际上是安全的,因为您使用的是占有组(?> )
,所以所有有效的标记将与第一个替代组'[(?<innertag>[^][/='s]+)[^][]*]
匹配,并且不能回溯和破坏标记。(记住启用单行标志,以便.
匹配换行符)
第二个要求,[unsuccessful]
,似乎违背了你的目标。从一开始的整个想法就是而不是来匹配这些未关闭的标记。如果允许打开标记,所有形式'[(.*?)'].*?[/'1]
的匹配都是有效的。不好的。在最好的情况下,您可以尝试将一些不允许匹配的标记列入白名单。
两个变化的例子:
(?>
'[ (?<tag>[^][/='s]+) 's*
(?: = 's* (?<val>[^][]*) 's*)?
']
)
(?<content>
(?>
'[(?:unsuccessful)'] # self closing
|
'[(?<innertag>[^][/='s]+)[^][]*]
|
'[/(?<-innertag>'k<innertag>)]
|
.
)*
(?(innertag)(?!))
)
'[/'k<tag>']
Regex Hero的工作示例
好的。这是另一个尝试。这个有点复杂。
其思想是从头到尾匹配整个文本,并将其解析为单个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>)
捕获B
后的所有文本到A
(并从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.
这非常接近完整解析的文档,但是您仍然需要使用索引和长度来确定文档的确切顺序和结构(尽管它并不比对所有捕获进行排序更复杂)
在这一点上,我将陈述其他人所说的-这可能是编写解析器的好时机,这种模式不是很漂亮…