【填坑】關於 LaTeX 的中文處理(長文慎入)

看板LaTeX作者 (Ch'enMeng)時間10年前 (2014/03/27 01:14), 編輯推噓13(1300)
留言13則, 13人參與, 最新討論串1/2 (看更多)
首先要對板上的同好們說一聲抱歉。上次回答完問題之後﹐就消失了很長時間。一方面是 課業繁重﹔一方面是 TeX Live 2014 就要出來了﹐手頭幾個宏包急著更新(希望能隨 TeX Live 2014 發布)﹔一方面是手頭在修一本書﹐關於 GRE 考試的填空教程﹐工程 比較浩大﹐不過好在今天完成了它手稿的的四分之三﹔另外是最近自己身體欠佳﹐去了幾 次醫院。 雖然有諸多理由﹐不過還是要對大家說一聲抱歉。真的沒想到一拖就拖了這麼久。 今天來詳細說一說關於 TeX 處理中文字的一些事情﹐會比較長﹐希望大家做好心理準備。 下文中﹐一些術語的說法可能和台灣方面不大一樣﹐盡我所能總結如下﹕ macro 宏 巨集 package 宏包 巨集套件 file 文件 檔案 如果文章中有看不懂的術語﹐歡迎大家回復補充。 ## 關於 TeX 系統的一點基礎知識 板上同好們水平較高﹐所以分不清發行版、宏包、格式、引擎這些概念的應該是少數﹐不 過這篇文章之後也會在大陸的知乎網上發布﹐為了統一﹐所以一並講了吧。 TeX 家族是一個很龐大的概念﹐雖然 TeX 程序自己很小﹐龐大的原因﹐主要是後來人們 不斷地對它的功能進行擴展導致的。 TeX 家族最開始的概念是引擎。Knuth 老爹開發出的 TeX 是用 WEB 語言寫的﹐這是一 個文學編程的語言﹐需要用 Pascal 來編譯──不過現在的 Pascal 編譯器也是不能編 譯它的﹐版本不匹配──比較的繁瑣﹐效率也不夠高。於是有人開發出了 WEB2C 這個小 工具來將 WEB 語言的代碼轉成 C 語言﹐這樣能提高不少效率。 眾所周知﹐TeX 最開始是 Knuth 老爹為了自己的 TAOCP (不知道的請上網爬文)的排 版而創作的﹐所以當 Knuth 老爹自己對 TeX 滿意之後﹐就不樂意修改它了。然而盡管 TeX 是 Knuth 老爹和其他人(他的學生)一起做出來的東西﹐Knuth 老爹卻堅持隻有 自己才能修改名為 TeX 的東西。所以當 TeX 流傳開來之後﹐大家發現 TeX 功能上有 不如人意的地方的時候﹐隻能自己在 TeX 的基礎上做一些增強版。比如﹐著名的 e-TeX, 還有大家熟悉的 pdfTeX, XeTeX, LuaTeX 等。不是所有的擴展版都包含了 T, E, X 這三個字母﹐比如以前有一個引擎叫做 Omega, 它支持 16 種文字順序方向﹐ 十分神奇。每一個 TeX 引擎都有一套自己的原語﹐不過所有的增強版引擎﹐幾乎都兼容 原始的 TeX 的原語──語法一致﹐但功能可能變強大了。 這些增強版﹐包括最原始的 TeX 在內﹐都被稱為**引擎**。引擎是承擔最終排版工作的 可執行程序。 讓我們回到最開始的 TeX 上面來。眾所周知﹐TeX 是一個引擎﹐同時也是一種宏編程語 言的名字。Knuth 老爹的原始版 TeX 一共有三百多個最原始的宏﹐我們把它們叫做 TeX 原語(TeX primitive)。其他所有的宏都是通過他們構建出來的﹐所以從理論上 說﹐隻要掌握了這三百多個原語﹐就能讓 TeX 幫我們解決一切問題了。不過最原始的東 西往往是強大的﹐但同時是不方便使用的。為了方便使用﹐我們就要在這些原語的基礎上 為常用的功能編寫宏。 有一些宏非常常用﹐所以如果每次都載入這些宏會繁瑣且緩慢。所以 Knuth 老爹允許人 們將常用的宏編譯成為格式(format, 擴展名為 fmt)。Knuth 老爹自己設計了一個格 式﹐稱為 plain TeX, 加上原語﹐他一共有 900 多個命令。因為它太基礎﹐用得非常 頻繁﹐所以大多數時候人們講到 TeX 語言的時候﹐指的是 plain TeX 構成的那些指令 集合﹐而不是 TeX primitives. TeX 是面向排版的﹐所以許多命令晦澀難懂──通常寫文章的作者不需要知道這些細節﹐ 於是﹐剛剛獲得圖靈獎的 Leslie Lamport 博士在 plain TeX 的基礎上又編寫了一些 宏﹐並編譯為格式﹐取名為 LaTeX. La 就是他姓氏的頭兩個字母﹐仿照中國古代的叫法 這就是所謂的萊氏 TeX. 所以你看﹐不管是中國人還是外國人﹐都想著光宗耀祖呢。 LaTeX 優秀之處在於﹐它將內容與格式分開﹐使得作者不需要﹐或者隻需要知道很少的排 版細節就能排出漂亮的文章。 除掉 plain TeX 和 LaTeX, 常見的格式可能就隻有 ConTeXt 了。 在格式的基礎上﹐人們發現實現某一些特定的功能的時候﹐使用的宏命令總是一樣的。所 以如果把它們打包起來﹐就會變得很方便。所以出現了宏包(package)這個東西。宏包 總是和格式對應﹐LaTeX 格式的宏包往往不能用於使用 plain TeX 格式書寫的文章之 中。LaTeX 中最常用的幾個宏包可能是﹕amsmath, geometry, hyperref, xcolor, graphicx, booktabs 等。 除掉這些東西﹐人們還開發了一些輔助工具。比如﹐名為 BibTeX 的工具﹐就是用來生 成參考文獻的﹔名為 makeindex 的工具﹐就是用來生成索引的。另外對於引擎、格式、 宏包、工具﹐用戶要使用它必須有一份說明文檔來指引進步。所有這些東西﹐零零總總﹐ 加起來有成千上萬個文件﹐如果讓用戶去一一下載安裝﹐一則不方便﹐二則不方便控制 版本。所以就有人/組織/公司將他們全部打包在一起。比較著名的發行版有 Windows 系統下的 MikTeX, 跨平台的 TeX Live, 為中文做過優化的 CTeX (大陸吳凌雲研究 員)﹐cwTeX (台灣吳聰敏教授)等。 至於 TeXworks, WinEdt, TeXstudio, TeXmaker 這些東西﹐隻是編輯代碼的工具﹐ 和 TeX 系統並沒有直接的關系。 總的來說﹐引擎承擔排版工作﹐對應一套原語﹔格式對應一套書寫規則﹔宏包對應一定 的功能﹔發行版則是所有東西的大雜燴。如果用汽車來做比喻﹐那麼發行版是整個汽車﹐ 引擎是發動機和傳動系統﹐格式是方向盤、剎車、離合器這些控制系統﹐宏包則是音響、 車窗之類的各種功能的小物件。 ## 說說字體 所謂排版﹐就是要把每一個東西擺在合適的區域﹐然後在這個區域上畫出特定的形狀。 理解了這個概念﹐我們就知道﹐不管是排版文字﹐還是排版圖形﹐所做的工作都是一樣 的﹐差別隻在於最終畫什麼。對於 TeX 來說﹐畫出什麼樣的形狀其實是不重要的﹐畢 竟這個形狀是給我們人類看的。所以 TeX 完全可以隻關注什麼區域放什麼﹐至於這些 東西長什麼樣﹐完全可以交給閱讀器去處理。 因此﹐我們可以很容易地想到﹐一個字體應該可以分成兩個部分﹕描述尺寸的和描述形 狀的。前一個部分我們稱之為 metrics, 後一個部分我們稱之為 glyph. 對於 METAFONT 生成的字體﹐確實就是有這兩個部分的﹐前一個部分擴展名是 tfm, 後一 個部分的擴展名是 pk. 不過並不是所有的字體格式都將兩個部分分開﹐比如 OpenType 字體﹐字符的尺寸信息和形狀信息全都保存在一個文件裡面。 在 TeX 原語中﹐調用字體的命令是 \font. 這個命令是後續各種字體調用命令的基礎﹐ 包括 xeCJK 的 \setCJKmainfont 等命令﹐都是在它的基礎上建立起來的。 Knuth 老爹的 TeX 系統﹐限於當時的情況﹐隻支持自家的 METAFONT 生成的 tfm 文件。什麼意思呢﹖(Knuth) TeX 的原語 \font 隻認識 tfm 文件﹐別的什麼 ttf, otf 統統不認識。這無疑是不方便的﹐畢竟現代優秀的字體大都是 ttf 或者 otf 格式。於是 XeTeX 應運而生﹐它的原語 \font 認識 ttf, otf 這些格式﹐所以能夠在 讀取安裝在系統上的這些格式的字體──隻要想辦法讓它能夠找到就行了。另一個支持 ttf, otf 字體的引擎是 LuaTeX, 不過繼承自 pdfTeX 的 LuaTeX, 它的 \font 命令 也不能直接讀取 ttf, otf 格式的字體文件﹐需要使用名為 callback 的庫來間接調用。 這也就是為什麼使用 LuaTeX 處理字體會比 XeTeX 慢的緣故了﹐雖然 LuaTeX 相較 XeTeX 有不少優勢﹐但是讀取字體這一塊﹐確實是比不上。 說完了單個字符﹐我們再來說說字符在字體裡邊的情況。 可以想象﹐字符在字體文件裡邊是按照某種順序排列在一起的。那麼﹐假設你就是 TeX, 你想要獲得 A 這個字符﹐你怎麼找到它在字體文件裡的位置呢﹖沒錯﹐A 的 ASCII 編碼 是 65, 如果字體文件是按照 ASCII 編碼的順序排列的﹐我們隻需要找到第 66 個(從 0 開始)字符就可以了。 遺憾的是﹐事情並沒有這麼簡單。一則編碼有許多種﹐字體裡不一定要使用某一種特定的編 碼﹔二則東亞文字有許許多多﹐各地區使用的編碼也不盡相同﹐必須協調好各部分之間的關 系。於是 LaTeX 規定了名為 NFSS (新字體選擇機制)的方案﹐字體屬性分成五個方面﹕ 編碼(encoding)﹐比如 TeX text, TeX extended text; 字族(family)﹐比如 Roman, Typewriter; 字重(series)﹐比如 bold, narrow, medium; 字形(shape)﹐比如 Italic, Small Caps; 字號(size)﹐比如 10pt, 11pt. 明確了這些﹐你才能精確地說明白你到底需要哪一個字形﹐如何才能找到特定的字符。這些 東西你可能不熟悉﹐但是如果你用過一段時間的 LaTeX, 幾乎能說你肯定見過。LaTeX 的 關於溢出盒子的警告一般是這樣寫的﹕ Overfull \hbox (3.80855pt too wide) in paragraph at lines 314--318 []\OT1/cmr/m/n/10 Normally [] and [] will be iden-ti-cal, 其中的 \OT1/cmr/m/n/10 就表示這些內容。其中 OT1 是 encoding 部分﹐代表 TeX text 這種編碼﹔cmr 是 family 部分﹐代表 Computer Modern Roman 這個字族﹔ m 是 series 部分﹐代表 medium; n 是 shape 部分﹐代表 normal; 10 是 size 部 分﹐默認單位是 pt. 關於這部分內容﹐太繁瑣﹐如果想要詳細了解他們﹐可以閱讀﹕ texdoc fntguide texdoc fontenc 好了﹐現在假設你的 TeX 已經能夠讀到相應的字符數據了﹐也通過特定的算法將字符擺放 在相應的區域了。現在要做的事情﹐是讓閱讀器知道某個字符長什麼樣。 對於原始的 TeX, 它讀取 tfm 文件﹐然後將字體信息寫入 dvi, 讓 dvi 閱讀器檢索 tfm 文件對應的 pk 文件﹐然後把字形取出﹐畫在 dvi 的特定位置。對於 dvips 這個驅動來 說﹐他發現 dvi 文件裡提到需要 cmr 這個字體﹐於是就去到 config.ps 裡面找到一個 map 文件﹐記載著 cmr10 字體對應著 cmr10.pfb (Type1 字體的字形部分), 於是就找 到這個文件備用。 對於 pdfTeX 和 LuaTeX 來說﹐他們能夠直接生成 PDF 文件﹐並定義了特別的原語﹐用來 映射字體文件。比如 \pdfmapline{cmr10 CMR10 <cmr10.pfb} 表示﹐遇到 cmr10.tfm 這個字體尺寸文件之後﹐就將 cmr10.pfb 插入到 PDF 文件之中。 XeTeX 的情況﹐我不大清楚。 至此﹐我們已經知道 TeX 怎麼找到特定的字體尺寸文件﹐以及閱讀器如何找到對應的字體形 狀文件了。LaTeX 的 NFSS 機制特別細致﹐所以顯得有些繁瑣。所以對用戶來說並不推薦使 用﹐但如果要使用新的字體﹐還是得好好讀讀相關的文檔。好在為 XeTeX 和 LuaTeX 設計 的 fontspec 宏包出現了﹐這可是懶人的福音。xeCJK 和 LuaTeX-ja 分別對它的兩個版本 做了一些擴展。 ## 中文的處理 最開始的 TeX 是 7 位的﹐隻能處理 128 個 ASCII 字符﹔後來 Knuth 老爹將它擴展到了 8 位﹐可以處理 ISO8859-1 字符。這樣處理拉丁字符遊刃有余﹐但處理東亞文字就傻眼了。 TeX 默認一個字體文件最多隻有 256 個字符﹐但是動輒上千上萬的中文字體怎麼辦呢﹖有問 題﹐找 Knuth 老爹。老爹說﹐這簡單﹐既然我隻認識 256 個字符﹐你就把成千上萬個字符 分割成若幹塊﹐每一塊都隻有 256 個字符就好了。回頭隻要為不同的字符指定不同的塊就能 解決問題了。這些塊有一個名字﹐叫子字體。好吧﹐前輩們為這個吃盡了苦頭。 我們來看一段 LaTeX 代碼﹕ \documentclass{article} \usepackage{CJK} \begin{document} \begin{CJK*}{GBK}{song} 您好 \clearpage \end{CJK*} \end{document} 這幾乎是使用 CJK 方式支持中文的最簡單的代碼了﹐用過 CJK 的同好應該都不難理解。其中 GBK 是大陸這邊使用的編碼(Big5 不熟悉呀﹐抱歉)﹐song 是宋體(台灣似乎叫做明體﹖)。 我們來看看 TeX 會對它做什麼。之前說到﹐需要為不同的字符指定不同的子字體。這個工作由 CJK(*) 環境來完成。比如說﹐CJK(*) 環境將漢字您轉成了﹕ \C19/song/m/n/10/51 s 是不是有點熟悉﹖這裡 C19 是編碼﹐代表 GBK; song 是字族名字﹔m 是字族﹔n 是字形﹔10 是字號﹐51 指的是子字體的編號。這句話翻譯過來就是﹕ 您 = GBK 編碼﹐song 的字體﹐中等粗細﹐直立﹐10pt 字﹐51 號子字體中的 s 咦﹖s 是什麼﹖唔……這個子字體中的 s 這個字符﹐長得就是漢字“您”這個樣子﹗ 結合之前提到的內容﹐你應該已經清楚 CJK 是如何工作的了。唔﹐至於某個字如何對應到子 字體中的字符﹐這個問題比較復雜﹐總之知道有一個一一對應的關系就好了。 這樣的做法是可行的﹐但無疑是蹩腳的。不說那個惱人的“許蓋功”問題﹐單是排錯時候就夠 你喝一壺。想想看﹐如果漢字您後面出錯了﹐LaTeX 給出的錯誤應該類似﹕ ! <ERROR NAME> l.5 \C19/song/m/n/10/51 s ? 這種錯誤提示完全不可讀﹐因為你不可能知道 \C19/song/m/n/10/51 s 到底是哪一個字。 也就無法定位出錯的位置﹐更別說排錯了。 ## xeCJK 和 luatexja-fontspec 曙光是 XeTeX 和 LuaTeX 帶來的﹐它們支持 UTF-8 編碼﹐許蓋功問題就無影蹤了﹔它們 能夠使用 ttf 和 otf 字體﹐切割子字體、轉換成奇怪的命令這些事情也就不存在了。 下面分別是使用 xeCJK + XeLaTeX 和 luatexja-fontspec + LuaLaTeX 的兩個小例子。 \documentclass{article} \usepackage{xeCJK} \setCJKmainfont{SimSun} % 中易宋體 \newCJKfontfamily[hei]\heiti{SimHei} % 中易黑體 \begin{document} 大家好﹐這裡是中易宋體。 {\bfseries 這裡是中易宋體的粗體字}。 \heiti 可以很容易地切換到中易黑體。 \end{document} \documentclass{article} \usepackage{luatexja-fontspec} \setmainjfont{SimSun} % 中易宋體 \newjfontfamily[hei]\heiti{SimHei} % 中易黑體 \begin{document} 大家好﹐這裡是中易宋體。 編譯可能會卡在下面這個地方很久﹕ \begin{verbatim} luaotfload | db : Scanning TEXMF and OS fonts... \end{verbatim} 這是在刷新字庫﹐如果是在 Windows 系統下編譯﹐耗時可能會特別長﹐ 你可以去泡杯茶。 \heiti 切換到中易黑體一樣容易。 \end{document} 二者的用法其實相似﹐都是保存為 UTF-8 編碼﹐使用 XeLaTeX 或者 LuaLaTeX 編譯 即可﹔語法上有一點差異﹐xeCJK 在 fontspec 的基礎上﹐傾向於在命令中將 CJK 三 個字母添加在前面﹐而 luatexja-fontspec 在 fontspec 的基礎上﹐傾向於將 j 添 加在後面。 luatexja-fontspec 沒有對應的 \newmonojfamily 命令﹐因為流行的日文字體 基本都是等寬的。 luatexja-fontspec 中﹐\newmainjfont 修改的是 \mcfamily, \mewsansjfont 修改的是 \gtfamily, 這與 xeCJK 直接修改 \rmfamily 和 \sffamily 不同。這是因為﹐日系的 pTeX 系列引擎﹐將中文和英文完全分開處理﹐是 兩套機制﹔LuaTeX-ja 項目的目的﹐是在 LuaTeX 引擎上實現 pTeX 的功能﹐所以也 沿用了這個特性。這樣做有好處也有壞處﹐不過好處在 LaTeX 用戶身上基本體現不出來﹐ 不說也罷﹔壞處就是讓人稍稍有點不適應。 二者的說明文檔分別可以通過下面的命令來調取﹕ texdoc xeCJK texdoc luatexja-en 前者是簡體中文文檔﹐後者是英文文檔。 填在字體調用中的字體名﹐可以使用 fc-list 命令來查看──小心﹐列表可能會很長。 ## 標點壓縮和禁則 對於東亞文字排版來說﹐逃不開的一個話題是標點壓縮和禁則的處理。首先簡單介紹一下 他們分別是什麼東西。 以中文為例。中文的標點符號多種多樣﹐情形也有很大差別。我們來看引號和句號的組合。 “一句話的引用”。 “一段話的引用。” 第一種情況﹐引用一句話﹐句號放在引號的外邊﹔第二種情況﹐引用一段話﹐句號放在引號 的裡邊。在第一種情況﹐句號和引號之間的空隙太大﹐不好看﹐需要縮小這個空隙﹕將句號 左邊的間隔減小﹐將引號右邊的間隔減小。在第二種情況﹐引號和句號必須留有一定的空隙 但也必須縮小一定的距離﹐且這個縮小的距離應該比第一種情況要短一些。 再有例子。 一句話。另一句話 一句話﹐另一句話。| 第二種情況的 | 表示到了行尾。對比兩個情況下的句號﹐顯然第二種情況下﹐句號右邊的 空隙應該適當縮小﹐而第一種情況應該保留(作為斷句)。 這兩個都是標點壓縮的例子。簡單來說﹐標點壓縮就是處理標點符號在不同情況下兩端間距 的問題。 禁則就更容易理解了。比如﹕句號不能出現在行首﹐冒號引號連用不能出現在行尾。這些關 於標點符號位置的擺放﹐就是禁則。 這些情況在日語裡也是存在的。比如日語的拗音﹐縮小的假名就不能出現在行首﹐這也是禁 則。 CJK 方式對標點禁則的處理﹐額﹐咱們就不說它了。以前使用 CJK 方式做標點處理﹐都是 使用 CJKpunct 這個輔助宏包來做的。它的作者是張林波(CCT 的作者)和孫文昌(xeCJK 的作者)。 xeCJK 和 LuaTeX-ja 的標點和禁則處理就要優秀得多了。不過他倆還略有差別。xeCJK 的 思路﹐是先讓標點佔滿一個全角的位置﹐然後添加負距離“擠壓”﹔LuaTeX-ja 的思路(實 際上是 pTeX 系的思路)是先讓標點佔據最小的位置(在 metrics 文件裡做)﹐然後“撐 開”一定的距離。二者其實沒有優劣之分。 ## 中文版式解決方案──ctex 宏包/文檔類 中文能夠輸出了﹐你就滿足了嗎﹖其實距離 LaTeX 中文化的終點還有距離。 比如﹕ 1. 段首縮進兩個字符﹐怎麼實現﹖ 2. 符合中文習慣的字號調整﹐如何處理(五號字﹐小五號字)﹖ 3. 圖表目錄的標題﹐怎麼改成中文的﹖ 這些問題﹐在板上同好的使用過程中﹐肯定已經有一些解決方案了。不過﹐一來想必是零碎的﹐ 二來﹐恐怕並不足夠好。 舉例說段首縮進兩個字符的問題。這句話展開來講﹐應該是﹕第一行的第一個字的左側﹐與第二 行第三個字的左側切齊。所以真實情況下﹐首行縮進的長度﹐應該是兩個漢字的寬度﹐加上兩倍 的漢字之間的間距。 諸如此類的一些細節﹐在 ctex 宏包/文檔類中已經處理好了﹐歡迎使用。 一個最簡的例子﹕ \documentclass[UTF8, nofonts]{ctexart} \setCJKmainfont{SimSun} \setCJKsansfont{KaiTi} \setCJKmonofont{FangSong} \begin{document} 這裡是中文。 將文件保存為 UTF-8 編碼﹐使用 XeLaTeX 編譯。 \end{document} 如果使用的是簡體中文的 Windows 系統﹐例子還能更簡單﹕ \documentclass[UTF8]{ctexart} \begin{document} 這裡是中文。 將文件保存為 UTF-8 編碼﹐使用 XeLaTeX 編譯。 \end{document} 幾乎與 LaTeX 標準文檔類沒有使用上的差別。 ## 跋 跋就不寫了﹐九千多個字大家估計已經看疲勞了。 好困…… 去睡覺了。 MC 2014-3-27 -- 來自萌氣四溢的 M 君~ -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 183.216.61.143 ※ 文章網址: http://www.ptt.cc/bbs/LaTeX/M.1395854055.A.65F.html

03/27 01:17, , 1F
不推對不起自己!
03/27 01:17, 1F

03/27 02:26, , 2F
Great! Thank you!
03/27 02:26, 2F

03/27 03:16, , 3F
雖然我不寫中文paper還是要給大推!
03/27 03:16, 3F

03/27 08:12, , 4F
感謝原PO的用心
03/27 08:12, 4F

03/27 11:34, , 5F
推!!!!!
03/27 11:34, 5F

03/27 14:43, , 6F
推!
03/27 14:43, 6F

03/27 20:45, , 7F
謝謝,我收集起來了。
03/27 20:45, 7F

03/28 11:53, , 8F
不推對不起自己!
03/28 11:53, 8F

03/29 22:52, , 9F
毫無疑問的優文
03/29 22:52, 9F

03/30 11:31, , 10F
03/30 11:31, 10F

03/30 14:14, , 11F
厲害
03/30 14:14, 11F

04/09 19:27, , 12F
謝謝
04/09 19:27, 12F

06/07 23:44, , 13F
06/07 23:44, 13F
文章代碼(AID): #1JCmhdPV (LaTeX)
文章代碼(AID): #1JCmhdPV (LaTeX)