【问题标题】:Parse semi-structured values解析半结构化值
【发布时间】:2014-08-06 18:56:43
【问题描述】:

这是我的第一个问题。我试图找到答案,但老实说,我无法弄清楚我应该使用哪些术语,如果之前有人问过,很抱歉。

这里是: 我在 .txt 文件中有数千条记录,格式如下:

(1, 3, 2, 1, 'John (Finances)'),
(2, 7, 2, 1, 'Mary Jane'),
(3, 7, 3, 2, 'Gerald (Janitor), Broflowski'),

... 等等。第一个值是PK,其他3个是外键,第5个是字符串。

我需要在 Javascript 中将它们解析为 JSON(或其他内容),但我遇到了麻烦,因为某些字符串有括号+逗号(在第 3 条记录中,例如“看门人”),所以我不能使用子字符串。 ..也许修剪正确的部分,但我想知道是否有一些更聪明的方法来解析它。

任何帮助将不胜感激。

谢谢!

【问题讨论】:

  • 看起来你可以 .split(/'\)\,\s*/) 把每一个放在它自己的数组槽中,然后 [].map 从那里...
  • 好的,但是有些字符串只有“)”或逗号......那么它可能会导致数组中超过 5 个位置,不是吗?
  • 在迭代中,可以按逗号分割,然后设置 col[4]=col.slice(4).join(",") 以获取字符串中的任何逗号和文本。如果这种解决方案不起作用,您需要更一致或更明确的输入...
  • 等等,文件中的每个条目都在它自己的行上吗? (问题已编辑)如果是这样,那么只需按行拆分,然后将每一行变成一个数组

标签: javascript json parsing


【解决方案1】:

我回应 Ben's sentiments 关于正则表达式通常对此不利,并且我完全同意他的观点,即标记器是这里最好的工具。

然而,考虑到一些注意事项,您可以在此处使用正则表达式。这是因为您的(),' 中的任何歧义都可以归因于(AFAIK)到您的最后一列;因为所有其他列将始终为整数。

所以,给定:

  1. 输入格式完美(没有意外的(),')。
  2. 根据您的编辑,每条记录都位于新行中
  3. 您输入中的唯一行将打破到下一条记录

...以下应该可以工作(注意这里的“新行”是\n。如果它们是\r\n,请相应地更改它们):

var input = /* Your input */;
var output = input.split(/\n/g).map(function (cols) {
    cols = cols.match(/^\((\d+), (\d+), (\d+), (\d+), '(.*)'\)/).slice(1);

    return cols.slice(0, 4).map(Number).concat(cols[4]);
});

代码在新行上拆分,然后逐行遍历并使用正则表达式拆分为单元格,该表达式贪婪地尽可能多地归因于最后一个单元格。然后它将前 4 个元素转换为整数,并将第 5 个元素(字符串)粘贴到末尾。

这为您提供了一个记录数组,其中每条记录本身就是一个数组。前 4 个元素是您的 PK(作为整数),第 5 个元素是字符串。

例如,给定您的输入,使用output[0][4] 获取"Gerald (Janitor), Broflowski",使用output[1][0] 获取第二个记录的第一个PK 2(不要忘记JavaScript 数组是零索引的)。

你可以在这里看到它的工作原理:http://jsfiddle.net/56ThR/

【讨论】:

  • 我看到这种方法的主要问题是,一个放错位置的) 会错误地解析它,而不是告诉你它不起作用。也就是说,它 要短得多,这很好。顺便说一句,当你在 maping 时,你可以用 .map(Number) 替换那个丑陋的匿名函数 :)
  • @BenjaminGruenbaum:我假设输入是生成的(考虑到大量记录),所以输入应该格式正确;我会将其添加到不断增长的“ifs”列表中。啊! Number 太棒了!我几乎使用了parseInt,但不会让自己不指定基数;)。
  • @BenjaminGruenbaum:那个错位的括号需要落在哪里才能使这种方法像你描述的那样失败?在没有任何平衡的情况下似乎可以正常工作:jsfiddle.net/56ThR/1
