使用JS解析HTML字符串

406

我想解析一个包含HTML文本的字符串,我想在JavaScript中实现。

我尝试了Pure JavaScript HTML Parser库,但似乎它解析的是当前页面的HTML,而不是从字符串中解析。因为当我尝试下面的代码时,它会改变我的页面标题:

var parser = new HTMLtoDOM("<html><head><title>titleTest</title></head><body><a href='test0'>test01</a><a href='test1'>test02</a><a href='test2'>test03</a></body></html>", document);

我的目标是从一个像字符串一样读取的HTML外部页面中提取链接。

你知道有什么API可以做到吗?


2
可能是[JavaScript DOMParser访问innerHTML和其他属性]的重复问题(https://dev59.com/dGox5IYBdhLWcg3wWzN7) - Rob W
1
在链接的副本上,该方法从给定的字符串创建一个HTML文档。然后,您可以使用doc.getElementsByTagName('a')来读取链接(甚至doc.links)。 - Rob W
值得一提的是,如果您正在使用像React.js这样的框架,则可能有特定于该框架的方法来完成此操作,例如:https://dev59.com/hWAg5IYBdhLWcg3waKZ2 - Mike Lyons
这个回答解决了你的问题吗?JavaScript去除HTML标签 - Leif Arne Storset
16个回答

498

创建一个虚拟的DOM元素并将字符串添加到其中,然后您可以像任何DOM元素一样操作它。

var el = document.createElement( 'html' );
el.innerHTML = "<html><head><title>titleTest</title></head><body><a href='test0'>test01</a><a href='test1'>test02</a><a href='test2'>test03</a></body></html>";

el.getElementsByTagName( 'a' ); // Live NodeList of your anchor elements

编辑:加入一个jQuery的答案以满足粉丝们!

var el = $( '<div></div>' );
el.html("<html><head><title>titleTest</title></head><body><a href='test0'>test01</a><a href='test1'>test02</a><a href='test2'>test03</a></body></html>");

$('a', el) // All the anchor elements

12
注意: 使用此解决方案,如果我执行“alert(el.innerHTML)”,会丢失<html>、<body>和<head>标签... - stage
5
@第一阶段 我有点晚加入派对,但你应该能够使用document.createElement('html')来保留<head><body>标签。 - omninonsense
5
看起来你正在将一个HTML元素放到另一个HTML元素中。 - symbiont
17
我担心这个被投票为最佳答案。下面的parse()方法更具可重复性和优雅性。 - Justin
7
安全提示:此操作将执行输入中的任何脚本,因此不适用于不可信的输入。 - Leif Arne Storset
显示剩余17条评论

414

这很简单:

const parser = new DOMParser();
const htmlDoc = parser.parseFromString(txt, 'text/html');
// do whatever you want with htmlDoc.getElementsByTagName('a');

根据MDN的说法,在Chrome中要像下面这样解析为XML:

const parser = new DOMParser();
const htmlDoc = parser.parseFromString(txt, 'text/xml');
// do whatever you want with htmlDoc.getElementsByTagName('a');

它目前不受 WebKit 支持,您必须遵循 Florian 的答案,并且在大多数移动浏览器中无法正常工作。

编辑:现在得到了广泛的支持。


51
值得注意的是,现在许多浏览器已经广泛支持DOMParser,该方法在2016年被引入。http://caniuse.com/#feat=xml-serializer - aendra
7
值得注意的是,由于创建的文档继承了 windowdocumentURL,而该 URL 很可能与字符串的 URL 不同,因此创建的文档中所有相对链接都无法使用。需要注意的是,所有相对链接均已损坏。 - ceving
3
值得注意的是,您应该仅调用new DOMParser一次,然后在脚本的其余部分重复使用同一个对象。 - Jack G
1
下面的parse()解决方案更具可重用性和特定于HTML。如果您需要一个XML文档,这很好。 - Justin
@HardikMandankaa html 是一个字符串,所以不需要转换。它已经作为字符串表示存在。 - Timo
显示剩余4条评论

42

编辑:下面的解决方案仅适用于 HTML “片段”,因为 html、head 和 body 被删除了。我猜这个问题的解决方案是使用 DOMParser 的 parseFromString() 方法:

const parser = new DOMParser();
const document = parser.parseFromString(html, "text/html");

对于HTML片段,此处列出的解决方案适用于大多数HTML,但对于某些情况它们将不起作用。

例如,尝试解析<td>Test</td>。这个标签无法在div.innerHTML解决方案、DOMParser.prototype.parseFromString解决方案或range.createContextualFragment解决方案中工作。td标签丢失,只剩下文本。

只有jQuery能够很好地处理这种情况。

因此,未来的解决方案(MS Edge 13+)是使用template标签:

function parseHTML(html) {
    var t = document.createElement('template');
    t.innerHTML = html;
    return t.content;
}

var documentFragment = parseHTML('<td>Test</td>');

对于旧版本的浏览器,我已经将jQuery的parseHTML()方法提取到一个独立的gist中 - https://gist.github.com/Munawwar/6e6362dbdf77c7865a99


如果你想编写向前兼容的代码,同时也能在旧浏览器上运行,你可以使用polyfill <template>标签。它依赖于自定义元素,你可能还需要为其提供polyfill。实际上,你可能只需要使用webcomponents.js来一次性地为自定义元素、模板、影子DOM、Promises等多个功能提供polyfill。 - Jeff Laughlin
哇,非常高效! - Luis Lobo

30
var doc = new DOMParser().parseFromString(html, "text/html");
var links = doc.querySelectorAll("a");

4
你为什么在 $ 前面加上前缀?同时,正如链接的重复问题中提到的一样,text/html 支持不太好,需要使用 polyfill 来实现。 - Rob W
2
我从一个项目中复制了这行代码,我习惯在JavaScript应用程序中使用$前缀变量(而不是库)。这只是为了避免与库发生冲突。虽然几乎每个变量都有作用域,但这并不是非常有用的。它也(可能)有助于轻松识别变量。 - Mathieu
1
遗憾的是,在Chrome中DOMParser无法处理text/html此MDN页面提供了解决方法。 - Jokester
1
安全提示:此操作将在没有浏览器上下文的情况下执行,因此不会运行任何脚本。它应该适用于不受信任的输入。 - Leif Arne Storset

8
const parse = Range.prototype.createContextualFragment.bind(document.createRange());

document.body.appendChild( parse('<p><strong>Today is:</strong></p>') ),
document.body.appendChild( parse(`<p style="background: #eee">${new Date()}</p>`) );


只有在父NodeRange的开始)中有效的子Node将被解析。否则,可能会发生意外结果:

// <body> is "parent" Node, start of Range
const parseRange = document.createRange();
const parse = Range.prototype.createContextualFragment.bind(parseRange);

// Returns Text "1 2" because td, tr, tbody are not valid children of <body>
parse('<td>1</td> <td>2</td>');
parse('<tr><td>1</td> <td>2</td></tr>');
parse('<tbody><tr><td>1</td> <td>2</td></tr></tbody>');

// Returns <table>, which is a valid child of <body>
parse('<table> <td>1</td> <td>2</td> </table>');
parse('<table> <tr> <td>1</td> <td>2</td> </tr> </table>');
parse('<table> <tbody> <td>1</td> <td>2</td> </tbody> </table>');

// <tr> is parent Node, start of Range
parseRange.setStart(document.createElement('tr'), 0);

// Returns [<td>, <td>] element array
parse('<td>1</td> <td>2</td>');
parse('<tr> <td>1</td> <td>2</td> </tr>');
parse('<tbody> <td>1</td> <td>2</td> </tbody>');
parse('<table> <td>1</td> <td>2</td> </table>');

11
安全提示:这将执行输入中的任何脚本,因此不适合用于不受信任的输入。 - Leif Arne Storset

7

在Chrome和Firefox中解析HTML的最快方法是使用Range#createContextualFragment:

var range = document.createRange();
range.selectNode(document.body); // required in Safari
var fragment = range.createContextualFragment('<h1>html...</h1>');
var firstNode = fragment.firstChild;

我建议创建一个帮助函数,如果可用则使用createContextualFragment,否则退而使用innerHTML。
基准测试:http://jsperf.com/domparser-vs-createelement-innerhtml/3

请注意,与(简单的)innerHTML一样,这将执行<img>onerror - Ry-
1
这样做的问题是,在 document.body 上下文中,类似 '<td>test</td>' 的 html 会忽略 td 元素(只会创建 'test' 文本节点)。但是,如果它在模板引擎内部使用,则该元素的正确上下文将可用。 - Munawwar
顺便提一句,IE 11支持createContextualFragment。 - Munawwar
问题是如何使用JS解析,而不是Chrome或Firefox。 - sea26.2
5
安全提示:这将执行输入中的任何脚本,因此不适合用于不受信任的输入。 - Leif Arne Storset

7
以下函数parseHTML将返回以下内容之一:
  • 当您的文件以文档类型(doctype)开头时,它将返回Document

  • 当您的文件不以文档类型(doctype)开头时,它将返回DocumentFragment


代码:

function parseHTML(markup) {
    if (markup.toLowerCase().trim().indexOf('<!doctype') === 0) {
        var doc = document.implementation.createHTMLDocument("");
        doc.documentElement.innerHTML = markup;
        return doc;
    } else if ('content' in document.createElement('template')) {
       // Template tag exists!
       var el = document.createElement('template');
       el.innerHTML = markup;
       return el.content;
    } else {
       // Template tag doesn't exist!
       var docfrag = document.createDocumentFragment();
       var el = document.createElement('body');
       el.innerHTML = markup;
       for (i = 0; 0 < el.childNodes.length;) {
           docfrag.appendChild(el.childNodes[i]);
       }
       return docfrag;
    }
}

如何使用:
var links = parseHTML('<!doctype html><html><head></head><body><a>Link 1</a><a>Link 2</a></body></html>').getElementsByTagName('a');

我无法在IE8上使其工作。在函数的第一行中,我收到错误消息“对象不支持此属性或方法”。我认为createHTMLDocument函数不存在。 - Sebastian Carroll
你的具体使用场景是什么?如果你只是想解析HTML并且你的HTML是用于文档的主体部分,你可以这样做: (1) var div=document.createElement("DIV"); (2) div.innerHTML = markup; (3) result = div.childNodes; --- 这将给你一个子节点的集合,并且不仅适用于IE8,甚至还适用于IE6-7。 - John Slegers
谢谢提供备选方案,如果我需要再次执行此操作,我会尝试它。不过目前我使用了上面的JQuery解决方案。 - Sebastian Carroll
@SebastianCarroll 注意,IE8不支持字符串的trim方法。请参见https://dev59.com/VnE95IYBdhLWcg3wb9hb。 - Toothbrush
3
在2017年初,IE8的支持是否仍然具有相关性? - John Slegers
对于一些公司来说,是的。 - Toothbrush

6
我认为最好的方法是使用像这样的API:这个

//Table string in HTML format
const htmlString = '<table><tbody><tr><td>Cell 1</td><td>Cell 2</td></tr></tbody></table>';

//Parse using DOMParser native way
const parser = new DOMParser();
const $newTable = parser.parseFromString(htmlString, 'text/html');

//Here you can select parts of your parsed html and work with it
const $row = $newTable.querySelector('table > tbody > tr');

//Here i'm printing the number of columns (2)
const $containerHtml = document.getElementById('containerHtml');
$containerHtml.innerHTML = ['Your parsed table have ', $row.cells.length, 'columns.'].join(' ');
<div id="containerHtml"></div>


