用于大型 html 的 DOMParser
Posted
技术标签:
【中文标题】用于大型 html 的 DOMParser【英文标题】:DOMParser for large html 【发布时间】:2021-06-13 03:18:56 【问题描述】:我有大量来自 Excel 的 html 剪贴板数据,大约 250MB(虽然它包含很多格式,所以当实际粘贴时,数据要小得多)。
目前我正在使用以下DOMParser
,这只是一行代码,一切都发生在幕后:
const doc3 = parser.parseFromString(htmlString, "text/html");
但是,解析这个需要大约 18 秒,并且在此期间页面完全阻塞直到它完成 - 或者,如果卸载到网络工作者,一个没有进展并且只是“等待”的操作18 秒直到某事最终发生——我认为这几乎与冻结相同,即使是的,用户可以与页面进行真正的交互。
是否有其他方法来解析大型 html/xml 文件?也许使用不会一次加载所有内容的东西,因此可以响应,或者什么可能是一个好的解决方案?我想以下可能与它内联?但不太确定:https://github.com/isaacs/sax-js。
更新:这是一个示例 Excel 文件:https://drive.google.com/file/d/1GIK7q_aU5tLuDNBVtlsDput8Oo1Ocz01/view?usp=sharing。您可以下载文件,在 Excel 中打开它,按 Cmd-A(全选)和 Cmd-C(复制),它会将数据粘贴到剪贴板中。对我来说,复制剪贴板中的 text/html 格式需要 249MB。
是的,它也可用于 teext/plain(我们用作备份),但从 text/html 中获取它的目的是捕获格式(两种数据格式,例如 numberType=Percent,3 位小数和风格,例如,背景颜色=红色)。请使用它作为任何示例代码的测试。这是剪贴板中的实际 test/html
内容(以 asci 格式):https://drive.google.com/file/d/1ZUL2A4Rlk3KPqO4vSSEEGBWuGXj7j5Vh/view?usp=sharing
【问题讨论】:
是的,流 xml 解析器可能会有所帮助。见my comment here。但是,您声明要解析 html,但 xlsx 由 xml 文件组成,并且 html 比 xml 更难解析。那么你到底想做什么呢? (另外,Worker 无论如何都无法访问 DOMParser API) @Kaiido 这是从 Excel 中的复制粘贴生成的 html。这是一个示例:gyazo.com/e3b061f3de6eeff0117867c8d7ac9102 它来自应用程序“数字”吗?如果是这样,这些数据也可以作为剪贴板中的 tsv 访问(“text/plain”),可能更容易解析,而且内存也更小。如果是 Excel 或其他应用程序,我不知道它们是如何填充剪贴板的,但也可能值得检查一下替代方案。 @Kaiido 它来自 Excel,但是是的,Google 表格或任何其他应用程序可能应该具有类似的“输出为文本/html”格式。是的,解析文本/纯文本要简单得多,并且是我们的后备方案,但回到手头的问题……任何方法可以更快地解析它,或者至少让它响应:)? 拥有生成的 html 标记可能会更有用,所有软件在所有平台上都不会以相同的方式填充剪贴板。此外,在您的屏幕截图中,我们可以看到您的设置创建了一个<style>
标记,其规则必须与以下元素匹配=>您不仅需要 HTML 解析器,而不仅仅是简单的 XML 解析器,还需要一个CSS 解析器和 CSSOM 实现。如果我处于你的位置,我会与客户再次确认他们是否可以在粘贴大数据时省略样式,或者强制直接发送 XML 文件。
【参考方案1】:
我至少会尝试使用XMLHttpRequest
作为解析器。与DOMParser
不同,它是异步的(因此可以在加载过程中与网页进行交互),它能够报告进度并从您从Clipboard.read
获得的Blob
对象中读取,因此传递大字符串的开销是也最小化了。
不过,我上次检查过,这项技术并非总是适用于所有浏览器,所以暂时不要丢弃 DOMParser
,如果只是将其作为备用。
除了DOMParser
和XMLHttpRequest
之外,唯一提供DOM解析功能的原生Web API是DOM Level 3 Load & Save,据我所知,目前还没有主流浏览器实现过。这意味着XMLHttpRequest
基本上是您唯一的选择。
这是一个使用 XMLHttpRequest
作为解析器的简单粗暴的示例:
const parseHTML = (html, progress) =>
let cleanup = null;
let url;
if (typeof Blob !== 'undefined')
if (typeof html === 'string')
url = URL.createObjectURL(new Blob([html], 'type': 'text/html' ));
else if (html instanceof Blob)
url = URL.createObjectURL(html);
else
throw new TypeError('html is neither a string nor a Blob');
cleanup = () => URL.revokeObjectURL(url);
else if (typeof html === 'string')
/* fallback to using data: URIs */
url = 'data:text/html,' + encodeURIComponent(html);
else
throw new TypeError('html is neither a string nor a Blob');
return new Promise((accept, reject) =>
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.overrideMimeType('text/html');
xhr.responseType = 'document';
xhr.onload = () =>
accept(xhr.response || xhr.responseXML);
;
if (progress)
xhr.onprogress = (ev) =>
/* percentage = ev.loaded / ev.total * 100;
* (beware of ev.total === 0)
*/
progress(ev);
;
/* XXX: if the promise is awaited, this makes it
* throw a ProgressEvent on failure, which is…
* unusual, though workable */
xhr.onabort = xhr.onerror = (ev) =>
reject(ev);
;
xhr.onloadend = cleanup;
xhr.send(null);
);
;
当我自己对此进行测试时,虽然可以忍受,但性能并不出色(加载文件后,解析本身大约需要半分钟,在此期间浏览器相当无响应)。我还注意到这偶尔会为空字符串返回null
,所以也要小心。
【讨论】:
你真的试过了吗?这里有一个 60MB 的 xml,它只是在我的 Chrome 上失败,响应设置为空字符串,responseXML 设置为 null。 我在 HTML 上试过了。在 Firefox 上,物有所值。 不管是 HTML 还是 XML,重要的是大小和浏览器。 Ps:现在在 FF 上尝试,它会像 DOMParser 一样冻结浏览器。 (这是有道理的,因为它不应该使用其他线程,即使它是一个异步操作) 它是否使用单独的线程进行解析是站点不可见的实现细节,因此将来可能会透明地实现。还没有——运气不好。但有了DOMParser
,就根本不可能发生这种情况。【参考方案2】:
这里的问题不是html
文件大小而是它包含的大量DOM 节点。对于html
文件中的 900000 行和 8 列,我们有这些数字:
900000(TR元素)*(8(TD元素)+8 strong> (Text 节点)) = ~1400 万 个 DOM 节点!
我没能用DOMParser
加载它,浏览器选项卡在一段时间后崩溃(FF、Chrome、16GB RAM),不过看看成功加载时的浏览器行为会很有趣。
无论如何,我遇到了类似的挑战,要在浏览器中处理数百万条记录,我想出的解决方案是一次只为一个屏幕构建表格行。
考虑到您的text/html
文件的结构,下一个方法可能是:
使用
FileReader
将html文件加载为原始文本 抓取行,将它们保存为文本数组,从输出中删除它们 解析结果输出,将表格和样式插入 DOM 使用视图/分页,在分页/滚动或搜索时呈现当前批次的行 为鼠标/键盘控制附加事件
下面是一个简单的实现,它提供了基本的控件,如调整视图大小、分页/滚动、使用正则表达式过滤行。请注意,过滤是在行 html
上完成的,对于 text
仅搜索您可以取消注释行“//text: text.match...”,尽管在这种情况下文件解析时间会增加一点。
let tbody, style;
let rows = [], view = [], viewSize = 20, page = 0, time = 0;
const load = fRead =>
console.timeEnd('FILE LOAD');
console.time('GRAB ROWS');
let thead, trows = '', table = fRead.result
.replace(/<tr[^]+<\/tr>/i, text => (trows += text) && '');
console.timeEnd('GRAB ROWS');
console.time('PARSE/INSERT TABLE & STYLE');
const html = document.createElement('div');
html.innerHTML = table;
table = html.querySelector('table');
if (!table || !trows)
setInfo('NO DATA FOUND');
return;
if (style = html.querySelector('style'))
document.head.appendChild(style);
table.textContent = '';
el('viewport').appendChild(table);
console.timeEnd('PARSE/INSERT TABLE & STYLE');
console.time('PREPARE ROWS ARRAY');
rows = trows.split('<tr').slice(1).map(text => (
html: '<tr' + text, text,
//text: text.match(/>.*<\/td>/gi).map(s => s.slice(1, -5)).join(' '),
));
console.timeEnd('PREPARE ROWS ARRAY');
console.time('RENDER TABLE');
table.appendChild(thead = document.createElement('thead'));
table.appendChild(tbody = document.createElement('tbody'));
thead.innerHTML = rows[0].html;
view = rows = rows.slice(1);
renew();
console.timeEnd('RENDER TABLE');
console.timeEnd('INIT');
;
const reset = info =>
el('info').textContent = info ?? '';
el('viewport').textContent = '';
style?.remove();
style = null;
tbody = null;
view = rows = [];
;
const pages = () => Math.ceil(view.length / viewSize) - 1;
const renew = () =>
if (!tbody)
return;
console.time('RENDER VIEW');
const i = page * viewSize;
tbody.innerHTML = view.slice(i, i + viewSize)
.map(row => row.html).join('');
console.timeEnd('RENDER VIEW');
setInfo(`
rows total: $rows.length,
rows match: $view.length,
pages: $pages(), page: $page
`);
;
const gotoPage = num =>
el('page').value = page = Math.max(0, Math.min(pages(), num));
renew();
;
const fileInput = () =>
reset('LOADING...');
const fRead = new FileReader();
fRead.onload = load.bind(null, fRead);
console.time('INIT');
console.time('FILE LOAD');
fRead.readAsText(el('file').files[0]);
;
const fileReset = () =>
reset();
el('file').files = new DataTransfer().files;
;
const setInfo = text => el('info').innerHTML = text;
const setView = e =>
let value = +e.target.value;
value = Number.isNaN(value * 0) ? 20 : value;
e.target.value = viewSize = Math.max(1, Math.min(value, 100));
renew();
;
const setPage = e =>
const page = +e.target.value;
gotoPage(Number.isNaN(page * 0) ? 0 : page);
;
const setFilter = e =>
const filter = e.target.value;
let match;
try
match = new RegExp(filter);
catch (e)
setInfo(e);
return;
view = rows.filter(row => match.test(row.text));
page = 0;
renew();
;
const keys = 'PageUp': -1, 'PageDown': 1;
const scroll = e =>
const dir = e.key ? keys[e.key] ?? 0 : Math.sign(-e.deltaY);
if (!dir)
return;
e.preventDefault();
gotoPage(page += dir);
;
const el = id => document.getElementById(id);
el('file').addEventListener('input', fileInput);
el('reset').addEventListener('click', fileReset);
el('view').addEventListener('input', setView);
el('page').addEventListener('input', setPage);
el('filter').addEventListener('input', setFilter);
el('viewport').addEventListener('keydown', scroll);
el('viewport').addEventListener('wheel', scroll);
div
display: flex;
flex: 1;
align-items: center;
white-space: nowrap;
thead td,
tbody tr td:first-child
background: grey;
color: white;
td padding: 0 .5em;
#menu > * margin: 0 .25em;
#file min-width: 16em;
#view, #page width: 8em;
#filter flex: 1;
#info padding: .5em; color: red;
<div id="menu">
<span>FILE:</span>
<input id="file" type="file" accept="text/html">
<button id="reset">RESET</button>
<span>VIEW:</span><input id="view" type="number" value="20">
<span>PAGE:</span><input id="page" type="number" value="0">
<span>FILTER:</span><input id="filter">
</div>
<div id="info"></div>
<div id="viewport" tabindex="0"></div>
因此,对于 262 MB html 文件(900000 表行),我们在 Chromium 中有下一个计时:
文件加载:352.57421875 毫秒
抓取行:700.1943359375 毫秒
解析/插入表格和样式:0.78125 毫秒
准备行数组:755.763916015625 毫秒
渲染视图:0.926025390625 毫秒
渲染表:4.317138671875 毫秒
初始化:1814.19287109375 毫秒
渲染视图:5.275146484375 毫秒
渲染视图:4.6318359375 毫秒
因此,直到渲染第一批行的时间(屏幕时间)为~1.8 s
,即比 OP 指定的DOMParser
花费的时间低一个数量级,后续行渲染几乎是即时的: ~5 ms
【讨论】:
感谢您。一个问题:900000 (tr) * 8 (td) * 8 (text)
。什么是“文字”?
"text" 是 TextNode,即一个单元格中的实际文本,<tr[^]+
和 <tr[^>]*
将给出相同的输出,第一个变体也匹配 >
我明白了,但是为什么<td>text</td>
会产生 8 个 dom 节点而不是一个?
这是解析器的工作,我不能确定它会插入多少个节点,但任何文本字段至少会有一个 TextNode
可能是 OP 的最佳策略(至少比其他答案好很多)。但是应该有一个重要的注意事项,这仅适用于 OP 解析 HTML 表的特殊情况,不能像那样解析任意 HTML 文件。另请注意,它可能无法正确处理合并的单元格。例如,您可以有一个 rowspan
为 3 的 以上是关于用于大型 html 的 DOMParser的主要内容,如果未能解决你的问题,请参考以下文章