如何解析HTML为DOM树并正确关联其原始源地址?
解决用户脚本中跨域HTML解析的相对链接与编码问题
核心问题
在运行于https://example.net的用户脚本中,通过fetch获取https://example.com的HTML并使用DOMParser解析时,DOM元素的相对链接会被解析为当前页面(example.net)的绝对路径;同时直接用.text()获取内容会绕过HTML编码检测逻辑。
现有方案的不足
- 注入
<base>标签:可能与原HTML中已存在的<base>冲突 document.implementation.createHTMLDocument()配合.write():用户脚本中会抛出SecurityErrorProxy拦截属性访问:实现繁琐,维护成本高- 引入第三方解析库:体积大,没必要
推荐解决方案
方案1:手动修正所有相对链接(轻量高效)
利用URL API遍历解析后的DOM中所有带href/src属性的元素,手动将相对链接转换为目标域名的绝对路径。同时通过arrayBuffer()+Blob+FileReader组合处理编码,避免绕过浏览器的编码检测。
fetch('https://example.com') .then(response => { // 获取响应的Content-Type,用于正确解析编码 const contentType = response.headers.get('Content-Type'); return response.arrayBuffer().then(buffer => { const blob = new Blob([buffer], { type: contentType }); // 用FileReader根据Content-Type指定的编码读取内容 return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsText(blob); }); }); }) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const baseUrl = new URL('https://example.com'); // 批量修正所有带href的元素(a、link等) doc.querySelectorAll('[href]').forEach(elem => { const relativeHref = elem.getAttribute('href'); try { elem.href = new URL(relativeHref, baseUrl).toString(); } catch (e) { // 无效链接,跳过处理 } }); // 批量修正所有带src的元素(img、script等) doc.querySelectorAll('[src]').forEach(elem => { const relativeSrc = elem.getAttribute('src'); try { elem.src = new URL(relativeSrc, baseUrl).toString(); } catch (e) { // 无效链接,跳过处理 } }); // 此时doc中的所有链接均已正确解析为example.com的绝对路径 const targetLink = doc.querySelector('body > p > a'); console.log(targetLink.href); // 输出:https://example.com/foo }) .catch(err => console.error('处理失败:', err));
方案2:利用隐藏iframe的独立文档环境(彻底解决)
创建隐藏iframe,在其独立文档中设置目标域名的<base>标签后写入HTML,让浏览器自动处理相对链接解析,同时天然支持编码检测。
// 创建隐藏iframe const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); // 等待iframe加载空白文档 iframe.onload = () => { const iframeDoc = iframe.contentDocument; // 设置目标域名作为base,确保相对链接解析正确 const baseTag = iframeDoc.createElement('base'); baseTag.href = 'https://example.com'; iframeDoc.head.appendChild(baseTag); fetch('https://example.com') .then(response => response.blob()) .then(blob => { // 读取blob内容(自动处理编码) return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsText(blob); }); }) .then(html => { // 写入HTML到iframe文档 iframeDoc.open(); iframeDoc.write(html); iframeDoc.close(); // 此时iframe文档中的链接已正确解析 const targetLink = iframeDoc.querySelector('body > p > a'); console.log(targetLink.href); // 输出:https://example.com/foo // 可将iframe中的DOM导入到当前文档使用 const importedDom = document.importNode(iframeDoc.documentElement, true); // 处理完成后移除iframe document.body.removeChild(iframe); }) .catch(err => { console.error('处理失败:', err); document.body.removeChild(iframe); }); }; // 加载空白文档初始化iframe iframe.src = 'about:blank';
编码处理说明
两种方案均通过arrayBuffer()获取原始响应数据,再用Blob+FileReader组合读取内容,能自动根据响应头的Content-Type指定的编码解析HTML,避免直接用.text()时的编码检测绕过问题。
内容的提问来源于stack exchange,提问作者dumbass
相关产品推荐
相关产品推荐

