用JavaScript解析HTML的最佳方法

9

我在学习RegExp和编写一个好的算法时遇到了很多麻烦。我有一串HTML字符串需要解析。请注意,当我解析它时,它仍然是一个字符串对象,而不是在浏览器上的HTML,因为我需要在它到达那里之前对其进行解析。HTML看起来像这样:

<html>
  <head>
    <title>Geoserver GetFeatureInfo output</title>
  </head>
  <style type="text/css">
    table.featureInfo, table.featureInfo td, table.featureInfo th {
        border:1px solid #ddd;
        border-collapse:collapse;
        margin:0;
        padding:0;
        font-size: 90%;
        padding:.2em .1em;
    }
    table.featureInfo th {
        padding:.2em .2em;
        font-weight:bold;
        background:#eee;
    }
    table.featureInfo td{
        background:#fff;
    }
    table.featureInfo tr.odd td{
        background:#eee;
    }
    table.featureInfo caption{
        text-align:left;
        font-size:100%;
        font-weight:bold;
        text-transform:uppercase;
        padding:.2em .2em;
    }
  </style>

  <body>
    <table class="featureInfo2">
    <tr>
        <th class="dataLayer" colspan="5">Tibetan Villages</th>
    </tr>
    <!-- EOF Data Layer -->
    <tr class="dataHeaders">
        <th>ID</th>
        <th>Latitude</th>
        <th>Longitude</th>
        <th>Place Name</th>
        <th>English Translation</th>
    </tr>
    <!-- EOF Data Headers -->
    <!-- Data -->
    <tr>
    <!-- Feature Info Data -->
        <td>3394</td>
        <td>29.1</td>
        <td>93.15</td>
        <td>བསྡམས་གྲོང་ཚོ།</td>
        <td>Dam Drongtso </td>
    </tr>
    <!-- EOF Feature Info Data -->
    <!-- End Data -->
    </table>
    <br/>
  </body>
</html>

我需要以这种方式获取它。
3394,
29.1,
93.15,
བསྡམས་གྲོང་ཚོ།,
Dam Drongtso

基本上,一个数组...... 如果它根据其字段标头匹配,并且从某种程度上说它们来自哪个表格,那就更好了,看起来是这样的:
Tibetan Villages

ID
Latitude
Longitude
Place Name
English Translation

