地图集-html-流
叉子为 地图集-html-流 它是一个超快的html-parser流 输出标记,
文本和关闭节点。 见 CHANGELOG.md 用于在分叉中引入的更改.
安装
npm install --save @bonniernews/atlas-html-stream
为什么
我不喜欢 htmlparser2 的流 API , 我想要一个 html 解析器,在默认情况下 连接文本节点的白空间。 我还想看看能不能写一个更快的解析器.
业绩
htmlparser 基准标记
以下基准是用我的本地机器 htmlparser 基准标记下调更好 :
atlas-html-stream: 4.91208 ms/file ± 6.08846
htmlparser2: 7.06549 ms/file ± 5.09643
我只测试了htmlparser2,因为它是最快的以流为基础的剖析器. 这个基准使用两个解析器的流出界面.
内存使用量
一个简单的解析器需要身体在内存中,如果您的 HTML 非常大,则可能打破游戏. 这个解析器是一个流, 并且从使用块数据中受益 。 基本的想法是跟踪当前块 以及从上一个块溢出到当前块的任何东西。 例如,考虑以下两个块:
"first data chunk <di""v>second chunk</div>"
当解析器用第一个块完成时,它会认识到它需要坚持 "<di" 而只会冲出 "first chunk " 从记忆。 当解析器用第二块完成后,它会冲出记忆中的一切,因为不再有一个待决的节点.
实例
使用这个解析器很容易 extends 流界面 - 您只需要管道或写入 HTML 并听 "data" 事件:
有管道
建议使用流界面,因为它允许您消耗一定数量的内存(无论HTML大小):
const { createReadStream } = require("fs");
const HtmlParser = require("@bonniernews/atlas-html-stream");
const myHtmlFile = createReadStream("./index.html");
myHtmlFile.pipe(new HtmlParser()).on("data", ({name, data, text}) => {
if (text){
// is a text node
console.log(text);
} else if (name && data){
// is an opening tag with potential attributes
console.log(name, data)
} else {
// is a closing tag
console.log(name)
}
}).on("end", () => {
console.log("parsed all html")
})
无管道
除非您负担得起您的 HTML 完全存在于内存中,否则不推荐此选项 。 首先,我们将写一个小助手来取出流线界面:
// helpers.js
const HtmlParser = require("@bonniernews/atlas-html-stream");
const parser = new HtmlParser();
const parseHtml = html => {
const nodes = [];
parser.on("data", data => nodes.push(data))
parser.write(html)
// resets the parser back to vanilla state
parser.reset();
return nodes;
}
module.exports = { parseHtml };
接下来,我们将使用我们的助手来分析我们的内幕html文件:
const { readFileSync } = require("fs");
const { parseHtml } = require("./helpers");
const file = readFileSync("./index.html");
const nodes = parseHtml(file);
// do what you want with the parsed nodes.
注释、脚本和样式节点
上面的例子表明API是多么的微不足道,应该如此. 注释,脚本和样式节点被作为正则节点处理,因为它们发出打开和关闭标记. 主要的区别在于,这些节点内部的内容被作为单一的原始文本节点来对待,没有白空-collaps. 简而言之,让我们用我们的 parseHtml 帮助器在以下实例中的功能 :
脚本/样式标签
...
const scriptNodes = parseHtml(`
<script src="./script.js">
const myVar = 5;
</script>
`);
scriptNodes.forEach(n => console.log(n))
// { name: 'script', data: { src: './script.js' } }
// { text: '\n const myVar = 5;\n ' }
// { name: 'script' }
样式标记的处理方式相同,但 name 属性的值为 "style"。 。 。 .
注释标签
同样,这些也得到了同样的对待.
const commentNodes = parseHtml(`
<!--
this is
a comment
-->
`)
commentNodes.forEach(n => console.log(n));
// { name: '!--', data: {} }
// { text: ' \n this is \n a comment \n ' }
// { name: '!--' }
保持空白空间
默认情况下,解析器会将所有非标注/样式/comment 白空降为单个空格,如果您正在刮取并不在乎白空格,则白空格是有用的. 在某些情况下,你需要白空间。 你可以通过一个 preserveWS 选项到构造器,它将迫使解析器保留它在文本节点中找到的所有空白:
const nodes = parseHtml(`
This is some text
<b>
Hola
</b>
`)
nodes.forEach(n => console.log(n));
// { text: "\n This is some text\n\n " }
// { name: "b", data: {} }
// { text: " \n Hola\n " }
// { name: "b" }
// { text: " \n" }
然后,如果需要更具体化的话,可以处理节点。 请注意,这个例子不使用流线接口来简化,但是流线接口被高度推荐:
myFile.pipe(new HtmlParser({preserveWS: true})).pipe(myTransform).on("data", node => {...})
myTransform 可以有条件地检查文本节点,然后逐一处理.
待办事宜
parseHtml 帮助者
应否 parseHtml 除此包外, 还要导出 HtmlParser使用 parseHtml 在流媒体界面上不建议使用,但它似乎是对html字符串需要存在内存的情况的有效帮助.
还要快
我想让这个东西更快。 解析本身需要了解 3.5 ms/file (使用时) htmlparser 基准标记在我的电话机上 将节点推为 data 增加40%左右的处理时间 4.9 ms/file - 这是无法避免的,因为我们 请求 流媒体界面.
这个 SeqMatcher 减慢此解析器( 检查注释, 脚本和样式节点); 可能有一个更快的方法来处理这些特殊的节点 .
正在打开 state 首先是解析器,可能比先切换当前字符要快,但我还没有测试后者。 主要想法是,尽量减少每项指令所需的数量 [state, char] 根据每一对的概率 若为 [TEXT, !whitespace] 在“ 典型” html 中, 概率最高, 那么我们希望这对 需要最少的指令。 换句话说, sum_i(probabilityOfPair_i*numSteps_i) 应尽量减少.
我想是美国崩溃了 SCRIPT, (中文(简体) ), STYLE 财务报告和审定财务报表 COMMENT 变成单一状态 SPECIAL 可能会提高性能 而不是污染空间 SeqMatchers 每个人一开始都可以按需创建,无论哪个 SPECIAL 节点,我们正在解析, 把它们储存在一长串 以防我们看到相同的 SPECIAL 又是节点.
动态解析器想法
什么定义了“典型的”html文档? 一组解析“ 典型” html 文档的指令对于“ 非典型” html 文档可能较慢, 其差异很大 probabilityOfPair_i 分发。 如果解析器可以利用当前文档的对子分布来动态地改变其飞行时的状况呢? 例如,如果我们被原始文本所压倒,那么检查状态会更快 TEXT 先说 或者,如果我们的文件几乎 没有 原始文本,如果状态是 TEXT 最后一个 虽然动态剖析器听起来很有趣,但如果它增加了一吨间接费用,就可能不值得执行.
说明
大文本节点
由于解析器在内存中一直等待节点的数据,一个大的文本节点会引发内存问题. 在绝大多数情况下,你不会碰到比块本身更大的文本节点。 如果这成为问题,可修改解析器以接受 maxTextSize 用于块块单个文本节点的参数。 现在,这是一个极端的边缘案件,没有处理.
文档类型
道克类型节点作为任何其他节点处理. 投入 "<!DOCTYPE html>" 将产生以下数据事件: {name: "!DOCTYPE", data: {html: ""}}
无值的属性
没有值的标记属性不被特殊处理;一个没有值的密钥会在节点的数据字段中有一个空字符串值. 我可以执行 true 作为默认值,但这似乎是可以很容易地由呼叫者完成的.