这两天在帮一个项目组调LaTeX编译流水线,结果一跑到文档编译那步就挂,日志里明晃晃一行:
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ! ! fontspec error: "font-not-found" ! ! The font "FangSong_GB2312" cannot be found. ! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!这个报错在texlive2025环境里太典型了,尤其是在Linux服务器、WSL或者Docker容器里编译带中文的文档时。Windows上跑得好好的项目,一挪到Linux就炸,十有八九都是字体问题。我这几年踩过的坑,从挖fontconfig配置到手动改字体名,基本都集中在这条报错上。这篇文章就完整梳理一下:FangSong_GB2312到底是什么,为什么texlive找不到它,以及在不同场景下怎么彻底解决。
不管你是刚接触LaTeX的新手,还是被CI构建折磨的运维,又或者是写毕业论文被模板坑过的人,这篇都能给你一套能直接抄的解决方案。
1. 先别急着改代码:弄懂FangSong_GB2312到底去哪了
1.1 公文排版和论文模板为什么非要FangSong_GB2312
先说字体本身。仿宋_GB2312是汉字排版里的老熟人,国内党政机关公文格式要求正文用仿宋_GB2312,高校毕业论文模板里也大量出现这个字体名。它的罗马化字体族名通常写作FangSong_GB2312,在Windows系统里经常以“仿宋_GB2312”这个名字出现在字体列表中。
这里有个特别容易搞混的点:Windows系统自带的“仿宋”字体,字体族名是FangSong,文件名是simfang.ttf,这个是系统自带的;而“仿宋_GB2312”字体族名是FangSong_GB2312,它往往不是Win系统默认自带的,很多是跟着Office套件或者用户自己安装进去的。这俩是两套不同的字体,字形细节和字库范围都有差异。很多人在Linux上装了个simfang.ttf,以为问题解决了,结果LaTeX还是报cannot find FangSong_GB2312,就是因为实际装错了字体。
在LaTeX文档里,这个字体名出现的典型方式有两种:
% 方式一:ctex宏包配合fontset=windows时,部分模板会显式覆盖字体 \documentclass[fontset=windows]{ctexart} \setCJKmainfont{FangSong_GB2312} % 方式二:直接用fontspec/xeCJK声明 \usepackage{xeCJK} \setCJKmainfont{FangSong_GB2312}问题就出在这:当你用xelatex或lualatex编译时,引擎不会自己去扫描字体文件,而是通过系统的字体服务(Linux/macOS上是fontconfig,Windows上是DirectWrite)来查找字体。查不到这个字体名,就直接抛异常退出,报错信息就是cannot find FangSong_GB2312。
1.2 为什么Windows编译正常、Linux/WSL上就报错
核心原因很简单:字体不是跨平台的。Windows字体是Windows的,Linux系统默认不会给你装任何Windows专有字体。texlive2025自带的中文字体是Fandol系列(FandolSong、FandolHei、FandolKai、FandolFang)和Arphic系列,这些是开源字体,覆盖了宋、黑、楷、仿四类基本书体,但字体名里没有FangSong_GB2312。
所以只要文档里写了\setCJKmainfont{FangSong_GB2312},编译时fontconfig就会去字体库里找这个名字。找不到,报错,结束。这个过程不区分你用的是texlive2023还是texlive2025,也不管你的LaTeX宏包是不是最新版——字体查找是引擎层面的行为,跟LaTeX版本关系不大。
我把常见平台的默认情况整理成了一张表:
| 平台 | 是否自带FangSong_GB2312 | 常见自带仿宋字体 | 说明 |
|---|---|---|---|
| Windows(本机已安装字体) | 部分有 | FangSong(仿宋)、仿宋_GB2312 | 能否编译取决于到底装没装过“仿宋_GB2312” |
| Windows(纯净系统) | 不一定 | FangSong(仿宋) | 如果模板里写的是FangSong_GB2312,可能也会报错 |
| Linux(Ubuntu/Debian/CentOS) | 无 | 无 | texlive自带FandolFang,但名字不同 |
| WSL(默认配置) | 无 | 无 | WSL默认不加载Windows字体,需要额外配置 |
| macOS | 无 | STFangsong(华文仿宋) | 字体名完全不同,需要映射或改名 |
| Docker容器 | 无 | 无 | 需要额外安装字体包或COPY字体镜像 |
这也是为什么同一个项目在不同人电脑上编译结果天差地别的原因。
1.3 ctex的fontset机制:自动选择不等于万事大吉
ctex宏包提供了一个fontset参数,可以指定从哪套字体配置里取字。比如:
\documentclass[fontset=windows]{ctexart} % 使用Windows字体 \documentclass[fontset=fandol]{ctexart} % 使用Fandol开源字体 \documentclass[fontset=mac]{ctexart} % 使用macOS华文字体如果不显式指定,ctex 2.x会根据当前操作系统自动推断:在Windows上用SimSun那一套,在macOS上找华文字体,在Linux上用Fandol。理论上这样设计很智能,但问题在于很多老模板里会同时手动覆盖\setCJKmainfont,把字体写死成FangSong_GB2312。一旦模板里写了这种硬编码,ctex的fontset再怎么自动选择也白搭——你手动声明的字体会覆盖掉ctex的默认配置。
所以排查这个报错的时候,第一件事不是急着装字体,而是先搞清楚:这个字体名到底是在哪里被声明的。是文档导言区显式写的,还是模板.cls文件里写的,又或者是从ctex的某个fontset配置里带出来的。定位源头的思路不同,后续处理方式完全不同。
2. 排查不是瞎猜:三个命令定位问题
2.1 先确认你的编译引擎和字体配置来源
在动手修之前,先把问题范围缩小。我的习惯是分三步走:
确认编译引擎:打开项目,看是用xelatex还是lualatex编译。字体查找机制的入口不同,但最终都落在fontconfig上。用latexmk的话,可以看下latexmkrc文件里配置的是xelatex还是lualatex。
打开文档源码或模板.cls文件,全局搜索一下“FangSong_GB2312”这个字符串,看它出现在哪里。这一步能直接告诉你问题源头。
把编译命令完整跑一遍,确保报错确实是字体错误而不是其他宏包错误。有时候前提搞错了,后面全是白费功夫。
在我见过的项目里,90%的情况是模板.cls文件里硬编码了\setCJKmainfont{FangSong_GB2312},剩下10%是用户自己写的导言区。
2.2 用fc-list确认系统里到底有没有这个字体
在Linux或WSL终端里跑这个命令:
fc-list :lang=zh这条命令会列出系统里所有支持中文的字体。输出里会包含字体文件路径、字体族名(中英文)、样式等信息。如果系统里装了仿宋相关的字体,你多半能在输出里看到类似这样的行:
/usr/share/fonts/truetype/fangsong/simfang.ttf: 仿宋,FangSong:style=Regular注意看冒号后面的是字体族名。如果有FangSong但没FangSong_GB2312,那说明你(或系统)安装的是“仿宋”而不是“仿宋_GB2312”。如果连仿宋的影子都没有,那就更直接了——系统里根本没有对应的字体。
再精确验证一下:
fc-match "FangSong_GB2312"如果fontconfig里能找到这个字体,fc-match会输出字体文件路径,比如:
/usr/share/fonts/truetype/custom/FangSong_GB2312.ttf: 仿宋_GB2312,FangSong_GB2312:style=Regular如果找不到,fc-match会输出一个空行或一个替代字体的路径(fallback结果)。这也能侧面说明,LaTeX报cannot find FangSong_GB2312是完全正常的——系统层面就不存在这个字体。
2.3 字体存在但LaTeX还是找不到?排查字体名的不一致
还有一种情况比较隐蔽:字体文件装好了,fc-list也能看到了,但LaTeX编译还是报找不到。这多半是字体名不一致的问题。
Linux上安装的字体,如果字体文件内部的family name是中文字段“仿宋_GB2312”,那么fc-list里显示的主名可能是中文“仿宋_GB2312”,而罗马化的“FangSong_GB2312”可能只作为别名出现,甚至完全没有。xelatex在匹配字体名时,对中文名和英文名的处理策略不完全一样,有时候你写中文名能匹配上,写英文名就匹配不上。
判断方法是用fc-scan查看字体文件的详细信息:
fc-scan /usr/share/fonts/truetype/custom/FangSong_GB2312.ttf输出里会有family和fullname字段,能直观看到字体的真实名字。如果family是“仿宋_GB2312”而没有包含FangSong_GB2312这个别名,那你在文档里写\setCJKmainfont{FangSong_GB2312}就可能认不出来,这时需要改成\setCJKmainfont{仿宋_GB2312}(中文名)来试。
这一步排查很重要,很多人栽在这个细节上,以为装好了字体就应该万事大吉,结果卡在名字匹配上几个小时。
3. 四种解决方案:从改系统到改模板
3.1 方案一:把字体装进系统(治本)
如果项目必须用FangSong_GB2312这个字体,且你不能改模板,那最可靠的做法就是把字体装进系统里。
字体来源:从有FangSong_GB2312的Windows机器上,将字体文件复制出来。注意版权问题,这种字体的使用许可通常不允许随意分发,但个人项目、公司内网使用一般问题不大,不能用于商业再分发。
拿到字体文件后,在Linux上安装到用户目录即可,不需要sudo:
mkdir -p ~/.fonts cp FangSong_GB2312.ttf ~/.fonts/ fc-cache -fv ~/.fonts如果想装到系统全局,给所有用户使用:
sudo mkdir -p /usr/share/fonts/truetype/custom sudo cp FangSong_GB2312.ttf /usr/share/fonts/truetype/custom/ sudo fc-cache -fv装完验证:
fc-list | grep -i fangsong看到FangSong_GB2312出现在输出里,再重新编译LaTeX文档,应该就不会报这个错了。
这里有个重要提醒:如果你复制的是Windows自带的simfang.ttf,那只是“仿宋”(FangSong),不是“仿宋_GB2312”。必须确认字体文件对应的family name真的是FangSong_GB2312,用fc-scan看清,否则装完还是会报错。
3.2 方案二:改用texlive自带的Fandol字体(省事)
如果不强制要求“仿宋_GB2312”这个字体,只是需要一份仿宋风格的中文正文,那最省事的办法是改用Fandol字体。texlive2025本身就带了Fandol系列,无需额外安装任何字体。
改法很简单,把文档导言区的字体声明改成:
\documentclass[fontset=fandol]{ctexart}如果之前没有用ctex文档类,而是手动写的字体配置,那把\setCJKmainfont的参数改掉:
\usepackage{xeCJK} \setCJKmainfont{FandolFang}FandolFang是Fandol系列的仿宋体,字形规整,排版效果和仿宋_GB2312其实很接近。至少从“正文是仿宋风格”这个需求上讲,FandolFang完全够用。
这个方案的优点是零依赖、部署简单,CI环境里不用有额外字体也能编译。缺点也很明显:如果是一个严格要求“仿宋_GB2312”字形的公文或学校模板,FandolFang的笔画细节和GB2312标准字库还是有差异的,在一些笔画结构上能看出区别。如果你只是想让编译通过,不在乎字体严格一致,这个方案性价比最高。
3.3 方案三:把字体文件放进项目目录(迁移友好)
如果既不想改模板,又不想动系统字体,还有一个折中方案:把字体文件放在项目目录下,通过fontspec的Path参数指定字体路径。
项目结构示例:
project/ ├── main.tex ├── fonts/ │ └── FangSong_GB2312.ttf └── compile.sh文档里这样写:
\usepackage{xeCJK} \setCJKmainfont{FangSong_GB2312}[ Path = ./fonts/ , Extension = .ttf , UprightFont = FangSong_GB2312 , BoldFont = FangSong_GB2312 ]这样编译时,xelatex会直接去指定目录找字体文件,而不依赖系统字体库。好处是项目自包含,换机器clone后直接编译就能过,不需要额外装字体。坏处是如果文档里很多地方用到这个字体,每处都要写Path参数,比较啰嗦;而且改路径时文件层级变了容易出错。
另外一个变体:也可以不指定Path,而是把字体文件放到texlive的字体搜索路径里,比如texmf-local目录。但这样做维护成本高,不如直接在项目里指定路径来得直观。
3.4 方案四:WSL里让fontconfig直接识别Windows字体
如果你用的是WSL(Windows Subsystem for Linux),还有一个独特的手段:让WSL里的fontconfig直接读取Windows字体目录。Windows字体在WSL里通常挂载在/mnt/c/Windows/Fonts。
创建或编辑fontconfig配置文件:
mkdir -p ~/.config/fontconfig cat > ~/.config/fontconfig/fonts.conf << 'EOF' <?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <fontconfig> <dir>/mnt/c/Windows/Fonts</dir> </fontconfig> EOF然后刷新缓存:
fc-cache -fv执行完后再用fc-list验证:
fc-list | grep -i fangsong如果Windows系统里装了仿宋_GB2312字体,这时WSL里就能直接看到它了。这个方案的优点是完全不用复制字体文件,WSL和Windows共享字体;缺点是走/mnt/c的I/O效率较低,大量字体加载时会稍慢,另外如果后续把编译环境从WSL迁到纯Linux服务器,这套配置就失效了。
我个人的使用建议:WSL里调试阶段可以用这个方案快速验证,但最终要上CI/服务器时,还是走方案一或方案三更稳。
4. 实操记录:一次完整的Linux服务器LaTeX编译修复
4.1 报错现场还原
事情是这样的:一个论文模板项目,在Windows本机用TeXstudio编译一切正常。代码推到GitLab后,CI用流水线执行编译,结果在那台Ubuntu 22.04的构建机上直接挂了。日志片段长这样:
Executing command: xelatex -interaction=nonstopmode -halt-on-error main.tex ... Package fontspec Error: The font "FangSong_GB2312" cannot be found.项目里用的是ctexart文档类,导言区长这样:
\documentclass[fontset=windows]{ctexart} \setCJKmainfont{FangSong_GB2312} \setCJKsansfont{SimHei} \setCJKmonofont{KaiTi}问题很清楚:fontset=windows要求系统里有Windows字体,但构建机是干净的Ubuntu,什么都没装。而且模板里硬编码了FangSong_GB2312,所以ctex的自动回退机制也失效了。
4.2 修复步骤
我用的是“模板改Fandol + 项目内字体文件兜底”的双保险策略,这样既解决当前编译,又避免以后换机器还踩同样的坑。
第一步,先在服务器上确认texlive2025已装好,xelatex可用:
which xelatex xelatex --version | head -n 1第二步,检查系统里有没有中文字体:
fc-list :lang=zh输出是空的,果然一个中文字体都没装。这时候如果直接用Fandol,理论上不需要额外装字体——texlive自带。但保险起见,我还是安排了一个字体文件放入项目目录,方便将来有需求时直接调用。
第三步,修改模板文件的字体配置。把导言区改为:
\documentclass[fontset=fandol]{ctexart}同时把所有\setCJKmainfont{FangSong_GB2312}这样显式指定字体的行注释掉或改成:
%\setCJKmainfont{FangSong_GB2312} \setCJKmainfont{FandolFang}并把\setCJKsansfont{SimHei}改成FandolHei,\setCJKmonofont{KaiTi}改成FandolKai。
第四步,重新编译:
latexmk -xelatex -halt-on-error main.tex编译通过,PDF正常生成,内容和Windows上编译的版式几乎无差别。
4.3 如果要强行保留FangSong_GB2312呢
如果模板不能改,那就在构建机上装字体。我在另一台测试机上试过完整流程:
sudo mkdir -p /usr/share/fonts/truetype/custom sudo cp FangSong_GB2312.ttf /usr/share/fonts/truetype/custom/ sudo fc-cache -fv fc-match "FangSong_GB2312"输出显示匹配到了字体文件后,直接latexmk编译,同样通过,不再报cannot find。
这里我的建议是:如果项目逻辑允许改模板,优先用Fandol方案,因为部署成本为零,还不用处理字体版权分发问题。如果必须用原字体,那就老老实实在服务器上装字体,并且把字体文件放到CI的构建配置里一起管理,不要指望每次构建时手工操作。
5. 你可能还会遇到的五个坑
5.1 字体装好了但LaTeX还是报找不到
如果你用的是lualatex编译,它有自己的字体缓存(luaotfload),跟fontconfig是两层。装了新字体后,lualatex不一定能立刻识别。
解决办法是清缓存重建:
luaotfload-tool --clear-cache luaotfload-tool --update-cache清完再编译,通常就能找到了。这个问题在xelatex里基本不会遇到,xelatex走的是fontconfig,实时读取。
5.2 字体名对不上:中文名vs英文名
再强调一次,Linux上安装字体后,字体在fc-list里显示的名字可能和Windows注册名不一样。上面提到过,用fc-scan查看实际名称:
fc-scan FangSong_GB2312.ttf | grep -E "family|fullname"如果family是“仿宋_GB2312”,没有FangSong_GB2312别名,那在文档里可以这么写:
\setCJKmainfont{仿宋_GB2312}不过这个写法在不同系统和字体配置下兼容性略差,能想办法补充字体别名最好。比较稳妥的做法是用FontForge等工具修改字体文件的family name,加一个FangSong_GB2312的别名。但这样做会改变字体文件指纹,一般人不建议折腾。我的经验就是:先试文档里写中文名,能跑通就不用折腾;跑不通再考虑改字体元数据。
5.3 模板里除了仿宋_GB2312还有楷体_GB2312等一整套GB字体
这类模板一般不止用仿宋_GB2312一种字体,往往还有黑体、楷体、宋体的一整套GB2312系列。在Linux上想全套装齐,最好一次性处理掉。常见需要的有:
| 字体名 | 用途 | 对应Linux替代 |
|---|---|---|
| 仿宋_GB2312 | 正文 | FandolFang |
| 楷体_GB2312 | 二级标题、引文 | FandolKai |
| 黑体(SimHei) | 一级标题 | FandolHei |
| 宋体(SimSun) | 部分模板正文 | FandolSong |
如果不想逐个复制Windows字体,也可以直接考虑安装fonts-noto-cjk包,然后手动把字体映射到GB2312字体名上,但字形会不是仿宋风格,一般不建议这么干。
5.4 CI环境没有sudo权限怎么办
很多CI Runner不会给你sudo。这时不能用/usr/share/fonts目录,改为装到用户级目录:
mkdir -p ~/.fonts cp FangSong_GB2312.ttf ~/.fonts/ fc-cache -fv ~/.fonts如果连~/.fonts方案也不行(比如Runner是容器,每次构建环境都是全新的),那就只能走方案三——把字体文件放进项目,用Path指定。这是最保险的做法,不依赖任何环境预装。
5.5 Docker镜像里字体丢失问题
Docker镜像默认只有极简字体,连中文字体包都没有。建议在Dockerfile里直接安装字体包:
RUN apt-get update && apt-get install -y fonts-noto-cjk \ && rm -rf /var/lib/apt/lists/*如果必须用FangSong_GB2312,就把字体COPY进镜像:
COPY FangSong_GB2312.ttf /usr/share/fonts/truetype/custom/ RUN fc-cache -fv还有一点:Docker构建时如果用了多阶段构建,要注意最终运行阶段里有没有把字体文件COPY过去。经常有人第一阶段装好了字体,第二阶段换了基础镜像,结果字体全没了,编译照样失败。
6. 写到最后的一点个人体会
搞定了好几个项目的这个问题之后,我最大的感受是:字体问题看似简单,背后的机制却串联了操作系统字体管理、文本渲染引擎、LaTeX字体配置三个层面。真正上手时,不要一开始就想着“把字体装上”,而是先弄清楚系统层面有没有这个字体、引擎是怎么找字体的、文档里是怎么声明的。把这三件事对齐了,很多匪夷所思的报错会瞬间变得简单。
另外,如果是长期维护的项目,我强烈建议把字体方案固化到项目配置里——要么明确用Fandol,要么把字体文件和路径配置提交到仓库。千万不要依赖“我这台机器上有这个字体”这种隐式前提,换环境时一定会出问题。CI环境里尤其如此,能在构建配置里显式写明依赖的字体,就能省下一堆半夜排查构建失败的痛苦。
最后再分享一个小技巧:如果你临时要在一台机器上快速判断问题是不是字体缺失,其实不用一行行翻LaTeX日志。先fc-match那个报错里的字体名,如果输出为空,直接去看文档里是不是显式声明了这个字体——两个问题一确认,根因就锁定了。剩下的事情,就是选一种适合你场景的方案执行而已。