手搓HTML解析器:500行代码实现完整的DOM树构建

手搓HTML解析器:500行代码实现完整的DOM树构建

引言:为什么需要理解HTML解析器?

在Web开发中,DOM(文档对象模型)是我们与网页交互的核心接口。现代前端框架如React、Vue都构建在DOM之上,但很少有人真正理解浏览器是如何将HTML文本转换为可操作的DOM树的。本文将带领你从头实现一个完整的HTML解析器,用约500行代码构建出功能完整的DOM树。

HTML解析是一个复杂但迷人的过程,涉及词法分析、语法分析、树构建等多个阶段。通过自己实现解析器,我们能更深入理解浏览器工作原理,提升调试能力,并掌握编译原理的基本概念。

第一部分:HTML解析的基本原理

1.1 HTML解析概述

HTML解析器的工作是将HTML字符串转换为DOM树。这个过程分为三个阶段:

  1. 词法分析:将HTML字符串分解为令牌(tokens)

  2. 语法分析:根据令牌构建节点

  3. 树构建:将节点组织成树状结构

1.2 解析器设计思路

我们将采用有限状态机(FSM)的方法实现解析器。状态机根据当前字符和解析状态决定下一步操作,这种方法简洁高效,非常适合解析结构化文本。

第二部分:HTML解析器完整实现

下面是完整的HTML解析器实现代码,约500行,包含详细的注释:

javascript

