任何事都有个由头, 下面是需求.
我公司的软件将专有的二进制格式转换成XML 供技术支持人员修改, 修改完之后用同一个工具可转回原来的二进制格式. 对于比较大的数组, 保存成XML的做法是把数组中的每个元素用逗号隔开, 作为XML的节点的属性值.
问题在于数组很大, 典型情况是256个元素, 这样生成的XML文件就会有很长的一行, 使用者往往要修改特定序号的某个值, 比如第17个元素的值, 在256个元素水平连续排列的情况下, 这种工作有多可怕可以想见.
上面给出的是可读性强的版本, 是使用了正则表达式转换之后的样子, 每16个元素一行, 每4个元素一个分组.
只要想象一下把所有这些行的东西连接成一行, 中间的空格全部删除, 就会知道可读性有多差. 没有把它连成一行只是因为在HTML页面上弄成这样会导致这篇贴子很招人嫌.
为什么不在保存的时候直接弄成上述这样? 答案是通过XmlDocument 这种最常用的方案做到上述这样的输出格式并不容易, 即使借助于 XmlTextWriter 这样的类, 一点一点控制输出的XML内容, 工作量也不会小.
何况, 还要冒着修改已经工作的代码的风险.
下面是用.NET中的正则表达式在已经保存成单行格式的XML文件之后, 如何把它变成上面的这样.
1. 把整个文件的内容一次性全部读入一个字符串
|
string whole_txt = string.Empty;
using (StreamReader tr = new StreamReader(xml_fname, Encoding.UTF8))
{
whole_txt = tr.ReadToEnd();
}
|
2. 把XML的属性值部分分隔成16个元素一行
|
Regex re_split16 = new Regex(@"(?<=^ # from begin of the string
(?:
(?:[^,]+,) # repeat of non-comma followed by a comma
{16} # treat the above as atom, repeat it 16 times
)
+ # and treat the bigger groups as an atom, repeat >=1 times
)
# then a special place which eats nothing,
# but it's the target position this regex looking for
(?=[^,]) # and assert the special position followed by a non-comma
", RegexOptions.IgnorePatternWhitespace);
|
这只是一个正则表达式, 为了可读性, 几乎所有稍微复杂一点的正则表达式我都这样逐项解释.
3. 准备一个跟2中正则表达式极为类似的正则表达式, 把每行的16个元素, 4个一组再次拆分
|
Regex re_split4 = new Regex(re_split16.ToString().Replace("16", "4"),
RegexOptions.IgnorePatternWhitespace);
|
这里有个小技巧, 因为这个正则表达式跟上面的那个除了分组的元素数个不同, 其它部分完全一样, 所以不必把上面的内容复制一遍到源代码里. 正则表达式的 ToString() 可以返回它的未编译的, 构造该Regex对象时所指定的正则表达式, 把它里面的16替换成4, 就有了一个4x4 分组的对象.
4. 定义抽取属性值的部分, 注意只要属性的值
Regex re_long_attr = new Regex(@"
(?<=(?^\s*) # get the leading space for this XML node
.*? # skip chars as less as possible
\w+="" # to find the target attribute
) # All the above is a look-behind assert
([^,""]+)(,[^,""]+){16,}[^""]* # The target content is the value of the attribute
# which must be more than 16 items
# separated by comma, and content up to the closing
# double-quote character
", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);
|
5. 开始替换, 在要替换的内容内部, 又使用了另外的正则表达式, 这样的做法完全得宜于.NET对正则表达式支持的这一特殊功能:
Regex.Replace(input_string, "search", MatchEvaluator );
这个 MatchEvaluator 是一个delegate, 输出一个参数, 即Match对象, 每次Replace时匹配到的那个对象. 返回值是一个string, 作为被替换的内容.
whole_txt = re_long_attr.Replace(whole_txt,
delegate(Match m)
{
string leading_space = string.Empty;
leading_space = m.Groups["leading_space"].Value;
StringBuilder sb = new StringBuilder();
foreach (string grp16 in re_split16.Split(m.Value))
{
sb.Append(Environment.NewLine + leading_space + " " +
string.Join(" ", re_split4.Split(grp16)));
}
return sb.ToString();
}
);
|
在Replace函数的内部就地定义了一个匿名的函数, 该函数必需符合MatchEvaluator 的原型.
该函数内部是正则魔法的主要舞台. 先是把属性的值用Split切分成16个元素一组, 然后对每一组再次应用4分组的正则表达式进一步细分, 成为4个元素一组, 再用string.Join方法, 以连续的空格来分隔它们以提供更好的可读性, 最后, 把分分合合之后的一行内容前面加上换行符, 和前导的空格.
前导空格的个数是与当前的XML节点本身的缩进级别相关的, 其缩进空白由正则表达式
re_long_attr中指定的命名分组leading_space捕获. 完美畅快!
整个解决方法的要点是正则表达式在.NET中是一个对象, 所以你可以定义多个互相独立的正则表达式工作, 在一个正则表达式工作的内部, 如上面的Replace调用过程中, 还可以继续深度应用其它的正则表达式, 这在Perl中是不行的, Perl的正则表达式是不能递归调用的.
s/src/dest/;
在Perl中, dest部分, 可以是一段Perl代码, 但这段代码不能再使用正则表达式.
阅读(1900) | 评论(0) | 转发(0) |