把一列数字用 r(右对齐)排出来,是 LaTeX 表格里最常见的错误。把 182.5、95.0、1450.25、7 右对齐排列,小数点会 正好错开一个数字的宽度(5pt),而单独的 7 会落在十分位而不是个位上。这是可以量出来的。修正只需改列指定中的一个字符——换成 siunitx 的 S 列,小数点的位置就能对齐到 0.001pt 以内。本页讲这种数值对齐,以及在编译时读取 .csv 生成表格的 csvsimple、pgfplotstable、datatool,还有 tabularray 的现代做法。贯穿始终的原则是:把数据留在数据文件里,让 LaTeX 去读。
在 LaTeX 表格中对齐小数点——siunitx 的 S 列
只需载入 \usepackage{siunitx},把列指定中的 r 换成 S。S 列会解析数值并 按小数点(decimal marker)对齐,于是位数不同的数字排在一起也依然易读。把 PDF 中字形的坐标读出来对比,差别非常明显。在 r 列中,182.5 和 95.0 的小数点位于 x = 62.0,而 1450.25 的小数点位于 x = 57.0——相差约 5pt,正好一个数字的宽度;而且 7 被放在十分位而不是个位上。把同样的数据用 S[table-format=4.2] 排版,四个数的个位全都终止于 x = 122.11,小数点也全部从同一位置开始,偏差不到 0.001pt。
\usepackage{siunitx}
\usepackage{booktabs}
% table-format = <integer digits>.<decimal digits> of the widest entry
\begin{tabular}{l S[table-format=4.2]}
\toprule
Sample & {Mass / \unit{\gram}} \\
\midrule
A & 182.5 \\
B & 95.0 \\
C & 1450.25 \\
D & 7 \\
\bottomrule
\end{tabular}要点有两个。第一,把 table-format=整数位数.小数位数 设成该列中最大的数值。不写它、只用 S 也能工作,但列宽预留会变差——同一个例子中列宽从 88.44pt 涨到了 98.44pt。反过来,即使写小了,比如 table-format=2.1,siunitx 3 也 不会报错,对齐同样保持;只是数值会超出预留的空间,容易和相邻列相撞。第二,凡是可能被当成数字的内容,都要用花括号 {…} 保护。siunitx 的说明书写得很明确:如果该素材可能被误认为数字的一部分,就应当用花括号保护。反过来说,像 Sample 这样的普通文本,在现在的 v3 中即使不加花括号也能正确居中。
来量一量「可能被误认」的实例。把表头写成不加花括号的 2024 sales,siunitx 会 把开头的 2024 当作数值 取走,与其他行的数字对齐,再把剩下的 sales 作为后随素材排出。结果 2024 与 sales 之间张开了 7.75bp,而用花括号包成 {2024 sales} 时只有 3.32bp(普通词距),间距明显被拉长。这不会报错——坏掉的只是外观,所以表头里含数字时千万别忘了花括号。在 \multicolumn 或 \multirow 的参数中无法使用 S 列,这时要改用宏形式 \tablenum[table-format=4.2]{1450.25};按说明书的说法,它「实际上就是 S 列的宏版本」。
\num、\qty、\unit 的写法,不确定度(如 \num{1.234(5)} 这类括号记法),指数的处理,以及 v2 与 v3 的差异等 siunitx 的整体内容,归「单位(siunitx)」页面所有。正文中的量用 \qty,表格中的数用 S 列——这样分工,整份文档的数字格式就能保持一致。表格这一侧需要记住的是一条排版惯例:单位写在列标题里,而不是每个单元格里。booktabs 的说明书也把同一条列为准则。
为什么要让 LaTeX 读取 CSV
因为这样一来,数字改动一位也不必再去编辑正文。实验结果和汇总表通常都是以 CSV(逗号分隔的文本) 的形式从电子表格或仪器里导出的。把它们手工抄进 tabular 的单元格既枯燥,又会在每加一行、每改一个值时多一次抄错的机会。把思路反过来——把数据留在数据文件里,让 LaTeX「读取并排版」——那么更新数据后重新编译,表格就会自动跟上。同一份 CSV 无论在正文、幻灯片还是附录里复用多少次,都不会出现转录错误。这正是 LaTeX「把逻辑结构与外观分开」的做法的自然延伸。
本页所有示例都使用下面这个小小的 CSV 文件。第一行是 表头行,列名为 product、price、weight;其余是数据。把它以 data.csv 保存在与 .tex 相同的目录下,后面的代码原样就能编译。
product,price,weight
Apple,380,182.5
Orange,120,95.0
Melon,1280,1450.25用 csvsimple 读取 CSV
载入 \usepackage{csvsimple},只写一行 \csvautotabular{data.csv},整个 CSV 就变成了一个 tabular。第一行会被排成带线的表头,只想先看看内容时这就够了。但它几乎不给你任何排版自由。要自行决定对齐、线条和输出哪些列,就用 \csvreader——它才是这个宏包的主力。有一点值得知道:TeX Live 2024 附带的 v2.6.0 中,只写 \usepackage{csvsimple} 会载入 旧实现 csvsimple-legacy(csvsimple.sty 里写着 \SetKeys{ legacy })。若想使用 LaTeX3 实现,请显式写成 \usepackage[l3]{csvsimple} 或 \usepackage{csvsimple-l3}。
\usepackage{csvsimple}
\usepackage{booktabs}
\usepackage{siunitx}
% NOTE the braces around the column spec: an unbraced S[...] breaks the key list
\csvreader[
tabular = {l r S[table-format=4.2]},
table head = \toprule Product & {Price} & {Weight} \\ \midrule,
table foot = \bottomrule,
late after line = \\]
{data.csv}
{product=\product, price=\price, weight=\weight}
{\product & \price & \weight}\csvreader[选项]{文件}{赋值}{正文} 的四个部分依次负责:要读取的文件、列名到宏的绑定,以及每一行要输出的内容。写下 price=\price 后,正文中的 \price 就会展开为该行的值。外框由选项给出:tabular= 是列指定,table head= 是表头行,table foot= 是结尾,late after line = \\ 表示 在每行末尾追加行终止符 \\(这是避免最后一行后留下多余换行的惯用写法)。当表头名称含有空格或符号时,可以把赋值留空,用 \csvcoli、\csvcolii、\csvcoliii(第 1、2、3 列)按编号引用。
本页唯一真正的「坑」就在这里。上面例子中 tabular 的值 被花括号包住 并不是装饰。如果裸写成 tabular = l r S[table-format=4.2],分隔键值对的逗号会和 S[…] 内部的逗号冲突,编译会以 ! Paragraph ended before \NC@rewrite@S was complete. 中断。随后还会报 ! Missing $ inserted. 和 ! Package csvsimple Error: File ',' not existent…,让人很难想到问题出在列指定上。加上花括号写成 tabular = {l r S[table-format=4.2]} 就能通过。这个习惯的适用范围比 S 更广:只要把带方括号的列指定写进键值列表里,就一定用花括号护住。
默认情况下,第一行会被当作 表头 并从数据中排除。要读取没有表头行的 CSV,可以用带星号的 \csvreader*,这样第一行也会作为数据读入。此外还有按条件筛选行的 filter,以及复用一组赋值的 \csvstyle / \csvnames——这些功能超出表格范畴,可用于一般的「逐行处理」,例如从通讯录生成邮寄标签。
用 pgfplotstable 精细控制数值格式
如果你想设计数字本身的 呈现方式,最强大的选择是 pgfplotstable。它属于 pgfplots,用 \usepackage{pgfplotstable} 载入,核心命令只有一个——\pgfplotstabletypeset[选项]{data.csv}。它会读取 CSV,按指定的精度和数值样式整形,并在内部组装成 tabular 输出。读取 CSV 时必须用 col sep=comma 声明分隔符(默认是空格分隔)。所有控制都通过 键值选项 完成。
| 选项 | 作用 |
|---|---|
col sep=comma | 按 CSV(逗号分隔)读取;默认是空格分隔 |
header=has colnames | 把第 1 行当作列名;header=false 表示没有表头行 |
columns | columns={a,b,…} 选择要输出的列及其顺序 |
columns/NAME/.style | columns/price/.style={…} 只对某个具名列应用格式 |
column name | 替换打印出的表头文字,可与 CSV 中的列名不同 |
fixed | 定点表示;fixed zerofill 补齐末尾零,precision=n 决定小数位数 |
sci | 以科学计数法(指数)排版;sci zerofill 补齐尾数的零 |
string type | 文本列;完全不做数值格式化 |
dec sep align | 按小数点位置对齐该列(需要 array) |
\usepackage{pgfplotstable}
\usepackage{booktabs}
\pgfplotsset{compat=1.18}
\pgfplotstabletypeset[
col sep = comma,
header = has colnames,
columns = {product, price, weight},
columns/product/.style = {string type, column name = Product},
columns/price/.style = {column name = Price, fixed, precision = 0},
columns/weight/.style = {column name = {Weight / g}, fixed, fixed zerofill,
precision = 1, dec sep align},
every head row/.style = {before row = \toprule, after row = \midrule},
every last row/.style = {after row = \bottomrule},
]{data.csv}product 列设为 string type(文本),price 为整数(precision=0),weight 保留一位小数并补零,用 dec sep align 对齐小数点。表头由 column name 替换,线条则通过 every head row 与 every last row 注入 booktabs 的命令。这里 只要改变 precision,同一份数据的位数就会随之改变——无需触碰 CSV 就能控制数字的呈现,正是 pgfplotstable 的看家本领。但也要当心:在这套设置下,1450.25 会被 四舍五入为 1,450.3 输出。舍去位数是一条格式指令,而取整是悄悄进行的,所以请有意识地决定有效数字。(若不需要千位分隔符,可加上 1000 sep={}。)
pgfplotstable 还能从读入的列派生出 计算列:用 create on use 定义「使用时才计算的列」,或在 columns/…/.style 中通过 postproc cell content 对数值做后处理——简言之,可以在 LaTeX 内部完成接近电子表格的工作。功能强大的代价是语法笨重,因此经验法则是:复杂的数值表用 pgfplotstable,单纯的 CSV → 表格用 csvsimple。如果还要用同一份 CSV 绘图,能与 pgfplots 的 \addplot table 共用同一套工具,也是它的优势。
datatool — 把 CSV 当作数据库
第三个宏包 datatool 把 CSV 当作 数据库 读入,擅长逐行处理(类似邮件合并的工作)。载入 \usepackage{datatool} 后,写 \DTLloaddb{名称}{data.csv} 就能把文件读进一个具名数据库。默认第一行是表头,其列名成为每个值的 键。对于没有表头的 CSV,\DTLloaddb[noheader]{…}{…} 会自动把列命名为 Column1、Column2……载入后,用 \DTLforeach{名称}{赋值}{正文} 依次遍历各行。赋值写作「宏 = 列名」,例如 \DTLforeach{db}{\Product=product,\Price=price}{…},正文中 \Product、\Price 就会展开为该行的值。
\usepackage{datatool}
\usepackage{booktabs}
\DTLloaddb{goods}{data.csv}
% the row break goes at the START of the loop body, not the end
\begin{tabular}{l r}
\toprule
Product & Price
\DTLforeach{goods}{\Product=product, \Price=price}{%
\\ \Product & \Price}
\\ \bottomrule
\end{tabular}这里把 CSV 读入为 goods,tabular 中的 \DTLforeach 逐行输出「品名 & 价格」。请注意 行终止符 \\ 位于循环体的开头而不是末尾。若放在末尾,最后一次迭代会开启一个空行,紧随其后的 \bottomrule(或 \hline)就会落进该行的单元格中,编译以 ! Misplaced \noalign. 中断。把它放在开头,并在循环之后补一个 \\,才是稳妥的写法。datatool 的真正强项更多在于 数据操作 而非表格排版:它可以用宏对数值求和、求平均、排序、按条件排除行,也被用来生成参考文献和合并文档。反过来说,如果只是想把 CSV 变成表格,csvsimple 更为简洁。
用 tabularray 的 Q 列对齐数值
载入 \UseTblrLibrary{siunitx} 后,tabularray 就能以 Q[si={table-format=4.2}] 的形式,把与 S 列相同的数值对齐带进 tblr 环境。再加上 \UseTblrLibrary{booktabs},\toprule/\midrule/\bottomrule 也照常可用,而格式则完全以键值形式给出,例如 colspec 或 row{1}={font=\bfseries}。最大的吸引力在于不必重新学习 >{…}、\multirow 之类的语法。不过 tabularray 本身——tblr 的设计思路、width=、单元格合并、rowsep/colsep——归「高级表格环境」页面所有,因此这里只聚焦 数值列。
\usepackage{tabularray}
\UseTblrLibrary{booktabs}
\UseTblrLibrary{siunitx}
\begin{tblr}{colspec = {l r Q[si={table-format=4.2}]}}
\toprule
Product & Price & {{{Weight}}} \\
\midrule
Apple & 380 & 182.5 \\
Orange & 120 & 95.0 \\
Melon & 1280 & 1450.25 \\
\bottomrule
\end{tblr}该用哪一种
按目的来选最快。如果数据只有几十行,而你只想让手写表格里的数字对齐,那么 S 列就够了。如果 CSV 会不断更新,就转向读取类宏包。大致指引如下。
- 只想让小数点对齐 —
siunitx的S列。可直接插入手写的tabular;在\multicolumn内部改用\tablenum。 - 单纯的 CSV → 表格 —
csvsimple。\csvautotabular立等可取,\csvreader则可控制对齐、线条以及输出哪些列。 - 需要精细控制位数与数值表记,或需要计算列 —
pgfplotstable。功能最强,但语法笨重,且取整是悄悄进行的。 - 目的本身就是数据操作(求和、排序、条件处理、合并文档)—
datatool。 - 想用键值语法,或想给已有的
tblr表加一列数值 —tabularray加siunitx库的Q[si={…}]。
无论用哪种方法,最终产出的都是同一套 tabular 词汇——列指定、&、\\ 和线条。如果读取本身没问题,表格却依然难读,原因多半出在线条上:把 \hline 换成 booktabs 的三条线,并把单位移到列标题里,同样的数据就会好读得多。放不下版心的表格见「高级表格环境」,跨页的长表见「跨页表格」,标题与放置见「表格放置与样式」。