html解析器工作原理

当前位置 : 首页 > 网页制作 > CSS > html解析器工作原理

html解析器工作原理

来源: 作者: 时间:2016-02-17 10:24
先看一个简单的html文档[html] html head titletest/title /head body div style=height: 100px; border: 1px solid #ff0000; font-size: 24px; font...
先看一个简单的html文档
[html]  
<html>  
    <head>  
        <title>test</title>  
    </head>  
    <body>  
        <div style="height: 100px; border: 1px solid #ff0000; font-size: 24px; font-weight: bold;">Hello World!</div>  
    </body>  
</html>  
 
1. 首先用一个类来描述一个节点
[java]  
public class Node{  
    private String nodeName;  
    private int nodeType;  
    private Map<String, String> attributes;  
    private List<Node> childNodes;  
    private Node parent;  
  
    // getter & setter  
    ...  
}  
 
然后我们开始对输入内容进行解析,解析的过程其实就是解析字符串的过程,为了便于解析先把源字符串封装成一个HtmlStream对象.
[java]  
String source = IO.read(new File("test.html"), "UTF-8");  
HtmlStream stream = new HtmlStream(source);  
  
char c;  
int i = 0;  
  
// 忽略掉文档开头的空格  
while((i == stream.read()) != -1)  
{  
    if(i != ' ')  
    {  
        // 回退一个字符  
        stream.back();  
        break;  
    }  
}  
  
Stack<Node> stack = new Stack<Node>();  
StringBuilder buffer = new StringBuilder();  
  
// 为了便于程序,先分成两部分  
// 第一部分解析节点,通过startTag来完成  
// 第二部分读取文本内容,遇到<的时候终止  
while((i == stream.read()) != -1)  
{  
    if(i == '<'){  
        this.startTag();  
    }  
    else if{  
        buffer.append((char)i);  
  
        while((i == stream.read()) != -1)  
        {  
            if(i == '<')  
            {  
                stream.back();  
                break;  
            }  
  
            buffer.append((char)i);  
        }  
  
        this.pushTextNode(stack, buffer.toString());  
        buffer.setLength(0);  
    }  
}  
 
再来看startTag
[java]  
public void startTag(Stack<Node> stack)  
{  
    int i = this.stream.peek();  
  
    if(i == '/')  
    {  
        String nodeName = this.readNodeName();  
        this.endTag(stack, nodeName);  
    }  
    else if(i == '!')  
    {  
        // 注释...  
    }  
    else  
    {  
        String nodeName = this.readNodeName();  
  
        if(nodeName.length > 0)  
        {  
            Node node = new Node(nodeName);  
            this.readAttributes(node.getAttributes());  
            this.pushNode(stack, node);  
        }  
        else  
        {  
            this.pushTextNode(stack, "<");  
        }  
    }  
}  
  
// 当标签结束时  
public void endTag(Stack<Node> stack, String nodeName){  
    Node node = stack.peek();  
  
    if(node == null)  
    {  
        // 读取到>, 并写入文本节点, 略去  
        this.pushTextNode(stack, "<" + nodeName + ...);  
        return;  
    }  
  
    if(node.getNodeName().equalsIgnoreCase(nodeName))  
    {  
        stack.pop();  
        // 其他处理...  
    }  
}  
 
先说一下栈的结构, 这个是解析中一个很重要的东西.
当我们用Node这个类来描述一个节点的时候,很容易把一个树形结构的数据串起来, 只需要建立父子关系即可。
但是当解析一个html文件的时候,怎么把读取到的一个结束节点跟之前读取的n个开始节点中的某一对应呢?
为了简单的说明这个问题,可以用类json格式的数据来表示一下:
var array = []; // 定义一个数组
现在假设这个数组里面有4个节点,它描述了下面的一个html片段
<html><head><title>test</title></head><body></body></html>
现在整个数组是这样:
[{node: html}, {node: head}, {node: title}, {text: test}, {node: /title}, {node: /head}, {node: body}, {node: /body}, {node: /html}]
这样看起来它其实跟原始的数据没什么区别,只不过变了中描述方式。
现在我们用一个指针指向这个数组的末端, 并且始终指向末端, 就变成了一种栈结构. 当某一个节点结束时,取指针位置的元素,正常情况下,这个元素一定是这个结束节点对应的开始节点.
如果结束节点的节点名跟指针位置对应的节点的节点名不一致,那就说明某一个节点没有正确闭合, 这个时候需要一些容错处理, 如果是xml解析直接抛异常即可.
 
 
现在详细的描述一下这个步骤:
先解析第一个节点, 这个节点是<html>, 并且是开始节点, 把它压入栈, 现在的栈中的数组元素应该是这样的: 
[{node: html}]
只有一个节点,指针指向0
然后是第二个节点, 它也是一个开始节点,因此也压入栈, 现在的栈中的数组元素应该是这样的: 
[{node: html}, {node: head}]
依次类推, 直到遇到文本节点和结束节点, 当遇到文本节点的时候, 栈中的元素如下:
[{node: html}, {node: head}, {node: title}]
现在处理下一个,发现是个文本节点, 节点内容是: test, 先从栈顶弹出一个节点,如果是文本节点,直接把该文本追加,如果是元素节点
[java]  
Node node = stack.pop();  
  
// 文本节点  
if(node.nodeType == TEXT){  
    node.append("test");  
}  
else if(node.nodeType == NODE){  
    TextNode textNode = new TextNode('test');  
    stack.push(node);  
    stack.push(textNode);  
    node.appendChild(textNode);  
}  
 
现在的栈结构如下:
[{node: html}, {node: head}, {node: title}, {text: test}]
 
 
接着下一步,下一个节点是一个结束节点</title>, 首先弹出所有的文本节点, 直到遇到元素节点
[java] 
void popNode(Stack<Node> stack, String nodeName){  
    Node node = null;  
    // 如果是文本节点  
    while((node = stack.pop()) != null && node.nodeType == TEXT);  
  
    if(node != null)  
    {  
        if(node.nodeName.equalsIgnoreCase(nodeName))  
        {  
            stack.pop();  
        }  
    }  
    else  
    {  
        // 容错处理, 说明遇到了一个没有开始节点的结束节点  
    }  
}  
 
正常情况下现在的栈应该是这样的:
[{node: html}, {node: head}]
接着下一步,下一个节点仍然是一个结束节点</head>, 跟上一步的处理一样, 处理完之后的栈应该是这样的:
[{node: html}]
下一步, 节点是一个开始节点<body>, 压入栈:
[{node: html}, {node: body}]
接着下一步,下一个节点仍然是一个结束节点</body>, 跟上面的处理一样, 处理完之后的栈应该是这样的:
[{node: html}]
接着下一步,下一个节点仍然是一个结束节点</html>, 跟上面的处理一样, 处理完之后的栈应该是这样的:
[]
正常情况下,所有的节点处理完, 栈应该是空的.
为了尽可能的说明处理流程,尽可能的省去了一些容错处理的代码. 另外自己编码实现的时候还有很多其他的问题需要考虑。
最后这种处理方式性能稍微有点差, 以后有时间再介绍另外一种方式。
Tag:
网友评论

<