如何为 XML 编写正则表达式以删除除 CDATA 之外的未转义的符号字符?

How to write regex for XML which removes unescaped ampersand characters except CDATA?

例如,我 XML 是这样的:

<title>Very bad XML with & (unescaped)</title>
<title>Good XML with &amp; and &#x3E; (escaped)</title>
<title><![CDATA[ Good XML with & in CDATA ]]></title>

我的任务是从 XML 中删除无效的符号字符,但排除 CDATA 中的那些符号字符。 我找到了一个正则表达式:

&(?!(?:apos|quot|[gl]t|amp);|#)

但不幸的是,它还从 CDATA 中删除了 & 字符。如何更改此正则表达式以满足我的任务?

如您所知,"XML" 不是 XML,因为 CDATA 之外的未转义 &。 因此,您必须在没有 XML 解析器区分 CDATA 和 PCDATA 的情况下进行预处理。这很粗糙,由于 regex isn't up to parsing XML.

的所有原因,正则表达式不能胜任这项任务

这是一种可以提供帮助的方法:

  1. 使用正则表达式将所有孤立的(不是字符实体的一部分)& 字符替换为 &amp;TEMP,包括 CDATA 中的字符。
  2. 在现在格式正确的 XML 上使用 XML 解析器,将 CDATA 中出现的 &amp;TEMP 恢复为 &

另请参阅:

  • 关于解析混乱的一般建议"XML"
  • 容忍解析器
  • 用于匹配无效字符的正则表达式和&

作为对@kjughes 回答的补充,编写一个程序来提取“&”字符是相当简单的,尽管这是一个相当无聊的练习。由于CDATAs不能嵌套,所以很容易标记标签的开闭。

这是一个这样的程序:

    final int NOCDATA = -1;
    final int OPEN_CDATA0 = 0;   //!
    final int OPEN_CDATA1 = 1;   //![
    final int OPEN_CDATA2 = 2;   //![C
    final int OPEN_CDATA3 = 3;   //![CD
    final int OPEN_CDATA4 = 4;   //![CDA
    final int OPEN_CDATA5 = 5;   //![CDAT
    final int OPEN_CDATA6 = 6;   //![CDATA
    final int INSIDE_CDATA = 7;  //![CDATA[

    final int CLOSE_CDATA0 = 8;  //]

    String xml = "<title>Very bad XML with & (unescaped)</title>\n" +
            "<title>Good XML with &amp; and &#x3E; (escaped)</title>\n" +
            "<title><![CDATA[ Good XML with & in CDATA && ]]></title><title>Very bad XML with ![CDATA[&]] && (unescaped)</title>";

    StringBuilder result = new StringBuilder();
    Reader reader = new BufferedReader(new StringReader(xml));

    int r;
    int state = NOCDATA;

    while((r = reader.read()) != -1) {
        char c = (char)r;
        switch(c) {
            case '!':
                if(state == NOCDATA)
                    state = OPEN_CDATA0;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case '[':
                if(state == OPEN_CDATA0)
                    state = OPEN_CDATA1;
                else if(state == OPEN_CDATA6)
                    state = INSIDE_CDATA;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case 'C':
                if(state == OPEN_CDATA1)
                    state = OPEN_CDATA2;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case 'D':
                if(state == OPEN_CDATA2)
                    state = OPEN_CDATA3;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case 'A':
                if(state == OPEN_CDATA3)
                    state = OPEN_CDATA4;
                else if(state == OPEN_CDATA5)
                    state = OPEN_CDATA6;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case 'T':
                if(state == OPEN_CDATA4)
                    state = OPEN_CDATA5;
                else if(state != INSIDE_CDATA)
                    state = NOCDATA;
                break;
            case ']':
                if(state == INSIDE_CDATA)
                    state = CLOSE_CDATA0;
                else if(state == CLOSE_CDATA0)
                    state = NOCDATA;
                break;
            default:
                break;
        }
        if(state == CLOSE_CDATA0 && c != ']') {
            System.err.println("ERROR CLOSING");
            System.out.println(result);
            System.exit(1);
        }
        if(c !='&' || state == INSIDE_CDATA)
            result.append(c);
    }
    System.out.println(result);

此程序为问题中的输入输出以下内容(输入中第一个字符串的副本已附加到整个字符串的末尾,并带有一个额外的 CDATA 标记以检查右括号):

<title>Very bad XML with  (unescaped)</title>
<title>Good XML with amp; and #x3E; (escaped)</title>
<title><![CDATA[ Good XML with & in CDATA && ]]></title><title>Very bad XML with ![CDATA[&]]  (unescaped)</title>

它实际上是一个使用 switch/case 语句构建的简单状态机。我没有对此进行过广泛的测试,我怀疑嵌套 CDATA 可能会导致失败(无论如何问题中似乎都不允许这样做)。我也懒得在 CDATA 关闭标记中添加最后一个 >。但修改它以涵盖任何失败案例应该很容易。 This answer 为 CDATA 标签的词法分析提供了正确的结构。