简介:这是一份面向电子书整理、文本批量处理与编码转换场景的轻量工具资源,适合经常阅读 TXT 小说、制作仿真电子书或管理 TCR 格式文档的读者与编辑,也适合需要在多个文档间完成合并、拆分、替换等预处理工作的办公用户。它集成文件合并、TXT 段落合并与分行、HTML 转 TXT、HTML 代码整理、文本替换、文件切分、文本提取、正则表达式匹配以及 TCR 批量压缩/解压等功能,并支持 GB/GBK/Big5/Shift-JIS/Unicode 等常见编码在 Windows 2K/XP 环境下相互转换。压缩包内共 2 个文件,包含一个可直接运行的 exe 主程序和一个 htm 帮助说明文档,整体仅 245KB,无需安装即可使用。目前已有 178 人浏览学习,借助该工具可针对大量 TXT 或 HTML 文件执行自动化批量操作,利用正则表达式精准提取关键章节,也能统一处理不同编码的文档,减少重复劳动。整体短小实用,适合各类文本处理需求的初学者快速上手。
1. TextForever 是什么:这个老牌文本工具凭什么解决 TXT 合并的麻烦
你手上有一套分卷下载的 TXT 小说,或者从后台导出的几十个 CSV 转存文本,想拼成一个干净的大文件,打开 TextForever 之前得先想明白一件事:文件合并不是「把文件凑在一起」,而是「把编码、换行、段落规则统一成一个标准」。TextForever 是个老牌的 Windows 文本整理工具,开源免费,最常见的用途就是 TXT 文件合并和段落合并——前者解决几十个文件拼成一个,后者解决一个文件里几百行短行拼成通顺段落。它界面朴素,功能却比记事本和 Excel 扎实得多,处理这类脏活很少掉链子。适合分卷小说整理、语料清洗、报告文本汇总这类需求,也适合不想为一个合并动作专门去写脚本的人。
2. 把多个 TXT 合并成一个文件:文件合并的操作路径与编码处理
文件合并看起来是「全选 → 合成」两步,实际上涉及文件读取顺序、编码识别、输出编码三个环节。TextForever 的合并原理并不神秘:把每个文件按行读进来,转成内部统一的 Unicode 表示,再按你指定的目标编码写出去。我一般会把合并拆成三个动作来理解——读入、编码归一、写出。读入阶段的坑在于有的文件是 GBK 有的是 UTF-8,不归一直接拼,出来的就是一半中文一半乱码;归一阶段决定输出编码是 UTF-8 还是 ANSI;写出阶段才决定要不要在文件之间插空行、要不要保留原文件末尾的换行。这三个环节里,顺序搞错、编码选错、换行没处理,是绝大多数合并翻车的根源。
2.1 添加文件、排序与合并:按钮背后的处理顺序
TextForever 合并操作的常见路径是:把文件逐个添加到左侧列表,确认顺序,点合并,选择输出文件名。界面不复杂,真正影响结果的不是按钮本身,而是文件列表里的顺序。合并是严格按照列表顺序逐文件追加的,不是按照文件在磁盘上的创建时间或名称来排。你看到列表里第 10 卷排在第 2 卷前面,合出来的文件就会从第 1 卷跳到第 10 卷再跳回第 2 卷,整本书章节直接错乱。
这里有个 Windows 用户很容易忽视的差异:资源管理器用的是自然排序,同一个目录里「第 2 章.txt」和「第 10 章.txt」,资源管理器会把 2 排在 10 前面;但很多文本工具的文件列表用的是字典序,也就是逐字符比较,结果第 10 章反而排到第 2 章前面。下表是这个差异的直观对比:
| 文件名 | 资源管理器自然排序 | 字典序排序 |
|---|---|---|
| 第1章.txt | 1 | 1 |
| 第2章.txt | 2 | 10 |
| 第10章.txt | 3 | 2 |
| 第11章.txt | 4 | 3 |
所以合并分卷文件之前,我要做的第一件事不是点合并,而是核对列表里的实际顺序。TextForever 这类工具一般支持在列表内上下移动条目,顺序不对就直接拖拽调整;如果文件数量太大,更省事的做法是先把文件名统一补零成 01、02、10 这种格式,字典序和自然序就一致了。
合并的内部动作还有一个容易忽略的细节:最后一个文件末尾如果没有换行符,下一个文件的第一行就会直接贴上来,形成「上一章结尾和下一章标题挤在同一行」的怪象。TextForever 一般有控制选项,我习惯勾选「文件间插入换行」。这相当于在每个文件交界处显式补一个换行符,从根上避免粘连。合并完成后先别急着关,直接看合并文件开头、第一个文件交界处和文件末尾三个位置,再决定要不要保留原文件。
2.2 编码识别与统一:合并前先解决 GBK 和 UTF-8 打架的问题
TXT 文件最常见的编码有四种:ANSI(在中文 Windows 下即 GBK)、UTF-8 无 BOM、UTF-8 带 BOM、UTF-16(少见但存在)。「带 BOM」的意思是文件开头有三个字节 EF BB BF,用来标记自己是 UTF-8;记事本打开看不到,但把这个文件和别的文件拼在一起时,三个字节会原样保留下来,合并结果的开头或交界处就会多出一个不可见字符,严重时显示成「锟斤拷」或空方框。
TextForever 处理编码有两种常见策略:自动识别和手动指定。自动识别的原理是先看文件头有没有 BOM 标记,有就直接判定;没 BOM 就尝试按 UTF-8 解码,解不开就退回到 GBK。这个策略对大多数文件是准的,但碰到短文件、全是字母数字的文件、或者恰好是 GBK 编码的 UTF-8 兼容文本时,识别结果就成了玄学。我合并之前不会把识别交给黑匣子,而是先跑一个检查脚本,把这个目录下每个 TXT 的编码和末尾换行情况列出来:
import glob from pathlib import Path def sniff_head(path, n=4096): with open(path, 'rb') as f: return f.read(n) def guess_encoding(raw): if raw.startswith(b'\xef\xbb\xbf'): return 'utf-8-sig' if raw.startswith(b'\xff\xfe'): return 'utf-16-le' if raw.startswith(b'\xfe\xff'): return 'utf-16-be' try: raw.decode('utf-8') return 'utf-8' except UnicodeDecodeError: return 'gbk' for p in glob.glob('*.txt'): raw = sniff_head(p) enc = guess_encoding(raw) has_eol = raw.rstrip().endswith(b'\n') print(f'{Path(p).name}: {enc}, 末尾换行={has_eol}')这个脚本的逻辑分两层:先看 BOM 魔数,BOM 是文件自己声明的身份,比解码推断更可靠;再看能否严格按 UTF-8 解码,能解就是 UTF-8,不能解就按 GBK 处理。参数里 n=4096 表示只看文件头 4KB,对编码判断足够,速度也快;末尾换行检查用于决定合并时要不要勾「文件间插入换行」。跑完你会发现一个目录里的文件编码经常是混的——这种情况就别在合并选项里纠结了,先把所有文件统一转成 UTF-8 无 BOM,再谈合并。
顺带说一句,输出编码的选择:我永远优先选 UTF-8。GBK 是老编码,跨平台时经常出乱码,而 UTF-8 在文本编辑器、Python、手机阅读器里都是默认兼容的。如果合并后的文件要给别人在 Windows 记事本里打开,选 UTF-8 带 BOM 会更稳,但代价是每个文件交界处的 BOM 要额外处理。二选一的话,UTF-8 无 BOM 加文件间换行,是最稳的组合。
2.3 分卷小说与 CSV 批量合并:两种常见场景的参数设置
分卷小说是 TextForever 最典型的应用场景。很多人从番茄小说这类阅读器导出分卷文本,或者整理《剑来》这种几百章的超长连载,一套下来几十个文件。这类文件的特征:每个分卷 1-2MB,内部章节标题格式统一,正文段落有缩进,但卷与卷之间可能存在重复的封面信息或分卷标号。合并参数我会这样设:文件间插入一个空行,输出编码 UTF-8 无 BOM,文件交界处不保留原分卷的空白页。插入空行的目的是让卷与卷之间有一个明显的分隔边界,后续按章节切分时不容易把上一卷的尾巴和下一卷的头部粘在一起。
合并完还有一个常见问题:分卷文件里往往自带「本卷完」「下一卷预告」这类内容,如果这些杂质在卷首或卷尾,合并后就会混进正文。这类行靠合并工具本身删不干净,需要在合并前先用「行处理」功能把包含特定关键词的行挑出来,或者合并后用搜索定位再手动清理。我的习惯是:合并不动内容,只动结构;杂质清理放到合并之后的统一处理阶段。
另一个高频场景是把多个 CSV 格式的文件合并在一起。CSV 本质是纯文本,合并动作本身没问题,麻烦在表头和编码。大多数业务系统导出的 CSV 是 GBK 编码,用记事本打开正常,但和 UTF-8 文件合并后必乱。另一个坑是表头:多个 CSV 每个文件都带一行列名,合并完整个文件每隔几百行就重复出现一次表头,直接没法导入数据库。正确做法是保留第一个文件的表头,跳过其余文件的表头行。TextForever 没有专门的 CSV 模式,但可以先做一步行处理:把「不需要表头」的文件逐一删掉第一行,再执行合并。文件多的时候,我倾向直接全量合并后用脚本清理重复表头,这一步也没什么难度:
import csv, glob header = None with open('merged.csv', 'w', encoding='utf-8-sig', newline='') as out: w = csv.writer(out) for name in glob.glob('part_*.csv'): with open(name, encoding='gbk', errors='replace') as f: rows = csv.reader(f) for i, row in enumerate(rows): if i == 0: if header is None: header = row w.writerow(row) continue w.writerow(row)这段逻辑的核心是一个状态变量 header:第一个文件的第一行被当作真正的表头写入,之后所有文件的第一行统统跳过,其余行原样写入。参数里 encoding='gbk' 是承接这批文件的导出编码,如果文件是 UTF-8 就改成 utf-8-sig;errors='replace' 是遇到无法解码的坏字节时用替换符顶替,避免单个坏字符导致整个合并中断。注意输出用的是 utf-8-sig 而不是 utf-8,这是为了让 Excel 直接双击打开时能正确识别 UTF-8,不会出现中文列名乱码。
3. TXT 段落合并:把「一行一短句」恢复成通顺的段落文本
段落合并是 TextForever 另一个核心功能,解决的是完全不同的痛点:单个文件里内容没乱,但排版乱了——每行只有几个字,一段话被硬拆成十几行,或者整篇文本堆成一大坨没有任何分段。这个需求主要来自三种文本:网页复制出来的文章、OCR 识别结果、字幕或聊天记录导出。它们的共同点是「行」和「段落」不是一回事,需要按语义把行重新拼起来。TextForever 的段落合并做的就是这件事:把连续若干行按规则拼成一个段落,同时保留真正的段落边界。
3.1 段落是怎么定义的:空行、缩进、硬回车与逻辑段
要理解段落合并,先分清两种换行。第一种是「硬回车」,它是真正的段落边界,一个段落结束、另一个段落开始的地方;第二种是「软换行」,它只是排版层面的断行,同一句话因为行宽被拆成了两行,语义上还是一个段落。段落合并的核心动作,就是把软换行替换成空格(或不加任何字符),而把硬回车原样保留。
问题来了:软件怎么知道一个换行是硬的还是软的?TextForever 这类工具的判断依据通常是行特征。中文文本里,一个段落内的续行往往以两个全角空格开头,而段落的首行才会有缩进;段落结尾的一行,通常以句号、问号、感叹号、引号收尾;章节标题行则既没有行首缩进,也没有句尾标点,需要单独保护。这几个特征组合起来,基本能覆盖绝大多数网页复制文本的识别需求。
行的特征与实际含义的对应关系大致如下:
| 行的特征 | 大概率是 | 段落合并时的建议 |
|---|---|---|
| 以全角空格或制表符开头 | 段内续行 | 直接并入当前段 |
| 以句号、问号、叹号、引号结尾 | 段落结束行 | 合并时在此换段 |
| 行首有「第…章/节/卷/回」 | 章节标题 | 合并前单独加空行保护 |
| 整行为空 | 段落间分隔 | 保留作为段边界 |
| 只有几个字且无标点 | 续行或标题 | 结合上下文判断 |
实际操作里没有哪个规则是百分百准的,所以我一般把段落合并拆成两步:先做「空行规整」,再做「行合并」。空行规整的意思是,连续多个空行压缩成一个,让段落边界变得唯一且可预测;行合并则把所有非空行按上述特征拼起来。这样即使个别行的判断错了,也不会把两段内容彻底粘死,边界还在,手动修复的成本很低。
3.2 段落合并的四个参数:分隔符、空行策略、行宽与处理范围
段落合并界面的参数不算多,但每个参数都对结果有直接影响。我把最常调的四个列出来,按影响程度排序。
| 参数 | 可选值 | 中文场景建议 | 英文场景建议 |
|---|---|---|---|
| 合并分隔符 | 无 / 半角空格 / 全角空格 / 制表符 | 无或全角空格 | 半角空格 |
| 空行策略 | 保留空行 / 删除空行 / 空行作为段边界 | 保留空行 | 保留空行 |
| 行宽折行 | 不折行 / 按字数折行 | 60-80 字折行 | 80 字符折行 |
| 处理范围 | 选中区域 / 整个文件 | 先做选区测试 | 先做选区测试 |
合并分隔符是第一个要定的参数。中文文本合并时,行与行之间加不加空格要看原文本:如果原文是网页复制出来的,行尾断在「的」「了」这种虚词后面,不加空格直接拼接最自然;如果原文是英文或中英混排,行尾可能直接断开一个单词,这时必须用半角空格分隔,否则单字会被硬拼在一起。全角空格和制表符一般用于特殊排版需求,比如保留原文的行首缩进结构,日常处理基本用不到。
空行策略关系到的不是行,而是段。TextForever 的常见选项是「保留空行」「删除空行」「把空行当作段落边界」。网页复制文本最常见的形态是:段落之间有空行,段内行之间没有空行,这时候选「保留空行」,空行就是天然的段落边界,段内行会被自动合并。反过来,如果文本里空行很多且无规律,选「删除空行」再做行合并,可能把两段言论粘成一段,所以删除空行这个选项我基本不用。
行宽折行是在段落合并完成后对长段落做二次切割,切到指定宽度好让手机阅读器或后续处理工具能正常显示。这个参数容易让人误解:它不是把「长行」变「短行」,而是把合并出来的超长段落按字数重新切行。中文场景建议 60-80 字,这个宽度在手机和 PC 上显示都比较舒适;如果文本后续要进脚本处理,建议不折行,让脚本自己处理换行。
处理范围决定了这次段落合并对哪些行生效。我强烈建议第一次操作时只选一小段文本做测试,确认合并效果符合预期,再扩大到整个文件。段落合并是不可逆的——它把多行并成一行,行结构被破坏,没有后悔药。就算 TextForever 界面里可能有撤销,几十万行的文件撤销一次也够你等的。测试选区这个习惯能省掉大量返工时间。
3.3 从网页复制的文章乱成一坨:段落合并的实战顺序
网页复制过来的文本有个典型形态:每行十几个字,行尾有时有标点有时没有,行首有时有缩进有时没有,段落之间有零到多个空行。这种文本直接点段落合并,出来的结果往往是一半段落正确、一半段落粘死,原因不是工具不行,而是处理顺序不对。正确的顺序是这样的:
第一步,把文本粘贴进 TextForever,先关掉编辑区的自动换行显示。这一步很关键,因为启用自动换行时,你看到的「行」是显示层折行,不是真实行,段落合并按真实行操作,两者不一致会让你误判。
第二步,压缩空行。把连续多个空行统一成最多一个空行,这一步用替换功能即可,把「两个以上换行」替换成「一个换行」。经过这一步,段落边界就变成唯一的:有且仅有一个空行的地方就是段与段的交界。
第三步,去掉每行行首的全角空格和制表符。网页复制文本的行首经常带着缩进,保留缩进去做合并,缩进字符会被拼到段落中间,变成段落里突兀的空格。用正则替换,匹配行首的空白字符,替换为空即可。
第四步,执行段落合并,分隔符选「无」,空行策略选「保留空行」。
第五步,检查合并结果。重点看每段末尾是不是以句号、引号收尾,段首是不是顶格。如果个别段落仍然粘在一起,直接手动在正确位置插入一个空行,再重新跑一次段落合并。
这五步的顺序有讲究。先合并再去空行是错的:段落合并已经把行拼起来了,空行虽然还在,但行首缩进已经不在行首而在段中,替换就失效了;先合并再检查缩进也是错的,全角空格混在段落文字中间,肉眼极难发现。顺序对,这个流程能在几分钟内把一篇几十页的网页文章还原成干净的分段文本。OCR 场景同理,只是 OCR 文本往往连空行都没有,整块文字堆在一起,需要先手动在语义断点处插入空行,再走合并流程。
4. 文本合并避坑:乱码、错序、大文件卡死与章名被吞
合并工具本身不难,难的是合并前的脏数据和合并后的验收。这一章集中写我踩过、也看别人反复踩的四类坑。每个都按「现象 → 原因 → 解决」说清楚,遇到类似情况直接对照着排查。
4.1 合并后满屏乱码:编码混用还是 BOM 残留
现象:合并出来的文件一部分段落正常,一部分显示成「锟斤拷」「�」这类不可读字符,或者文件的某个交界处凭空多出一个空行和一个方框字符。
原因分两种。第一种是编码混用,部分文件是 GBK 部分文件是 UTF-8,合并工具按同一种编码解读所有内容,读错的文件自然乱码。第二种是 BOM 残留,带 BOM 的 UTF-8 文件合并时,开头的 EF BB BF 三个字节被当成正文写进输出文件,在交界处表现为一个不可见字符或乱码符号。
解决:合并前先跑第 2.2 节的编码检查脚本,把编码不一致的文件全部找出来,统一转成 UTF-8 无 BOM 再做合并。如果已经合并完了,可以用脚本重新处理:读入原始合并文件时按 utf-8 解码失败就用 gbk 兜底,再整体转成 utf-8 输出。BOM 残留的清理更简单,读入时用 utf-8-sig 编码把开头三个字节吞掉,写出时用 utf-8,BOM 自然消失。这个坑的根源在源头,所以我的习惯是:编码检查脚本不离手,合并之前必跑一次。
4.2 第 10 卷排在 第 2 卷前面:文件名排序规则不一致
现象:文件列表显示第 1 卷、第 10 卷、第 11 卷、第 2 卷,合并结果章节顺序跳变,但看目录时文件明明是按顺序排的。
原因:Windows 资源管理器默认自然排序,文件名中的数字按数值大小排列;TextForever 这类工具的文件列表往往按字典序排列,字符串逐字符比较,第 10 卷的「1」比第 2 卷的「2」小,所以排在前面。这不是工具 bug,是两种排序规则的固有差异。出错场景集中在文件名带数字编号的文件集,章节名带「一、二、三」汉字的反而不受影响。
解决:合并前核对列表顺序,发现错乱就手动上下调整;文件多的时候,更可靠的办法是把文件名统一补零——「第 1 卷」改成「第 01 卷」,「第 10 卷」保持「第 10 卷」,字典序和自然序就一致了。批量改名可以用系统自带的重命名功能,也可以用一个简单脚本处理。补零之后再合并,顺序问题从根上消失,这个操作我每次做分卷合并都会先做一遍,省心。
4.3 段落合并把「第 1 章」并进正文:合并前没保护标题行
现象:段落合并之后,「第1章 风起」和正文第一句话连成了一行,章节标题消失,整章文本变成一坨。
原因:段落合并的判断规则认为「不以句号结尾的行是段内续行」,章节标题恰恰符合这个特征——没有句尾标点,长度短,后面紧跟正文第一句。于是标题被当作上一段或下一段的组成部分拼进去。这个问题在中文网文里高频出现,因为章节标题的格式和普通短行太像了。
解决:在段落合并之前,先用正则给每个章节标题行前后各加一个空行,把标题单独隔离出来。这样段落合并时,空行策略会把标题当作一个独立的块,而不是和正文粘连。这个预处理可以直接在 TextForever 的替换功能里完成,也可以用下面的脚本:
import re text = open('raw.txt', encoding='utf-8').read() text = re.sub( r'^(第[0-9一二三四五六七八九十百千]+[章节卷回][^\n]*)$', r'\n\n\1\n\n', text, flags=re.M ) open('protected.txt', 'w', encoding='utf-8').write(text)这段正则匹配「行首是第 + 数字或中文数字 + 章/节/卷/回 + 任意字符到行尾」的整行,在前后的换行替换成空行隔开。flags=re.M 是让 ^ 匹配每一行的行首而不是只匹配文件开头,这是它生效的前提。字符类里把「一二三四五六七八九十百千」写全,是为了覆盖「第一百二十章」这类中文数字编号。注意替换后标题行自身前后各多了一个空行,后续段落合并时「保留空行」策略就能正确识别标题边界。
4.4 几百 MB 的 TXT 打开即卡死:把大文件先拆再合
现象:一个 800MB 的小说合集导入 TextForever,界面直接未响应;或者合并过程走到一半程序崩溃,已合并的文件损坏。
原因:这类文本工具的常规实现是一次性把文件读入内存,行结构、编码转换、界面显示都要占用内存。800MB 的文本进入内存后轻松超过 2GB 占用,加上实时预览更是雪上加霜。大文件卡死多数是内存或单线程处理瓶颈,不是文件坏了。
解决:先把大文件拆小,再逐块处理,最后合并。TextForever 自带拆分功能,可以按行数或按大小把大文件切成 30-50MB 的块,每一块单独做段落合并、编码转换这类操作,处理完再用文件合并拼回去。另外两个实操细节:合并时不要勾选「同时转码」这类加重负担的选项;处理期间关掉实时预览,预览是内存大户。等所有块处理完再合并,速度反而比一次性处理一个大文件快得多。完成合并之后,先别急着删原文件,打开合并结果翻几页确认无乱码无跳章,再做清理。
5. 进阶用法:做一个分卷小说到可训练语料的合并流水线
TextForever 能把文件合并和段落合并做成「批处理式」的稳定流程,但工具本身只解决形态问题,不解决语义问题。我在处理「把整本小说转成可用的文本语料」时,会把 TextForever 和脚本配合起来,各干各的活。这个场景在实际需求里很常见,比如把 txt 章节整理成统一的 instruction-input-output 格式并生成 train.json 文件,用来做微调数据集。TextForever 负责形态整理,脚本负责语义切片,两者错开,流程可控。
完整流程是四步:先在 TextForever 里完成文件合并,输出 UTF-8 无 BOM 的整本文件;再做段落合并和章节标题保护,确保每个章节标题独立成行;接着用脚本按「第 N 章」把整本文本切成章节块;最后清洗并组装成目标格式。切章这一步是语义操作,TextForever 帮不上忙,我通常这样处理:
import json, re text = open('novel_merged.txt', encoding='utf-8').read() parts = re.split(r'(?=^第[0-9一二三四五六七八九十百千]+章)', text, flags=re.M) data = [] for i in range(1, len(parts)): seg = parts[i].strip() if len(seg) < 20: continue m = re.match(r'^(第[0-9一二三四五六七八九十百千]+章[^\n]*)', seg) title = m.group(1) if m else f'第{i}章' body = seg[m.end():].strip() data.append({ "instruction": "请阅读下面这章小说内容。", "input": f"{title}\n{body[:2000]}", "output": "已阅读。" }) json.dump(data, open('train.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2)这段代码的正则部分和上一章保护标题用的是同一套思路,但这里用到了零宽断言(?=...),作用是匹配「第 N 章」这一行的行首位置但不消费字符,分割结果里章节标题仍然保留在每一段的开头,不会被切丢。参数里body[:2000]是单条样本长度上限,控制单条数据不要过长;len(seg) < 20用来过滤掉只含标题的空白章节,这类章节在分割后经常出现。输出时ensure_ascii=False保证中文以可读形式写入 JSON,indent=2只是让文件便于人工检查。
这套流程跑通之后,我心里的那个结也解开了:文件合并、段落合并这类工具,价值不在于按钮本身,而在于它能把「形态整理」这一步稳定地批量完成,把时间和精力留给真正需要判断力的事。我现在的习惯是,合并之前先跑一遍编码检查脚本,合并之后先看首行、边界和末尾换行,确认无误再删原文件。这个习惯救过我很多次,也希望帮到你。
本文还有配套的精品资源,点击获取