ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

无需字体支持的符号大全:跨平台安全分级与宽度避坑指南

无需字体支持的符号大全:跨平台安全分级与宽度避坑指南 前阵子帮一个做工业触摸屏的朋友收尾导出的配置里有一堆用方框和箭头拼出来的分隔线在开发机上一切正常烧到设备上整片变成豆腐块连行号都对不齐了。问题不在代码在于那批字符依赖了设备上没有的字体。从那次之后我把手边常打交道的符号做了一遍系统梳理按在不同系统、不同设备上是否真的能画出来分了级同时把宽度、回退、输入方式这些容易被忽略的细节都记了下来。这篇就是这份梳理的完整版主题叫无需字体支持的符号——也就是不装任何额外字体、不改系统设置靠系统自带字形就能正常显示的字符集合。内容分成三块先是为什么有的符号到处都能显示、有的却是方块再是按安全等级整理的符号大全清单和它们各自适用的场景最后是我自己写的检测脚本和一份踩坑速查。做网名 ID 装饰的、写纯文本工具和 CLI 的、跑嵌入式界面的、天天在代码注释里画分隔线的都能直接从里面抄。文中所有结论都在 Windows、macOS、Android、iOS 和几个主流 Linux 发行版上实测过清单部分可以直接复制使用。1. 先把无需字体支持这件事说透很多人对字体支持的理解停留在我电脑上有这个字体就行实际链条比这长得多。文本要显示成一个可见的字形中间要经过字符编码、字体回退、字形查找、宽度计算四道关卡任何一道出问题看到的就不是你想要的符号。理解这四道关卡是后面所有清单和判断的前提也是我这些年排查显示问题时的固定切入点。1.1 字形从哪来字体回退链到底怎么选字文本渲染引擎处理一段字符串时不会拿一个字体从头画到尾。它会先按语言和字符类别把字符串切成若干 run每个 run 再交给字体栈里第一个声称自己有这个码位的字体去画。以 Windows 为例默认栈大致是 Segoe UI 打底缺字时依次尝试 Segoe UI Symbol、Segoe UI Emoji、Microsoft YaHei 等macOS 和 iOS 上是 SF Pro 打底配合 PingFang SC、Apple Symbols、Apple Color EmojiAndroid 和多数 Linux 桌面走 Noto 家族的 Sans、Symbols、Symbols 2、CJK 这一套。当所有候选字体都没有这个码位时引擎会画 .notdef 字形也就是我们俗称的豆腐块通常是空心方框或者带问号的方框。这里有两个很关键但经常被忽略的事实。第一回退是逐个 run而不是逐字符串的所以你会看到一句话里前半段正常、中间一个字符变方块后面又恢复正常这种局部损坏正是回退链在起作用。第二不同平台的默认字体集合差别极大Windows 的 Segoe UI Symbol 覆盖了相当广的符号区macOS 的 Apple Symbols 覆盖略有不同而一台只装了最小化系统的 Linux 服务器可能只有 DejaVu Sans连中文都没有。所以判断一个符号是否无需字体支持本质是在问目标环境的默认字体集合的交集里有没有这个码位。这也是为什么我坚决不用我机器上显示正常作为结论。我在公司几台不同系统的机器、两部手机、一台工控屏上并排测过同一批符号差异最大的几个区集中在 U2600 到 U27BF 这一段桌面上是黑白线条画手机上直接变成彩色图标工控屏上则是整片方块。能显示、显示成什么样、宽度占几格是三件完全不同的事必须分开评估。注意判断符号安全性时目标环境要按最差的那个来算而不是按自己手边最舒服的那台机器。工程上永远取交集不取并集。1.2 我用的三级安全分级标准基于上面那套机制我把常见符号按覆盖范围分成三级加一个灰区。分级依据不是好看不好看而是在哪些默认字体里存在这个标准比按语义分类实用得多。等级覆盖依据典型环境风险一级ASCII 与拉丁补充几乎所有字体都有段码屏、点阵字库、串口终端、老式设备极低二级需要系统自带符号字体现代 Windows、macOS、Android、iOS 桌面低老设备会挂三级需要 CJK 或地区性字体中文、日文环境中纯英文系统会翻车灰区默认彩色呈现、辅助平面、稀有码位不可预测高不建议用在关键位置一级的核心是 U0020 到 U007E 的可打印 ASCII加上 Latin-1 补充区里那些真正通用的部分货币符号、度数、正负号、乘除号、段落号、版权商标、上标数字和几个分数。这些字符的地位特殊因为它们出现在最早的字符集里任何一款有字形的字体都必须先有它们连便宜到几块钱的字符点阵屏驱动芯片内置的 CGROM 里都躺着完整的 ASCII。你要做的东西只要可能跑在不可控的环境上就往这一级退。二级的典型代表是制表符、方块元素、几何图形、箭头和数学运算符这几段。它们在现代桌面和手机上几乎都有靠的是 Segoe UI Symbol、Apple Symbols、Noto Sans Symbols 这类专门补符号的字体。风险点是老和精简Windows 7 时代的覆盖就比现在窄很多精简版 Linux 镜像会砍掉符号字体一些终端模拟器自带的等宽字体也不含这类字形。二级符号能不能用取决于你是否能确定目标机器不是精简环境。三级就是我说的 CJK 依赖区。像、。这类中文标点、《》「」【】这些括号、①到⑳的圆圈数字、全角字母数字、半宽片假名都需要系统里有中文字体或日文字体。在纯英文环境下这些东西要么变方块要么被回退到某个意外字体上导致宽度错乱。如果你的产物会流出中文环境三级符号一律要给出降级方案。1.3 宽度属性比能不能显示更容易翻车的一环我第一次在终端里画表格时就栽在宽度上。Unicode 给每个字符定义了一个 East Asian Width 属性取值有六种Na窄、N中性、H半宽、W宽、F全角、A模糊Ambiguous。前三种在终端里通常按 1 列算W 和 F 按 2 列算最坑的是 A——它按当前 locale 决定算 1 列还是 2 列。举个例子★ U2605、☆ U2606、○ U25CB、● U25CF、◆ U25C6、① U2460、※ U203B、× U00D7 这些全都带 A 属性。在en_US.UTF-8环境下按 1 列算在zh_CN.UTF-8或ja_JP.UTF-8下按 2 列算。你在英文环境调好的对齐表格同事切到中文环境打开就散架。更麻烦的是终端模拟器实现不统一有的完全不管 locale 一律按 1 列有的提供模糊宽度开关。我的处理办法很简单要精确对齐的场合只用 Na、N、H 三种属性的字符。二级安全区里的制表符 U2500 到 U257F、方块元素 U2580 到 U259F 都是 Na几何图形里的 ■ □ ▲ ▼ 也是 Na这些配起来画表格最稳。而 ○ ● ◆ ★ ☆ 这些带 A 属性的我只用在不需要对齐的装饰位置。第三章会给你一段 Python两行就能把任意字符的宽度属性打出来。2. 安全区符号清单按用途分组整理这一章是清单主体。我不按 Unicode 区块罗列那样查起来太累改成按你实际想干什么分组画线、做标记、排序号、当箭头用。每组下面标出安全等级和宽度属性方便你直接抄。2.1 一级安全区ASCII 加拉丁补充闭眼用第一组是纯 ASCII 可打印符号从空格到波浪号共 95 个! # $ % ( ) * , - . / 0-9 : ; ? A-Z [ \ ] ^ _ a-z { | } ~这组不用多解释所有字体都有宽度一律 1 列。要注意的是空格和不可见字符的坑U0020 是普通空格U00A0 是不换行空格看起来一样但语义不同复制到某些系统里会显示成奇怪的符号U200B 是零宽空格肉眼完全看不见却会让你明明一样的两串字符比较失败。做 ID、做配置键名时别混用这几种空格。第二组是拉丁补充区里真正通用的部分我日常用得最多的是这些¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ × ÷这里面有几个值得单独说的。° 是度数± 是正负号× 和 ÷ 是乘除号——这三个在写规格、写参数、写计算过程时几乎无法替代。¼ ½ ¾ 和 ² ³ ¹ 这几个分数与上标数字在标注尺寸和面积时特别顺手而且它们都是 1 列宽不会破坏对齐。µ 是微符号写单位时会用到§ 是章节号写规范文档时很常见¶ 是段落符我习惯用它标注注释块。第三组是通用标点区里被广泛覆盖的一批我把实测最稳的挑出来– — … ‘ ’ “ ” „ † ‡ • ‰ ′ ″ ‹ › ※ ⁄破折号 — 和连接号 – 是两个不同码位长文里混用会很难看省略号 … 是单个字符比三个点更整齐弯引号 ‘ ’ “ ” 在中英混排里比直引号好看但注意它们在代码里会引发语法错误只在文档和界面文案里用。′ ″ 是分秒号写角度和经纬度必备。※ 这个符号在中文语境里表示注意宽度是 A 属性终端里慎用。实践建议把上面三组存成一个纯文本参考文件放在项目里命名safe-chars-1.txt。新人接手时直接给他这个文件比口头说别用奇怪符号有效得多。2.2 二级安全区制表符、方块、几何图形做版式这一组是我最喜欢的一批因为它们是用文本画图的主力而且在现代系统上覆盖度非常高。制表符区 U2500 到 U257F轻线一套、重线一套、双线一套─ │ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼ ━ ┃ ┏ ┓ ┗ ┛ ┣ ┫ ┳ ┻ ╋ ═ ║ ╔ ╗ ╚ ╝ ╠ ╣ ╦ ╩ ╬轻线用来画目录树和表格边框重线用来强调双线我一般只用在文档顶部的标题框。一个关键细节制表符之间的连接必须用同一套线型轻线配 ┼、重线配 ╋、双线配 ╬混用会出现明显的断口。另外有些字体里轻线和重线的粗细差异很小打印出来几乎看不出区别这种场合建议直接换双线。方块元素区 U2580 到 U259F是纯文本画进度条和像素画的基石█ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ░ ▒ ▓前八个是不同宽度的实心块属于同一个字符位置内的部分填充后面三个是不同密度的网点。用 █ 加 ░ 拼进度条十格代表 100%一格 10%在终端里非常直观。要注意 ▏到█这一串的宽度属性都是 Na但在某些等宽字体里它们会渲染成整格宽度视觉上比半格宽所以做精确像素画时最好先在你目标字体里验证一遍。几何图形区 U25A0 到 U25FF常用的这些■ □ ▪ ▫ ▬ ▲ ▼ ▶ △ ▽ ◁ ▷ ◆ ◇ ○ ● ◐ ◑ ◘ ◙ ◢ ◣ ◤ ◥实心和空心成对出现的这几组特别适合做状态标记■ 表示已完成□ 表示未完成▲▼ 表示排序方向◢◣◤◥ 四个三角我可以拼成简单的圆角框。这里要提醒一句○ 和 ● 是 A 属性、单列环境 1 格中文环境 2 格而 ■ 和 □ 是 Na 属性始终 1 格。做需要对齐的清单时用 ■□别用 ○●。2.3 三级安全区箭头、数学符号、序号与圆圈字箭头区 U2190 到 U21FF 覆盖度很高我常用的这批← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ⇐ ⇑ ⇒ ⇓ ⇔单线箭头用于流程和指向双线箭头适合表示映射和等价。写文档描述数据流、写注释说明参数方向、写日志标记上下游关系这些箭头都很好用。注意 ← ↑ → ↓ 是 A 属性在代码注释里没问题在需要对齐的终端表格里要小心。数学运算符区 U2200 到 U22FF这批在技术文档里出场率极高± ∓ × ÷ ≠ ≈ ≤ ≥ ≡ ∞ √ ∑ ∏ ∫ ∴ ∵ ∠ ⊥ ∆ ∇ ∈ ∉ ∩ ∪ ⊂ ⊃其中 ≠ ≈ ≤ ≥ 这四个是写规格区间的标准写法比 更规整√ 用于写公式∑ ∏ ∫ 用于写求和求积积分∴ ∵ 是所以和因为写推导过程特别省字∈ ∉ 用于集合和成员判断写类型系统说明时常出现。这批符号在 Windows、macOS、Android、iOS 上都能正常显示宽度多为 Na 或 A中文环境下建议只用 ≠ ≤ ≥ ∞ √ 这几个最保险的。序号类我分两组。一组是圆圈数字和括号数字① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑴ ⑵ ⑷ ⒈ ⒊ ⒋ ⒌ Ⓐ Ⓑ Ⓒ ⓑ这组全部是 A 属性而且需要 CJK 字体或带圈字母字体支持纯英文 Linux 环境下大概率是方块。它们在我这里的定位是给人看的界面文案绝对不能用在数据、键名、文件名里——一旦涉及排序和比较带圈数字的码位顺序和数值顺序不一致会出大问题。另一组是罗马数字和分数Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ ⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞以及一批量写符号℃ ℉ № ℡ ℓ ℗ ™。这组里 ℃ 和 ™ 覆盖度极高基本无风险№ 和 在中文日文环境没问题ℓ 和 ℗ 就小众一些。我的取舍是℃ ™ 放心用其余的在关键位置一律退回普通写法比如用 No. 代替 №。2.4 灰区清单默认会变彩、变宽、变豆腐的那些灰区不是一定不能用而是用之前必须知道自己在冒什么风险。我把踩过的都列出来。第一类是默认彩色呈现的符号。Unicode 里有一批码位被标记为 Emoji_Presentation只要没加特殊修饰系统会直接给你画成彩色图标而不受字体限制。这类字符我在这里不打印出来只给码位U2600太阳、U2601云、U2602伞、U26A0警告三角、U26BD球、U2708飞机。它们在桌面上可能是黑白线条在手机上是彩色块在工控屏上是方块同一个字符三种长相。要强制文本呈现可以加变体选择符 UFE0E但老驱动常常不认这个修饰符等于没加。第二类是辅助平面字符也就是码位超过 UFFFF 的那些包括数学粗体字母、各种装饰性字母变体、部分历史文字。这批在 Segoe UI Symbol 里覆盖很差Windows 上大量是方块macOS 和 Android 的 Noto 覆盖好一些。做网名装饰的人特别喜欢这类字符因为它们能做出花体效果但代价是另一半用户看不到。第三类是盲文点阵区U2800 到 U28FF。它的覆盖情况有点特殊Windows 的 Segoe UI Symbol 支持macOS 自带 Apple Braille 字体Linux 上装了 Noto 就有没装就全挂。但它的价值很高——每个字符是一个 2×4 的点阵八个字符就能拼出一幅小图的轮廓网上流行的点阵头像就是拿它做的。我的建议是把它当锦上添花用主内容仍然用一级二级符号承载。第四类是半宽片假名 这一批以及中文标点。《》「」【】、。这些在 CJK 环境完全没问题在纯英文容器镜像里比如只装了fonts-dejavu的 Docker 基础镜像会整片变方块。这个坑我踩过一个导出 PDF 的服务本地跑得好好的进容器所有中文标点变成方框最后是靠往镜像里装fonts-noto-cjk解决的。如果你的产物要进容器先把字体依赖写进 Dockerfile别指望基础镜像。3. 场景落地符号真正能派上用场的地方清单只是素材怎么用才是关键。这一章我按四个具体场景讲每个场景都会说明选哪一级符号、为什么这么选、以及我自己的实操习惯。3.1 网名、ID 与纯文本装饰很多人找符号是为了装饰名字这个需求的真实约束和表面看起来不一样。第一约束是对方能不能看见你用一个三级符号装饰的名字在别人那里可能变成一个空心方块效果比不加还糟。我的做法是只从一级和二级里挑★ ☆ ■ □ ◆ ◇ ▲ ▼ ● ○ 以及 ‹ › 「 」 『 』这类括号。实际排列时有个技巧叫重心平衡。中文字符是方块宽度 2 列上面的符号大多是 1 列宽直接贴在中文后面会让整个名字左轻右重。所以我习惯成对使用比如★ 名字 ★左右各放一个 1 列符号视觉上就平衡了。如果用 ‹ › 这种更细的符号可以左右各加两个‹‹ 名字 ››观感更精致。另一个高频需求是做分隔符。我用得最多的是这几种组合· · ·、— — —、◆ ◇ ◆、◦ ◦ ◦根据整体风格挑。这里有个小坑中点 · U00B7 和间隔号 · U30FB 长得几乎一样但前者是拉丁补充区、后者是 CJK 区混用会导致某些系统里间距不一致。我统一用 U00B7。实操心得做装饰性 ID 时先在目标平台的输入框里完整打一遍并保存再刷新页面看一遍。有些平台会对特殊字符做过滤或者转义你在编辑器里看到的和存进去的可能不是一回事。3.2 注释分隔线、目录树与纯文本表格这是我用得最狠的场景天天在写。代码注释里的分隔线我用─铺一行长度对齐到 80 列更醒目一点就用═需要区分主次的时候顶部═底部─。注意别用减号和等号去铺用-和铺出来的线在比例字体里会断成一节一节而制表符在等宽和非等宽字体里都是连续直线。目录树我用这套组合项目根目录 ├── src │ ├── core │ │ └── parser.py │ └── utils.py └── README.md这套是├│└─四个字符拼出来的一级目录用├──最后一项用└──中间层用│竖线贯通。关键点在于竖线的位置必须由上层是否还有兄弟节点决定很多人手工画出来的树上下竖线对不齐就是因为没管这个逻辑。用程序生成时递归函数里维护一个祖先是否最后一个的布尔数组逐层拼接前缀就不会错。表格我建议加一层保护。纯文本表格用┌┬┐ ├┼┤ └┴┘加─│拼理论上很漂亮但一旦单元格里混进中文就会错位因为中文是 2 列宽而制表符是 1 列。正确做法是先算每个单元格的显示宽度再补空格填充不能直接用字符串长度。第三章会给一段算宽度的代码。实在懒得算就退回 ASCII 的---和|虽然朴素但绝不会错。3.3 终端与嵌入式屏的选型硬约束这两个场景的约束比桌面严得多我得单独说。终端方面最大的变量是字体。等宽字体里包含的符号集差异极大有些只到 ASCII有些带制表符但不带几何图形。判断方法很直接把候选符号全部打到终端里看有没有方块和错位。还要注意终端模拟器的模糊宽度设置同一批带 A 属性的符号在不同终端里可能差一格。我的保守做法是终端里只用到二级安全区里的制表符和方块元素避开所有 A 属性符号。嵌入式这块坑更深。低成本的字符点阵屏驱动芯片内置的 CGROM 通常只有三段ASCII、半宽片假名、以及少量希腊字母或符号。真正的安全集合就是 ASCII顶多加上半宽片假名。我见过有人往段码屏上推箭头和方块结果整屏乱码。工业 HMI 好用一些通常是 Linux 或 Windows CE 系统加自烧字库可以自己决定包含哪些区段。这种情况下我的做法是自己维护一个精简字库只烧一级安全区加制表符虽然丑但十年后换设备也不会出问题。如果你想在嵌入式屏上做得好看一点有个折中方案用 ASCII 的字符自己拼图形。比如进度条用[####------]方块用[]和##拼箭头用-和。这样在任何环境下渲染结果完全一致代价是精细度低。3.4 输入方式从键盘把符号敲出来知道有哪些符号还得能打出来。我把各平台的输入方式列一遍。Windows 上可以用 Alt 加小键盘数字输入码位按住 Alt在小键盘上敲四位数松手出字符。比如 Alt0169 是 ©Alt0176 是 °Alt0215 是 ×。注意必须用小键盘笔记本上要开 NumLock且码位只能到 0255 这一段超出的用不了这招。Windows 10 之后有个更快的办法按 Win 加句点打开符号面板里面按类别分了符号和表情找几何图形很方便。macOS 上按 ControlCommand空格召唤字符检视器左侧选类别双击插入还能把常用符号拖进收藏。我习惯把制表符和箭头都收藏起来用的时候一键插入。Linux 桌面和终端里是 ControlShiftU 加十六进制码位再回车比如CtrlShiftU然后敲2 6 0 5回车出 ★。Vim 里用CtrlK加两个助记字符或者:dig查表Emacs 用CtrlX 8 Enter后输码位。写代码时还有一个更可靠的路径HTML 实体和语言内置转义。HTML 里copy;是 ©#8593;是 ↑Python 里\u2191是 ↑JavaScript 里\u2191同样。这些写法把字符和编码脱钩特别适合放进源码和配置模板因为你在哪个编辑器里打开都不会乱。提醒用输入法软键盘插入符号时务必确认插入的是目标码位。有些输入法的符号大全实际插入的是兼容区或者全角形式看起来一样但码位不同一旦进入数据比对就会暴露问题。4. 动手做一套自己的符号检测与生成工具清单再多也是别人的结论环境一变就失效。这三段脚本我在自己的项目里一直在用作用分别是算出真正安全的字符交集、算准显示宽度、在前端判断某个字符会不会变方块。4.1 用 fontTools 扫描字体 cmap算出真正安全的交集思路很简单把目标环境的默认字体文件收集起来读出每款字体的 cmap 表码位到字形的映射然后求交集。交集里的码位就是所有目标字体都能画的安全集合。先装依赖pip install fonttools wcwidth然后是核心脚本from fontTools.ttLib import TTFont, TTCollection from pathlib import Path # 按你的目标环境替换成真实路径 FONT_FILES [ rC:\Windows\Fonts\segoeui.ttf, rC:\Windows\Fonts\seguisym.ttf, rC:\Windows\Fonts\msyh.ttc, ] def load_cmap(path): 读取一个字体文件或字体集合的全部 Unicode 码位 p Path(path) if not p.exists(): print(f[跳过] 文件不存在: {path}) return None cmap set() if p.suffix.lower() in (.ttc, .otc): for f in TTCollection(str(p)).fonts: cmap | extract(f) else: cmap | extract(TTFont(str(p), lazyTrue)) return cmap def extract(font): s set() for table in font[cmap].tables: if table.isUnicode(): s.update(table.cmap.keys()) return s sets [] for fp in FONT_FILES: s load_cmap(fp) if s is not None: sets.append(s) safe set.intersection(*sets) if sets else set() print(f参与计算的字体数: {len(sets)}) print(f共同覆盖的码位数量: {len(safe)}) # 导出安全区清单 with open(safe-chars.txt, w, encodingutf-8) as out: for cp in sorted(safe): if 0x20 cp 0xFFFF: out.write(chr(cp))跑完你会得到一个safe-chars.txt里面就是这几款字体的真实交集。我第一次跑的时候有个发现很有意思几何图形区里那些看起来最保险的符号在某个版本的等宽字体里居然是缺的而制表符区的覆盖反而比我想象中稳。这也印证了必须实测这条原则。脚本里用了lazyTrue因为加载几十兆的中文字体很慢懒加载只读表头速度快很多。另外.ttc是字体集合一个文件里可能打包了简繁日多套必须用TTCollection逐个读直接当TTFont读会报错或者只拿到第一套。4.2 宽度与对齐用 wcwidth 算出显示列数有了安全字符集下一步是算宽度。标准库的unicodedata能给出 East Asian Width 属性wcwidth库能给出终端里的实际列数两个配合用。import unicodedata from wcwidth import wcswidth, wcwidth samples [A, 中, ─, ★, ○, ①, →, ±, █, ] print(f{字符:4}{码位:8}{EAW:6}{wcwidth:8}{判定}) for ch in samples: cp fU{ord(ch):04X} eaw unicodedata.east_asian_width(ch) w wcwidth(ch) verdict {F: 全角, W: 宽, H: 半宽, Na: 窄, N: 中性, A: 模糊}[eaw] print(f{ch:4}{cp:8}{eaw:6}{w:8}{verdict}) def display_width(s): 返回字符串在等宽终端的显示列数未知字符按 1 算 w wcswidth(s) return w if w 0 else len(s) # 对齐填充按显示宽度补空格而不是按字符数 def pad(text, total_width, alignleft): gap total_width - display_width(text) if gap 0: return text if align left: return text * gap if align right: return * gap text left gap // 2 return * left text * (gap - left) rows [(项目, 状态, 数量), (解析器 core, 已完成, 12), (导出模块 export, 进行中, 3)] widths [22, 12, 6] for r in rows: print(│ │ .join(pad(c, w) for c, w in zip(r, widths)) │)这段代码有两个要点。一是wcswidth遇到不可打印字符会返回 -1必须兜底否则整个计算链会崩。二是pad函数按显示列数补空格这是纯文本表格能对齐的唯一正确做法用len()或者ljust()都会在中文上错位。我在项目里把它封装成了一个texttable.py模块所有 CLI 输出都走它。关于模糊宽度还有一点要补充wcwidth库默认把 A 属性按 1 列处理。如果你的目标终端在中文 locale 下把 A 当 2 列就会差一格。稳妥的做法是在对齐计算时对 A 属性字符做额外标记或者干脆从表格里排除它们。我个人选择后者省心。4.3 前端侧检测canvas 量宽判断豆腐块后端能算字体前端就不行了——浏览器不给你访问系统字体的清单。不过有个曲线办法把字符画到 canvas 上量宽度和已知缺失字符比一比。const PUA \uE000; // 私用区几乎肯定缺字形 function isLikelyTofu(ch, fontSpec 16px sans-serif) { const c document.createElement(canvas); const ctx c.getContext(2d); ctx.font fontSpec; const refW ctx.measureText(PUA).width; const chW ctx.measureText(ch).width; // 宽度和已知缺失字形一致高度疑似豆腐块 return Math.abs(chW - refW) 0.5; } function renderAndCompare(ch, fontSpec 16px sans-serif) { const size 32; const mk (text) { const c document.createElement(canvas); c.width c.height size; const ctx c.getContext(2d); ctx.font fontSpec; ctx.textBaseline top; ctx.fillText(text, 2, 2); return ctx.getImageData(0, 0, size, size).data; }; const a mk(ch); const b mk(PUA); let diff 0; for (let i 0; i a.length; i 4) { if (Math.abs(a[i 3] - b[i 3]) 16) diff; } // 轮廓几乎重合基本可以判定是同一个 .notdef 字形 return diff size * size * 0.05; }第一个函数用宽度做粗筛快但容易误判因为正巧同宽的字很多。第二个函数把字符和私用区字符各画一遍逐像素比较透明度通道如果轮廓几乎完全重合说明两者渲染的是同一个 .notdef 字形也就是同一个豆腐块。这个方法准确率高得多代价是要创建 canvas 和读像素别在循环里高频调用。要提醒一句document.fonts.check(16px sans-serif, ch)这个 API 看起来像是干这个的实际上它只检查通过font-face加载的字体有没有就绪对系统字体一律返回 true拿来判断豆腐块会得出完全错误的结论。我第一次就栽在这上面。4.4 生成一份可复制的符号清单最后把这些拼起来做一个小工具输入一组字体路径输出一份分级清单每行带码位、宽度属性和安全等级。import unicodedata from wcwidth import wcwidth def classify(ch, safe_set): cp ord(ch) eaw unicodedata.east_asian_width(ch) if cp not in safe_set: return D, 目标字体缺失 if cp 0x00FF: return A, 一级通用 if 0x2000 cp 0x22FF or 0x2500 cp 0x25FF: return A if eaw in (Na, N, H) else B, 二级符号字体 if 0x2460 cp 0x24FF or 0x3000 cp 0x30FF: return C, 三级依赖 CJK if cp 0xFFFF: return D, 辅助平面 return C, 需实测 def report(chars, safe_set, title): print(f\n {title} ) for ch in chars: cp ord(ch) level, note classify(ch, safe_set) w wcwidth(ch) print(f{ch} U{cp:04X} EAW{unicodedata.east_asian_width(ch)} f width{w} 等级{level} {note}) # safe 来自 4.1 的交集这里用一个小样本演示 demo ─│┌┐└├┤┬┴┼■□▲▼◆◇○●★☆←↑→↓±×÷≠≈≤≥√∞①Ⓐ℃™ report(demo, safe, 常用符号体检报告)输出会按等级标出每个字符的可用性D 级的直接剔掉。我把这个脚本和 4.1 的导出文件串成了一条流水线每次换目标平台比如从 Windows 切到 Docker 容器就重跑一遍重新生成常量文件给前端和后端共用。这套机制最大的价值不是省事而是把符号能不能显示从一个靠经验的判断题变成了一个有输出的计算题。5. 踩坑记录与速查前面讲的都是方法这一章讲症状。我把这两年遇到的显示问题按看到什么整理成排查路径都是真金白银换来的。5.1 典型症状到根因的排查路径症状最可能的根因排查动作处理办法局部空心方框目标字体缺该码位用 4.1 脚本查交集换成一级或二级符号整行错位一格混入了 A 属性字符用 4.2 打 EAW 属性换成 Na 属性符号或排除手机上变彩色图标Emoji_Presentation 为真查 Unicode 属性表加 UFE0E 或换码位容器里全变方块镜像没装字体进容器执行字体检测Dockerfile 里装 Noto粘贴后变问号编码链路丢失检查两端编码统一 UTF-8禁用 GBK两串相同文本比对失败归一化或不可见字符差异逐字节 diff 十六进制统一 NFC剔除零宽字符表格在别人电脑乱掉字体不等宽或宽度不同换目标机器复现退回 ASCII 版式同一个字符两处宽窄不同终端模糊宽度设置不同检查终端设置项只用非模糊符号这张表我贴在自己工位显示器边上。最常命中的是前两条也是最好解决的八成的显示问题换个码位就完事没必要去改字体配置。5.2 编码与归一化的坑有一类问题特别阴就是字符串看起来完全一样程序却说它们不相等。原因通常有两个。第一个是归一化形式不同。Unicode 允许同一个字形有多种编码方式比如带音标的字符可以是单个码位NFC 形式也可以是基础字符加组合记号NFD 形式。有些符号之间甚至互为兼容等价一个码位可以被另一个替换。处理办法是入库和比对前先做unicodedata.normalize(NFC, s)Python 和 JavaScript 都支持。我在一个用户昵称去重的功能里就吃过这个亏两个看起来一模一样的昵称被当成两个人。第二个是不可见字符。零宽空格 U200B、零宽非连接符 U200C、字节序标记 UFEFF、软连字符 U00AD这几个在编辑器里完全看不见但会参与长度计算和字符串比较。从网页复制文本时特别容易带上。排查方法是用十六进制查看器把字符串 dump 出来Python 里一行就够s 看起来一样的字符串 print( .join(f{ord(c):04X} for c in s)) # 清洗删掉常见不可见字符 INVISIBLE dict.fromkeys(map(ord, \u200b\u200c\u200d\ufeff\u00ad\u2060), None) clean s.translate(INVISIBLE) print(clean)这段清洗代码我几乎每个处理用户输入的项目都会放一份。另外还有一件事值得强调文件保存编码统一用 UTF-8不要用 GBK 或系统默认编码。跨平台协作时一个 GBK 编码的配置文件在 Linux 上打开就是乱码而且这种问题往往到很后期才暴露出来。5.3 常用符号速查表最后把这一级最常用的符号汇成一张按用途查的表宽度属性一并标出方便你在写东西时直接翻。用途推荐符号宽度等级备注画横线─ ═1二级轻/重/双三套别混用画竖线│ ┃ ║1二级与横线配套使用画拐角┌ ┐ └ ┘1二级目录树和表格边框树形图├ └ │1二级递归生成最稳进度条█ ▓ ▒ ░1二级十格制最直观状态标记■ □ ▪ ▫1二级实心已完成空心未完成方向箭头→ ← ↑ ↓ ⇒ ⇔1 或 2三级需对齐时改用 - 数学比较≠ ≈ ≤ ≥ ≡1三级写规格区间必备数学运算± × ÷ √ ∞ ∑ ∏1三级参数计算常用度数单位° µ ℃ ™1三级℃ ™ 最稳µ 次之序数标记① ② ③ ⒉2三级只用于界面不做数据引号括号“ ” ‘ ’ « »1一级代码里禁用破折省略— – …1一级长破折号 U2014分秒角标′ ″ ‰1一级角度和经纬度分隔装饰· • ◆ ◇ ★1 或 2二级成对使用更平衡我个人的习惯是任何要进入代码仓库的字符串常量只用一级和二级符号三级符号一律走变量或者资源文件方便按环境替换灰区符号基本不碰。这套规则执行了两年跨平台显示问题从每个月都要处理几次降到几乎为零。另外一个小建议把上面这张表复制到你项目的 CONTRIBUTING 文档里团队里来新人时不用反复解释直接在 review 时引用表格就行。字符这东西看着琐碎但一旦形成约定维护成本几乎为零。
返回列表