/** * 手搓HTML解析器 - 完整DOM树构建实现 * 作者:Web开发工程师 * 版本:1.0 */ // 定义节点类型常量 const NodeType = { ELEMENT_NODE: 1, TEXT_NODE: 3, COMMENT_NODE: 8, DOCUMENT_NODE: 9, DOCUMENT_FRAGMENT_NODE: 11 }; // 定义解析器状态 const ParserState = { DATA: 'DATA', // 初始状态,处理文本 TAG_OPEN: 'TAG_OPEN', // 遇到<,可能是开始标签或结束标签 TAG_NAME: 'TAG_NAME', // 读取标签名 BEFORE_ATTRIBUTE_NAME: 'BEFORE_ATTRIBUTE_NAME', // 标签名后,属性名前 ATTRIBUTE_NAME: 'ATTRIBUTE_NAME', // 读取属性名 AFTER_ATTRIBUTE_NAME: 'AFTER_ATTRIBUTE_NAME', // 属性名后 BEFORE_ATTRIBUTE_VALUE: 'BEFORE_ATTRIBUTE_VALUE', // 属性名后,值前 ATTRIBUTE_VALUE_DOUBLE_QUOTED: 'ATTRIBUTE_VALUE_DOUBLE_QUOTED', // 双引号属性值 ATTRIBUTE_VALUE_SINGLE_QUOTED: 'ATTRIBUTE_VALUE_SINGLE_QUOTED', // 单引号属性值 ATTRIBUTE_VALUE_UNQUOTED: 'ATTRIBUTE_VALUE_UNQUOTED', // 无引号属性值 AFTER_ATTRIBUTE_VALUE: 'AFTER_ATTRIBUTE_VALUE', // 属性值后 SELF_CLOSING_TAG: 'SELF_CLOSING_TAG', // 自闭合标签 END_TAG_OPEN: 'END_TAG_OPEN', // 结束标签 COMMENT: 'COMMENT', // 注释 DOCTYPE: 'DOCTYPE' // DOCTYPE声明 }; // HTML自闭合标签列表 const VOID_TAGS = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]); /** * DOM节点基类 */ class Node { constructor(nodeType, nodeName) { this.nodeType = nodeType; this.nodeName = nodeName; this.childNodes = []; this.parentNode = null; } appendChild(node) { node.parentNode = this; this.childNodes.push(node); } removeChild(node) { const index = this.childNodes.indexOf(node); if (index !== -1) { this.childNodes.splice(index, 1); node.parentNode = null; } } get firstChild() { return this.childNodes[0] || null; } get lastChild() { return this.childNodes[this.childNodes.length - 1] || null; } get nextSibling() { if (!this.parentNode) return null; const siblings = this.parentNode.childNodes; const index = siblings.indexOf(this); return siblings[index + 1] || null; } get previousSibling() { if (!this.parentNode) return null; const siblings = this.parentNode.childNodes; const index = siblings.indexOf(this); return siblings[index - 1] || null; } } /** * 文档节点 */ class Document extends Node { constructor() { super(NodeType.DOCUMENT_NODE, '#document'); this.documentElement = null; } } /** * 元素节点 */ class Element extends Node { constructor(tagName) { super(NodeType.ELEMENT_NODE, tagName.toUpperCase()); this.tagName = tagName.toLowerCase(); this.attributes = {}; this.className = ''; this.id = ''; this.style = {}; } setAttribute(name, value) { this.attributes[name] = value; // 特殊处理class和id if (name === 'class') { this.className = value; } else if (name === 'id') { this.id = value; } } getAttribute(name) { return this.attributes[name] || null; } removeAttribute(name) { delete this.attributes[name]; if (name === 'class') { this.className = ''; } else if (name === 'id') { this.id = ''; } } get innerHTML() { let html = ''; for (const child of this.childNodes) { if (child.nodeType === NodeType.ELEMENT_NODE) { html += child.outerHTML; } else if (child.nodeType === NodeType.TEXT_NODE) { html += this.escapeHTML(child.nodeValue); } } return html; } get outerHTML() { const tagName = this.tagName; let attrs = ''; // 构建属性字符串 for (const [name, value] of Object.entries(this.attributes)) { attrs += ` ${name}="${this.escapeHTML(value)}"`; } // 自闭合标签处理 if (VOID_TAGS.has(tagName)) { return `<${tagName}${attrs}>`; } return `<${tagName}${attrs}>${this.innerHTML}</${tagName}>`; } escapeHTML(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } // 简单的选择器支持 querySelector(selector) { // 简化实现,仅支持tag、#id、.class选择器 if (selector.startsWith('#')) { const id = selector.slice(1); return this.querySelectorAll(`[id="${id}"]`)[0] || null; } else if (selector.startsWith('.')) { const className = selector.slice(1); return this.querySelectorAll(`[class*="${className}"]`)[0] || null; } else { return this.querySelectorAll(selector)[0] || null; } } querySelectorAll(selector) { const results = []; this._querySelectorAll(selector, results); return results; } _querySelectorAll(selector, results) { // 检查当前元素是否匹配 let match = false; if (selector.startsWith('#')) { const id = selector.slice(1); match = this.id === id; } else if (selector.startsWith('.')) { const className = selector.slice(1); match = this.className.includes(className); } else if (selector.startsWith('[') && selector.endsWith(']')) { // 属性选择器简化实现 const attrSelector = selector.slice(1, -1); if (attrSelector.includes('=')) { const [name, value] = attrSelector.split('='); const cleanValue = value.replace(/["']/g, ''); match = this.getAttribute(name) === cleanValue; } else { match = this.getAttribute(attrSelector) !== null; } } else { // 标签选择器 match = this.tagName === selector.toLowerCase(); } if (match) { results.push(this); } // 递归检查子元素 for (const child of this.childNodes) { if (child.nodeType === NodeType.ELEMENT_NODE) { child._querySelectorAll(selector, results); } } } } /** * 文本节点 */ class Text extends Node { constructor(text) { super(NodeType.TEXT_NODE, '#text'); this.nodeValue = text; this.textContent = text; } } /** * 注释节点 */ class Comment extends Node { constructor(text) { super(NodeType.COMMENT_NODE, '#comment'); this.nodeValue = text; } } /** * HTML解析器核心类 */ class HTMLParser { constructor() { this.reset(); } // 重置解析器状态 reset() { this.state = ParserState.DATA; this.currentToken = null; this.currentAttribute = { name: '', value: '' }; this.stack = []; this.document = new Document(); this.currentNode = this.document; this.buffer = ''; this.commentBuffer = ''; this.pos = 0; this.html = ''; } // 主解析方法 parse(htmlString) { this.reset(); this.html = htmlString; while (this.pos < this.html.length) { const char = this.html[this.pos]; switch (this.state) { case ParserState.DATA: this.parseData(char); break; case ParserState.TAG_OPEN: this.parseTagOpen(char); break; case ParserState.TAG_NAME: this.parseTagName(char); break; case ParserState.BEFORE_ATTRIBUTE_NAME: this.parseBeforeAttributeName(char); break; case ParserState.ATTRIBUTE_NAME: this.parseAttributeName(char); break; case ParserState.AFTER_ATTRIBUTE_NAME: this.parseAfterAttributeName(char); break; case ParserState.BEFORE_ATTRIBUTE_VALUE: this.parseBeforeAttributeValue(char); break; case ParserState.ATTRIBUTE_VALUE_DOUBLE_QUOTED: this.parseAttributeValueDoubleQuoted(char); break; case ParserState.ATTRIBUTE_VALUE_SINGLE_QUOTED: this.parseAttributeValueSingleQuoted(char); break; case ParserState.ATTRIBUTE_VALUE_UNQUOTED: this.parseAttributeValueUnquoted(char); break; case ParserState.AFTER_ATTRIBUTE_VALUE: this.parseAfterAttributeValue(char); break; case ParserState.SELF_CLOSING_TAG: this.parseSelfClosingTag(char); break; case ParserState.END_TAG_OPEN: this.parseEndTagOpen(char); break; case ParserState.COMMENT: this.parseComment(char); break; case ParserState.DOCTYPE: this.parseDoctype(char); break; } this.pos++; } // 处理剩余的文本缓冲区 this.flushTextBuffer(); return this.document; } // 解析数据状态 parseData(char) { if (char === '<') { // 遇到<,切换到标签打开状态 this.flushTextBuffer(); this.state = ParserState.TAG_OPEN; } else { // 收集文本字符 this.buffer += char; } } // 解析标签打开状态 parseTagOpen(char) { if (char === '/') { // 结束标签 this.state = ParserState.END_TAG_OPEN; } else if (char === '!') { // 可能是注释或DOCTYPE if (this.html.substr(this.pos, 2) === '!--') { this.state = ParserState.COMMENT; this.pos += 2; // 跳过"!--" } else if (this.html.substr(this.pos, 7).toUpperCase() === '!DOCTYPE') { this.state = ParserState.DOCTYPE; this.pos += 6; // 跳过"!DOCTYPE" } } else if (/[a-zA-Z]/.test(char)) { // 开始标签 this.currentToken = { type: 'start', tagName: '', attributes: {}, selfClosing: false }; this.state = ParserState.TAG_NAME; this.parseTagName(char); // 处理当前字符 } } // 解析标签名 parseTagName(char) { if (/[\s/>]/.test(char)) { // 标签名结束 this.currentToken.tagName = this.currentToken.tagName.toLowerCase(); if (char === '>') { this.emitToken(); this.state = ParserState.DATA; } else if (/\s/.test(char)) { this.state = ParserState.BEFORE_ATTRIBUTE_NAME; } else if (char === '/') { this.state = ParserState.SELF_CLOSING_TAG; } } else { this.currentToken.tagName += char; } } // 解析属性名前状态 parseBeforeAttributeName(char) { if (/\s/.test(char)) { // 忽略空白字符 return; } else if (char === '>') { this.emitToken(); this.state = ParserState.DATA; } else if (char === '/') { this.state = ParserState.SELF_CLOSING_TAG; } else { // 开始属性名 this.currentAttribute.name = ''; this.currentAttribute.value = ''; this.state = ParserState.ATTRIBUTE_NAME; this.parseAttributeName(char); // 处理当前字符 } } // 解析属性名 parseAttributeName(char) { if (/\s/.test(char) || char === '>' || char === '/' || char === '=') { // 属性名结束 this.currentAttribute.name = this.currentAttribute.name.toLowerCase(); if (char === '=') { this.state = ParserState.BEFORE_ATTRIBUTE_VALUE; } else if (/\s/.test(char)) { this.state = ParserState.AFTER_ATTRIBUTE_NAME; } else if (char === '>') { this.emitAttribute(); this.emitToken(); this.state = ParserState.DATA; } else if (char === '/') { this.emitAttribute(); this.state = ParserState.SELF_CLOSING_TAG; } } else { this.currentAttribute.name += char; } } // 解析属性名后状态 parseAfterAttributeName(char) { if (/\s/.test(char)) { return; } else if (char === '=') { this.state = ParserState.BEFORE_ATTRIBUTE_VALUE; } else if (char === '>') { this.emitAttribute(); this.emitToken(); this.state = ParserState.DATA; } else if (char === '/') { this.emitAttribute(); this.state = ParserState.SELF_CLOSING_TAG; } else { // 开始新属性 this.emitAttribute(); this.currentAttribute.name = ''; this.currentAttribute.value = ''; this.state = ParserState.ATTRIBUTE_NAME; this.parseAttributeName(char); } } // 解析属性值前状态 parseBeforeAttributeValue(char) { if (/\s/.test(char)) { return; } else if (char === '"') { this.state = ParserState.ATTRIBUTE_VALUE_DOUBLE_QUOTED; } else if (char === "'") { this.state = ParserState.ATTRIBUTE_VALUE_SINGLE_QUOTED; } else if (char === '>') { this.emitAttribute(); this.emitToken(); this.state = ParserState.DATA; } else { this.state = ParserState.ATTRIBUTE_VALUE_UNQUOTED; this.parseAttributeValueUnquoted(char); } } // 解析双引号属性值 parseAttributeValueDoubleQuoted(char) { if (char === '"') { // 属性值结束 this.emitAttribute(); this.state = ParserState.AFTER_ATTRIBUTE_VALUE; } else { this.currentAttribute.value += char; } } // 解析单引号属性值 parseAttributeValueSingleQuoted(char) { if (char === "'") { // 属性值结束 this.emitAttribute(); this.state = ParserState.AFTER_ATTRIBUTE_VALUE; } else { this.currentAttribute.value += char; } } // 解析无引号属性值 parseAttributeValueUnquoted(char) { if (/\s/.test(char) || char === '>') { // 属性值结束 this.emitAttribute(); if (char === '>') { this.emitToken(); this.state = ParserState.DATA; } else { this.state = ParserState.BEFORE_ATTRIBUTE_NAME; } } else { this.currentAttribute.value += char; } } // 解析属性值后状态 parseAfterAttributeValue(char) { if (/\s/.test(char)) { this.state = ParserState.BEFORE_ATTRIBUTE_NAME; } else if (char === '>') { this.emitToken(); this.state = ParserState.DATA; } else if (char === '/') { this.state = ParserState.SELF_CLOSING_TAG; } } // 解析自闭合标签 parseSelfClosingTag(char) { if (char === '>') { this.currentToken.selfClosing = true; this.emitToken(); this.state = ParserState.DATA; } } // 解析结束标签打开状态 parseEndTagOpen(char) { if (/[a-zA-Z]/.test(char)) { this.currentToken = { type: 'end', tagName: '' }; this.state = ParserState.TAG_NAME; this.parseTagName(char); // 处理当前字符 } } // 解析注释 parseComment(char) { if (char === '>' && this.html.substr(this.pos - 2, 2) === '--') { // 注释结束 const comment = new Comment(this.commentBuffer.slice(0, -2)); // 移除最后的'--' this.currentNode.appendChild(comment); this.commentBuffer = ''; this.state = ParserState.DATA; } else { this.commentBuffer += char; } } // 解析DOCTYPE(简化处理) parseDoctype(char) { if (char === '>') { this.state = ParserState.DATA; } } // 发出属性到当前token emitAttribute() { if (this.currentAttribute.name) { this.currentToken.attributes[this.currentAttribute.name] = this.currentAttribute.value; } } // 发出token,构建DOM节点 emitToken() { const token = this.currentToken; if (token.type === 'start') { // 创建元素节点 const element = new Element(token.tagName); // 设置属性 for (const [name, value] of Object.entries(token.attributes)) { element.setAttribute(name, value); } // 将元素添加到当前节点 this.currentNode.appendChild(element); if (!token.selfClosing && !VOID_TAGS.has(token.tagName.toLowerCase())) { // 非自闭合标签,推入栈并设为当前节点 this.stack.push(this.currentNode); this.currentNode = element; } // 如果是自闭合标签或void标签,不改变当前节点 } else if (token.type === 'end') { // 结束标签,弹出栈 if (this.stack.length > 0) { this.currentNode = this.stack.pop(); } } this.currentToken = null; } // 清空文本缓冲区,创建文本节点 flushTextBuffer() { if (this.buffer.trim()) { const textNode = new Text(this.buffer); this.currentNode.appendChild(textNode); } this.buffer = ''; } } /** * 工具函数:创建DOM树的可视化表示 */ function visualizeDOM(node, depth = 0) { const indent = ' '.repeat(depth); let result = ''; if (node.nodeType === NodeType.DOCUMENT_NODE) { result += `${indent}#document\n`; for (const child of node.childNodes) { result += visualizeDOM(child, depth + 1); } } else if (node.nodeType === NodeType.ELEMENT_NODE) { // 构建属性字符串 const attrs = Object.entries(node.attributes) .map(([key, value]) => `${key}="${value}"`) .join(' '); result += `${indent}<${node.tagName}${attrs ? ' ' + attrs : ''}>\n`; for (const child of node.childNodes) { result += visualizeDOM(child, depth + 1); } } else if (node.nodeType === NodeType.TEXT_NODE) { const text = node.nodeValue.trim(); if (text) { result += `${indent}"${text}"\n`; } } else if (node.nodeType === NodeType.COMMENT_NODE) { result += `${indent}<!--${node.nodeValue}-->\n`; } return result; } /** * 简化版HTML解析函数(对外暴露的API) */ function parseHTML(htmlString) { const parser = new HTMLParser(); return parser.parse(htmlString); } // 导出模块 if (typeof module !== 'undefined' && module.exports) { module.exports = { parseHTML, NodeType, Element, Text, Comment, Document, visualizeDOM }; }

