Analizar una cadena HTML con JS

259

Busqué una solución pero nada era relevante, así que aquí está mi problema:

Quiero analizar una cadena que contiene texto HTML. Quiero hacerlo en JavaScript.

Probé esta biblioteca pero parece que analiza el HTML de mi página actual, no de una cadena. Porque cuando pruebo el siguiente código, cambia el título de mi 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);

Mi objetivo es extraer enlaces de una página externa HTML que leo como una cadena.

¿Conoces una API para hacerlo?

etapa
fuente
1
posible duplicado del acceso DOMParser
Rob W
1
El método en el duplicado vinculado crea un documento HTML a partir de una cadena dada. Luego, puede usar doc.getElementsByTagName('a')para leer los enlaces (o incluso doc.links).
Rob W
Vale la pena mencionar que si está utilizando un marco como React.js, entonces puede haber formas de hacerlo que sean específicas del marco, tales como: stackoverflow.com/questions/23616226/…
Mike Lyons
¿Responde esto a tu pregunta? Eliminar HTML del texto JavaScript
Leif Arne Storset

Respuestas:

373

Cree un elemento DOM ficticio y agréguele la cadena. Luego, puede manipularlo como cualquier 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

Editar: agregar una respuesta jQuery para complacer a los fanáticos.

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
fuente
99
Solo una nota: con esta solución, si hago una "alerta (el.innerHTML)", pierdo la etiqueta <html>, <body> y <head> ...
etapa
2
Problema: Necesito obtener enlaces de la etiqueta <frame>. Pero con esta solución, la etiqueta del marco se elimina ...
etapa
3
@stage Llego un poco tarde a la fiesta, pero deberías poder usarla document.createElement('html');para conservar las etiquetas <head>y <body>.
omninonsense
3
parece que estás poniendo un elemento html dentro de un elemento html
simbionte
66
Me preocupa que se haya votado como la mejor respuesta. La parse()solución a continuación es más reutilizable y elegante.
Justin
233

Es bastante simple:

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

Según MDN , para hacer esto en Chrome, debe analizarlo como XML de la siguiente manera:

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

Actualmente no es compatible con webkit y tendría que seguir la respuesta de Florian, y se desconoce que funcione en la mayoría de los casos en navegadores móviles.

Editar: ahora ampliamente compatible

Cilan
fuente
35
Vale la pena señalar que en 2016 DOMParser ahora es ampliamente compatible. caniuse.com/#feat=xml-serializer
aendrew
55
Vale la pena señalar que todos los enlaces relativos en el documento creado están rotos, porque el documento se crea heredando el documentURLde window, lo que probablemente difiere de la URL de la cadena.
ceving
2
Vale la pena señalar que solo debe llamar new DOMParseruna vez y luego reutilizar ese mismo objeto en el resto de su secuencia de comandos.
Jack Giffin
1
La parse()solución a continuación es más reutilizable y específica para HTML. Sin embargo, esto es bueno si necesita un documento XML.
Justin
¿Cómo puedo mostrar esta página web analizada en un cuadro de diálogo o algo así? No pude encontrar una solución para eso
Shariq Musharaf
18

EDITAR: La solución a continuación es solo para "fragmentos" HTML ya que se eliminan html, head y body. Supongo que la solución para esta pregunta es el método parseFromString () de DOMParser.


Para los fragmentos HTML, las soluciones enumeradas aquí funcionan para la mayoría de HTML, sin embargo, en ciertos casos no funcionará.

Por ejemplo, intente analizar <td>Test</td>. Este no funcionará en la solución div.innerHTML ni DOMParser.prototype.parseFromString ni en la solución range.createContextualFragment. La etiqueta td se pierde y solo queda el texto.

Solo jQuery maneja bien ese caso.

Entonces, la solución futura (MS Edge 13+) es usar la etiqueta de plantilla:

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

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

Para navegadores más antiguos, he extraído el método parseHTML () de jQuery en una esencia independiente: https://gist.github.com/Munawwar/6e6362dbdf77c7865a99

