关于xml包在Unmarshal时将\r\n重写为\n的问题

今年4月份,中国移动、中国电信、中国联通三大运营商联合举行线上发布会,发布了《5G消息白皮书》。所谓5G消息,即传统短信消息(仅能进行文本展示)的升级版,是由GSMA组织制定的RCS(Rich Communication Suite)消息规范所定义。2019年RCS UP(unified profile)更新到2.4版本,并成为了5G终端标准的一部分,该版本也是第一个具备商用能力的版本,为5G消息商用奠定了基础。中国移动计划2020.6月末正式实现5G消息的商用,目前已经在浙江和广东建立了两个5G消息的支撑节点(分别由中兴和华为承建)。作为电信移动增值领域的厂商,我方也参与了与浙江节点进行行业5G消息平台(MaaP)联调与应用开发。

这引子有些长,本文重点不在5G消息,而在于与行业5G消息平台对接时遇到的一个Go xml包的问题,这是记录一下,以供自己备忘,同时也供广大gopher们参考。

1. 问题现象

行业5G消息使用的通信协议本质上就是xml over http(s)。在http Body的xml中,有一个字段bodyText承载了真正到达5G智能终端上的有效信息载荷,且这个字段是一个CDATA包裹的字段。在我们系统的某个转发流程中,我们解析了从Chatbot(5G行业消息机器人)下发的行业5G消息,但我们发现解析后的bodyText字段中的“\r\n”都被转换为“\n”了。我们用一个例子来直观描述一下该问题:

// xml-rewrite-carriage-return/test2.go

package main

import (
    "encoding/hex"
    "encoding/xml"
    "fmt"
)

type DescCDATA struct {
    Desc string `xml:",cdata"`
}

type Person struct {
    Name string    `xml:"name"`
    Age  int       `xml:"age"`
    Desc DescCDATA `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc><![CDATA[%s]]></desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc.Desc)))
}

运行该例子:

$go run test2.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0a 78 6d  6c                       |hello.xml|

这是一个非常简单的xml unmarshal(反序列化)的例子。我们看到反序列化后,结构体desc字段中的内容相比于原始的xml中desc的内容少了一个字符:0x0d,即“\r”(carriage-return)。我们一直以为针对原xml中CDATA包裹的数据内容,xml包在unmarshal时会原封不动的拷贝下来。为什么”\r”字符会被删除掉呢?我们接下来找找原因。

2. 问题原因

Go是开源的编程语言,它最大的优势就是遇到问题后可以直接看Go标准库源码,当然也可以通过调试工具跟踪到标准库源码中。xml包并不复杂,我选择了直接看xml unmarshal代码的方式。在$GOROOT/src/encoding/xml/xml.go(go 1.14版本)中,我们在Decoder的text方法中找到如下几行代码:

// $GOROOT/src/encoding/xml/xml.go

... ...

func (d *Decoder) text(quote int, cdata bool) []byte {

... ...

                // We must rewrite unescaped \r and \r\n into \n.
                if b == '\r' {
                        d.buf.WriteByte('\n')
                } else if b1 == '\r' && b == '\n' {
                        // Skip \r\n--we already wrote \n.
                } else {
                        d.buf.WriteByte(b)
                }
... ...

}

Decoder的text方法是xml unmarshal在解析如下面name字段的值(xxxx)时被调用的:

<name>xxxx</name>

这段代码的逻辑是:将xxxx中的\r重写为\n,如果存在\r\n,则将其重写为\n。并且无论是否是CDATA字段,这块的逻辑均是生效的。比如我们将上面例子中的desc字段改为非CDATA类型:

// xml-rewrite-carriage-return/test1.go

type Person struct {
    Name string `xml:"name"`
    Age  int    `xml:"age"`
    Desc string `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc>%s</desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc)))
}

该例子的输出:

$go run test1.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0a 78 6d  6c                       |hello.xml|

我们看到:非CDATA包裹的数据,其中的”\r\n”也被重写为“\n”了。