发现 JavaScript 不支持精美的映射真是令人失望,但我已经实现了我想要的功能。然而,它非常非常硬编码,我认为我应该使用 RegExp 更好地处理它。不幸的是,我遇到了很大的困难 :(。这是我的函数来解析我的字符串(在我看来非常丑陋):
    function parseHTML(html){

    //Getting the layer name
    alert(html);
    //Lousy attempt at RegExp
    var somestring = html.replace('/m//\<html\>+\<body\>//m/',' ');
    alert(somestring);
    var startPos = html.indexOf('<th class="dataLayer" colspan="5">');
    var length = ('<th class="dataLayer" colspan="5">').length;
    var endPos = html.indexOf('</th></tr><!-- EOF Data Layer -->');
    var dataLayer = html.substring(startPos + length, endPos);

    //Getting the data headers
    startPos = html.indexOf('<tr class="dataHeaders">');
    length = ('<tr class="dataHeaders">').length;
    endPos = html.indexOf('</tr><!-- EOF Data Headers -->');
    var newString = html.substring(startPos + length, endPos);
    newString = newString.replace(/<th>/g, '');
    newString = newString.substring(0, newString.lastIndexOf('</th>'));
    var featureInfoHeaders = new Array();
    featureInfoHeaders = newString.split('</th>');

    //Getting the data
    startPos = html.indexOf('<!-- Data -->');
    length = ('<!-- Data -->').length;
    endPos = html.indexOf('<!-- End Data -->');
    newString = html.substring(startPos + length, endPos);
    newString = newString.substring(0, newString.lastIndexOf('</tr><!-- EOF Feature Info Data -->'));
    var featureInfoData = new Array();
    featureInfoData = newString.split('</tr><!-- EOF Feature Info Data -->');

    for(var s = 0; s < featureInfoData.length; s++){
        startPos = featureInfoData[s].indexOf('<!-- Feature Info Data -->');
        length = ('<!-- Feature Info Data -->').length;
        endPos = featureInfoData[s].lastIndexOf('</td>');
        featureInfoData[s] = featureInfoData[s].substring(startPos + length, endPos);
        featureInfoData[s] = featureInfoData[s].replace(/<td>/g, '');
        featureInfoData[s] = featureInfoData[s].split('</td>');
    }//end for

    alert(featureInfoData);

    //Put all the feature info in one array
    var featureInfo = new Array();
    var len = featureInfoData.length;
    for(var j = 0; j < len; j++){
        featureInfo[j] = new Object();
        featureInfo[j].id = featureInfoData[j][0];
        featureInfo[j].latitude = featureInfoData[j][1];
        featureInfo[j].longitude = featureInfoData[j][2];
        featureInfo[j].placeName = featureInfoData[j][3];
        featureInfo[j].translation = featureInfoData[j][4];
        }//end for 

    //This can be ignored for now...
        var string = redesignHTML(featureInfoHeaders, featureInfo);
        return string;

    }//end parseHTML

正如你所看到的,如果该字符串中的内容发生变化,我的代码将会出现严重错误。我希望尽可能避免这种情况,并努力编写更好的代码。非常感谢您能给予我的所有帮助和建议。


1
如果您是在服务器端生成HTML的人,那么您也可以在那里生成JSON,并将其与内容一起传递到HTML中。这样您就不必解析任何东西了。 - Robert Koritnik
9
用正则表达式解析HTML(或XML)几乎从来都不是一个好主意。 - Shawn Chin
3
这是一个关于如何使用正则表达式匹配HTML标签的问题。回答建议使用负向预查来排除自闭合标签,然后使用正则表达式来匹配剩余的包含属性和内容的标签。答案还提供了一些代码示例以及说明它们的工作原理。 - Mark Thomas
1
在SO上有一个黄金法则:不要使用正则表达式解析HTML。 - Richard H
我正在使用一个服务器创建这个字符串(它是HTML,以便可以由浏览器呈现),但在我解析的阶段,浏览器还没有看到它,它实际上只是一个字符串... - elshae
2
我重申一遍:https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454 如果我们的心是纯洁的,我们可以在我们的有生之年消除对HTML的正则表达式解析!或者Tony会来。 - Prof. Falken
6个回答

25

请按照以下步骤进行:

  1. 创建一个新的documentFragment
  2. 将您的HTML字符串放入其中
  3. 使用选择器获取您想要的内容

为什么要做所有的解析工作呢?(这样做也不会奏效,因为HTML不能通过RegExp进行解析)当您有最好的HTML解析器(浏览器)可用时,为什么不使用它呢?


12

您可以使用jQuery轻松遍历DOM并自动创建具有结构的对象。

var $dom = $('<html>').html(the_html_string_variable_goes_here);
var featureInfo = {};

$('table:has(.dataLayer)', $dom).each(function(){
    var $tbl = $(this);
    var section = $tbl.find('.dataLayer').text();
    var obj = [];
    var $structure = $tbl.find('.dataHeaders');
    var structure = $structure.find('th').map(function(){return $(this).text().toLowerCase();});
    var $datarows= $structure.nextAll('tr');
    $datarows.each(function(i){
        obj[i] = {};
        $(this).find('td').each(function(index,element){
            obj[i][structure[index]] = $(element).text();
        });
    });
    featureInfo[section] = obj;
});

工作演示

此代码可以与具有不同结构的多个表格以及每个表格内的多个数据行一起使用。

featureInfo 将保存最终的结构和数据,并且可以像这样访问:

alert( featureInfo['Tibetan Villages'][0]['English Translation'] );

或者

alert( featureInfo['Tibetan Villages'][0].id );

那段代码真的很好,但我认为让大家困惑的是我展示了HTML。理想情况下,在解析这个字符串时,它不应该是“HTML”,因为浏览器还没有看到它。我之前尝试过使用一些DOM方法等,但失败了。后来我意识到,如果这个HTML还没有被发送到浏览器,我怎么能使用DOM函数呢?我的想法正确还是非常混乱? - elshae
使用jQuery,您可以执行var dom = $(htmlstring);并将其作为上下文用于代码的其余部分,方法是将其作为$('table:has(.dataLayer)', dom)开头。正在更新答案... - Gabriele Petrioli
哇,这真的很棒,你太好了。我对JavaScript仍然很陌生,还有很多东西需要学习!我会阅读这段代码,并在将其应用到我的应用程序中时让您知道:) - elshae
@elshae并没有提到我的代码使用了jQuery框架。 - Gabriele Petrioli
1
我知道这一点,因为我已经深入研究了jQuery,它似乎是JavaScript的革命 :)。你的代码运行得非常好,我真的很感谢你向我展示了那里的可用内容 :)。 - elshae

10

