由数据生成表格

把一列数字用 r(右对齐)排出来,是 LaTeX 表格里最常见的错误。把 182.595.01450.257 右对齐排列,小数点会 正好错开一个数字的宽度(5pt),而单独的 7 会落在十分位而不是个位上。这是可以量出来的。修正只需改列指定中的一个字符——换成 siunitxS,小数点的位置就能对齐到 0.001pt 以内。本页讲这种数值对齐,以及在编译时读取 .csv 生成表格的 csvsimplepgfplotstabledatatool,还有 tabularray 的现代做法。贯穿始终的原则是:把数据留在数据文件里,让 LaTeX 去读。

在 LaTeX 表格中对齐小数点——siunitx 的 S 列

只需载入 \usepackage{siunitx},把列指定中的 r 换成 SS 列会解析数值并 按小数点(decimal marker)对齐,于是位数不同的数字排在一起也依然易读。把 PDF 中字形的坐标读出来对比,差别非常明显。在 r 列中,182.595.0 的小数点位于 x = 62.0,而 1450.25 的小数点位于 x = 57.0——相差约 5pt,正好一个数字的宽度;而且 7 被放在十分位而不是个位上。把同样的数据用 S[table-format=4.2] 排版,四个数的个位全都终止于 x = 122.11,小数点也全部从同一位置开始,偏差不到 0.001pt。

latex
\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 作为后随素材排出。结果 2024sales 之间张开了 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 文件。第一行是 表头行,列名为 productpriceweight;其余是数据。把它以 data.csv 保存在与 .tex 相同的目录下,后面的代码原样就能编译。

data.csv
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-legacycsvsimple.sty 里写着 \SetKeys{ legacy })。若想使用 LaTeX3 实现,请显式写成 \usepackage[l3]{csvsimple}\usepackage{csvsimple-l3}

latex
\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 表示没有表头行
columnscolumns={a,b,…} 选择要输出的列及其顺序
columns/NAME/.stylecolumns/price/.style={…} 只对某个具名列应用格式
column name替换打印出的表头文字,可与 CSV 中的列名不同
fixed定点表示;fixed zerofill 补齐末尾零,precision=n 决定小数位数
sci以科学计数法(指数)排版;sci zerofill 补齐尾数的零
string type文本列;完全不做数值格式化
dec sep align按小数点位置对齐该列(需要 array
latex
\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 rowevery 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]{…}{…} 会自动把列命名为 Column1Column2……载入后,用 \DTLforeach{名称}{赋值}{正文} 依次遍历各行。赋值写作「宏 = 列名」,例如 \DTLforeach{db}{\Product=product,\Price=price}{…},正文中 \Product\Price 就会展开为该行的值。

latex
\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 读入为 goodstabular 中的 \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 也照常可用,而格式则完全以键值形式给出,例如 colspecrow{1}={font=\bfseries}。最大的吸引力在于不必重新学习 >{…}\multirow 之类的语法。不过 tabularray 本身——tblr 的设计思路、width=、单元格合并、rowsepcolsep——归「高级表格环境」页面所有,因此这里只聚焦 数值列

latex
\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 会不断更新,就转向读取类宏包。大致指引如下。

  • 只想让小数点对齐siunitxS 列。可直接插入手写的 tabular;在 \multicolumn 内部改用 \tablenum
  • 单纯的 CSV → 表格csvsimple\csvautotabular 立等可取,\csvreader 则可控制对齐、线条以及输出哪些列。
  • 需要精细控制位数与数值表记,或需要计算列pgfplotstable。功能最强,但语法笨重,且取整是悄悄进行的。
  • 目的本身就是数据操作(求和、排序、条件处理、合并文档)— datatool
  • 想用键值语法,或想给已有的 tblr 表加一列数值tabularraysiunitx 库的 Q[si={…}]

无论用哪种方法,最终产出的都是同一套 tabular 词汇——列指定、&\\ 和线条。如果读取本身没问题,表格却依然难读,原因多半出在线条上:把 \hline 换成 booktabs 的三条线,并把单位移到列标题里,同样的数据就会好读得多。放不下版心的表格见「高级表格环境」,跨页的长表见「跨页表格」,标题与放置见「表格放置与样式」。