关于这个问题,在Go项目issue中也有人提及:https://github.com/golang/go/issues/24426 。从该issue的讨论中看,Go标准库xml包的实现应该还是参考了xml规范中关于line end的描述的:

XML parsed entities are often stored in computer files which, for editing convenience, are organized into lines. These lines are typically separated by some combination of the characters CARRIAGE RETURN (#xD) and LINE FEED (#xA).

To simplify the tasks of applications, the XML processor must behave as if it normalized all line breaks in external parsed entities (including the document entity) on input, before parsing, by translating both the two-character sequence #xD #xA and any #xD that is not followed by #xA to a single #xA character.

上面的英文规范翻译过来大致是:

XML解析的实体通常存储在计算机文件中,为了便于编辑,这些文件被组织成多行。 这些行通常由字符回车(#xD)和换行(#xA)的某种组合分隔。

为了简化应用程序的任务(解析回车和换行的组合),在解析之前,XML处理器必须对输入的外部解析实体(包括文档实体)进行转换使其规范化,转换规则是:将两字符序列#xD #xA以及后面未紧跟#xA字符的#xD字符转换为单个的#xA字符。

3. 解决方法

我们的述求就是对CDATA包裹的文本数据中的”\r\n”不做“重写”处理。我们采用了下面的方案:clone一份标准库中的xml包,将clone版本放入我们自己的项目路径下,然后在clone版本基础上修改Decoder的text方法的实现

// xml-rewrite-carriage-return/xml/xml.go

... ...

func (d *Decoder) text(quote int, cdata bool) []byte {

... ...

                // We must rewrite unescaped \r and \r\n into \n.
                //
                // tonybai change: only rewrite when text is not in CDATA section
                // (https://github.com/golang/go/issues/24426)
                if !cdata && b == '\r' {
                        d.buf.WriteByte('\n')
                } else if !cdata && b1 == '\r' && b == '\n' {
                        // Skip \r\n--we already wrote \n.
                } else {
                        d.buf.WriteByte(b)
                }

....

}

改造后的代码仅对非CDATA数据进行\r\n的重写,而对于CDATA类型数据,则原封不动的解析出来。我们将test2.go改造成使用我们的clone版本的xml包的示例代码:test3.go

// xml-rewrite-carriage-return/test3.go

package main

import (
    "encoding/hex"

    "github.com/bigwhite/xmltest/xml"

    "fmt"
)

type DescCDATA struct {
    Desc string `xml:",cdata"`
}

type Person struct {
    Name string    `xml:"name"`
    Age  int       `xml:"age"`
    Desc DescCDATA `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc><![CDATA[%s]]></desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc.Desc)))
}

运行该示例:

$go run test3.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

我们看到这次包裹在CDATA中的\r\n没有被重写,我们对xml包的修改是有效的。

4. 小结

XML作为上一代被设计用来传输和存储数据的标记语言格式,在Go中的支持并不完善,关于标准库xml包的issue还有好多处于open状态。在标准库xml包更新较慢的情况下,clone一份xml包并进行定制不失为一种好的折中方法。

本文所涉及源码在这里可以下载。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

果果十周岁了!

好久没有在我的博客上写关于果果的事情了,因为很多关于果果成长的经历都记录在她自己的博客中了。但今天是她十周岁的生日,是个值得纪念的日子。闺女成长的十年,也是我学习为人父的十年。作为父亲,我发自内心地想说点啥,是回顾,也是感受,亦有些寄语^_^。

img{512x368}

图:果果成长的十年

出生

老婆在2009年7月怀上了果果。那时我们刚刚新婚不久,二人世界还没过够^_^,小家伙的突然到来还让我们有些“手足无措”。为此,我们还认真地讨论了两天,最终老婆拍板:我要生下这个孩子,于是果果保住了^_^。如今,每当果果提及此事,都会“发狠”地盯上我几眼,我也只能呵呵呵呵地应对^_^。

老婆怀胎中段,我一直在福建出差,虽然有岳母陪在老婆身边,但年底那两个月,老婆心情十分差。直到那一年大年三十的下午2点,我才在桃仙机场下的飞机,匆匆赶回家。大街上连车的打不到,多亏还有公交系统。进入家门,心里满满的都是对老婆的愧疚。记忆中老婆似乎并没有说啥,只说了一句“吃饭吧”,我顿感心里热乎乎的。

果果似乎很享受在妈妈子宫中待着,预产期都过了几天了,她还没有反应,直到2010年5月3日凌晨1点多,规律性的宫缩“来袭”。我们匆忙赶到医院,早上6点半多,老婆进入产室,9点多,我在产室外听到了果果呱呱坠地后的第一声啼哭。

img{512x368}

图:刚出生的果果

果果出生后,恰逢徐峥的作品《人在囧途》上映,影片中徐峥扮演角色的孩子叫果果,我们觉得这个小名不错,于是便给我们宝宝起名为果果

第一次照顾这么小的孩子也着实让我们这些大人手忙脚乱一段时间。出月子后,生活逐渐恢复平稳。果果每天除了吃奶就是睡觉,也算是比较省心了。稍大一些后,果果似乎并不太愿意睡觉了,每次喂完奶都得放到小车上在厅里推过来推过去哄她睡觉,后来果果姥姥买的一个能发出大恐龙吼声的玩具也加入到促进果果睡眠的行列中^_^。

第一次走路(0岁)

果果一直母乳喂养,身体也很壮实。抬头、翻身、学会爬行和其他孩子相比都略有提前。最让我印象深刻是她第一次学会独立行走。记得那是2010年农历春节前,我们家当时的供暖非常好,室温在28度以上。果果在家只穿一套内衣裤,因此行动和玩耍起来非常方便。之前果果就可以扶着床沿踱步了并且她似乎也很喜欢站起来的感觉。那天她自己在卧室的地板上玩耍,我在门口偷偷观察她。她玩了一会儿就开始扶着床站起来,看我站在门口,她居然放开了扶着床沿的双手,向我摇摇晃晃地走了过来。从床边到卧室门口大约有不到2米的距离,我也兴奋地张开双臂,引导她向我走来。她边走边兴奋地笑,似乎也在惊讶于自己能独立行走了。在她扑到我的怀中的那一刻,我才意识到我见证了果果人生第一次独立行走,老婆听到这个消息也是兴奋不已。自从果果学会了走路,此后便一发不可收拾^_^。

打针不哭(1岁)

可能是因为母乳喂养,果果在一岁的这一年中没有患过任何感冒发烧的病症。但当母体带给她的免疫力逐渐失去作用后,果果便和其他小朋友一样,会得感冒,也会发烧。记得果果第一次感冒(刚过一岁生日没多久)就烧的特别厉害。由于我们也是第一次遇到这种情况,特别担心,于是就带她去医院检查。虽然我们期望医生开立口服药,但医生最终还是开了点滴。在护士站门口,果果看到其他孩子扎针时都哇哇的哭,心里也胆怯了。果不其然,当针头穿透她的皮肤进入血管的那一刻,果果也像其他孩子一样,哇哇大哭起来。

有了这次“痛苦”的扎针经历后,我们也对她进行了心理疏导,教她要学会坚强,从她的眼神看得出来,她似乎听懂了。在2011年的秋冬换季,果果又患感冒发烧。同样去了医院,同样开了点滴,但在护士站扎针的时候,果果居然很坚强的忍住了,没有哇哇的哭泣,这让看惯了孩子痛哭的护士也是惊奇不已。看着泪水在眼圈里打转但没有哭出来的果果,我们心里更是心疼她了。

送去幼儿园学说话(2岁)

果果在11个月的时候就会喊妈妈了,但直到2岁半她能吐出的字依然只有“妈妈”,偶尔也有“爸爸”。现在看来,果果说话晚是因为我们给她的语言刺激太少了。果果不愿意睡觉,一旦睡着了,老人生怕声音大吵醒她,于是就命令我们不许出声。久而久之,果果在潜意思中得到的声音刺激、语言刺激照比其他小朋友就要少很多。为此,我们还带着果果去看了生长发育门诊、做过筛查,结果都显示果果没有任何生理性的疾病。

我们需要找到一个让果果接受更多语言刺激的方法,最终我们决定在果果2岁零5个月的时候送她去幼儿园“学说话”。做过家长的都知道,送孩子去幼儿园的过程是“痛苦”的,孩子哭闹,家长心疼,但这个过程是必须要经历的。付出了就有收获。在果果上幼儿园后的一个月,果果的“话匣子”就彻底打开了^_^。

更像女娃了,但怕大海(3岁)

出生时,果果头发稀少,为了让果果长出更好的头发,我们每隔一段时间就把她的头发剃的很短(几近光头)。在2岁之内,果果更像一个“男娃”。直到3岁以后,我们开始给她留头发了。小家伙似乎也知道留头发后自己更好看了,姥姥每次给她梳头扎辫她都很喜欢。留着还有些短的头发让她更像女娃了:

img{512x368}

图:更像女娃的果果

下半年,我们把果果转到了更大的幼儿园,并且果果每天上幼儿园都不再费劲了。她在幼儿园也学到了许多知识、技能和礼节。

3岁的果果的身体显得比同龄的女宝高大一圈,我们也开始带着她到处出行游玩。劳动节黄金周我们第一次带孩子去海边。那天的风浪比较大,浪花拍击礁石的声音震耳欲聋,果果显得很害怕。我们抱着她向海边靠近,但她却一直在挣扎并大喊:“离开、离开、走、走”。当我自己独自向大海靠近时,她也大喊:“爸爸,你回来,你回来”。见此情景,我们都哈哈大笑起来。

后来我们去了一处比较海浪比较舒缓的地带,没有了海浪拍打的巨大轰隆声,果果镇定了许多。也开始站在沙滩上和其他小朋友一起挖起沙子了。

独立爬山(4岁)

4岁的果果不仅个头高,而且壮实了。我们在权衡了之后,决定在假期带她去爬山,并且我已经做好了背她上山的准备。那次我们爬的是千山。千山在整个省内的爬山困难指数排行榜上也是名列前茅的。不过小家伙似乎很喜欢爬山,在登山栈道上显得十分兴奋,我们也给她做心里建设,希望她自己爬到顶峰。虽然在中段她也曾打过退堂鼓,但最终小家伙还是凭借自己的双腿和毅力爬上了山顶,我和老婆也都是非常惊讶。下山过程中,小家伙也是一路欢喜,并没用我们费心。只是由于累了,在回程的车上,小家伙呼呼的睡了一道。

正因为此次爬山的经历,果果爱上了爬山。后续选择旅游景点,她总是先挑那些有山可爬的地方,比如:2019年的陕西的华山骊山等。

和妈妈一起去普吉岛(5岁)

孩子小的时候,出行很麻烦,而且孩子能收获的东西有限。5岁是一个很好的节点,她基本能自立了,而且感知和吸收外界信息的能力已经很强了。

5岁这一年是果果第一次和妈妈出国旅游,此次出行的目的是泰国普吉岛,和她们一起去的还有老婆的同事,这些同事也能帮助老婆照顾照顾果果,顺道还能锻炼一下果果的交际能力。这也是果果第一次乘飞机出行,在机场她十分兴奋。她们的航班在首尔中转,从首尔飞到普吉需要5-6个小时,这下让果果过了一把飞行瘾,她尤其喜欢飞机起降过程中的那种感觉,以至于以后每次出行,她都嚷嚷着要买多次经停或中转的航班^_^。

更难得可贵的是,这次的旅游经历深深印在果果的记忆中,至今每当翻看那时的照片时,她还能头头是道的给我们讲当时发生的故事。有些事情,我老婆都已经记不得了。

上小学了(6岁)

转眼间,果果来到了6周岁,已经到了上学的年龄了。9月份,果果正式成为一名珠江街第五小学一年级的“小豆包”。和第一次上幼儿园不同,这次果果适应的很快,也没有哭鼻子的情况。反倒是回来和我们说她班级有小朋友一直哭,她还很疑惑这些小朋友为什么要哭^_^。

上学后,更多的教育责任“甩”给了班主任老师,我们平时更多是帮忙批改批改作业,督促读读书,带着上上才艺班。果果的古筝是各门才艺课中学的最好的,果果也有了那么一些古典的气质:

img{512x368}

图:有一丝古典气质的果果

这一年我还给果果开了博客。有些东西,光靠脑子是记不住的,写下来,留给多年后的自己和孩子慢慢回味。在果果能自己维护这个博客之前,我就先替她维护了。

叛逆与独立(7~9岁)

进入到7岁以后,果果受到的教育多了,读的书多了,渐渐了有了自己的主见和小脾气,再也不是那个将父母话“奉为圭臬”的小女娃了。如果就某事“辩论”,她姥姥已经完全不是对手了。也只有我和老婆偶尔还能“恩威并举”的降住她:(。

果果喜欢读故事书。她最喜欢读郑渊洁的童话,按照她的说法,市面上郑渊洁的书她基本都读完了,有些书,她已经读过不止一遍了。受郑渊洁风格的影响,她喜欢写幻想类的作文,喜欢天马行空,因此在细节描写上就差了一些。

她还喜欢“米小圈上学记”,每天晚上都是在天猫精灵播放的米小圈上学记中入睡的。

她喜欢宜家买来的老虎和小狗玩偶,一个起名为花果,一个起名为木果,每天一左一右的陪她入睡。

天猫精灵是她每天不可或缺的“伙伴”。早上听新闻早报,天气预报;晚上听故事,听历史,听音乐;偶尔还和天猫精灵玩玩互动猜谜游戏。真不愧为互联网和智能时代的原著民。

8岁的果果,其古筝考级已经通过了10级,这还是在她不是很勤奋练琴的情况下取得的。

这个阶段的果果也十分贪玩,喜欢去游乐场,玩老爸都不敢玩的惊险刺激的项目(只能由她妈妈陪着)。

9岁时,她偶尔和妈妈看了一集“家有儿女”,从那时起就“沉迷”于该剧:只要拿起iPad就必然打开“家有儿女”视频。她看电视剧和她看书的特点一样,如果某一集是她喜欢的,她会反复看上好多遍,丝毫没有不耐烦的迹象。有些集的台词她都能背下来,并粘着我和我老婆要给我们讲。还别说,讲的还头头是道的^_^。

亭亭玉立的大姑娘(10岁)

由于新冠疫情的影响,果果的10岁生日在家里过的,我们也没法带她出去“玩耍”一番。10岁的她已经是一个亭亭玉立的大姑娘了。她的个头快赶上她妈妈了,大长腿,身材是“青出于蓝而胜于蓝”。

img{512x368}

图:十岁的果果

img{512x368}

图:亭亭玉立的果果

这次10周岁的生日除了生日蛋糕,还有一个更为特殊的礼物,那就是妈妈肚里的二宝,这也是果果一直想要的弟弟/妹妹。自从知道妈妈怀了二宝之后,果果变得更加懂事了。每天晚上对着妈妈的肚子给二宝讲故事,晚上睡觉前也会对着妈妈肚子猛“亲”几口^_^。

为了留下美好回忆,我们还特地在果果十周岁生日的时候在影楼留下了一家四口的合影:

img{512x368}

图:十岁果果生日时一家四口合影

小结

果果成长的十年给我的最大感受就是:!一晃间,果果都10岁了,二宝也即将出生。我和老婆也即将步入中年。这里做的这个阶段性的回顾,以期若干年后当记忆模糊时还能通过这篇文章回忆起当年果果小时候的点点滴滴。

这里也希望果果在未来的人生道路中能继续一帆风顺,身体健健康康,每天快快乐乐。

希望果果和即将出生的二宝一起姐妹情深,相濡以沫,共同走好人生之路。

最后,人生在于经历,而不在于得失


我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats