js解析html字符串

用js解析html字符串

目标用js将html字符串解析为一个类似于虚拟dom的对象

    const htmlStr = `<html>
    <head></head>
    <body>
      <h1>我是标签</h1>
      <div>我是div标签</div>
      <span id="root" style="color:red">我是span标签</span>
      </body>
    </html>`;
  htmlTransform(htmlStr);
  // 期望结果格式:
  // {  nodeName: 'html', children: [ ...,{ nodeName: 'body', id: 'xxxx',  }, .... ]  }
开发的htmlstr-parser-n插件

npm i htmlstr-parser-n 使用

const { htmlObjParser, htmlStrParser } = require("html-parser-n");
const fs = require("fs");

fs.writeFileSync('./demo.json', JSON.stringify(
  htmlStrParser(`
  <html>
    <body>
      <span id="root" style="color:red;">我是span标签</span>
    </body>
  </html>
`)
))

console.log(htmlObjParser(require("./demo.json")))
实现原理
  1. 状态机记录执行状态
    let sign_enum = {
      SIGN_END: "SIGN_END",           // 结束标签读取 如 </xxxxx>
      SIGN_END_OK: "SIGN_EN_OK",      // 结束标签读取完成
      SIGN_START: "SIGN_START",       // 开始标签读取 如 <xxxxx>
      SIGN_START_OK: "SIGN_START_OK", // 开始标签读取完成 
    };
  1. 字符串轮训读取,根据特殊符号< 、</、>来标注状态
  2. 标记每次读取的内容 sign
  3. 用浅拷贝来标记每次操作的节点

完整代码

    let sign_enum = {
      SIGN_END: "SIGN_END",           // 结束标签读取 如 </xxxxx>
      SIGN_END_OK: "SIGN_EN_OK",      // 结束标签读取完成
      SIGN_START: "SIGN_START",       // 开始标签读取 如 <xxxxx>
      SIGN_START_OK: "SIGN_START_OK", // 开始标签读取完成 
    };
    function htmlStrParser(htmlStr) {
      const str = htmlStr.replace(/\n/g, "");
      let result = { nodeName: "root", children: [] };
    // 默认 result.children[0]插入, ,这里记录调试用的栈信息
      let use_line = [0];               
      let current_index = 0;            // 记录当前插入children的下标
      let node = result;                // 当前操作的节点
      let sign = "";                    // 标记标签字符串(可能包含属性字符)、文本信息
      let status = "";                  // 当前状态,为空的时候我们认为是在读取当前节点(node)的文本信息
      for (var i = 0; i < str.length; i++) {
        var current = str.charAt(i);
        var next = str.charAt(i + 1);
        if (current === "<") {
          // 在开始标签完成后记录文本信息到当前节点
          if (sign && status === sign_enum.SIGN_START_OK) {
            node.text = sign;
            sign = "";
          }
          // 根据“</”来区分是 结束标签的(</xxx>)读取中  还是开始的标签(<xxx>) 读取中
          if (next === "/") {
            status = sign_enum.SIGN_END;
          } else {
            status = sign_enum.SIGN_START;
          }
        } else if (current === ">") {
          // (<xxx>) 读取中,遇到“>”, (<xxx>) 读取中完成
          if (status === sign_enum.SIGN_START) {
            // 记录当前node所在的位置,并更改node
            node = result;
            use_line.map((_, index) => {
              if (!node.children) node.children = [];
              if (index === use_line.length - 1) {
                sign = sign.replace(/^\s*/g, "").replace(/\"/g, "");
                let mark = sign.match(/^[a-zA-Z0-9]*\s*/)[0].replace(/\s/g, ""); // 记录标签
                // 标签上定义的属性获取
                let attributeStr = sign.replace(mark, '').replace(/\s+/g, ",").split(",");
                let attrbuteObj = {};
                let style = {};
                attributeStr.map(attr => {
                  if (attr) {
                    let value = attr.split("=")[1];
                    let key = attr.split("=")[0];
                    if (key === "style") {
                      value.split(";").map(s => {
                        if (s) {
                          style[s.split(":")[0]] = s.split(":")[1]
                        }
                      })
                      return attrbuteObj[key] = style;
                    }
                    attrbuteObj[key] = value;
                  }
                })
                node.children.push({ nodeName: mark, children: [], ...attrbuteObj })
              }
              current_index = node.children.length - 1;
              node = node.children[current_index];
            });
            use_line.push(current_index);
            sign = "";
            status = sign_enum.SIGN_START_OK;
          }
          // (</xxx>) 读取中,遇到“>”, (</xxx>) 读取中完成
          if (status === sign_enum.SIGN_END) {
            use_line.pop();
            node = result;
            // 重新寻找操作的node
            use_line.map((i) => {
              node = node.children[i];
            });
            sign = "";
            status = sign_enum.SIGN_END_OK;
          }
        } else {
          sign = sign + current;
        }
      }
      return result;
    }

    console.dir(htmlStrParser(htmlStr))
fs.writeFileSync("htmlObj.text", JSON.stringify(htmlStrParser(htmlStr)))

格式化查看

{
    "nodeName":"root",
    "children":[
        {
            "nodeName":"html",
            "children":[
                {
                    "nodeName":"head",
                    "children":[]
                },
                {
                    "nodeName":"body",
                    "children":[
                        {
                            "nodeName":"h1",
                            "children":[],
                            "text":"我是标签"
                        },
                        {
                            "nodeName":"div",
                            "children":[],
                            "text":"我是div标签"
                        },
                        {
                            "nodeName":"span",
                            "children":[],
                            "id":"root",
                            "style":{
                                "color":"red"
                            },
                            "text":"我是span标签"
                        }
                    ],
                    "text":"  "
                }
            ]
        }
    ]
}
用js解析html对象

实现html的增删查改可以先转成对象数组的形式,然后操作对象数组,操作完成后再转成字符串

function htmlObjParser(obj) {
  let htmlStr = "";
  function work(obj) {
    const children = obj.children;
    let attrStr = "";
    Object.keys(obj).map(key => {
      if (key !== 'nodeName' && key !== 'text' && key !== "children") {
        if (key !== 'style') {
          attrStr += ` ${key}=${obj[key]}`
        } else if (key === 'style') {
          let styleStr = '';
          Object.keys(obj[key]).map(k => {
            styleStr += ` ${k}:${obj[key][k]};`
          })
          attrStr += styleStr;
        }
      }
    })
    htmlStr += `<${obj.nodeName}${attrStr}>${obj.text ? obj.text : ''}`;
    if (children && children.length) {
      children.map(c => {
        work(c)
      });
    }
    htmlStr += `</${obj.nodeName}>`;
  }
  work(obj);
  return htmlStr;
}
htmlObjParser(require("demo.text"))
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343