回溯-html2json

上一篇文章写的是获取页面的html处理下供使用,但是在实际操作中,html的字符串不好做diff的,如果能转成json或者Array那就可以了。在转的时候需要解决的问题:

  • html标签有哪些类型,这个需要了解下nodeType
  • html标签有空标签需要枚举,例如<hr>、<br>
  • 如何获取每个html标签上的属性
  • 既可以转局部某块的html也可以转整个html文档

nodeType

大家写习惯了react,vue后对于原生js的一些属性可能比较生疏,咱们看看

只读属性 Node.nodeType 表示的是该节点的类型

节点类型常量

常量 描述
Node.ELEMENT_NODE 1 一个 元素 节点,例如 <p><div>
Node.TEXT_NODE 3 Element 或者 Attr 中实际的文字
Node.CDATA_SECTION_NODE 4 一个 CDATASection,例如 ``。
Node.PROCESSING_INSTRUCTION_NODE 7 一个用于 XML 文档的 ProcessingInstruction (en-US) ,例如`` 声明。
Node.COMMENT_NODE 8 一个 Comment 节点。
Node.DOCUMENT_NODE 9 一个 Document 节点。
Node.DOCUMENT_TYPE_NODE 10 描述文档类型的 DocumentType 节点。例如``就是用于 HTML5 的。
Node.DOCUMENT_FRAGMENT_NODE 11 一个 DocumentFragment 节点

从上面的表格我们可以看到,我们如果说对页面进行转换,其实只需要考虑nodeType值为13的类型,其余不会在页面中表现出来可以不考虑,比如Comment节点。

空标签

为什么要考虑这个呢?其实在html2json的时候是可以不考虑的,但是反序列json2html的时候就需要使用到了,看下例子:

1
2
3
<hr/>
<div>内容</div>
<link href="xxxx.css"/>

我们在反序列的时候如果不区分就写成这样

1
2
3
<hr></hr>
<div>内容</div>
<link href="xxxx.css"></link>

这样显然是不正确的,我们大致枚举下空标签的情况

“area”,
“base”,
“basefont”,
“br”,
“col”,
“frame”,
“hr”,
“img”,
“input”,
“isindex”,
“link”,
“meta”,
“param”,
“embed”

如何获取当前标签上的所有属性

咱们知道的是getAttribute,但是这个需要知道属性名字,今天咱们需要使用attributes这个api,咱们先看看官方文档

Element.attributes 属性返回该元素所有属性节点的一个实时集合。该集合是一个 NamedNodeMap 对象,不是一个数组,所以它没有 数组 的方法,其包含的 属性 节点的索引顺序随浏览器不同而不同。更确切地说,attributes 是字符串形式的名/值对,每一对名/值对对应一个属性节点。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html>
<head>
<title>Attributes example</title>
<script type="text/javascript">
function listAttributes() {
var paragraph= document.getElementById("paragraph");
var result= document.getElementById("result");
// First, let's verify that the paragraph has some attributes
if (paragraph.hasAttributes()) {
var attrs = paragraph.attributes;
var output= "";
for(var i=attrs.length-1; i>=0; i--) {
output+= attrs[i].name + "->" + attrs[i].value;
}
result.value = output;
} else {
result.value = "没有属性可显示"
}
}
</script>
</head>

<body>
<p id="paragraph" style="color: green;">Sample Paragraph</p>
<form action="">
<p>
<input type="button" value="显示属性及其值"
onclick="listAttributes();">
<input id="result" type="text" value="">
</p>
</form>
</body>
</html>

code

这个知识点都搞定了,下面就开始写工具方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
const utils = {
div: null,
empty: [
"area",
"base",
"basefont",
"br",
"col",
"frame",
"hr",
"img",
"input",
"isindex",
"link",
"meta",
"param",
"embed"
],
//属性转成json数据结构
attr2json: (node) => {
const attrs = node.attributes;
const obj = {};
for (let i = 0; i < attrs.length; i++) {
obj[attrs[i].name] = attrs[i].value;
}
return obj;
},
//json转成数组字符串
json2ArrString: (json) => {
if (!json) return "";
const arr = [];
for (let i in json) {
arr.push(`${i}="${json[i]}"`);
}
return arr.join(" ");
},
getJson: function (childNodes, attrs, length) {
const result = [];
for (let i = 0, len = childNodes.length; i < len; i++) {
let item = childNodes[i];
if (item.nodeType === 3) {
item.nodeValue.trim() !== "" &&
result.push({
node: "text",
text: item.nodeValue.trim()
});
} else if (item.nodeType === 1) {
let obj = {
node: "element",
tag: item.nodeName.toLowerCase(),
attr: {}
};
let flag = false;
if (item.attributes.length) {
for (let n = 0, l = item.attributes.length; n < l; n++) {
let value = item.attributes[n].value;
if (value) {
flag = true;
//把相对地址的改成绝对地址,为什么呢?上篇又讲到,相对地址会导致404
if (
item.attributes[n].name === "src" ||
item.attributes[n].name === "href"
) {
obj.attr[item.attributes[n].name] = item.src || item.href;
} else {
obj.attr[item.attributes[n].name] = value;
}
}
}
}
if (!flag) {
delete obj.attr;
}
if (item.childNodes.length < 1) {
let text = item.innerText;
if (text) {
obj.text = text;
}
} else {
obj.child = utils.getJson(item.childNodes, attrs, length);
}
result.push(obj);
}
}
return result;
}
};

html2json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @param {[text]} text
* @param {[boolean]} hasDoc 是否含有html
* @return {[text]}
*/
const html2json = function (text, hasDoc = false) {
if (!utils.div) {
utils.div = document.createElement("div");
}
utils.div.innerHTML = text;
let allAttrs, len;
let result = utils.getJson(utils.div.childNodes, allAttrs, len);
return hasDoc
? {
node: "root",
tag: "html",
attr: utils.attr2json(document.documentElement),
child: result
}
: {
node: "root",
child: result
};
};

下面也实现下json2html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const json2string = function (json) {
// Empty Elements
const empty = utils.empty;

let child = "";
if (json.child) {
child = json.child
.map(function (c) {
return json2html(c);
})
.join("");
}

let attr = "";
if (json.attr) {
attr = utils.json2ArrString(json.attr);
}

if (json.node === "element") {
var tag = json.tag;
if (empty.indexOf(tag) !== -1) {
// 空元素无需填写内容
return `<${json.tag} ${attr}/>`;
}
// 非空元素
return `<${json.tag} ${attr}>${child}</${json.tag}>`;
}
//文本节点
if (json.node === "text") {
return json.text;
}
// 如果是root的话返回
if (json.node === "root") {
return json.tag ? `<${json.tag} ${attr}>${child}</${json.tag}>` : child;
}
};
/**
*
* @param {[json]} json
* @param {[boolean]} fullDoc 是否是完整的html
* @param {[boolean]} delScript 是否要删除script标签
*
**/
const json2html = (json, fullDoc = false, delScript = false) => {
const resHtml = json2string(json);
if (fullDoc) {
const newDoc = new DOMParser().parseFromString(resHtml, "text/html");
if (delScript) {
newDoc.querySelectorAll("script").forEach((el) => {
el.remove();
});
}
return newDoc.documentElement.outerHTML;
} else {
return resHtml;
}
};

回溯-html2json
http://yoursite.com/2022/06/29/record-page-2/
作者
昂藏君子
发布于
2022年6月29日
许可协议