如何在Prolog中取消转义HTML属性值?

3

我在SWI-Prolog的库(sgml)中找到了一个谓词xml_quote_attribute/2。这个谓词的第一个参数是输入,第二个参数是输出:

?- xml_quote_attribute('<abc>', X).
X = '&lt;abc&gt;'.

但是我无法想象出如何进行相反的转化。 例如,以下查询不起作用:

?- xml_quote_attribute(X, '&lt;abc&gt;').
ERROR: Arguments are not sufficiently instantiated

还有其他能完成此任务的谓词吗?

再见


似乎是一个不错的赏金任务候选者。 - false
当我需要这个功能时,我亲自编写了它。但那已经是将近5年前的事情了...也许期间已经添加了一些东西。 - CapelliC
4个回答

3
这是Ruud的解决方案,使用DCG符号加上推后列表/半语境符号。
:- use_module(library(dcg/basics)).

html_unescape --> sgml_entity, !, html_unescape.
html_unescape, [C] --> [C], !, html_unescape.
html_unescape --> [].

sgml_entity, [C] --> "&#", integer(C), ";".
sgml_entity, "<" --> "&lt;".
sgml_entity, ">" --> "&gt;".
sgml_entity, "&" --> "&amp;".

使用DCGs使代码更易读。它还消除了使用append/3导致的一些多余回溯,这是Cookie Monster指出的问题。

还有提到“右侧上下文”的地方吗?这个概念实际上很误导人。相反,使用半上下文。 - false
@CookieMonster:你偏爱的并不是稳定的选择。 - false
1
@false 我已将“右侧语境”一词更改为“半语境”。这确实更少令人困惑。 - Wouter Beek
@CookieMonster:“最好的情况是两者都保持不变”,这几乎是不可能的:在规则体中添加一个额外的非终结符epsilon --> [].就已经破坏了任何这样的属性。只有坚定不移的特性仍然存在。您可以(内部和透明地)检查第二个列表参数是否具有某种形式,但这必须是一种保守的检查。 - false
我在G+上发布的内容可以实现一种友好的双向性,使其再次稳定下来不应该是一个问题。唯一的问题是它是用Prolog手工编码的,虽然使用了var/1,但更多地是以CLP的方式,而我还没有找到一种能够做到同样效果的DCG编译技术。 - user502187

1
这是一个朴素的解决方案,使用字符编码列表。很可能它不会给你最佳的性能,但对于不是非常长的字符串来说,它可能还可以。
html_unescape("", "") :- !.

html_unescape(Escaped, Unescaped) :-
    append("&", _, Escaped),
    !,
    append(E1, E2, Escaped),
    sgml_entity(E1, U1),
    !,
    html_unescape(E2, U2),
    append(U1, U2, Unescaped).

html_unescape(Escaped, Unescaped) :-
    append([C], E2, Escaped),
    html_unescape(E2, U2),
    append([C], U2, Unescaped).

sgml_entity(Escaped, [C]) :-
    append(["&#", L, ";"], Escaped),
    catch(number_codes(C, L), error(syntax_error(_), _), fail),
    !.

sgml_entity("&lt;", "<").
sgml_entity("&gt;", ">").
sgml_entity("&amp;", "&").

你需要自己完成 SGML 实体列表。
示例输出:
?- html_unescape("&lt;a&gt; &#26361;&#25805;", L), format('~s', [L]).
<a> 曹操
L = [60, 97, 62, 32, 26361, 25805].

