前文,我们实现了:在WPS中通过JavaScript宏(JSA)调用DeepSeek官方API自动识别标题级别和目录,具体可看我CSDN文章:在WPS中通过JavaScript宏(JSA)调用DeepSeek官方API自动识别标题级别和目录-CSDN博客。
还有诸多地方要改进,今天这是其中一个版本。主要是方便自己写文章之后,对文章的排版更方便一些,今天就分析出来。
要让代码能够正确识别不同层级的标题,尤其是在存在 “一” 作为标题 1 时,把 “(二)” 识别成标题 2,将 “1” 和 “(1)” 识别成标题 3,需要对标题识别的逻辑进行优化。
1.问题分析
当前代码在识别标题层级时,虽然有一定的规则判断,但可能因为正则匹配或者判断逻辑不够完善,导致 “(二)” 这类括号中文序号没有被正确识别为标题 2,“1” 和 “(1)” 也未能正确区分和归类。
2.改进思路
增强正则表达式的匹配规则,确保能精准识别不同类型的标题序号。
优化标题层级判断逻辑,保证在存在 “一” 作为标题 1 时,“(二)” 被识别为标题 2,“1” 和 “(1)” 被识别为标题 3。
3.标题序号主要修改点
(1)新增序号类型判断:添加了 hasBracketChineseNumbers 数组,用于记录是否存在括号中文序号(如 “(一)”)的标题。
(2)增强正则匹配:在遍历段落时,增加了 bracketChineseRegex 正则表达式,用于匹配括号中文序号。
(3)优化标题层级判断:在判断标题层级时,当存在 “一” 作为标题 1 时,将匹配到的括号中文序号(如 “(二)”)的标题识别为标题 2,将括号数字序号(如 “(1)”)和普通数字序号(如 “1”)的标题识别为标题 3。
4.拓展标题4
为了将“(1)”这类格式的内容识别并设置为标题 4,需要对代码进行以下几个方面的改进:
(1)扩展标题类型的记录数组,增加对标题 4 类型的记录。
(2)完善正则表达式,使其能够匹配标题 4 的格式。
(3)增加标题 4 的处理逻辑,将匹配到的内容添加到对应的标题 4 数组中,并在设置标题样式时正确应用标题 4 的样式。
5.拓展标题4主要修改点
(1)新增标题级别:在 headings 和 apiHeadings 对象里增添了 "标题4" 数组,用来存储标题 4 的内容。
(2)新增序号类型记录:添加 hasSubBracketNumbers 数组,用于记录是否存在 (1) 格式的标题序号。
(3)完善正则匹配:在遍历段落时,添加 subBracketRegex 正则表达式,用于匹配 (1) 格式的标题。
(4)调整标题级别判断:在设置标题级别时,把匹配到的 (1) 格式内容添加到 headings["标题4"] 数组中。
(5)修改 API 请求与结果处理:向 DeepSeek API 发送请求时,要求按 4 个级别分类,并在解析 API 结果时处理标题 4 的内容。
(6)检查和设置标题 4 样式:检查样式是否存在时,增加对标题 4 样式的检查;设置标题级别时,正确应用标题 4 的样式。
(7)更新目录设置:创建目录时,将 LowerHeadingLevel 设置为 4,以此包含标题 4。
6.改完后的完整代码
function identifyAndSetHeadingsAndCreateTOC() {
// 获取文档内容
var doc = this.Application.ActiveDocument;
// 尝试获取文档的保护类型,若抛出异常则认为文档可能存在问题
try {
var protectionType = doc.ProtectionType;
if (protectionType!== -1) {
alert("文档处于受保护状态,请先解除保护。");
return;
}
} catch (error) {
alert("获取文档保护状态时出现异常,请检查文档是否正常。");
return;
}
var paragraphs = doc.Content.Paragraphs;
// 定义标题级别映射
var headings = {
"标题1": [],
"标题2": [],
"标题3": [],
"标题4": []
};
// 先判断文档中是否存在不同类型的标题序号
var hasOne = false;
var hasBracketOne = false;
var hasNumberOne = false;
var hasChineseNumbers = [];
var hasBracketNumbers = [];
var hasPlainNumbers = [];
var hasBracketChineseNumbers = [];
var hasSubBracketNumbers = [];
for (var i = 1; i <= 10; i++) {
hasChineseNumbers[i] = false;
hasBracketNumbers[i] = false;
hasPlainNumbers[i] = false;
hasBracketChineseNumbers[i] = false;
hasSubBracketNumbers[i] = false;
}
for (var i = 1; i <= paragraphs.Count; i++) {
var para = paragraphs.Item(i);
var text = para.Range.Text.trim();
if (text) {
for (var j = 1; j <= 10; j++) {
var chineseRegex = new RegExp(`^${getChineseNumber(j)}、|^${getChineseNumber(j)}\\.|^${getChineseNumber(j)}\\s`);
var bracketRegex = new RegExp(`^(${j})`);
var plainRegex = new RegExp(`^${j}、|^${j}\\.|^${j}\\s`);
var bracketChineseRegex = new RegExp(`^(${getChineseNumber(j)})`);
var subBracketRegex = new RegExp(`^(${j})`);
if (chineseRegex.test(text)) {
if (j === 1) hasOne = true;
hasChineseNumbers[j] = true;
} else if (bracketRegex.test(text)) {
if (j === 1) hasBracketOne = true;
hasBracketNumbers[j] = true;
} else if (plainRegex.test(text)) {
if (j === 1) hasNumberOne = true;
hasPlainNumbers[j] = true;
} else if (bracketChineseRegex.test(text)) {
hasBracketChineseNumbers[j] = true;
} else if (subBracketRegex.test(text)) {
hasSubBracketNumbers[j] = true;
}
}
}
}
// 统计存在的标题序号类型数量
var titleTypeCount = 0;
for (var i = 1; i <= 10; i++) {
if (hasChineseNumbers[i]) titleTypeCount++;
if (hasBracketNumbers[i]) titleTypeCount++;
if (hasPlainNumbers[i]) titleTypeCount++;
if (hasBracketChineseNumbers[i]) titleTypeCount++;
if (hasSubBracketNumbers[i]) titleTypeCount++;
}
// 根据存在情况设置标题级别
for (var i = 1; i <= paragraphs.Count; i++) {
var para = paragraphs.Item(i);
var text = para.Range.Text.trim();
if (text) {
if (titleTypeCount > 1) {
if (hasOne) {
for (var j = 1; j <= 10; j++) {
var chineseRegex = new RegExp(`^${getChineseNumber(j)}、|^${getChineseNumber(j)}\\.|^${getChineseNumber(j)}\\s`);
var bracketRegex = new RegExp(`^(${j})`);
var plainRegex = new RegExp(`^${j}、|^${j}\\.|^${j}\\s`);
var bracketChineseRegex = new RegExp(`^(${getChineseNumber(j)})`);
var subBracketRegex = new RegExp(`^(${j})`);
if (chineseRegex.test(text)) {
headings["标题1"].push(text);
} else if (bracketChineseRegex.test(text)) {
headings["标题2"].push(text);
} else if (bracketRegex.test(text) || plainRegex.test(text)) {
headings["标题3"].push(text);
} else if (subBracketRegex.test(text)) {
headings["标题4"].push(text);
}
}
} else if (hasBracketOne) {
for (var j = 1; j <= 10; j++) {
var bracketRegex = new RegExp(`^(${j})`);
var plainRegex = new RegExp(`^${j}、|^${j}\\.|^${j}\\s`);
var subBracketRegex = new RegExp(`^(${j})`);
if (bracketRegex.test(text)) {
headings["标题1"].push(text);
} else if (plainRegex.test(text)) {
headings["标题2"].push(text);
} else if (subBracketRegex.test(text)) {
headings["标题3"].push(text);
}
}
} else if (hasNumberOne) {
for (var j = 1; j <= 10; j++) {
var plainRegex = new RegExp(`^${j}、|^${j}\\.|^${j}\\s`);
var subBracketRegex = new RegExp(`^(${j})`);
var subPlainRegex = new RegExp(`^${j})`);
if (plainRegex.test(text)) {
headings["标题1"].push(text);
} else if (subBracketRegex.test(text)) {
headings["标题2"].push(text);
} else if (subPlainRegex.test(text)) {
headings["标题3"].push(text);
}
}
}
} else {
// 只有一种标题序号类型,全部设为标题1
for (var j = 1; j <= 10; j++) {
var chineseRegex = new RegExp(`^${getChineseNumber(j)}、|^${getChineseNumber(j)}\\.|^${getChineseNumber(j)}\\s`);
var bracketRegex = new RegExp(`^(${j})`);
var plainRegex = new RegExp(`^${j}、|^${j}\\.|^${j}\\s`);
var subBracketRegex = new RegExp(`^(${j})`);
var subPlainRegex = new RegExp(`^${j})`);
var bracketChineseRegex = new RegExp(`^(${getChineseNumber(j)})`);
if (chineseRegex.test(text) || bracketRegex.test(text) || plainRegex.test(text) || subBracketRegex.test(text) || subPlainRegex.test(text) || bracketChineseRegex.test(text)) {
headings["标题1"].push(text);
}
}
}
}
}
// 显示正在执行的提示
alert("正在调用 DeepSeek API 进行标题识别,请稍候...\n点击确定,将进入后台运行!");
// 尝试调用 DeepSeek API 辅助识别标题
var apiHeadings = {
"标题1": [],
"标题2": [],
"标题3": [],
"标题4": []
};
try {
var content = "";
for (var i = 1; i <= paragraphs.Count; i++) {
var para = paragraphs.Item(i);
content += para.Range.Text;
}
// DeepSeek API 配置
var apiUrl = 'https://api.deepseek.com/v1/chat/completions';
var apiKey = 'sk-e4df3c51537048af980934467b594163';
var model = 'deepseek-chat';
// 构建请求体,明确告知 API 识别标题序号并分类
var requestBody = {
"model": model,
"messages": [
{
"role": "user",
"content": `请识别以下文档内容中以 "一、"或"一."或"一 "、"(一)"、"1、"或"1."或"1 "、"(1)"、"1)" 等开头的标题,并按照4个级别进行分类,格式为:
标题1: [标题1内容1, 标题1内容2, ...]
标题2: [标题2内容1, 标题2内容2, ...]
标题3: [标题3内容1, 标题3内容2, ...]
标题4: [标题4内容1, 标题4内容2, ...]
${content}`
}
],
"stream": false
};
requestBody = JSON.stringify(requestBody);
// 创建 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl, false);
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + apiKey);
// 设置超时时间(单位:毫秒)
xhr.timeout = 30000;
// 超时处理函数
xhr.ontimeout = function () {
alert('请求超时,请稍后重试!');
return;
};
// 发送请求
xhr.send(requestBody);
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
var result = response.choices[0].message.content;
// 解析 API 结果
var lines = result.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.startsWith('标题1:')) {
var titles = line.substring(4).trim().replace('[', '').replace(']', '').split(',');
for (var j = 0; j < titles.length; j++) {
apiHeadings["标题1"].push(titles[j].trim());
}
} else if (line.startsWith('标题2:')) {
var titles = line.substring(4).trim().replace('[', '').replace(']', '').split(',');
for (var j = 0; j < titles.length; j++) {
apiHeadings["标题2"].push(titles[j].trim());
}
} else if (line.startsWith('标题3:')) {
var titles = line.substring(4).trim().replace('[', '').replace(']', '').split(',');
for (var j = 0; j < titles.length; j++) {
apiHeadings["标题3"].push(titles[j].trim());
}
} else if (line.startsWith('标题4:')) {
var titles = line.substring(4).trim().replace('[', '').replace(']', '').split(',');
for (var j = 0; j < titles.length; j++) {
apiHeadings["标题4"].push(titles[j].trim());
}
}
}
} else {
alert('请求失败,状态码:' + xhr.status);
return;
}
} catch (e) {
alert('调用 DeepSeek API 出错:' + e.message);
return;
}
if (headings["标题1"].length!== 0) {
if (apiHeadings["标题1"].length === 0) {
alert("请继续等待30秒...");
}
}
// 合并自定义规则和 API 结果
var allHeadings = {
"标题1": headings["标题1"].concat(apiHeadings["标题1"]),
"标题2": headings["标题2"].concat(apiHeadings["标题2"]),
"标题3": headings["标题3"].concat(apiHeadings["标题3"]),
"标题4": headings["标题4"].concat(apiHeadings["标题4"])
};
// 隐藏正在执行的提示(这里只是逻辑上的隐藏,alert 无法真正隐藏)
// 可以考虑使用更复杂的 UI 组件来实现显示和隐藏
// 检查样式是否存在
if (!doc.Styles("标题 1") ||!doc.Styles("标题 2") ||!doc.Styles("标题 3") ||!doc.Styles("标题 4")) {
alert("样式 标题 1、标题 2、标题 3 或 标题 4 不存在,请检查!");
return;
}
// 设置标题级别
for (var i = 1; i <= paragraphs.Count; i++) {
var para = paragraphs.Item(i);
var text = para.Range.Text.trim();
if (allHeadings["标题1"].includes(text)) {
try {
para.Style = doc.Styles("标题 1");
} catch (e) {
alert(`设置“标题 1”样式时出现意外错误,段落内容:${text},错误信息:${e.message}`);
}
} else if (allHeadings["标题2"].includes(text)) {
try {
para.Style = doc.Styles("标题 2");
} catch (e) {
alert(`设置“标题 2”样式时出现意外错误,段落内容:${text},错误信息:${e.message}`);
}
} else if (allHeadings["标题3"].includes(text)) {
try {
para.Style = doc.Styles("标题 3");
} catch (e) {
alert(`设置“标题 3”样式时出现意外错误,段落内容:${text},错误信息:${e.message}`);
}
} else if (allHeadings["标题4"].includes(text)) {
try {
para.Style = doc.Styles("标题 4");
} catch (e) {
alert(`设置“标题 4”样式时出现意外错误,段落内容:${text},错误信息:${e.message}`);
}
}
}
// 创建目录
var tocPara = doc.Content.Paragraphs.Add();
var tocRange = tocPara.Range;
tocRange.InsertAfter("目录\n");
try {
var toc = doc.TablesOfContents.Add(tocRange, {
"UseHeadingStyles": true,
"UpperHeadingLevel": 1,
"LowerHeadingLevel": 4
});
toc.Update();
alert('设置标题并识别目录,完成!');
} catch (e) {
alert("创建目录失败,请检查标题设置!");
}
}
function getChineseNumber(num) {
var chineseNumbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
return chineseNumbers[num - 1];
}
效果如下: