整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HtmlParse:一款超轻量级的HTML文件解析和爬取工具

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,用于指定获取超链接标签ahref**属性。

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(二)结束标签)

如有错误,欢迎指正,谢谢。