【解决方案2】:

另一种选择是将其转换为类似于Arrayeval 的东西。我知道不建议使用 eval,但这是一个很酷的解决方案 :)

var lines = input.split("\n");
var output = [];

for(var v in lines){

    // Remove opening ( 
    lines[v] = lines[v].slice(1);

    // Remove closing ) and what is after
    lines[v] = lines[v].slice(0, lines[v].lastIndexOf(')'));

    output[v] = eval("[" + lines[v] + "]");       
}

因此,eval parameter 看起来像:[1, 3, 2, 1, 'John (Finances)'],这确实是一个数组。

演示:http://jsfiddle.net/56ThR/3/

而且,它也可以写得更短:

var lines = input.split("\n");
var output = lines.map( function(el) { 
    return eval("[" + el.slice(1).slice(0, el.lastIndexOf(')') - 1) + "]");
});

演示:http://jsfiddle.net/56ThR/4/

【讨论】:

  • 这行得通:lines.replace(/\((.*)\)(,?)/g,function(a,b){ return "["+b+"]";}).split("\n").map(eval) 但这更邪恶eval("["+input.replace(/\((.*)\)/g,function(a,b){return"["+b+"]"})+"]")
  • 我猜你可以通过不使用 eval 而使用 JSON.parse 来改进它。在这种情况下,即使是纯粹主义者也应该感到满意......
【解决方案3】:

您总是可以“手动”完成 :)

var lines = input.split("\n");
var output = [];

for(var v in lines){

    output[v] = [];

    // Remove opening (
    lines[v] = lines[v].slice(1);

    // Get integers
    for(var i = 0; i < 4; ++i){
         var pos = lines[v].indexOf(',');
         output[v][i] = parseInt(lines[v].slice(0, pos));
         lines[v] = lines[v].slice(pos+1);   
    }

    // Get string betwen apostrophes
    lines[v] = lines[v].slice(lines[v].indexOf("'") + 1);
    output[v][4] = lines[v].slice(0, lines[v].indexOf("'"));
}

演示:http://jsfiddle.net/56ThR/2/