第三部分:解析器使用示例和测试

下面是解析器的使用示例和测试代码:

javascript

// 测试HTML解析器 function testHTMLParser() { console.log('=== HTML解析器测试 ===\n'); // 测试用例1:简单HTML结构 const html1 = ` <div class="container"> <h1 id="title">Hello World</h1> <p>这是一个段落文本</p> <img src="image.jpg" alt="示例图片"> <br> <!-- 这是一个注释 --> </div> `; console.log('测试用例1:简单HTML结构'); const doc1 = parseHTML(html1); console.log(visualizeDOM(doc1)); // 测试用例2:嵌套结构 const html2 = ` <ul class="list"> <li>项目1</li> <li>项目2</li> <li>项目3</li> </ul> `; console.log('\n测试用例2:嵌套列表'); const doc2 = parseHTML(html2); console.log(visualizeDOM(doc2)); // 测试用例3:自闭合和void标签 const html3 = ` <input type="text" name="username" value="test"> <input type="checkbox" checked> <hr> <meta charset="UTF-8"> `; console.log('\n测试用例3:自闭合和void标签'); const doc3 = parseHTML(html3); console.log(visualizeDOM(doc3)); // 测试用例4:属性值含特殊字符 const html4 = ` <div data-info='{"name": "test", "value": 123}'> <a href="/path?q=search&sort=desc">链接</a> </div> `; console.log('\n测试用例4:属性值含特殊字符'); const doc4 = parseHTML(html4); console.log(visualizeDOM(doc4)); // 测试用例5:复杂嵌套和混合内容 const html5 = ` <article> <header> <h1>文章标题</h1> <p>发布时间:<time datetime="2023-10-01">2023年10月1日</time></p> </header> <section> <p>这是第一段内容。</p> <p>这是第二段内容,包含<strong>强调文本</strong>和<em>斜体文本</em>。</p> </section> <footer> <p>文章结束</p> </footer> </article> `; console.log('\n测试用例5:复杂嵌套和混合内容'); const doc5 = parseHTML(html5); console.log(visualizeDOM(doc5)); // 测试querySelector功能 console.log('\n=== 选择器测试 ==='); const root = doc5.documentElement || doc5.firstChild; if (root) { const header = root.querySelector('header'); console.log('找到header元素:', header ? '是' : '否'); const strongElements = root.querySelectorAll('strong'); console.log(`找到${strongElements.length}个strong元素`); const paragraphs = root.querySelectorAll('p'); console.log(`找到${paragraphs.length}个p元素`); } // 测试outerHTML功能 console.log('\n=== outerHTML测试 ==='); if (root && root.firstChild) { const firstChild = root.firstChild; console.log('第一个子元素的outerHTML:'); console.log(firstChild.outerHTML); } } // 运行测试 testHTMLParser();

