用于大型 html 的 DOMParser

car*_*ass 6 html javascript xml dom domparser

我有大量来自 Excel 的 html 剪贴板数据,大约 250MB(虽然它包含很多格式,所以在实际粘贴时,数据比那个小得多)。

目前我正在使用以下内容DOMParser,这只是一行代码,一切都发生在幕后:

const doc3 = parser.parseFromString(htmlString, "text/html");
Run Code Online (Sandbox Code Playgroud)

然而,解析它需要大约 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 位小数和 stylistic ,例如,背景颜色=红色)。请使用它作为任何示例代码的测试。这是test/html剪贴板中的实际内容(asci):https ://drive.google.com/file/d/1ZUL2A4Rlk3KPqO4vSSEEGBWuGXj7j5Vh/view?usp=sharing

syd*_*uki 5

这里的问题不是html文件大小,而是它包含的大量 DOM 节点。对于文件中的 900000 行和 8 列,html我们有以下数字:

900000TR元素)*(8TD元素)+ 8文本节点))= 〜14个百万DOM节点!

我没有设法加载它DOMParser,一段时间后浏览器选项卡崩溃(FF,Chrome,16GB RAM),尽管在成功加载时查看浏览器行为会很有趣。无论如何,我遇到了类似的挑战,要在浏览器中处理数百万条记录,我提出的解决方案是一次只为一个屏幕构建表格行。

考虑到text/html文件的结构,接下来的方法可能是:

  1. 用于FileReader将 html 文件加载为原始文本
  2. 抓取行,将它们保存为文本数组,从输出中删除它们
  3. 解析结果输出,将表格和样式插入 DOM
  4. 使用视图/分页,在分页/滚动或搜索上呈现当前批次的行
  5. 为鼠标/键盘控制附加事件

下面是一个简单的实现,它提供了基本控件,如调整视图大小、分页/滚动、使用正则表达式过滤行。请注意,过滤是在 row 上完成的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);
Run Code Online (Sandbox Code Playgroud)
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; }
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

结果,对于262 MB 的html 文件(900000表行),我们在 Chromium 中有下一个计时:

文件负载:352.57421875 毫秒

抓斗ROWS:700.1943359375毫秒

解析/插入表和样式:0.78125 毫秒

准备行数组:755.763916015625 毫秒

渲染视图:0.926025390625 毫秒

渲染表:4.317138671875 毫秒

初始化:1814.19287109375 毫秒

渲染视图:5.275146484375 毫秒

渲染视图:4.6318359375 毫秒

因此,渲染第一批行的时间(屏幕时间)是~1.8 s,即比DOMParserOP 指定的时间低一个数量级,后续行渲染几乎是即时的:~5 ms


use*_*170 2

I\xe2\x80\x99d 至少尝试用作XMLHttpRequest解析器。与 不同DOMParser,it\xe2\x80\x99s 是异步的(因此可以在加载过程中与网页进行交互),it\xe2\x80\x99s 能够报告进度并读取从Blob获取的对象Clipboard.read,因此传递的开销大字符串也被最小化。

\n

然而,最后我检查了一下,这种技术并不总是在所有浏览器中都有效,所以不要\xe2\x80\x99t扔掉它DOMParser,如果只是把它作为后备。

\n

除了DOMParser和之外XMLHttpRequest,唯一提供 DOM 解析功能的原生 Web API 是DOM Level 3 Load & Save,据我所知,还没有主流浏览器实现过。这XMLHttpRequest基本上是您唯一的选择。

\n

XMLHttpRequestHere\xe2\x80\x99s 是一个用作解析器的快速示例:

\n
const parseHTML = (html, progress) => {\n    let cleanup = null;\n    let url;\n\n    if (typeof Blob !== \'undefined\') {\n        if (typeof html === \'string\') {\n            url = URL.createObjectURL(new Blob([html], { \'type\': \'text/html\' }));\n        } else if (html instanceof Blob) {\n            url = URL.createObjectURL(html);\n        } else {\n            throw new TypeError(\'html is neither a string nor a Blob\');\n        }\n        cleanup = () => { URL.revokeObjectURL(url); }\n    } else if (typeof html === \'string\') {\n        /* fallback to using data: URIs */\n        url = \'data:text/html,\' + encodeURIComponent(html);\n    } else {\n        throw new TypeError(\'html is neither a string nor a Blob\');     \n    }\n    \n    return new Promise((accept, reject) => {\n        const xhr = new XMLHttpRequest();\n        xhr.open(\'GET\', url);\n        xhr.overrideMimeType(\'text/html\');\n        xhr.responseType = \'document\';\n    \n        xhr.onload = () => {\n            accept(xhr.response || xhr.responseXML);\n        };\n        \n        if (progress) {\n            xhr.onprogress = (ev) => {\n                /* percentage = ev.loaded / ev.total * 100;\n                 * (beware of ev.total === 0)\n                 */\n                progress(ev);\n            };\n        }\n        \n        /* XXX: if the promise is awaited, this makes it\n         * throw a ProgressEvent on failure, which is\xe2\x80\xa6\n         * unusual, though workable */\n        xhr.onabort = xhr.onerror = (ev) => {\n            reject(ev);\n        };\n        \n        xhr.onloadend = cleanup;\n        \n        xhr.send(null);\n    });\n};\n
Run Code Online (Sandbox Code Playgroud)\n

当我自己测试时,性能虽然还算可以忍受,但并不是很出色(文件加载后,解析本身需要大约半分钟,在此期间浏览器相当没有响应)。我还注意到这有时会返回null空字符串,所以也要小心这一点。

\n