目录
问题描述
一个zip包,里面有文件名包含中文,解压出来发现变成了“井号”+“U”+十六进制的表达形式,算不上是乱码,但文件名变了,这是无法接受的。于是开始了查找问题原因的过程。
项目背景
随着前端技术的发展,越来越多的前端工程师采用ejs、scss等技术实现UI开发。如果Java技术栈的工程师想要使用该静态模板(velocity或者freemarker,或者jsp),需要前端工程师对其进行编译,将其静态化(其中还涉及js、css编译后上传至CDN)后才可使用。
在开发实践中,前端工程师通过本地开启http服务(基于koa、express结合webpack等框架)来对页面进行调试。正值公司的云平台改版,本来耗时繁琐的编译、上传操作就想接入到该平台。这样,对于前端工程师,只关注于本地调试开发。对于Java工程师,只关注静态页。中间转换的过程交给云平台自己完成就行了。
分析运行环境
云平台的工作流水线大致为:
- 编译以及静态资源上传,打包(一个项目中会有很多页面)
- 将打好的包上传到统一的存储系统
- 将之前编译的版本部署到负载机器上(实际是发送指令,负载机从存储系统中按版本下载指定的包文件并进行解压部署)
注:本流水线各个环节均工作在Linux系统下(CentOS 6.9/7.3)。
回到一开始我们遇到的问题。我们在最后一步解压部署时发现,解压出来的中文文件名被改变了。然后我随便找了一个文件:
它的文件名是这样的:#u9996#u9875-2019.html
把这个文件名贴进去,然后将“井号”改成"反斜杠"(“\”),然后点击“Unicode转中文”。果然,内容变成了:“首页-2019.html”。这说明,原本中文的文件名被变成了十六进制表示的Unicode代码。
问题找到了,按照下面的步骤进行排查:
- 根据日志,将存储系统中的包下载下来。在Windows和Mac下尝试打开查看,都没有问题,说明打包本身没有问题;
- 登录目标负载机,将压缩包使用unzip命令进行解压,文件名正常;
因为解压功能不仅可以通过unzip来实现,很多工具或者编程语言都支持zip解压,于是询问运维人员如何实现的解压功能。后来运维工程师发给我如下命令:
"unzip 带版本的压缩包.zip"
原来使用的是saltstack(一个基于python的自动化运维管理平台)的salt-call命令。于是我复用上述命令进行解压,问题得到了复现。不过解压的核心命令和我本地直接执行的命令并无明显的差别,分析导致该现象的原因应该是环境问题。
于是我在目标负载机上分别执行了
env
和
salt-call cmd.run "env"
这样就能在不同的上下文环境中显示系统环境变量。经过比对,发现了一些不同。
在直接解压的环境中,关于语言、地区的系统环境变量只有一个:
LANG=en_US.UTF-8
而在salt-call环境下,相关系统变量有这些:
LC_PAPER=C
LC_ADDRESS=C
LC_MONETARY=C
LC_TELEPHONE=C
LC_MESSAGES=C
LC_COLLATE=C
LC_IDENTIFICATION=C
LC_MEASUREMENT=C
LC_CTYPE=C
LC_TIME=C
LC_NAME=C
LANG=en_US.UTF-8
复现问题
为了复现上述问题,我在直接解压的环境中试图将LANG环境变量抹掉,看看会发生什么:
export LANG=
然后用同样的解压命令对压缩包进行解压,果然,文件名里的中文字符全都变成了Unicode代码。
#U8ba2#U5355#U7ba1#U7406#U2014#U7b5b#U9009#U6761#U4ef6.html
#U8ba2#U5355#U7ba1#U7406#U2014#U8ba2#U5355#U5217#U8868.html
#U8ba2#U5355#U7ba1#U7406#U2014#U8ba2#U5355#U8be6#U60c5.html
解决问题
问题原因找到了:unzip命令会获取系统中的语言设置,如果压缩包中的文件名字符属于该字符集下,则使用该字符集;如果文件名字符不属于该字符集下,则采用Unicode代码方式表示,由于文件名中不允许出现“反斜杠”(“\”)这类字符,稳妥起见,会将该字符转换为“井号”(#)。(后来经过实验证实,是LC_CTYPE环境变量导致的该问题)
解决方法:在salt-call代码中,实际执行解压操作之前,设置一个环境变量即可:
salt-call cmd.run "export LC_ALL=en_US.UTF-8 ; unzip 带版本的压缩包.zip"
设置LC_ALL环境变量的好处是:不管LC_*和LANG变量设置的是什么,一旦发现有环境变量LC_ALL,则会用一致使用LC_ALL的设置。
扩展阅读
以下内容摘录自参考文献:
locale把按照所涉及到的文化传统的各个方面分成12个大类,这12个大类分别是:
1、语言符号及其分类(LC_CTYPE)
2、数字(LC_NUMERIC)
3、比较和排序习惯(LC_COLLATE)
4、时间显示格式(LC_TIME)
5、货币单位(LC_MONETARY)
6、信息主要是提示信息,错误信息, 状态信息, 标题, 标签, 按钮和菜单等(LC_MESSAGES)
7、姓名书写方式(LC_NAME)
8、地址书写方式(LC_ADDRESS)
9、电话号码书写方式(LC_TELEPHONE)
10、度量衡表达方式(LC_MEASUREMENT)
11、默认纸张尺寸大小(LC_PAPER)
12、对locale自身包含信息的概述(LC_IDENTIFICATION)。
其中,与中文输入关系最密切的就是 LC_CTYPE, LC_CTYPE 规定了系统内有效的字符以及这些字符的分类,诸如什么是大写字母,小写字母,大小写转换,标点符号、可打印字符和其他的字符属性等方面。而locale定 义zh_CN中最最重要的一项就是定义了汉字(Class “hanzi”)这一个大类,当然也是用Unicode描述的,这就让中文字符在Linux系统中成为合法的有效字符,而且不论它们是用什么字符集编码 的。
设定locale就是设定12大类的locale分类属性,即 12个LC_*。除了这12个变量可以设定以外,为了简便起见,还有两个变量:LC_ALL和LANG。它们之间有一个优先级的关系:
LC_ALL>LC_*>LANG
可以这么说,LC_ALL是最上级设定或者强制设定,而LANG是默认设定值。
参考文献:[1]freeners.关于locale国际化与本土化的设定[EB/OL].http://blog.chinaunix.net/uid-21269292-id-444559.html,2009-06-26.