第四部分:解析器技术细节解析

4.1 有限状态机设计

我们的解析器核心是一个有限状态机(FSM),它根据当前字符和状态决定下一步操作。这种设计有以下几个优势:

  1. 清晰的状态转换:每个状态只处理特定类型的输入

  2. 易于调试:状态转换明确,便于跟踪解析过程

  3. 高效性能:避免了复杂的正则表达式匹配

4.2 标签解析状态流程

  1. DATA状态:解析文本内容,遇到<进入TAG_OPEN状态

  2. TAG_OPEN状态:判断标签类型(开始/结束/注释/DOCTYPE)

  3. TAG_NAME状态:收集标签名

  4. 属性解析状态:处理各种属性格式(带引号、不带引号等)

  5. 标签闭合:处理>/>,返回DATA状态

4.3 DOM树构建算法

DOM树构建使用栈结构管理节点层次关系:

  1. 遇到开始标签时,创建元素节点并添加到当前节点,然后将当前节点压栈

  2. 遇到结束标签时,从栈中弹出节点,恢复为当前节点

  3. 文本节点直接添加到当前节点

  4. 自闭合标签不改变当前节点和栈状态

4.4 特殊标签处理

  1. Void标签:如<img><br>等,无需闭合标签

  2. 注释<!-- 注释内容 -->,创建注释节点

  3. DOCTYPE<!DOCTYPE html>,简单跳过

  4. 自闭合标签<input />,按void标签处理

