R 可以读取 html 编码的表情符号字符吗?

Can R read html-encoded emoji characters?

问题

我的问题是:

如何使用 R 读取包含 HTML 表情符号代码的字符串,例如 ��

我想:
(1) 在解析的字符串中表示表情符号(例如,作为 unicode 符号:</code>),<strong>OR</strong><br>(2)将其转换为等效文本 ("<code>:hugging face:")

背景

我有一个 XML 文本消息数据集(来自 Android/iOS 应用程序 Signal),我正在将其读入 R 以用于文本挖掘项目。数据如下所示,每条短信都在 sms 节点中表示:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!-- File Created By Signal -->
    <smses count="1">
        <sms protocol="0" address="+15555555555" contact_name="Jane Doe" date="1483256850399" readable_date="Sat, 31 Dec 2016 23:47:30 PST" type="1" subject="null" body="Hug emoji: &#55358;&#56599;" toa="null" sc_toa="null" service_center="null" read="1" status="-1" locked="0" />
</smses>

问题

我目前正在使用 R 的 xml2 包读取数据。但是,当我使用 xml2::read_xml 函数时,我收到以下错误消息:

Error in doc_parse_raw(x, encoding = encoding, base_url = base_url, as_html = as_html,  : 
  xmlParseCharRef: invalid xmlChar value 55358

据我了解,这表明表情符号字符未被识别为有效 XML。

使用 xml2::read_html 功能 可以 工作,但会删除表情符号字符。这里有一个小例子:

example_text <- "Hugging emoji: &#55358;&#56599;"
xml2::xml_text(xml2::read_html(paste0("<x>", example_text, "</x>")))

(输出:[1] "Hugging emoji: "

这个字符 有效的 HTML -- 谷歌搜索 &#55358;&#56599; 实际上将它在搜索栏中转换为 "hugging face" 表情符号,并带来与该表情符号相关的结果。

我发现似乎与此问题相关的其他信息

我一直在搜索 Stack Overflow,但没有找到与此特定问题相关的任何问题。我也找不到 table 可以直接在它们所代表的表情符号旁边给出 HTML 代码,因此我无法对这些 [=67= 进行(尽管效率低下)转换] 在解析数据集之前在一个大循环中编码为它们的文本等价物;例如,this list nor its underlying dataset 似乎都不包含字符串 55358.

tl;dr: 表情符号无效 HTML 实体; UTF-16 数字已用于构建它们而不是 Unicode 代码点。我在答案的底部描述了一种算法来转换它们,使它们有效 XML.


确定问题

R 绝对可以处理表情符号:

事实上,在 R 中存在一些用于处理表情符号的包。例如,emojifont and emo 包都可以让您根据 Slack 风格的关键字检索表情符号。这只是从 HTML-转义格式获取源字符以便转换它们的问题。

xml2::read_xml 似乎可以很好地处理其他 HTML 实体,例如&符号或双引号。我查看了 以查看 HTML 实体是否存在任何 XML 特定的约束,并且它们似乎可以很好地存储表情符号。所以我尝试将您的代表中的表情符号代码更改为该答案中的表情符号代码:

body="Hug emoji: &#128512;&#128515;"

而且,果然,它们被保存下来了(尽管它们显然不再是拥抱表情符号了):

> test8 = read_html('Desktop/test.xml')
> test8 %>% xml_child() %>% xml_child() %>% xml_child() %>% xml_attr('body')
[1] "Hug emoji: \U0001f600\U0001f603"

我在 this page 上查找拥抱表情符号,给出的十进制 HTML 实体 不是 &#55358;&#56599;。看起来表情符号的 UTF-16 十进制代码已包含在 &#;.

总而言之,我认为答案是您的表情符号实际上是无效的 HTML 实体。如果您无法控制来源,则可能需要进行一些预处理以解决这些错误。

那么,为什么浏览器会正确转换它们?我想知道浏览器是否对这些东西更灵活一点,并且正在猜测这些代码可能是什么。不过我只是猜测。


将 UTF-16 转换为 Unicode 代码点

经过更多调查,看起来有效的表情符号 HTML 实体使用 Unicode 代码点(如果是 &#...;,则为十进制;如果是 &#x...;,则为十六进制)。 The Unicode code point is different from the UTF-8 or UTF-16 code.(那个 link 解释了很多 很多 关于表情符号和其他字符是如何编码的,顺便说一句!好读。)

因此我们需要将您的源数据中使用的 UTF-16 代码转换为 Unicode 代码点。参考this Wikipedia article on UTF-16,我已经验证了它是如何完成的。每个 Unicode 代码点(我们的目标)是一个 20 位数字,或五个十六进制数字。从 Unicode 到 UTF-16 时,您将它分成两个 10 位数字(中间的十六进制数字被切成两半,其中两个位进入每个块),对它们进行一些数学计算并得到您的结果) .

往回走,如你所愿,是这样的:

  • 您的十进制 UTF-16 数字(目前位于两个单独的块中)是 55358 56599
  • 将这些块转换为十六进制(单独)得到 0x0d83e 0x0dd17
  • 您从第一个块中减去 0xd800 并从第二个块中减去 0xdc00 得到 0x3e 0x117
  • 将它们转换为二进制,将它们填充到 10 位并连接它们,它是 0b0000 1111 1001 0001 0111
  • 然后我们将其转换回十六进制,即 0x0f917
  • 最后,我们添加 0x10000,得到 0x1f917
  • 因此,我们的(十六进制)HTML 实体是 &#x1f917;。或者,在十进制中,&#129303

因此,要预处理此数据集,您需要提取现有数字,使用上述算法,然后将结果放回(使用一个 &#...;,而不是两个)。


在 R 中显示表情符号

据我所知,没有在 R 控制台中打印表情符号的解决方案:它们总是以 "U0001f600"(或者你有什么)的形式出现。然而,我上面描述的包可以帮助你在某些情况下绘制表情符号(我希望将 ggflags to display arbitrary full-colour emoji at some point). They can also help you search for emoji to get their codes, but they can't get names given the codes AFAIK. But maybe you could try importing the emoji list from emojilib 扩展到 R 并与你的数据框进行连接,如果你已经将表情符号代码提取到一列中,获取英文名称。

我已经实现了算法 in R, and am sharing it here. I am happy to release the code snippet below under a CC0 dedication(即,将此实现放入 public 域以供免费重用)。

这是 rensa 算法的快速且未经修饰的实现,但它确实有效!

utf16_double_dec_code_to_utf8 <- function(utf16_decimal_code){
  string_elements <- str_match_all(utf16_decimal_code, "&#(.*?);")[[1]][,2]

  string3a <- string_elements[1]
  string3b <- string_elements[2]

  string4a <- sprintf("0x0%x", as.numeric(string3a))
  string4b <- sprintf("0x0%x", as.numeric(string3b))

  string5a <- paste0(
    # "0x", 
    as.hexmode(string4a) - 0xd800
  )
  string5b <- paste0(
    # "0x",
    as.hexmode(string4b) - 0xdc00
  )

  string6 <- paste0(
    stringi::stri_pad(
      paste0(BMS::hex2bin(string5a), collapse = ""),
      10,
      pad = "0"
    ) %>%
      stringr::str_trunc(10, side = "left", ellipsis = ""),
    stringi::stri_pad(
      paste0(BMS::hex2bin(string5b), collapse = ""),
      10,
      pad = "0"
    ) %>%
      stringr::str_trunc(10, side = "left", ellipsis = "")
  )

  string7 <- BMS::bin2hex(as.numeric(strsplit(string6, split = "")[[1]]))

  string8 <- as.hexmode(string7) + 0x10000

  unicode_pattern <- string8
  unicode_pattern
}

make_unicode_entity <- function(x) {
  paste0("\U000", utf16_double_dec_code_to_utf8(x))
}
make_html_entity <- function(x) {
  paste0("&#x", utf16_double_dec_code_to_utf8(x), ";")
}

# An example string, using the "hug" emoji:
example_string <- "test &#55358;&#56599; test"

output_string <- stringr::str_replace_all(
  example_string,
  "(&#[0-9]*?;){2}",  # Find all two-character "&#...;&#...;" codes.
  make_unicode_entity
  # make_html_entity
)

cat(output_string)

# To print Unicode string (doesn't display in R console, but can be copied and
# pasted elsewhere:
# (This assumes you've used 'make_unicode_entity' above in the str_replace_all
# call):
stringi::stri_unescape_unicode(output_string)

JavaScript解决方案

我有这个 exact 相同的问题,但需要 JavaScript 中的解决方案,而不是 R。使用 rensa's (非常有帮助!),我创建了以下代码来解决这个问题,我只是想分享它以防其他人像我一样遇到这个线程,但需要它在 JavaScript.

str.replace(/(&#\d+;){2}/g, function(match) {
    match = match.replace(/&#/g,'').split(';');
    var binFirst = (parseInt('0x' + parseInt(match[0]).toString(16)) - 0xd800).toString(2);
    var binSecond = (parseInt('0x' + parseInt(match[1]).toString(16)) - 0xdc00).toString(2);
    binFirst = '0000000000'.substr(binFirst.length) + binFirst;
    binSecond = '0000000000'.substr(binSecond.length) + binSecond;
    return '&#x' + (('0x' + (parseInt(binFirst + binSecond, 2).toString(16))) - (-0x10000)).toString(16) + ';';
});

而且,如果您想 运行 它,这里有一个完整的片段:

var str = '&#55357;&#56842;&#55357;&#56856;&#55357;&#56832;&#55357;&#56838;&#55357;&#56834;&#55357;&#56833;'

str = str.replace(/(&#\d+;){2}/g, function(match) {
 match = match.replace(/&#/g,'').split(';');
 var binFirst = (parseInt('0x' + parseInt(match[0]).toString(16)) - 0xd800).toString(2);
 var binSecond = (parseInt('0x' + parseInt(match[1]).toString(16)) - 0xdc00).toString(2);
 binFirst = '0000000000'.substr(binFirst.length) + binFirst;
 binSecond = '0000000000'.substr(binSecond.length) + binSecond;
 return '&#x' + (('0x' + (parseInt(binFirst + binSecond, 2).toString(16))) - (-0x10000)).toString(16) + ';';
});

document.getElementById('result').innerHTML = str;

//  &#55357;&#56842;&#55357;&#56856;&#55357;&#56832;&#55357;&#56838;&#55357;&#56834;&#55357;&#56833;
//  is turned into
//  &#x1f60a;&#x1f618;&#x1f600;&#x1f606;&#x1f602;&#x1f601;
//  which is rendered by the browser as the emojis
Original:<br>&#55357;&#56842;&#55357;&#56856;&#55357;&#56832;&#55357;&#56838;&#55357;&#56834;&#55357;&#56833;<br><br>
Result:<br>
<div id='result'></div>

我的 SMS XML Parser 应用程序现在运行良好,但它在大型 XML 文件上停滞不前,所以我正在考虑在 PHP 中重写它。 If/when 我愿意,我也会 post 该代码。

翻译了 Chad JavaScript 对 Go 的回答,因为我也有同样的问题,但需要 Go 中的解决方案。

https://play.golang.org/p/h9JBFzqcd90

package main

import (
    "fmt"
    "html"
    "regexp"
    "strconv"
    "strings"
)

func main() {
    emoji := "&#55357;&#56842;&#55357;&#56856;&#55357;&#56832;&#55357;&#56838;&#55357;&#56834;&#55357;&#56833;"

    regexp := regexp.MustCompile(`(&#\d+;){2}`)
    matches := regexp.FindAllString(emoji, -1)

    var builder strings.Builder

    for _, match := range matches {
        s := strings.Replace(match, "&#", "", -1)

        parts := strings.Split(s, ";")
        a := parts[0]
        b := parts[1]

        c, err := strconv.Atoi(a)
        if err != nil {
            panic(err)
        }

        d, err := strconv.Atoi(b)
        if err != nil {
            panic(err)
        }

        c = c - 0xd800
        d = d - 0xdc00

        e := strconv.FormatInt(int64(c), 2)
        f := strconv.FormatInt(int64(d), 2)

        g := "0000000000"[2:len(e)] + e
        h := "0000000000"[10:len(f)] + f

        j, err := strconv.ParseInt(g + h, 2, 64)
        if err != nil {
            panic(err)
        }

        k := j + 0x10000

        _, err = builder.WriteString("&#x" + strconv.FormatInt(k, 16) + ";")
        if err != nil {
            panic(err)
        }
    }

    fmt.Println(html.UnescapeString(emoji))
    emoji = html.UnescapeString(builder.String())
    fmt.Println(emoji)
}