在开发视频编辑器的时候,遇到一个需求是将原始字幕文本根据标点符号自动分割成适合显示的短句。这是一个很常见的功能,在实现过程中遇到了复杂的边界情况,最终通过一种“遮蔽-恢复”的策略得以解决。
一、 问题背景
功能要求很简单:给定一行字幕文本,需要根据中英文的逗号、句号、问号等标点将其分割成多个片段。
例如,对于输入:
"等等,让我想想..."
期望的输出应该是一个包含两个字符串的数组:
["等等", "让我想想..."]
这里的关键点在于,像逗号(,
)这样的普通标点应该作为分割符,而像省略号(...
)这样的特殊序列则需要被完整地保留在片段的末尾。
二、 初步实现
我的第一反应是使用 String.prototype.split()
方法,并结合一个包含所有分割标点的正则表达式。
const text = "等等,让我想想...";
// 模式中包含了逗号和句点
const result = text.split(/[,.]/);
console.log(result);
// 实际输出: ["等等", " 让我想想", "", "", ""]
这个结果显然不符合预期。split()
方法忠实地将字符串在每一个句点处都进行了分割,导致我们希望保留的省略号被完全破坏了。
问题的关键在于:我们用于分割的字符(.
),同时也是我们需要保留的序列(...
)的一部分。
在尝试了多种复杂的正则表达式(例如使用前瞻和后顾断言)后,发现这些方案不仅让正则表达式变得异常复杂、难以维护,而且在处理更多边缘情况(如 ....
或 ~~~
)时,扩展性很差,需要寻找一种更具结构性的解决方案。
三、 解决方案:“遮蔽-恢复”策略
仔细分析后发现,这个问题的症结在于角色冲突——句点.
既是分割符,又是省略号...
的组成部分。就像一个人既要当裁判又要当运动员一样,职责不清必然导致混乱。
既然如此,为什么不让它们各司其职呢?我灵光一闪:能不能先把那些"身兼数职"的字符暂时"请出场外",等主要工作完成后再"请回来"?
就像我们整理房间时,会先把贵重物品收起来,避免在大扫除时被误伤一样。我可以:
- 先保护好特殊序列:把
...
、~~~
这些需要完整保留的内容暂时"藏起来" - 安心处理主要任务:在一个"干净"的环境中进行字符串分割
- 最后物归原主:把之前保护的内容重新放回它们该在的位置
经过一番搜索研究,我发现已经存在这样的方法:"遮蔽-恢复"(Mask-and-Restore)策略。
这个思路的核心是,暂时将需要保护的子字符串替换为不会被分割的安全占位符,在完成分割操作后,再将这些占位符恢复为原始的子字符串 。
这个过程可以分解为三个清晰的步骤:
1. 遮蔽 (Masking)
首先,遍历原始字符串,找到所有需要被完整保留的序列(如 ...
, ~~~
),将它们替换为一个唯一的、临时的占位符。同时,将原始序列存入一个数组中,以便后续恢复。
const text = "等等,让我想想...";
const preservable = /[~~….。]{2,}/g; // 匹配所有需要保留的序列
const placeholders = [];
// 查找并替换,实现“遮蔽”
const maskedText = text.replace(preservable, (match) => {
placeholders.push(match); // 将原始序列存入数组
return `%%PROTECTED_${placeholders.length - 1}%%`; // 返回一个唯一的占位符
});
// maskedText 变为: "等等,让我想想%%PROTECTED_0%%"
// placeholders 变为: ["..."]
2. 分割 (Splitting)
现在,字符串中的冲突字符已经被安全的占位符所替代。我们可以放心地使用一个简单的正则表达式来执行分割操作。
const splitPattern = /[,.!?,。!?]/;
const splitChunks = maskedText.split(splitPattern);
// splitChunks 的结果: ["等等", " 让我想想%%PROTECTED_0%%"]
可以看到,分割操作正确地处理了逗号,并且占位符也如预期般保持完整。
3. 恢复 (Restoring)
最后一步,我们遍历分割后的片段数组,将所有占位符替换回它们对应的原始序列。
const finalResult = splitChunks.map((chunk) => {
// 检查并恢复占位符
return chunk.replace(/%%PROTECTED_(\d+)%%/g, (match, index) => {
return placeholders[parseInt(index, 10)];
});
}).map(s => s.trim()).filter(s => s.length > 0); // 进行必要的清理
// finalResult 的结果: ["等等", "让我想想..."]
至此,我们便得到了完全正确的输出结果。
四、 完整实现与总结
/**
* 使用“遮蔽-恢复”策略安全地分割字符串,同时保留指定序列的完整性。
* @param {string} text 待处理的原始字符串。
* @returns {string[]} 分割后的字符串数组。
*/
function robustSplit(text) {
// 1. 遮蔽
const preservable = /[~~….。]{2,}/g;
const placeholders = [];
const maskedText = text.replace(preservable, (match) => {
placeholders.push(match);
return `%%PROTECTED_${placeholders.length - 1}%%`;
});
// 2. 分割
const splitPattern = /[,.!?,。!?]/;
const splitChunks = maskedText.split(splitPattern);
// 3. 恢复与清理
return splitChunks
.map((chunk) => {
return chunk.replace(/%%PROTECTED_(\d+)%%/g, (match, index) => {
return placeholders[parseInt(index, 10)];
});
})
.map(s => s.trim())
.filter(s => s.length > 0);
}
// 测试
const line = "等等,让我想想.... 这样对吗?嗯~~~";
console.log(robustSplit(line));
// 输出: ["等等", "让我想想....", "这样对吗", "嗯~~~"]
这次问题解决过程给我的启示是,当遇到看似需要用一个极其复杂的单一方案来解决的问题时,不妨退一步思考,能否将问题分解成多个更简单、更纯粹的步骤。
“遮蔽-恢复”策略的优点在于:
- 稳健性:它从根本上消除了逻辑冲突,而不是试图绕过它。
- 清晰度:代码的意图(保护、分割、恢复)一目了然,易于理解和维护。
- 扩展性:未来如果需要支持更多需要保留的序列,只需简单地更新
preservable
正则表达式即可,核心逻辑无需变动。
这是一个非常实用的模式,它也是分治思想的一种体现,记录下来以备后续参考。