第五部分:性能优化和扩展

5.1 性能优化建议

  1. 减少字符串拼接:使用数组收集字符,最后join成字符串

  2. 预编译正则表达式:在循环外定义正则表达式

  3. 避免不必要的对象创建:重用属性对象

  4. 使用字符代码比较:代替字符串比较提升性能

5.2 扩展功能

我们的解析器可以进一步扩展以下功能:

  1. CSS解析和样式计算:实现完整的样式继承和计算

  2. JavaScript执行:支持<script>标签解析和执行

  3. 事件系统:实现DOM事件绑定和派发

  4. 虚拟DOM:基于解析器实现虚拟DOM diff算法

  5. 错误恢复:像浏览器一样处理畸形HTML

  6. 编码检测:自动检测HTML文档编码

5.3 与现代浏览器的差异

我们的简化解析器与浏览器实现的主要差异:

  1. 缺乏命名空间支持:如SVG、MathML

  2. 没有解析错误恢复机制:浏览器会尝试修复错误HTML

  3. 缺少某些特殊元素处理:如<template><slot>

  4. 没有异步解析支持:浏览器可以边下载边解析

第六部分:实际应用场景

6.1 服务端HTML处理

解析器可用于服务端HTML处理,如:

  1. 模板引擎:实现类似Mustache/Handlebars的模板系统

  2. 内容提取:从HTML中提取特定内容(如网页正文)

  3. HTML净化:过滤不安全标签和属性

  4. DOM操作:在服务端进行DOM操作

