论坛
潜水/灌水快乐,沉淀知识,认识更多同行。
ToB圈子
加入IT圈,遇到更多同好之人。
朋友圈
看朋友圈动态,了解ToB世界。
ToB门户
了解全球最新的ToB事件
博客
Blog
排行榜
Ranklist
文库
业界最专业的IT文库,上传资料也可以赚钱
下载
分享
Share
导读
Guide
相册
Album
记录
Doing
搜索
本版
文章
帖子
ToB圈子
用户
免费入驻
产品入驻
解决方案入驻
公司入驻
案例入驻
登录
·
注册
只需一步,快速开始
账号登录
立即注册
找回密码
用户名
Email
自动登录
找回密码
密码
登录
立即注册
首页
找靠谱产品
找解决方案
找靠谱公司
找案例
找对的人
专家智库
悬赏任务
圈子
SAAS
ToB企服应用市场:ToB评测及商务社交产业平台
»
论坛
›
数据库
›
分布式数据库
›
Golang 的字符编码与 regexp
Golang 的字符编码与 regexp
花瓣小跑
金牌会员
|
2025-2-16 23:56:20
|
显示全部楼层
|
阅读模式
楼主
主题
855
|
帖子
855
|
积分
2565
媒介
最近在使用 Golang 的 regexp 对网络流量做正则匹配时,发现有些情况无法正确进行匹配,找到资料发现 regexp 内部以 UTF-8 编码的方式来处理正则表达式,而网络流量是字节序列,由其中的非 UTF-8 字符造成的问题。
我们这里从 Golang 的字符编码和 regexp 处理机制开始学习和分析问题,并寻找一个有用且比较通用的办理方法,本文对此进行记载。
本文代码测试环境 go version go1.14.2 darwin/amd64
regexp匹配字节序列
我们将匹配网络流量所遇到的问题,进行抽象和最小化复现,如下:
我们可以看到 \xff 没有按照预期被匹配到,那么问题出在哪里呢?
UTF-8编码
翻阅 Golang 的资料,我们知道 Golang 的源码采用 UTF-8 编码, regexp 库的正则表达式也是采用 UTF-8 进行解析编译(而且 Golang 的作者也是 UTF-8 的作者),那我们先来看看 UTF-8 编码规范。
1.ASCII
在计算机的世界,字符终极都由二进制来存储,尺度 ASCII 编码使用一个字节(低7位),以是只能表示 127 个字符,而差别国家有差别的字符,以是创建了自己的编码规范,当差别国家相互通讯的时间,由于编码规范差别,就会造成乱码问题。
2.Unicode
为相识决乱码问题,提出了 Unicode 字符集,为全部字符分配一个独一无二的编码,随着 Unicode 的发展,不断添加新的字符,目前最新的 Unicode 采用 UCS-4(Unicode-32) 尺度,也就是使用 4 字节(32位) 来进行编码,理论上可以涵盖全部字符。
但是 Unicode 只是字符集,没有考虑计算机中的使用和存储问题,好比:
与已存在的 ASCII 编码不兼容,ASCII(A)=65 / UCS-2(A)=0065
由于 Unicode 编码高字节大概为 0,C 语言字符串串函数将出现 00 截断问题
从全世界来看原来 ASCII 的字符串使用得最多,而换成 Unicode 过后,这些 ASCII 字符的存储都将额外占用字节(存储0x00)
3.UTF-8
后来提出了 UTF-8 编码方案,UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式;UTF-8 是一种变长的编码方式,编码规则如下:
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 的码点,兼容 ASCII
对于需要 n 字节来表示的符号(n > 1),第一个字节的前 n 位都设为 1,第 n+1 位设置为 0;后面字节的前两位同等设为 10,剩下的的二进制位则用于存储这个符号的 Unicode 码点(从低位开始)。
编码规则如下:
编码中文 你 如下:
1.根据 UTF-8 编码规则,当需要编码的符号超过 1 个字节时,其第一个字节前面的 1 的个数表示该字符占用了几个字节。
2.UTF-8 是自同步码(Self-synchronizing_code),在 UTF-8 编码规则中,任意字符的第一个字节一定以 0 / 110 / 1110 / 11110 开头,UTF-8 选择 10 作为后续字节的前缀码,以此进行区分。自同步码可以便于步伐寻找字符边界,快速跳过字符,当遇到错误字符时,可以跳过该字符完成后续字符的解析,如许不会造成乱码扩散的问题(GB2312存在该问题)
byte/rune/string
在 Golang 中源码使用 UTF-8 编码,我们编写的代码/字符会按照 UTF-8 进行编码,而和字符相关的有三种范例 byte/rune/string。
byte 是最简单的字节范例(uint8),string 是固定长度的字节序列,其定义和初始化在 https://github.com/golang/go/blob/master/src/runtime/string.go,可以看到 string 底层就是使用 []byte 实现的:
rune 范例则是 Golang 中用来处理 UTF-8 编码的范例,实际范例为 int32,存储的值是字符的 Unicode 码点,以是 rune 范例可以便于我们更直观的遍历字符(对比遍历字节)如下:
范例转换
byte(uint8) 和 rune(int32) 可以直接通过位扩展大概舍弃高位来进行转换。
string 转换比较复杂,我们一步一步来看:
string 和 byte 范例相互转换时,底层都是 byte 可以直接相互转换,但是当单字节 byte 转 string 范例时,会调用底层函数 intstring() (https://github.com/golang/go/blob/master/src/runtime/string.go#L244),然后调用 encoderune() 函数,对该字节进行 UTF-8 编码,测试如下:
string 和 rune 范例相互转换时,对于 UTF-8 字符的相互转换,底层数据发生厘革 UTF-8编码 <=> Unicode编码;而对于非 UTF-8 字符,将以底层单字节进行处理:
string => rune 时,会调用 stringtoslicerune() (https://github.com/golang/go/blob/master/src/runtime/string.go#L178),终极跟进到 Golang 编译器的 for-range 实现(https://github.com/golang/go/blob/master/src/cmd/compile/internal/walk/range.go#L220),转换时调用 decoderune() 对字符进行 UTF-8 解码,解码失败时(非 UTF-8 字符)将返回 RuneError = \uFFFD;
rune => string 时,和 byte 单字节转换一样,会调用 intstring() 函数,对值进行 UTF-8 编码。
测试如下:
regexp处理表达式
在 regexp 中全部的字符都必须为 UTF-8 编码,在正则表达式编译前会对字符进行检查,非 UTF-8 字符将直接提示错误;固然他也支持转义字符,好比:\t \a 大概 16进制,在代码中我们一般需要使用反引号包裹正则表达式(原始字符串),转义字符由 regexp 在内部进行解析处理,如下:
固然为了让 regexp 编译包含非 UTF-8 编码字符的表达式,必须用反引号包裹才行
我们在使用 regexp 时,其内部首先会对正则表达式进行编译,然后再进行匹配。
1.编译
编译主要是构建主动机表达式,其底层终极使用 rune 范例存储字符(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L112),以是 \xff 通过转义后终极存储为 0x00ff (rune)
除此之外,在编译阶段 regexp 还会提前生成正则表达式中的前缀字符串,在执行主动机匹配前,先用匹配前缀字符串,以进步匹配效率。需要注意的是,生成前缀字符串时其底层将调用 strings.Builder 的 WriteRune() 函数(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L147),内部将调用 utf8.EncodeRune() 逼迫转换表达式的字符为 UTF-8 编码(如:\xff => \xc3\xbf)。
2.匹配
当匹配时,首先使用前缀字符串匹配,这里使用常规的字符串匹配。UTF-8 可以正常进行匹配,但当我们的字符串中包含非 UTF-8 字符就会出现问题,缘故起因正则表达式中的前缀字符串已经被逼迫 UTF-8 编码了,示例如下:
当执行主动机匹配时,将终极调用 tryBacktrace() 函数进行逐字节回溯匹配(https://github.com/golang/go/blob/master/src/regexp/backtrack.go#L140),使用 step() 函数遍历字符串(https://github.com/golang/go/blob/master/src/regexp/regexp.go#L383),该函数有 string/byte/rune 三种实现,其中 string/byte 将调用 utf8.DecodeRune*() 逼迫为 rune 范例,以是三种实现终极都返回 rune 范例,然后和主动机表达式存储的 rune 值进行比较,完成匹配。而这里当非 UTF-8 字符通过 utf8.DecodeRune*() 函数时,将返回 RuneError=0xfffd,示例如下:
比较复杂,不过简而言之就是 regexp 内部会对表达式进行 UTF-8 编码,会对字符串进行 UTF-8 解码。
相识 regexp 底层匹配运行原理过后,我们甚至可以构造出更希奇的匹配:
办理方法
在相识以上知识点过后,就很轻易办理问题了:表达式可以使用任意字符,待匹配字符串在匹配前手动转换为正当的 UTF-8 字符串。
由于当 regexp 使用前缀字符串匹配时,会主动转换表达式字符为 UTF-8 编码,和我们的字符串同等;当 regexp 使用主动机匹配时,底层使用 rune 进行比较,我们传入的 UTF-8 字符串将被正确通过 UTF-8 解码,可以正确进行匹配。
实现测试如下:
总结
关于开头提出的 regexp 匹配的问题到这里就办理了,在不断深入语言实现细节的过程中发现:Golang 本身在尽大概的保持 UTF-8 编码的同等性,但在编程中字节序列是不可克制的,Golang 中使用 string/byte 范例来进行处理,在 regexp 底层实现同样使用了 UTF-8 编码,以是问题就出现了,字节序列数据和编码后的数据差别等。
个人感觉 regexp 用于匹配字节流并不是一个预期的使用场景,像是 Golang 官方在 UTF-8 方面的一个取舍。
固然这个过程中,我们翻阅了很多 Golang 底层的知识,如字符集、源码等,让我们相识了一些 Golang 的实现细节;在实际常见下我们不是一定要使用尺度库 regexp,还可以使用其他的正则表达式库来绕过这个问题。
© 著作权归作者全部,转载或内容互助请联系作者
喜好的朋侪记得点赞、收藏、关注哦!!!
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
本帖子中包含更多资源
您需要
登录
才可以下载或查看,没有账号?
立即注册
x
回复
使用道具
举报
0 个回复
倒序浏览
返回列表
快速回复
高级模式
B
Color
Image
Link
Quote
Code
Smilies
您需要登录后才可以回帖
登录
or
立即注册
本版积分规则
发表回复
回帖并转播
回帖后跳转到最后一页
发新帖
回复
花瓣小跑
金牌会员
这个人很懒什么都没写!
楼主热帖
【电脑配置】新电脑买回来怎么配置? ...
数理逻辑第4-5章
使用axios发送post请求上传文件(multip ...
20天等待,申请终于通过,安装和体验In ...
计算机网络原理(谢希仁第八版)第六章课 ...
最简单易懂的ios p12证书 和描述文件的 ...
应急响应(总)
Java代码中如何判断一个字符串中是否包 ...
【网络攻防】常见的网络攻防技术——黑 ...
.NET 分布式缓存中的发布和订阅模式 ...
标签云
挺好的
服务器
浏览过的版块
Oracle
快速回复
返回顶部
返回列表