JavaScript实现HTML文本转翻译JSON及还原HTML的方案问询
正则处理HTML天生就容易踩坑——毕竟HTML是嵌套结构,正则根本没法理解DOM的层级关系,很容易误拆分文本或者匹配到不该匹配的内容。你之前遇到的问题就是典型的正则局限性导致的,下面给你一套在JavaScript环境下的可靠实现方案,完全满足你保留文本上下文、不拆分带内部标签内容的需求。
先说说你的正则方案为啥不行
你的正则/\>(.*?)\</g是在匹配转义后的>和<之间的内容,但它的非贪婪匹配特性会遇到下一个<就停止,所以会把<a>hello world<strong> this is gonna be hard</strong></a>拆成两个独立的文本片段,刚好违背了你“保留完整上下文”的需求。而且正则没法处理空白字符、嵌套标签、特殊字符转义等复杂情况,出错概率极高。
方案一:逐文本节点提取(适合保留标签内部分隔的场景)
如果你的需求是保留原有的标签结构,把每个独立的文本节点(比如<a>里的hello world和<strong>里的 this is gonna be hard)作为单独的翻译项,这个方案简单又可靠:
浏览器环境代码
function extractTranslatableText(htmlString) { // 创建临时DOM容器解析HTML const tempContainer = document.createElement('div'); tempContainer.innerHTML = htmlString; const translationMap = {}; let id = 1; // 递归遍历DOM树,找到所有非空白文本节点 function traverseNode(node) { if (node.nodeType === Node.TEXT_NODE) { const cleanText = node.textContent.trim(); if (cleanText) { // 用唯一占位符替换原文本 node.textContent = `~${id}~`; translationMap[id] = cleanText; id++; } } else if (node.nodeType === Node.ELEMENT_NODE) { // 遍历子元素 Array.from(node.childNodes).forEach(traverseNode); } } traverseNode(tempContainer); return { placeholderHtml: tempContainer.innerHTML, translationJson: JSON.stringify(translationMap, null, 2) }; } function applyTranslation(placeholderHtml, translatedJson) { const tempContainer = document.createElement('div'); tempContainer.innerHTML = placeholderHtml; const translatedMap = JSON.parse(translatedJson); // 替换占位符为翻译后的文本 function replacePlaceholders(node) { if (node.nodeType === Node.TEXT_NODE) { const match = node.textContent.match(/~(\d+)~/); if (match) { node.textContent = translatedMap[match[1]] || node.textContent; } } else if (node.nodeType === Node.ELEMENT_NODE) { Array.from(node.childNodes).forEach(replacePlaceholders); } } replacePlaceholders(tempContainer); return tempContainer.innerHTML; } // 示例使用 const originalHtml = '<a class="test">hello world<strong> this is gonna be hard</strong></a>'; const { placeholderHtml, translationJson } = extractTranslatableText(originalHtml); console.log('带占位符的HTML:', placeholderHtml); console.log('翻译用JSON:', translationJson); // 模拟翻译后的JSON const translatedJson = '{"1":"Hola Mundo","2":" esto va a ser difícil"}'; const finalHtml = applyTranslation(placeholderHtml, translatedJson); console.log('翻译后HTML:', finalHtml);
Node.js环境(需安装jsdom)
先安装依赖:
npm install jsdom
然后使用代码:
const { JSDOM } = require('jsdom'); function extractTranslatableText(htmlString) { const dom = new JSDOM(''); const tempContainer = dom.window.document.createElement('div'); tempContainer.innerHTML = htmlString; const translationMap = {}; let id = 1; function traverseNode(node) { if (node.nodeType === node.TEXT_NODE) { const cleanText = node.textContent.trim(); if (cleanText) { node.textContent = `~${id}~`; translationMap[id] = cleanText; id++; } } else if (node.nodeType === node.ELEMENT_NODE) { Array.from(node.childNodes).forEach(traverseNode); } } traverseNode(tempContainer); return { placeholderHtml: tempContainer.innerHTML, translationJson: JSON.stringify(translationMap, null, 2) }; } function applyTranslation(placeholderHtml, translatedJson) { const dom = new JSDOM(''); const tempContainer = dom.window.document.createElement('div'); tempContainer.innerHTML = placeholderHtml; const translatedMap = JSON.parse(translatedJson); function replacePlaceholders(node) { if (node.nodeType === node.TEXT_NODE) { const match = node.textContent.match(/~(\d+)~/); if (match) { node.textContent = translatedMap[match[1]] || node.textContent; } } else if (node.nodeType === node.ELEMENT_NODE) { Array.from(node.childNodes).forEach(replacePlaceholders); } } replacePlaceholders(tempContainer); return tempContainer.innerHTML; } // 示例使用 const originalHtml = '<a class="test">hello world<strong> this is gonna be hard</strong></a>'; const { placeholderHtml, translationJson } = extractTranslatableText(originalHtml); console.log('带占位符的HTML:', placeholderHtml); console.log('翻译用JSON:', translationJson); const translatedJson = '{"1":"Hola Mundo","2":" esto va a ser difícil"}'; const finalHtml = applyTranslation(placeholderHtml, translatedJson); console.log('翻译后HTML:', finalHtml);
这个方案会输出:
- 带占位符的HTML:
<a class="test">~1~<strong>~2~</strong></a> - 翻译用JSON:
{ "1": "hello world", "2": " this is gonna be hard" }
- 翻译后HTML:
<a class="test">Hola Mundo<strong> esto va a ser difícil</strong></a>
方案二:提取连贯语义文本(适合跨标签文本作为单个翻译项的场景)
如果你的需求是把跨标签的连贯文本(比如<p>Click <a>here</a></p>里的Click here)作为一个翻译项,同时还原时保留原标签结构,那需要更复杂的处理(涉及文本对齐)。下面是一个简化版实现(假设翻译后的文本结构和原文本一致):
function extractSemanticText(htmlString) { const tempContainer = document.createElement('div'); tempContainer.innerHTML = htmlString; const translationMap = {}; let id = 1; // 定义需要提取语义文本的标签(可根据业务扩展) const targetTags = ['p', 'a', 'span', 'strong', 'em']; function processElement(element) { // 避免处理嵌套的目标标签,防止重复提取 const hasNestedTarget = Array.from(element.children).some(child => targetTags.includes(child.tagName.toLowerCase())); if (!hasNestedTarget) { const fullText = element.textContent.trim(); if (fullText) { // 记录当前元素内的所有文本节点 const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()); // 给每个文本节点添加带索引的占位符 textNodes.forEach((node, idx) => { node.textContent = `~${id}-${idx}~`; }); translationMap[id] = fullText; id++; } } // 递归处理子元素 Array.from(element.children).forEach(processElement); } Array.from(tempContainer.children).forEach(processElement); return { placeholderHtml: tempContainer.innerHTML, translationJson: JSON.stringify(translationMap, null, 2) }; } function applySemanticTranslation(placeholderHtml, translatedJson) { const tempContainer = document.createElement('div'); tempContainer.innerHTML = placeholderHtml; const translatedMap = JSON.parse(translatedJson); function processElement(element) { const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()); if (textNodes.length > 0) { // 提取占位符中的翻译ID const idMatch = textNodes[0].textContent.match(/~(\d+)-\d+~/); if (idMatch) { const id = idMatch[1]; const translatedText = translatedMap[id]; if (translatedText) { // 简化版文本拆分:按原文本片段长度比例拆分翻译后的文本 const originalLengths = textNodes.map(node => node.textContent.replace(/~\d+-\d+~/g, '').length); const totalOriginalLength = originalLengths.reduce((sum, len) => sum + len, 0); let remainingText = translatedText; textNodes.forEach((node, idx) => { const ratio = originalLengths[idx] / totalOriginalLength; const takeLength = Math.round(translatedText.length * ratio); node.textContent = remainingText.slice(0, takeLength); remainingText = remainingText.slice(takeLength); }); } } } Array.from(element.children).forEach(processElement); } Array.from(tempContainer.children).forEach(processElement); return tempContainer.innerHTML; } // 示例使用 const originalHtml = '<p>Click <a href="/">here</a> to continue</p>'; const { placeholderHtml, translationJson } = extractSemanticText(originalHtml); console.log('占位符HTML:', placeholderHtml); console.log('翻译用JSON:', translationJson); // 模拟翻译后的JSON const translatedJson = '{"1":"单击此处继续"}'; const finalHtml = applySemanticTranslation(placeholderHtml, translatedJson); console.log('翻译后HTML:', finalHtml);
注意:这个简化版的文本拆分逻辑可能不够精准,实际项目中如果需要更准确的对齐,建议使用专业的文本对齐工具(比如Google Translate API的对齐功能)或者NLP库。
总结
永远不要用正则解析HTML——HTML是嵌套结构,正则属于正则语言,根本没法处理这种层级关系。用DOM API(浏览器原生或Node.js的jsdom)是最可靠的方式,能准确识别文本节点和元素结构,完美满足你的需求。
内容的提问来源于stack exchange,提问作者dk111989