你的意思是:使用(字符)代码列表而不是字符。例如:char_code(a, 0'a) - false
你是对的,抱歉。我已经调整了那段文字,但如果需要的话,请随意进行其他编辑。 - Ruud Helderman
我想我可以通过在每个规则后加上一个剪枝来使sgml_entity/2更有效率,但我猜你不是这个意思。即使没有剪枝,sgml_entity/2似乎也是确定性的。可能是因为我对逻辑编程缺乏经验,所以我仍然无法发现这里缺乏坚定性。你能给出一个具体的例子吗?在这个例子中,错误的输出会强制我的谓词走上错误的路径? - Ruud Helderman
1
希望您还能改进您的解决方案。无论如何,我还是授予了悬赏以防止它被浪费。 - false
1
问题不在编译器技术上,而是 append/3 生成了太多的回溯。例如,它将首先使用 "" 调用谓词 sgml_entity/2,然后使用 "&"、"&l"、"&lt" 和 "<"。如果您使用 DCG,您可以通过仅扫描谓词 sgml_entity/2 的子句一次直接找到匹配的前缀。 - user502187
显示剩余3条评论

1
如果您不介意链接外部模块,那么您可以使用C语言来制作一个非常高效的实现。

html_unescape.pl:

:- module(html_unescape, [ html_unescape/2 ]).
:- use_foreign_library(foreign('./html_unescape.so')).

html_unescape.c:

#include <stdio.h>
#include <string.h>
#include <SWI-Prolog.h>

static int to_utf8(char **unesc, unsigned ccode)
{
    int ok = 1;
    if (ccode < 0x80)
    {
        *(*unesc)++ = ccode;
    }
    else if (ccode < 0x800)
    {
        *(*unesc)++ = 192 + ccode / 64;
        *(*unesc)++ = 128 + ccode % 64;
    }
    else if (ccode - 0xd800u < 0x800)
    {
        ok = 0;
    }
    else if (ccode < 0x10000)
    {
        *(*unesc)++ = 224 + ccode / 4096;
        *(*unesc)++ = 128 + ccode / 64 % 64;
        *(*unesc)++ = 128 + ccode % 64;
    }
    else if (ccode < 0x110000)
    {
        *(*unesc)++ = 240 + ccode / 262144;
        *(*unesc)++ = 128 + ccode / 4096 % 64;
        *(*unesc)++ = 128 + ccode / 64 % 64;
        *(*unesc)++ = 128 + ccode % 64;
    }
    else
    {
        ok = 0;
    }
    return ok;
}

static int numeric_entity(char **esc, char **unesc)
{
    int consumed;
    unsigned ccode;
    int ok = (sscanf(*esc, "&#%u;%n", &ccode, &consumed) > 0 ||
              sscanf(*esc, "&#x%x;%n", &ccode, &consumed) > 0) &&
             consumed > 0 &&
             to_utf8(unesc, ccode);
    if (ok)
    {
        *esc += consumed;
    }
    return ok;
}

static int symbolic_entity(char **esc, char **unesc, char *name, int ccode)
{
    int ok = strncmp(*esc, name, strlen(name)) == 0 &&
             to_utf8(unesc, ccode);
    if (ok)
    {
        *esc += strlen(name);
    }
    return ok;
}

static foreign_t pl_html_unescape(term_t escaped, term_t unescaped)
{
    char *esc;
    if (!PL_get_chars(escaped, &esc, CVT_ATOM | REP_UTF8))
    {
        PL_fail;
    }
    else if (strchr(esc, '&') == NULL)
    {
        return PL_unify(escaped, unescaped);
    }
    else
    {
        char buffer[strlen(esc) + 1];
        char *unesc = buffer;
        while (*esc != '\0')
        {
            if (*esc != '&' || !(numeric_entity(&esc, &unesc) ||
                                 symbolic_entity(&esc, &unesc, "&lt;", '<') ||
                                 symbolic_entity(&esc, &unesc, "&gt;", '>') ||
                                 symbolic_entity(&esc, &unesc, "&amp;", '&')))
                                    // TODO: more entities...
            {
                *unesc++ = *esc++;
            }
        }
        return PL_unify_chars(unescaped, PL_ATOM | REP_UTF8, unesc - buffer, buffer);
    }
}

install_t install_html_unescape()
{
    PL_register_foreign("html_unescape", 2, pl_html_unescape, 0);
}

以下语句将从html_unescape.c编译出一个共享库html_unescape.so。在Ubuntu 14.04上进行过测试;在Windows上可能有所不同。
swipl-ld -shared -o html_unescape html_unescape.c

启动 SWI-Prolog:

swipl html_unescape.pl

样例输出:

?- html_unescape('&lt;a&gt; &#26361;&#25805;', S).
S = '<a> 曹操'.

感谢SWI-Prolog文档和源代码,以及C库将Unicode代码点转换为UTF8?的帮助。

html_unescape/2函数是否总是进行完整的字符串复制,例如如果将“abc”提供给谓词?也就是说,如果没有实体出现? - user502187
@CookieMonster:为了可读性,我故意将C实现尽可能简单。有很多优化的空间,但问题是你是否应该这样做。请参阅Ward's Wiki上的过早优化稍后优化优化规则 - Ruud Helderman
我猜应该有一个解决方案。因为Prolog系统不喜欢太多的原子。例如,SWI-Prolog将计算出原子的murmur哈希值,并在字典中查找它。从源代码来看,我不是SWI-Prolog的专家。但当然,这种优化只是可选的,只是想知道是否在你的源代码中忽略了什么。也点赞了这个答案。 - user502187
@CookieMonster:你说得有道理;我刚刚检查了xml_quote_attribute的实现,并注意到当没有进行替换时,会特别调用PL_unify。Jan Wielemaker可能不是为了好玩而这样做的。我已经相应地调整了我的代码。感谢您的建议和点赞! - Ruud Helderman

0

并不把它作为终极答案,因为它不能解决SWI-Prolog的问题。对于基于Java的解释器,问题在于XML转义不是J2SE的一部分,至少不是以简单的形式存在(没有想出如何使用Xerxes或类似工具)。

一个可能的方法是与来自Apache Commons的StringEscapeUtils(*)接口。但是,在Android上这将是不必要的,因为有一个名为TextUtil的类。所以我们编写了自己的(**)小型转换。它的工作方式如下:

?- text_escape('<abc>', X).
X = '&lt;abc&gt;'
?- text_escape(X, '&lt;abc&gt;').
X = '<abc>'

请注意Java源代码中使用的Java方法codePointAt()和charCount(),以及相应的appendCodePoint()。因此,它还可以转义和取消转义基本平面之上的代码点,即在范围>0xFFFF(目前未实现,留作练习)。
另一方面,Apache库(至少版本2.6)不支持代理对,并且将每个代码点放置两个十进制实体,而不是一个。
再见
(*) Java: Class StringEscapeUtils Source http://grepcode.com/file/repo1.maven.org/maven2/commons-lang/commons-lang/2.6/org/apache/commons/lang/Entities.java#Entities.escape%28java.io.Writer,java.lang.String%29

( * * ) Jekejeke Prolog: 模块 xml
http://www.jekejeke.ch/idatab/doclet/prod/en/docs/05_run/10_docu/05_frequent/07_theories/20_system/03_xml.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接