6.2 前端工具开发

  1. 代码高亮工具:解析HTML结构实现语法高亮

  2. 组件提取工具:从HTML中提取可复用组件

  3. 自动化测试:生成DOM快照进行测试对比

  4. 代码转换:HTML到其他格式的转换(如Markdown)

6.3 教育和学习

  1. 浏览器原理教学:帮助学生理解浏览器工作原理

  2. 编译原理实践:有限状态机的实际应用案例

  3. 面试准备:深入理解前端基础技术

结论

通过实现这个约500行的HTML解析器,我们深入理解了浏览器如何将HTML文本转换为DOM树。虽然我们的实现是简化版本,但它涵盖了HTML解析的核心概念:

  1. 词法分析:将字符流分解为有意义的令牌

  2. 语法分析:根据HTML语法规则构建节点

  3. 树构建:使用栈管理节点层次关系

  4. 错误处理:基本的异常情况处理

理解HTML解析过程对前端开发者至关重要,它帮助我们:

  • 更有效地调试DOM相关问题

  • 理解前端框架的底层原理

  • 优化Web应用性能

  • 开发更高效的Web工具

这个解析器项目不仅是一个学习工具,也可以作为更复杂项目的基础。你可以基于它构建自己的模板引擎、静态站点生成器或其他HTML处理工具。

记住,真正的浏览器HTML解析器要复杂得多,需要考虑性能、兼容性、安全性等众多因素。但这个简化实现为你理解这些复杂系统提供了坚实的基础。