【讨论】:

    【解决方案4】:

    您不能(阅读可能不应该)为此使用正则表达式。如果括号包含另一对或一对不匹配怎么办?

    好消息是您可以轻松地为此构建一个标记器/解析器。 我们的想法是跟踪您当前的状态并采取相应的行动。

    这是我刚刚在这里编写的解析器的草图,重点是向您展示总体思路。如果您对此有任何概念性问题,请告诉我。

    工作 demo here 但我请求你在理解和修补它之前不要在生产中使用它。


    工作原理

    那么,我们如何构建解析器:

    var State = { // remember which state the parser is at.
         BeforeRecord:0, // at the (
         DuringInts:1, // at one of the integers
         DuringString:2, // reading the name string
         AfterRecord:3 // after the )
    };
    

    我们需要跟踪输出和当前工作对象,因为我们将一次解析这些。

    var records = []; // to contain the results
    var state = State.BeforeRecord;
    

    现在,我们迭代字符串,在其中继续前进并读取下一个字符

    for(var i = 0;i < input.length; i++){
        if(state === State.BeforeRecord){
            // handle logic when in (
        }
        ...
        if(state === State.AfterRecord){
            // handle that state
        }
    }
    

    现在,剩下的就是在每个状态下将其消耗到对象中:

    • 如果它位于 (,我们将开始解析并跳过所有空格
    • 读取所有整数并丢弃,
    • 在四个整数之后,将字符串从' 读取到下一个' 直到它的末尾
    • 在字符串之后,一直读到),存储对象,重新开始循环。

    实现起来也不是很困难。


    解析器

    var State = { // keep track of the state
         BeforeRecord:0,
         DuringInts:1,
         DuringString:2,
         AfterRecord:3
    };
    var records = []; // to contain the results
    var state = State.BeforeRecord;
    var input = " (1, 3, 2, 1, 'John (Finances)'), (2, 7, 2, 1, 'Mary Jane'), (3, 7, 3, 2, 'Gerald (Janitor), Broflowski')," // sample input
    
    var workingRecord = {}; // what we're reading into.
    
    for(var i = 0;i < input.length; i++){
        var token = input[i]; // read the current input
        if(state === State.BeforeRecord){ // before reading a record
            if(token === ' ') continue; // ignore whitespaces between records
            if(token === '('){ state = State.DuringInts; continue; }
            throw new Error("Expected ( before new record");
        }
        if(state === State.DuringInts){
            if(token === ' ') continue; // ignore whitespace
            for(var j = 0; j < 4; j++){
                if(token === ' ') {token = input[++i]; j--; continue;} // ignore whitespace 
                 var curNum = '';
                 while(token != ","){
                      if(!/[0-9]/.test(token)) throw new Error("Expected number, got " + token);
                      curNum += token;
                      token = input[++i]; // get the next token
                 }
                 workingRecord[j] = Number(curNum); // set the data on the record
                 token = input[++i]; // remove the comma
            }
            state = State.DuringString;
            continue; // progress the loop
        }
        if(state === State.DuringString){
             if(token === ' ') continue; // skip whitespace
             if(token === "'"){
                 var str = "";
                 token = input[++i];
                 var lenGuard = 1000;
                 while(token !== "'"){
                     str+=token;
                     if(lenGuard-- === 0) throw new Error("Error, string length bounded by 1000");
                     token = input[++i];
                 }
                 workingRecord.str = str;
                 token = input[++i]; // remove )
                 state = State.AfterRecord;
                 continue;
             }
        }
        if(state === State.AfterRecord){
            if(token === ' ') continue; // ignore whitespace
            if(token === ',') { // got the "," between records
                state = State.BeforeRecord;
                records.push(workingRecord);
                workingRecord = {}; // new record;
                continue;
            }
            throw new Error("Invalid token found " + token);
        }
    }
    console.log(records); // logs [Object, Object, Object]
                          // each object has four numbers and a string, for example
                          // records[0][0] is 1, records[0][1] is 3 and so on,
                          // records[0].str is "John (Finances)"
    

    【讨论】:

    • 我不认为使用 RegExp 来完成这项任务就像你想象的那样脆弱:没有嵌套重复来欺骗它......
    • @dandavis 格式 is 常规(证明:构造一个 DFA 来验证它,这是一项非常简单的任务),但是 - 用正则表达式解析它可能会很痛苦当格式可能发生变化时解析器并在需要时调试输入,这涉及到更多的魔法。这个手工解析器只花了我几分钟的时间来写(假设是 5 分钟),而且说它是如何工作的非常简单。我可以创建您的特定解析器不匹配所有内容的病态示例 - 但您可以很容易地修复这些示例(允许灵活的空格、行尾等)。所以主要是调试。
    • 我也喜欢 Cristy 的回答,但是通过你的算法,我学到了一种新技术。谢谢!
    【解决方案5】:

    您在这里拥有的基本上是您希望解析的 csv(逗号分隔值)文件。

    最简单的方法是使用 wxternal 库来解决您遇到的大部分问题

    示例:jquery csv 库是一个很好的库。 https://code.google.com/p/jquery-csv/

    【讨论】:

    • 如果您包含示例代码,我认为这个答案的反对票会更少。除非你证明这个问题可以在一行中解决,否则没有人会使用外部库。
    猜你喜欢
    • 2015-01-15
    • 1970-01-01
    • 1970-01-01
    • 2015-09-14
    • 2016-02-02
    • 1970-01-01
    • 2019-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多