tmlParse 是一款基于windwos平台的HTML文档解析工具,可快速构建DOM树,从而轻松实现网页元素的爬取工作。DOM树就是一个HTML文档的节点树,每个节点由:标签(Tag)、属性(Attribute)、文本(Text)三个值来描述。
所谓的HTML文档解析,指的就是如何构建一颗DOM树,只有成功构建出DOM树,才有可能进行后续的数据爬取和分析工作。显然,构建DOM树是比较复杂的过程,因为不是每一个HTML文档都会严格按照规范来书写,因此解析过程需要具有一定容错能力。此外,解析效率也是一个需要考虑的因素,也就是说最好通过一次文档扫描即可建立起DOM树,而不是反复扫描。
下面是HtmlParse介绍。
1、绿色纯天然,无任何第三方依赖库,文件大小不到150K; 2、解析速度快,具有一定的HTML语法容错能力,可快速将HMTL文档解析为DOM树; 3、基于命令行参数,可通过不同参数获取指定TAG的属性值和文本内容,从而实现网页爬取功能; 4、可将爬取数据输出为json格式,方便第三方程序进一步分析和使用; 5、可爬取script脚本到指定的js文件中;
下载地址:http://softlee.cn/HtmlParse.zip
HtmlParse HtmlPathFile -tag TagName [-attr] [Attribute] [-o] [JsonPathFile]
解析指定的HTML文档,并将文档中指定的标签及属性输出到指定文件中。
HtmlPathFile:必选参数,要解析的HTML文档路径名,如果文件路径中有空格,可使用双引号将文件路径包含;
-tag:必选参数,用于指定要抓取的HTML标签名称; -attr:可选参数,用于指定标签的属性值,如果不指定,则返回该标签的所有属性值; -o:可选参数,用于指定抓取内容输出的文件,可将抓取的内容保存为json格式的文件。 如果该参数不指定,则进行控制台输出。 如果抓取的是script、style则会保存为js格式文件。
如果要抓取doctype,可使用-tag doctype,将整个doctype内容获取。此时将会忽略-attr指定的任何属性值。
1、爬取网页中所有超链接
HtmlParse c:/sina.html -tag a -attr href -o c:/sina.json
解析C盘下的sina.html文档,并提取该文档中的所有超链接到sina.json文件中。其中**-tag a -attr href,用于指定获取超链接标签a的href**属性。
2、爬取网页中所有图片链接
HtmlParse c:/sina.html -tag img -attr src -o c:/sina.json
解析C盘下的sina.html文档,并提取该文档中的所有图片链接到sina.json文件中。
3、爬取网页中所有脚本
HtmlParse c:/sina.html -tag script -o c:/sina.js
解析C盘下的sina.html文档,并提取该文档中的所有脚本函数到sina.js文件中。
如果通过-o参数指定输出文件,则会生成一个json格式的文档。 TagName为爬取的标签名称,比如超链接的a,其值是一个json数组,数组中的每个内容为Json对象,每个Json对象,有属性和文本构成。如果-attr 指定了要爬取的属性,则AttrName为指定的属性名称,比如href或src。text为该标签的文本内容,有些标签不存在文本内容,比如img、meta等,则该值为空。json格式如下:
{
"TagName":
{
{"AttrName":"AttrValue1", "text":"text1"}
{"AttrName":"AttrValue1", "text":"text2"}
}
}
下面是一个sina网页的所有超链接json
{
"a": [{
"href": "javascript:;",
"text": "设为首页"
}, {
"href": "javascript:;",
"text": "我的菜单"
}, {
"href": "https://sina.cn/",
"text": "手机新浪网"
}, {
"href": "",
"text": "移动客户端"
}, {
"href": "https://c.weibo.cn/client/guide/download",
"text": "新浪微博"
}, {
"href": "https://so.sina.cn/palmnews/web-sinanews-app-download.d.html",
"text": "新浪新闻"
}, {
"href": "https://finance.sina.com.cn/mobile/comfinanceweb.shtml",
"text": "新浪财经"
}, {
"href": "https://m.sina.com.cn/m/sinasports.shtml",
"text": "新浪体育"
}, {
"href": "https://tousu.sina.com.cn/about_app/index?frompage=heimaopc",
"text": "黑猫投诉"
}, {
"href": "http://blog.sina.com.cn/lm/z/app/",
"text": "新浪博客"
}, {
"href": "https://games.sina.com.cn/o/kb/12392.shtml",
"text": "新浪游戏"
}, {
"href": "https://zhongce.sina.com.cn/about/app",
"text": "新浪众测"
}, {
"href": "https://mail.sina.com.cn/client/mobile/index.php?suda-key=mail_app&suda-value=login",
"text": "新浪邮箱客户端"
}, {
"href": "javascript:;",
"text": "关闭置顶"
}, {
来源:https://www.cnblogs.com/softlee/p/16374079.html
们上一章分析了html解析开始标签的处理,我们本章来看一下结束标签的处理。
以下就是匹配结束标签代码:
// End tag:
const endTagMatch = html.match(endTag) // 匹配 </
if (endTagMatch) {
// 开始位置
const curIndex = index
// 减去对应的长度
advance(endTagMatch[0].length)
// 解析结束标签
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
我们先简单说一下 parseEndTag 的分析,为了减少篇幅我们就只帖出关键代码了
1、匹配 “stack” 数组里面相同标签名称的对象
// 转换为小写
lowerCasedTagName = tagName.toLowerCase()
// 匹配相同的tag标签
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
2、调用 “end” 函数
if (options.end) {
options.end(stack[i].tag, start, end)
}
那么我们来看一下 “end” 函数的代码:
end (tag, start, end) {
// 取出 stack 数组的最后一项
const element = stack[stack.length - 1]
// 删除数组的最后一项
stack.length -= 1
// 当前标签的附件标签
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 标签结束位置
element.end = end
}
closeElement(element)
}
可看到其实主要的工作是在closeElement 函数,我们简单总结一下该函数的功能,先看代码:
function closeElement (element) {
trimEndingWhitespace(element)
c
// tree management
if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
if (process.env.NODE_ENV !== 'production') {
checkRootConstraints(element)
}
addIfCondition(root, {
exp: element.elseif,
block: element
})
} else if (process.env.NODE_ENV !== 'production') {
warnOnce(
`Component template should contain exactly one root element. ` +
`If you are using v-if on multiple elements, ` +
`use v-else-if to chain them instead.`,
{ start: element.start }
)
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
} else {
if (element.slotScope) {
// scoped slot
// keep it in the children list so that v-else(-if) conditions can
// find it as the prev node.
const name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
}
currentParent.children.push(element)
element.parent = currentParent
}
}
// final children cleanup
// filter out scoped slots
element.children = element.children.filter(c => !(c: any).slotScope)
// remove trailing whitespace node again
trimEndingWhitespace(element)
// check pre state
if (element.pre) {
inVPre = false
}
if (platformIsPreTag(element.tag)) {
inPre = false
}
// apply post-transforms
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
}
接下来我们拆分一下该函数。
1、移除子节点是空白节点的标签(node.type === 3 && node.text === '')
代码如下:
trimEndingWhitespace(element)
2、处理该节点的属性以及指令
代码如下:
if (!inVPre && !element.processed) {
element = processElement(element, options)
}
processElement 函数有如下处理:
1、处理v-bind指令
2、处理 ref 属性
3、处理 slot 插槽一个 slot 标签
4、处理component 组件
5、处理标签属性(attr)
6、是否存在 style和 :style冲突
7、是否存在 class和 :class冲突
class
我们来看看以上代码的作用:
允许根元素具有v-if、v-else-if和v-else。
checkRootConstraints(element)
checkRootConstraints 函数,我们已经看过很多遍了,验证不能是 “slot” 和 “template” 标签,并且不是存在 v-for 指令,这个大家都知道,只能有一个根元素。
if (element.elseif || element.else) {
processIfConditions(element, currentParent)
}
如果 “elseif”指令存在或者“else” 指令存在,就需要 验证是否存在 "v-if" 指令是存在存在,因为他们是对应的。
element.children = element.children.filter(c => !(c: any).slotScope)
处理子元素的作用域。
if (platformIsPreTag(element.tag)) {
inPre = false
}
是否为平台的 pre 标签。
for (let i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options)
}
最后调用 postTransforms 函数,如果存在的话,
这个我也没搞明白怎么用的,如果有人知道欢迎指点迷津啊,感激不尽。
于parseHTML函数代码实在过于庞大,我这里就不一次性贴出源代码了,大家可以前往(https://github.com/vuejs/vue/blob/dev/src/compiler/parser/html-parser.js)查看源代码。
我们来总结一下该函数的主要功能:
1、匹配标签的 "<" 字符
匹配的标签名称不能是:script、style、textarea
有如下情况:
1、注释标签 /^<!\--/
2、条件注释 /^<!\[/
3、html文档头部 /^<!DOCTYPE [^>]+>/i
4、标签结束 /^<\/ 开头
5、标签开始 /^</ 开头
然后开始匹配标签的属性包括w3的标准属性(id、class)或者自定义的任何属性,以及vue的指令(v-、:、@)等,直到匹配到 "/>" 标签的结尾。然后把已匹配的从字符串中删除,一直 while 循环匹配。
解析开始标签函数代码:
function parseStartTag () {
// 标签的开始 如<div
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1], // 标签名称
attrs: [], // 标签属性
start: index // 开始位置
}
// 减去已匹配的长度
advance(start[0].length)
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index
v
advance(attr[0].length)
attr.end = index
match.attrs.push(attr) // 把匹配到的属性添加到attrs数组
}
if (end) { // 标签的结束符 ">"
match.unarySlash = end[1]
advance(end[0].length) // 减去已匹配的长度
match.end = index // 结束位置
return match
}
}
}
处理过后结构如下:
接下来就是处理组合属性,调用 “handleStartTag” 函数
function handleStartTag (match) {
const tagName = match.tagName // 标签名称
const unarySlash = match.unarySlash // 一元标签
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
// 解析标签结束
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// 是否为一元标签
const unary = isUnaryTag(tagName) || !!unarySlash
const l = match.attrs.length
// 标签属性集合
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines
attrs[i] = {
name: args[1], // 属性名称
value: decodeAttr(value, shouldDecodeNewlines) // 属性值
}
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
// 开始位置
attrs[i].start = args.start + args[0].match(/^\s*/).length
// 结束位置
attrs[i].end = args.end
}
}
if (!unary) {
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName
}
// 调用start函数
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
我们简单说一下最后调用的start函数的作用:
1、判断是否为svg标签,并处理svg在ie下的兼容性问题
2、遍历标签属性,验证其名称是否有效
3、标签名是否为 style 或者 script ,如果在服务端会提示warn警告
4、检查属性是否存在 v-for、v-if、v-once指令
5、如果是更元素就验证其合法性,不能是 slot 和 template 标签,不能存在 v-for指令
以上就是界面html模板的开始标签的分析,接下来我们来分析如何匹配结束标签。
请看:Vue源码全面解析三十 parseHTML函数(解析html(二)结束标签)
如有错误,欢迎指正,谢谢。
*请认真填写需求信息,我们会在24小时内与您取得联系。