Munawwar
fuente
Si desea escribir código compatible con reenvío que también funcione en navegadores antiguos, puede rellenar la <template>etiqueta . Depende de elementos personalizados que también puede necesitar rellenar . De hecho, es posible que solo desee utilizar webcomponents.js para rellenar elementos personalizados, plantillas, domos de sombra, promesas y algunas otras cosas de una sola vez.
Jeff Laughlin
12
var doc = new DOMParser().parseFromString(html, "text/html");
var links = doc.querySelectorAll("a");
Mathieu
fuente
44
¿Por qué prefijas $? Además, como se menciona en el duplicado vinculado , text/htmlno se admite muy bien y debe implementarse utilizando un polyfill.
Rob W
1
Copié esta línea de un proyecto, estoy acostumbrado a prefijar variables con $ en la aplicación de JavaScript (no en la biblioteca). es solo para evitar tener un conflicto con una biblioteca. eso no es muy útil ya que casi todas las variables tienen un alcance, pero solían ser útiles. También (quizás) ayuda a identificar variables fácilmente.
Mathieu
1
Lamentablemente, DOMParsertampoco funciona text/htmlen Chrome, esta página MDN ofrece una solución alternativa.
Jokester
Nota de seguridad: esto se ejecutará sin ningún contexto de navegador, por lo que no se ejecutarán scripts. Debe ser adecuado para entradas no confiables.
Leif Arne Storset
6

La forma más rápida de analizar HTML en Chrome y Firefox es Range # createContextualFragment:

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

Recomendaría crear una función auxiliar que use createContextualFragment si está disponible y de lo contrario recurrirá a innerHTML.

Punto de referencia: http://jsperf.com/domparser-vs-createelement-innerhtml/3

Joel Richard
fuente
Tenga en cuenta que, al igual que (lo simple) innerHTML, esto ejecutará un <img>'s onerror.
Ry-
Un problema con esto es que, html como '<td> test </td>' ignoraría el td en el contexto document.body (y solo crearía el nodo de texto 'test') .OTOH, si se usa internamente en un motor de plantillas entonces el contexto correcto estaría disponible.
Munawwar
Por cierto, IE 11 admite createContextualFragment.
Munawwar
La pregunta era cómo analizar con JS, no Chrome o Firefox
sea26.2
Nota de seguridad: esto ejecutará cualquier script en la entrada y, por lo tanto, no es adecuado para la entrada no confiable.
Leif Arne Storset
6

La siguiente función parseHTMLregresará:


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

Cómo utilizar :

var links = parseHTML('<!doctype html><html><head></head><body><a>Link 1</a><a>Link 2</a></body></html>').getElementsByTagName('a');
John Slegers
fuente
No pude hacer que esto funcione en IE8. Me aparece el error "El objeto no admite esta propiedad o método" para la primera línea de la función. No creo que exista la función createHTMLDocument
Sebastian Carroll
¿Cuál es exactamente su caso de uso? Si solo desea analizar HTML y su HTML está destinado al cuerpo de su documento, puede hacer lo siguiente: (1) var div = document.createElement ("DIV"); (2) div.innerHTML = marcado; (3) resultado = div.childNodes; --- Esto le proporciona una colección de nodos secundarios y debería funcionar no solo en IE8 sino incluso en IE6-7.
John Slegers
Gracias por la opción alternativa, lo intentaré si necesito hacerlo nuevamente. Por ahora, aunque usé la solución JQuery anterior.
Sebastian Carroll
@SebastianCarroll Tenga en cuenta que IE8 no admite el trimmétodo en cadenas. Ver stackoverflow.com/q/2308134/3210837 .
Cepillo de dientes
2
@Toothbrush: ¿El soporte de IE8 sigue siendo relevante a principios de 2017?
John Slegers
4

Si está abierto a usar jQuery, tiene algunas buenas instalaciones para crear elementos DOM separados a partir de cadenas de HTML. Estos se pueden consultar a través de los medios habituales, por ejemplo:

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: acabo de ver la respuesta de @ Florian, que es correcta. Esto es básicamente exactamente lo que dijo, pero con jQuery.

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


Solo se analizarán los hijos válidos Nodedentro del padre Node(inicio de Range). De lo contrario, pueden ocurrir 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
fuente
Nota de seguridad: esto ejecutará cualquier script en la entrada y, por lo tanto, no es adecuado para la entrada no confiable.
Leif Arne Storset
0

con este simple código puedes hacer eso:

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
fuente