使用sed为书籍制作索引 (1997)
摘要
一篇1997年的教程,解释了如何使用sed和sort从原始术语-页码列表中为书籍创建格式化索引。
暂无内容
查看缓存全文
缓存时间: 2026/07/27 01:43
来源:https://www.pement.org/sed/make_indexes.txt
(2001年11月12日 – 对早期版本的一些小错别字做了修正。)
—————– 转发的消息 —————–
日期:1997年3月15日(星期六)03:10:29 -0600
发件人:Eric Pement
收件人:Al Aab
主题:使用 sed 制作书籍索引(长文)
主题:使用 sed 制作书籍索引(长文)
我从事图书和杂志出版工作,不久前,在排版后需要为一本书制作索引。在校样页(纸质)上,我们用黄色记号笔标出要索引的术语,然后由几位志愿者在电脑上输入这些术语及其页码,并用分号分隔。每个术语单独占一行。初始输入文件大致如下:
Buddhism, Zen; 1
atheism; 1
dualism; 1
Solomon; 2
Lausanne Covenant; 4
Lewis, C.S.; 4
Lausanne Covenant; 5
Mormonism; 6
Latter-day Saints; 6
Trinity; 6
Lausanne, Switzerland; 8
Trinity; 8
.
.
.
请注意,这些数据是按我们完成每页或每章的顺序输入的。
接下来,我们使用排序工具对文件进行排序:要求不区分大小写并识别数字大小(即数字“3”应排在“19”之前;在普通 ASCII 排序中,“19”会排在“3”之前)。要同时满足这两个条件非常困难,即使使用 GNU sort 程序也是如此(GNU sort 的手册页对这些选项的解释并不清楚)。正确的语法是:
sort -t";" +0f -1 +1n input.file
简要说明一下这些选项:`-t";"` 将字段分隔符设为分号。字段从零(0)开始编号,而不是从一开始。因此,`+0f -1` 表示第一个排序键从字段 0(对于普通人来说,就是第一个字段)开始,在到达字段 1 之前结束,并且不区分大小写(“f”代表 folded)。`+1n` 表示下一个排序键从字段 1(第二个字段)开始,由于后面没有跟随 `-NUM` 值,因此将持续到行尾。“n”表示该字段将按数字值排序(甚至包括小数点),而不是按 ASCII 顺序排序。如果你使用其他排序工具,命令语法可能会有所不同。
排序后的文件条目现在看起来像这样:
Adam; 13
Adam; 21
Adam; 30-32
agnosticism; 9
agnosticism; 120
atheism; 1
atheism; 9
atheism; 40-41
atheism; 118
Bible; 3
Bible; 11-14
Bible; 22
我们想要将上述数据转换成如下格式,以便打印:
Adam, 13, 21, 30-32
agnosticism, 9, 120
atheism, 1, 9, 40-41, 118
Bible, 3, 11-14, 22
.
.
.
四年前,我用一个 awk 脚本来执行这个转换,但现在我意识到 sed 脚本可以更简单地完成这项工作,而且代码行数更少。我想出的用于执行此转换的 sed 脚本(最初)看起来像这样:
```
# INDEXER.SED v1.0 - indexes sorted input file
# Annotated for seders mailing list
{
# on every line of the file...
:loop
$! N; # if not the last line, get the Next line
s/^\([^;]*;\) \(.*\)\n\1 \(.*\)/\1 \2, \3/
t loop; # if previous substitution occurred, goto :loop
s/;/,/; # replace the semicolon with a comma
P; # print first line of pattern buffer
D; # delete 1st line of buffer & redo the loop
}
```
这个脚本能工作!嗯,某种程度上是的。只要输入文件的格式完全正确,脚本就能正常工作。但我发现,如果文件**任何**一行有错误,脚本就会导致之后的所有行都无法正确转换。考虑以下两组输入文件(这里为了解释而非常简短):
===集合1====== ===集合2======
Adam; 13 Adam; 13
Adam; 21 Adam; 21
Adam; 30-32 Adam; 30-32
agnosticism; 9 agnosticism; 9
agnosticism; 120 agnosticism; 120
atheism; 9 atheism, 9 # 这一行在集合2中不同
atheism; 40-41 atheism; 40-41
atheism; 118 atheism; 118
Bible; 3 Bible; 3
Bible; 11-14 Bible; 11-14
Bible; 22 Bible; 22
binitarian; 82 binitarian; 82
现在,比较一下“sed -f INDEXER.SED set1 set2”产生的输出:
Adam, 13, 21, 30-32 Adam, 13, 21, 30-32
agnosticism, 9, 120 agnosticism, 9, 120
atheism, 9, 40-41, 118 atheism, 9
Bible, 3, 11-14, 22 atheism, 40-41
binitarian, 82 atheism, 118
Bible, 3
Bible, 11-14
Bible, 22
binitarian, 82
正如你所看到的,一行中缺少了一个必需的分号(;)会导致整个脚本的其余部分出错。一开始,这似乎不太明显,但让我们更仔细地看看脚本:
1 {
2 :loop
3 $! N
4 s/^\([^;]*;\) \(.*\)\n\1 \(.*\)/\1 \2, \3/
5 t loop
6 s/;/,/
7 P
8 D
9 }
看第4行的搜索模式:`/^\([^;]*;\) \(.*\)\n\1 \(.*\)/`
它查找行的开头“^”,后跟一个或多个以分号结尾的单词“\([^;]*;\)”,然后是一个换行符,再后面是同一组单词及其分号。`\(...\)` 语法表示,匹配组内的表达式可以在表达式内部通过 `\NUM` 重复使用,编号从1开始。
我们真正做的,是检查下一行是否以与前一行相同的单词(或单词组)开头。如果是,搜索表达式就会匹配。替换模式 `/ \1 \2, \3/` 位于第4行的后半部分。因此,如果模式缓冲区中有两行,且两行都以相同的单词开头,则替换操作将删除换行符“\n”和第二行上的那一组单词。最终结果是,第二行的页码被追加到第一行的末尾,而第二行被丢弃。
当脚本遇到缺少分号的那一行时,它会位于模式的“后半部分”,即出现在换行符(\n)之后。由于模式不匹配,不会进行替换,第5行的分支命令也会被跳过。
在 sed 中,`t label_name` 意味着前一行出现了一个替换操作 `s/oldpattern/newpattern/`。如果替换成功(即执行了),则脚本分支到“t”后面指定的标签。每当脚本到达第6行时,模式缓冲区中必定有两行。第一行包含一个索引词,后跟分号,然后可能是一长串页码。第二行应该包含一个**不同的**索引词,后跟分号和一个页码。
第6行的命令 `s/;/,/` 替换模式缓冲区中的第一个分号,通常是在第一行上。第7行和第8行只将第一行打印到控制台,然后只从模式缓冲区中删除第一行。第二行保留在模式缓冲区中,并从头重复循环。
然而,一个错误行(没有分号的行)会被困在模式缓冲区中。最终,它会到达模式缓冲区的顶部,后面跟着一个正确行(有分号)。搜索表达式不会匹配,程序将再次流向第6行。但是,由于模式缓冲区中的第一行没有分号,`s/;/,/` 命令将提前删除模式缓冲区中第二行的分号!之后每一行都会被删除分号,使得第2-5行的循环部分无法正确判断!因此,文件其余的所有行都将被跳过。
我在这个练习上花费的时间多得我不想承认,尤其我的书是四年前印的。这个问题怎么解决?有两种方法。
第一种修复方法是通过在脚本顶部添加如下内容来检查错误的输入数据:
```
# INDEXER.SED, v1.1a - indexes sorted input file
/;/! {
# get lines without a ';'
i\
******************************\
ERROR - Each line of the input \
file MUST have a semicolon! \
******************************\
^G
Offending line occurs at this line number:
= # print line number & line
q # quit this script
}
{
:loop
. . . . # rest of script continues as before
}
```
这会处理输入文件,但如果遇到任何缺少所需分号的行,它会在屏幕上输出一条错误消息,指示错误行的行号(这是“=”指令的一个好用途),然后完全退出脚本(“q”)。顺便说一句,如果你想让 sed 响铃(你的电脑发声)以提醒错误情况,可以在脚本中嵌入一个真正的 Ctrl-G(十六进制 07),sed 将使电脑短暂蜂鸣。在此消息中,我在脚本中使用了两个字符的组合(脱字符和大写字母 G),但你必须在脚本中嵌入真正的 Control-G 才能让你的电脑发出蜂鸣。如果你真的很谨慎,你也可以检查带有两个分号的行 `/;.*;/`,因为那也会损坏输出文件。
第二种“修复”方法是按原样输出错误行,并修改脚本第6行的替换命令,使其只替换换行符**之前**的分号。因此,不用:
`s/;/,/` # 替换可以在换行符 \n 之后发生
我们可以这样写:
`s/^\(.[A-Za-z"''{}() .,/?\-]*\);/\1,/` # 必须在第一行
这个表达式更复杂,但它不会像另一种修复方法那样停止脚本。它的缺点是会输出“错误”的行,而不会将它们纳入到连接页码的流程中。
就个人而言,我会选择第一种解决方案,即遇到不正确的输入行时停止文件。因此,对我来说,使用 sed 的正式解决方案看起来像这样:
```
#---INDEXER.SED v1.2, by Eric Pement-----
# Sed script to alter files with lines with this input format:
# Christ, as "firstborn"; 22
# Christ, as "firstborn"; 155
# Christ, as "firstborn"; 194
# into one which replaces the semicolon with a comma, combining the
# page numbers into one line, like so:
# Christ, as "firstborn", 22, 155, 194
#
# It is essential that the input file be sorted prior to running this
# script, and each line of the input file contain only 1 semicolon.
# GNU sort syntax:
# sort -t";" +0f -1 +1n input.file > input.sort
#
# SYNTAX: sed -f INDEXER.SED input.sort > output.file
#
# The following command causes abort at lines missing a semicolon:
/;/! {
i\
******************************\
ERROR - Each line of the input \
file MUST have a semicolon! \
******************************\
^G
Offending line occurs at this line number:
=
q
}
# Following command causes abort at lines with 2 semicolons:
/;.*;/ {
i\
******************************\
ERROR - There may be only ONE \
semicolon on each line! \
******************************\
^G
Offending line occurs at this line number:
=
q
}
# Main body of sed script follows:
{
:loop
$! N
s/^\([^;]*;\) \(.*\)\n\1 \(.*\)/\1 \2, \3/
t loop
s/;/,/
P
D
}
#-------------------------END of SCRIPT---------------------------------
```
尽管我对此已经说了很多,但我希望这能成为一个有益的练习,让你看到如何用 sed 来辅助制作书籍索引。如果你对此有任何疑问或建议,请随时通过电子邮件联系我。
致以诚挚的问候,
Eric Pement
-- Eric Pement
高级编辑,Cornerstone 杂志
939 W. Wilson Ave., Chicago, IL 60640-5706
电话:773/561-2450,分机 2084
传真:773/989-2076
相似文章
我如何以简约方式打造我的书籍
Chris Kiehl 描述了他使用 Obsidian 和 Word 等基础工具编写技术书籍的简单、低工程化过程,并将其与更复杂的方法进行了对比。
对370,103个单词进行排序、哈希和草图计算
一篇技术博客文章,探索在包含370,103个英文单词的数据集上的排序、哈希和草图算法,衡量时间和内存成本,重点关注二分查找、快速排序和HyperLogLog等实际实现。
在线超文本风格指南(1992年)
这是一份1992年的历史风格指南,讨论了在早期网络环境中创建超文本内容的最佳实践。
构建了一个返回页面标签(而不仅仅是Markdown)的Fetch API
作者介绍了一个用于RAG和网页摄入的Fetch API,该API返回页面标签(死链接、内容类别、页面结构),以帮助在索引前过滤低价值页面。他们寻求关于哪些额外字段会有用的反馈。
抛弃Zotero改用纯文本文件
作者详细描述了从Zotero及其他参考文献管理工具转向单一BibTeX文本文件的过程,强调了简单性、基于grep的导航以及两年来的成功使用经历。