Analisar uma string HTML com JS

258

Eu procurei uma solução, mas nada era relevante, então aqui está o meu problema:

Eu quero analisar uma string que contém texto HTML. Eu quero fazer isso em JavaScript.

Eu tentei esta biblioteca, mas parece que ela analisa o HTML da minha página atual, não de uma string. Porque quando tento o código abaixo, ele altera o título da minha página:

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);

Meu objetivo é extrair links de uma página externa em HTML que eu li como uma string.

Você conhece uma API para fazer isso?

palco
fonte
1
O método na duplicata vinculada cria um documento HTML a partir de uma determinada sequência. Então, você pode usar doc.getElementsByTagName('a')para ler os links (ou mesmo doc.links).
22712 Rob Rob W
Vale a pena mencionar que, se você estiver usando uma estrutura como React.js então pode haver maneiras de fazer isso que são específicos para a estrutura, tais como: stackoverflow.com/questions/23616226/...
Mike Lyons
Isso responde sua pergunta? Strip HTML from Text JavaScript
Leif Arne Storset

Respostas:

373

Crie um elemento DOM fictício e adicione a string a ele. Em seguida, você pode manipulá-lo como qualquer elemento 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

Edit: adicionando uma resposta jQuery para agradar os fãs!

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
Florian Margaine
fonte
9
Apenas uma observação: com esta solução, se eu fizer um "alerta (el.innerHTML)", perco as tags <html>, <body> e <head> ....
etapa
2
Problema: preciso obter links da tag <frame>. Mas com esta solução, a tag frame é excluída ...
estágio
3
@stage Estou um pouco atrasado para a festa, mas você deve poder usar document.createElement('html');para preservar as tags <head>e <body>.
Omninonsense
3
parece que você está colocando um elemento HTML dentro de um elemento html
simbionte
6
Estou preocupado que seja votado como a melhor resposta. A parse()solução abaixo é mais reutilizável e elegante.
Justin
233

É bem simples:

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

De acordo com o MDN , para fazer isso no chrome, você precisa analisar como XML da seguinte forma:

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

No momento, ele não é suportado pelo webkit e você teria que seguir a resposta de Florian, e é desconhecido que na maioria dos casos funcione em navegadores móveis.

Edit: Agora amplamente suportado

Cilan
fonte
35
Vale ressaltar que em 2016 o DOMParser agora é amplamente suportado. caniuse.com/#feat=xml-serializer
aendrew
5
Vale ressaltar que todos os links relativos no documento criado estão quebrados, porque o documento é criado herdando o documentURLde window, o que provavelmente difere da URL da string.
ceving 03/11/19
2
Vale a pena notar que você deve ligar apenasnew DOMParser uma vez e, em seguida, reutilizar o mesmo objeto no restante do seu script.
Jack Giffin
1
A parse()solução abaixo é mais reutilizável e específica para HTML. Isso é bom se você precisar de um documento XML, no entanto.
Justin
Como posso exibir esta página da Web analisada em uma caixa de diálogo ou algo assim? Não consegui encontrar uma solução para isso #
Shariq Musharaf
18

EDIT: A solução abaixo é apenas para "fragmentos" de HTML, pois o html, a cabeça e o corpo são removidos. Eu acho que a solução para esta pergunta é o método parseFromString () do DOMParser.


Para fragmentos HTML, as soluções listadas aqui funcionam para a maioria dos HTML; no entanto, em certos casos, não funciona.

Por exemplo, tente analisar <td>Test</td> . Este não funcionará na solução div.innerHTML nem no DOMParser.prototype.parseFromString nem no range.createContextualFragment. A tag td desaparece e apenas o texto permanece.

Somente o jQuery lida bem com esse caso.

Portanto, a solução futura (MS Edge 13+) é usar a tag template:

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

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

Para navegadores mais antigos, extraí o método parseHTML () do jQuery em uma lista independente - https://gist.github.com/Munawwar/6e6362dbdf77c7865a99