5

要在node.js中实现这一功能,您可以使用像node-html-parser这样的HTML解析器。语法如下:

import { parse } from 'node-html-parser';

const root = parse('<ul id="list"><li>Hello World</li></ul>');

console.log(root.firstChild.structure);
// ul#list
//   li
//     #text

console.log(root.querySelector('#list'));
// { tagName: 'ul',
//   rawAttrs: 'id="list"',
//   childNodes:
//    [ { tagName: 'li',
//        rawAttrs: '',
//        childNodes: [Object],
//        classNames: [] } ],
//   id: 'list',
//   classNames: [] }
console.log(root.toString());
// <ul id="list"><li>Hello World</li></ul>
root.set_content('<li>Hello World</li>');
root.toString();    // <li>Hello World</li>

1
这是最佳解决方案,即使在浏览器上,如果您不想依赖于浏览器的实现。无论您使用哪个浏览器,这种实现始终会表现出相同的行为(现在这并不重要),而且解析是在javascript本身而不是c/c++中完成的! - Rainb
谢谢@Rainb。但是你如何在浏览器中使用这个解决方案呢? - Daniel Kaplan
1
(等待导入(“https://cdn.skypack.dev/node-html-parser”))。默认('<ul id="list"><li>Hello World</li></ul>')。firstChild.structure - Rainb
我从未知道那是一个选择。你能用任何节点库做到这一点,还是因为这个库没有使用任何仅限于节点的代码? - Daniel Kaplan
1
如果涉及到像tls、http、net、fs之类的node内容,它可能在浏览器中不起作用。但是它也不会在Deno中工作。因此,请寻找与Deno兼容的包。 - Rainb

5

第一种方法

使用document.cloneNode()

性能是:

调用document.cloneNode()花费了约0.22499999977299012毫秒。

可能还会更长时间。

var t0, t1, html;

t0 = performance.now();
   html = document.cloneNode(true);
t1 = performance.now();

console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")

html.documentElement.innerHTML = '<!DOCTYPE html><html><head><title>Test</title></head><body><div id="test1">test1</div></body></html>';

console.log(html.getElementById("test1"));

2 Way

使用 document.implementation.createHTMLDocument()

性能表现为:

调用document.implementation.createHTMLDocument()花费了约0.14000000010128133毫秒。

var t0, t1, html;

t0 = performance.now();
html = document.implementation.createHTMLDocument("test");
t1 = performance.now();

console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")

html.documentElement.innerHTML = '<!DOCTYPE html><html><head><title>Test</title></head><body><div id="test1">test1</div></body></html>';

console.log(html.getElementById("test1"));

三种方法

使用document.implementation.createDocument()

性能为:

调用document.implementation.createHTMLDocument()花费了约0.14000000010128133毫秒。

var t0 = performance.now();
  html = document.implementation.createDocument('', 'html', 
             document.implementation.createDocumentType('html', '', '')
         );
var t1 = performance.now();

console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")

html.documentElement.innerHTML = '<html><head><title>Test</title></head><body><div id="test1">test</div></body></html>';

console.log(html.getElementById("test1"));

四种方法

使用new Document()

性能是:

调用document.implementation.createHTMLDocument()花费了约0.13499999840860255毫秒。

  • 注意

ParentNode.append在2020年是实验性技术。

var t0, t1, html;

t0 = performance.now();
//---------------
html = new Document();

html.append(
  html.implementation.createDocumentType('html', '', '')
);
    
html.append(
  html.createElement('html')
);
//---------------
t1 = performance.now();

console.log("Call to doSomething took " + (t1 - t0) + " milliseconds.")

html.documentElement.innerHTML = '<html><head><title>Test</title></head><body><div id="test1">test1</div></body></html>';

console.log(html.getElementById("test1"));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接