使用 DOMParser 是“正确”的方法。可以按照以下步骤进行操作:

var parsed=new DOMParser.parseFromString(htmlString,'text/html');

或者,如果您担心浏览器兼容性问题,请在MDN文档中使用polyfill:

/*
 * DOMParser HTML extension
 * 2012-09-04
 * 
 * By Eli Grey, http://eligrey.com
 * Public domain.
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 */

/*! @source https://gist.github.com/1129031 */
/*global document, DOMParser*/

(function(DOMParser) {
    "use strict";

    var
      DOMParser_proto = DOMParser.prototype
    , real_parseFromString = DOMParser_proto.parseFromString
    ;

    // Firefox/Opera/IE throw errors on unsupported types
    try {
        // WebKit returns null on unsupported types
        if ((new DOMParser).parseFromString("", "text/html")) {
            // text/html parsing is natively supported
            return;
        }
    } catch (ex) {}

    DOMParser_proto.parseFromString = function(markup, type) {
        if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
            var
              doc = document.implementation.createHTMLDocument("")
            ;
                if (markup.toLowerCase().indexOf('<!doctype') > -1) {
                    doc.documentElement.innerHTML = markup;
                }
                else {
                    doc.body.innerHTML = markup;
                }
            return doc;
        } else {
            return real_parseFromString.apply(this, arguments);
        }
    };
}(DOMParser));

它在ie9上无法工作,SCRIPT600:此操作的目标元素无效。 - Mikalai
@Mikalai 抱歉,我不会为了兼容IE9而工作。它只被不到1%的人使用,而且实际上带来的麻烦比价值更低。 - markasoftware
你为什么决定使用HTMLHtmlElement而不是: var iframe = document.createElement("iframe"); iframe.innerHTML = markup;? - Mikalai
我没有编写此答案第二部分的代码,它来自于men文档。 - markasoftware

5

如果可以的话,更改服务器端代码(添加JSON)

如果您是在服务器端生成结果HTML的人,那么您也可以在那里生成JSON,并将其与内容一起传递到HTML中。您无需在客户端解析任何内容,所有数据都将立即可用于客户端脚本。

您可以将JSON轻松放入table元素中作为data属性值:

<table class="featureInfo2" data-json="{ID:3394, Latitude:29.1, Longitude:93.15, PlaceName:'བསྡམས་གྲོང་ཚོ།', Translation:'Dam Drongtso'}">
    ...
</table>

或者您可以向包含数据的TD添加data属性,仅使用jQuery选择器解析它们并生成JavaScript对象。无需使用RegExp解析。


我是该页面的所有者,或者至少我可以访问完整的后端。问题在于我正在使用一个为我生成此HTML字符串的服务器,这不是我的选择。 - elshae
@elshae:换句话说,我想问你是否有访问和更改页面服务器端代码的能力/知识?如果是的话,我建议您实际上将JSON与页面一起发送。 - Robert Koritnik
为 TD 添加数据属性,可以给我一个非常简单的示例吗?这是否意味着 <td attr="latitude">92.34</td> - elshae
如果我的回答不够清晰,我很抱歉。理论上,我可以访问并发送JSON到我的浏览器,但由于我使用的服务器已经为我完成了这部分工作,可以说这是对我进行了封装。换句话说,要进入服务器并重新发明它发送数据到浏览器的方式所需的努力似乎不值得我去做... - elshae
@elshae:我认为添加一个额外的元素属性值是值得的,因为你在服务器上有结构化对象方式的数据。将其生成JSON比解析HTML简单得多。如果以后更改HTML本身怎么办?你还需要重新开发解析器。在其中添加JSON不会改变任何客户端功能。请查看我添加的示例。 - Robert Koritnik
非常感谢您的反馈,您的答案也很好。不幸的是,由于我时间有点紧迫,并且这不是我的项目要求的一部分,所以我将选择下面 Gaby 的答案。谢谢您,如果其他人有时间深入代码,我希望他们会考虑您的建议。 - elshae

2

0

我有一个类似的需求,但由于对JavaScript不是很熟悉,所以我让jquery通过parseHTML和使用find来处理它。在我的情况下,我正在寻找具有特定类名的div。

function findElementsInHtmlString(document, htmlString, query) {
    var domArray = $.parseHTML(htmlString, document),
        dom = $();

    // create the dom collection from the array
    $.each(domArray, function(i, o) {
        dom = dom.add(o);
    }

    // return a collection of elements that match the query
    return dom.find(query);
}

var elementsWithClassBuild = findElementsInHtmlString(document, htmlString, '.build');

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