Munawwar
fonte
Se você deseja escrever um código compatível com a frente que também funcione em navegadores antigos, é possível preencher novamente a <template>tag . Depende dos elementos personalizados que também podem ser necessários para o polyfill . Na verdade, você pode apenas querer usar o webcomponents.js para preencher polyfill elementos personalizados, modelos, sombra dom, promessas e algumas outras coisas de uma só vez.
Jeff Laughlin
12
var doc = new DOMParser().parseFromString(html, "text/html");
var links = doc.querySelectorAll("a");
Mathieu
fonte
4
Por que você está prefixando $? Além disso, como mencionado na duplicata vinculada , text/htmlnão é muito bem suportado e deve ser implementado usando um polyfill.
Rob W
1
Copiei esta linha de um projeto, estou acostumado a prefixar variáveis ​​com $ no aplicativo javascript (não na biblioteca). é apenas para avoir ter um conflito com uma biblioteca. isso não é muito útil, pois quase todas as variáveis ​​têm escopo definido, mas costumavam ser úteis. também (talvez) ajuda a identificar variáveis ​​facilmente.
1619 Mathieu
1
Infelizmente, DOMParsernem funciona text/htmlno chrome, esta página MDN fornece uma solução alternativa.
Jokester
Nota de segurança: isso será executado sem nenhum contexto do navegador, portanto, nenhum script será executado. Deve ser adequado para entrada não confiável.
Leif Arne Storset 11/03
6

A maneira mais rápida de analisar HTML no Chrome e Firefox é Range # createContextualFragment:

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

Eu recomendaria criar uma função auxiliar que use createContextualFragment, se disponível, e retorne ao innerHTML.

Referência: http://jsperf.com/domparser-vs-createelement-innerhtml/3

Joel Richard
fonte
Observe que, como (o simples) innerHTML, isso executará um <img>'s onerror.
Ry-
Um problema é que html como '<td> test </td>' ignoraria o td no contexto document.body (e apenas criaria o nó de texto 'test') .OTOH, se usado internamente em um mecanismo de modelagem então o contexto correto estaria disponível.
Munawwar
Além disso, o IE 11 também suporta createContextualFragment.
Munawwar
A questão era como analisar com JS - não Chrome ou Firefox
sea26.2
Nota de segurança: isso executará qualquer script na entrada e, portanto, não é adequado para entradas não confiáveis.
Leif Arne Storset 11/03
6

A seguinte função parseHTMLretornará:

  • a Documentquando seu arquivo iniciar com um doctype.

  • a DocumentFragmentquando seu arquivo não iniciar com um doctype.


O código :

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;
    }
}

Como usar :

var links = parseHTML('<!doctype html><html><head></head><body><a>Link 1</a><a>Link 2</a></body></html>').getElementsByTagName('a');
John Slegers
fonte
Não consegui fazer isso funcionar no IE8. Recebo o erro "O objeto não suporta esta propriedade ou método" para a primeira linha da função. Eu não acho que a função createHTMLDocument existe
Sebastian Carroll
Qual é exatamente o seu caso de uso? Se você apenas deseja analisar o HTML e seu HTML é destinado ao corpo do seu documento, você pode fazer o seguinte: (1) var div = document.createElement ("DIV"); (2) div.innerHTML = marcação; (3) resultado = div.childNodes; --- Isso fornece uma coleção de nós filhos e deve funcionar não apenas no IE8, mas também no IE6-7.
John Slegers
Obrigado pela opção alternativa, tentarei se precisar fazer isso novamente. Por enquanto, embora eu usei a solução JQuery acima.
Sebastian Carroll
@SebastianCarroll Observe que o IE8 não suporta o trimmétodo em strings. Consulte stackoverflow.com/q/2308134/3210837 .
Escova de dentes
2
@ Toothbrush: O suporte ao IE8 ainda é relevante no início de 2017?
John Slegers
4

Se você está aberto a usar o jQuery, ele possui algumas boas instalações para criar elementos DOM desanexados a partir de strings de HTML. Estes podem ser consultados através dos meios habituais, por exemplo:

var 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>";
var anchors = $('<div/>').append(html).find('a').get();

Editar - apenas vi a resposta de @ Florian, que está correta. Isso é basicamente exatamente o que ele disse, mas com o jQuery.

jmar777
fonte
4
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>`) );


Somente filhos válidos Nodedentro do pai Node(início do Range) serão analisados. Caso contrário, poderão ocorrer resultados inesperados:

// <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>');
AnthumChris
fonte
Nota de segurança: isso executará qualquer script na entrada e, portanto, não é adequado para entradas não confiáveis.
Leif Arne Storset 11/03
0

Com este código simples, você pode fazer isso:

let el = $('<div></div>');
$(document.body).append(el);
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>`);
console.log(el.find('a[href="test0"]'));
NaabNuts
fonte