附录:完整代码整合

以上所有代码整合后大约500行,实现了完整的HTML解析和DOM树构建功能。你可以将代码复制到本地运行,或进一步扩展功能。通过这个项目,你不仅学会了如何解析HTML,还掌握了有限状态机、树结构算法等计算机科学基础知识,这些知识在前端开发和其他编程领域都有广泛应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1183962.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

三招速查本机端口占用

1.如何查看本机端口占用 查看本机端口占用情况是网络调试、服务部署和故障排查的常见需求。不同操作系统提供了不同的命令行工具&#xff0c;以下是 Windows、Linux、macOS 三大平台的详细方法&#xff1a; ✅ 一、通用原理 操作系统内核维护一张 “网络连接与监听表”&#…

护照阅读器:爱达魔都号邮轮的高效登船助力

爱达魔都号作为连接多国航线的邮轮&#xff0c;登船环节需完成旅客身份核验与出入境合规检查&#xff0c;护照阅读器的应用让这一流程更顺畅高效。根据邮轮出行的证件管理要求&#xff0c;旅客需凭有效护照及相关凭证登船&#xff0c;且护照需满足有效期等规范。以往人工核对护…

【读书笔记】《日常生活中的自我呈现》

《日常生活中的自我呈现》书籍解读整理 这是一本由加拿大社会学家欧文戈夫曼&#xff08;Erving Goffman&#xff09;撰写的经典著作&#xff0c;将戏剧表演框架引入社会学分析&#xff0c;提出“拟剧论”&#xff08;dramaturgical analysis&#xff09;。戈夫曼认为&#xff…

小程序毕设项目推荐-基于微信小程序的文化娱乐购票系统基于springboot+微信小程序的话剧票务管理系统【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

马斯克狂砸16亿「买」他五年!揭秘特斯拉2号人物,那个睡工厂的狠人

没有家庭、没有房子&#xff0c;只有一个使命——这就是朱晓彤。获授52万期权&#xff0c;他需坚守5年&#xff0c;完成累计2000万辆交付等KPI。最近&#xff0c;特斯拉向美国证券交易委员披露了一项重磅股权激励&#xff1a;授予全球汽车业务高级副总裁朱晓彤&#xff08;Tom …

【读书笔记】《傅雷家书》

《傅雷家书》精讲整理 《傅雷家书》是一本经典之作&#xff0c;记录了著名翻译家、文艺评论家傅雷与儿子、钢琴家傅聪之间长达十二年的书信往来。这些家书不仅是父子深情的真实流露&#xff0c;更是家庭教育、亲子关系、艺术修养与人生智慧的宝贵结晶。以下是对分享内容的系统整…

R8240数字电子计

R8240 数字电子计R8240 是一款高精度数字电子计&#xff0c;用于工业和实验环境中对电量、时间或其他参数进行精确测量和显示。它以可靠性高、操作简便和读数直观而著称。主要特点与应用&#xff1a;高精度测量&#xff1a;提供稳定、准确的数字读数&#xff0c;满足工业及科研…

双目摄像头:让人脸登录更安全可靠

人脸登录因无需密码、操作便捷&#xff0c;已广泛应用于手机解锁、APP登录等场景&#xff0c;但单目摄像头易被照片、视频等虚假手段破解&#xff0c;存在安全隐患。双目摄像头的出现&#xff0c;为解决这一问题提供了有效方案。 双目摄像头模拟人眼“双眼视物”的原理&#xf…

纽约时报:OpenAI或将在18个月内现金流枯竭

奥特曼的万亿豪赌或难以为继&#xff0c;OpenAI 恐面临被吞并结局&#xff0c;AI 泡沫时代即将硬着陆。 华尔街最近弥漫着一种「恐高症」。 AI 概念股已经涨到了让人眩晕的高度&#xff0c;似乎只要技术稍不达预期&#xff0c;崩盘就在眼前。 市值最高的 7 大科技巨头&#x…

手机也能跑AI?用DeepSeek-R1-Distill-Qwen-1.5B打造边缘计算助手

