字符串规范化(NFC/NFD)问题

  1. 前言
  2. 先说结论
  3. 问题现象
  4. 问题解决
    1. 前端
    2. 后端
    3. 数据库
  5. 最后

前言

最近同事在接入西班牙语的数据时, 发现了一个问题, 涉及到西班牙语中包含重音富符号的数据比对出了问题, 同事已经找出可能是字符串规范话(Normalize)的问题, 但是现象很奇怪, 今天就做了一些测试, 将测试结果记录下来. 以供大家参考.

常用规范化:

  • NFC: 规范化组合型, 如 é 是一个字符
  • NFD: 规范化分解型, 如 是两个字符 e´
  • NFKC: 兼容性规范化组合型
  • NFKD: 兼容性规范化分解型

先说结论

在 HTML/JavasCript/Java/PostGreSQL 中, 不会自动对字符串的规范话进行转换, 也就是说, 从前端(html/js)传递到后端(java), 再传递到数据库(PostGreSQL)的过程中, 字符串的规范化是不变的, 所以只是文字传递, 不会出现问题.

出问题的地方是文件系统:

  • Windows 文件系统(如 NTFS)通常使用 NFC 形式存储文件名。
  • macOS 文件系统(如 HFS+ 或 APFS)通常使用 NFD 形式存储文件名。
  • Linux 文件系统(如 ext4)通常使用 NFC,但这也可能因环境和设置而异。

导致上传文件时, 文件名的规范化不一致, 会导致文件名比对是不一致的.

问题现象

同名的文件名上传时, 会进入数据库比对一下文件名是否存在, 但是由于文件名的规范化不一致, 会导致文件名比对不一致. 从而重复上传文件.

比如我们做一个简单的测试, 如下代码, 页面上有两个元素, 一个文本输入框, 一个文件上传框, 当文件上传框选择文件后, 会比对文件名和文本输入框的值是否一致, 如果不一致, 则提示文件名不一致.

我们在本地新建一个文件, 文件名为 é.txt, 这个文件名的文本是 NFC 的形式, 当 MacOS 去页面上传文件, 会提示文件名不一致, windows 则提示文件名一致.

这是因为 MacOS 文件系统使用 NFD 形式存储文件名, 和文本输入框的值(NFC)不一致, 导致比对不一致.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title></title>
</head>
<body>
<input type="text" name="filename" value="é.txt" id='filename'>
<input type="file" multiple accept="*/*" onchange="previewFiles()" id="fileInput">
</body>
<script>
    // 监听事件,
    const previewFiles = (e) => {
        const files = document.querySelector('#fileInput').files;
        const filename = document.querySelector('#filename').value;
        console.log('filename normalize:', detectNormalizationForm(filename));
        console.log('file.name normalize:', detectNormalizationForm(files[0].name));
        // 比对 id 为 filename 的值 和 上传的文件名是否一致
        if (files.length === 0 || files[0].name !== filename) {
            alert('文件名不一致');
            return;
        } else {
            alert('文件名一致');
        }
    }

    function detectNormalizationForm(str) {
        if (str === str.normalize('NFC')) {
            return 'NFC';
        } else if (str === str.normalize('NFD')) {
            return 'NFD';
        } else if (str === str.normalize('NFKC')) {
            return 'NFKC';
        } else if (str === str.normalize('NFKD')) {
            return 'NFKD';
        } else {
            return 'Unknown';  // 如果没有匹配的规范化形式
        }
    }
</script>
</html>

问题解决

前端

前端可以在 axios/ajax 等统一请求的地方, 对请求的参数进行规范化, 保证传递的参数是 NFC 形式.

在前端处理是有局限性的, 比如上传文件时, 无法对文件内容进行处理

后端

后端也有几个地方需要处理

  1. 拦截 Controller, HandlerInterceptor 等请求处理的地方, 对请求参数进行规范化
  2. Excel 工具类, 解析成对象的地方, 对 Excel 中的字符串进行规范化(要求所有上传的 Excel 都使用这个方法进行解析)

数据库

可以在 Mybatis 拦截器中, 对所有的 SQL 进行规范化, 保证数据库中的数据都是 NFC 形式.

最后

如无必要, 勿增实体.

字符串作为系统中最常用的类型, 全部添加规范化会对系统的性能产生一定的影响. 系统如果涉及到重音符号的地方不多, 可以只在必要的地方进行规范化.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]

×

喜欢就点赞,疼爱就打赏