手机也能跑AI&#xff1f;用DeepSeek-R1-Distill-Qwen-1.5B打造边缘计算助手 1. 引言&#xff1a;当大模型走向终端设备 近年来&#xff0c;AI大模型的发展速度令人瞩目。从千亿参数的GPT系列到如今轻量级但性能强劲的小模型&#xff0c;边缘AI推理正成为技术演进的重要方向。…

盘点便宜好用的古籍识别OCR:6款古籍识别网站

做古籍研究这么多年&#xff0c;我跑过 17 个省份的古籍数字化项目&#xff0c;试过的古籍识别工具没有十几种也有七八种&#xff0c;论性价比和实用性&#xff0c;云聪古籍绝对是佼佼者。大家都清楚&#xff0c;简体字常用的也就六千多个&#xff0c;可古代繁体光异体字就有十…

英文文献检索技巧与高效策略:提升学术文献检索效率的实用指南

做科研的第一道坎&#xff0c;往往不是做实验&#xff0c;也不是写论文&#xff0c;而是——找文献。 很多新手科研小白会陷入一个怪圈&#xff1a;在知网、Google Scholar 上不断换关键词&#xff0c;结果要么信息过载&#xff0c;要么完全抓不到重点。今天分享几个长期使用的…

一个星期又赚了4387元

熟悉独孤的都知道。独孤今年全力all in AI供稿项目。所以在这个项目上&#xff0c;几乎投入了100%的力气。在过去的一个星期里。独孤除了带团队以外&#xff0c;还自己继续实操优化供稿内容。一个星期&#xff0c;干了4387元。这也是独孤说的。这个项目&#xff0c;没有上限。只…

AI 智能体工具与模型上下文协议 (MCP) 深度解析

我们将深入探讨 AI 工具的本质、设计原则&#xff0c;并对作为互操作性标准的模型上下文协议&#xff08;MCP&#xff09;进行深度解析。引言&#xff1a;为何工具是现代 AI 的基石即使是当今最先进的基础模型&#xff0c;若没有外部工具的辅助&#xff0c;本质上也仅仅是一个强…

Unsloth镜像免配置优势解析:10分钟完成Qwen微调部署

Unsloth镜像免配置优势解析&#xff1a;10分钟完成Qwen微调部署 1. Unsloth 简介 Unsloth 是一个开源的大型语言模型&#xff08;LLM&#xff09;微调与强化学习框架&#xff0c;致力于让人工智能技术更加准确、高效且易于获取。其核心目标是降低开发者在训练和部署主流大模型…

2026年山东土工格栅厂家实力榜:塑料土工格栅、玻纤土工格栅、钢塑土工格栅、高分子复合材料与生态护坡解决方案五家企业凭技术与工程应用脱颖而出 - 海棠依旧大

随着基础设施建设对生态环保与工程耐久性要求的不断提升,土工合成材料在边坡防护、路基加固、水土保持等场景中的作用日益凸显。其中,土工格室作为兼具结构稳定性与生态适应性的关键材料,其产品性能与施工适配性成为…

从文本到语音的极致加速|Supertonic ONNX Runtime性能实测

从文本到语音的极致加速&#xff5c;Supertonic ONNX Runtime性能实测 1. 引言&#xff1a;设备端TTS的新范式 1.1 背景与挑战 在人工智能驱动的语音交互场景中&#xff0c;文本转语音&#xff08;Text-to-Speech, TTS&#xff09;技术正被广泛应用于智能助手、有声读物、无…

避坑指南:Open Interpreter本地AI编程常见问题全解

避坑指南&#xff1a;Open Interpreter本地AI编程常见问题全解 1. 引言&#xff1a;为什么选择本地化AI编程&#xff1f; 随着大模型技术的普及&#xff0c;开发者对数据隐私、执行效率和系统可控性的要求日益提升。将AI代码生成能力部署在本地&#xff0c;已成为越来越多技术…

微信小程序毕设项目:基于springboot+微信小程序的话剧票务管理系统(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

CV-UNet问题排查:常见错误及解决方案大全

CV-UNet问题排查&#xff1a;常见错误及解决方案大全 1. 引言 1.1 背景与使用场景 CV-UNet Universal Matting 是基于 UNET 架构开发的通用图像抠图工具&#xff0c;支持一键式智能背景移除和 Alpha 通道提取。该工具由“科哥”进行二次开发并封装为 WebUI 界面&#xff0c;…