'tag.htm'; break; case 'flag': $pre .= $default_pre .= 'flag.htm'; break; case 'my': $pre .= $default_pre .= 'my.htm'; break; case 'my_password': $pre .= $default_pre .= 'my_password.htm'; break; case 'my_bind': $pre .= $default_pre .= 'my_bind.htm'; break; case 'my_avatar': $pre .= $default_pre .= 'my_avatar.htm'; break; case 'home_article': $pre .= $default_pre .= 'home_article.htm'; break; case 'home_comment': $pre .= $default_pre .= 'home_comment.htm'; break; case 'user': $pre .= $default_pre .= 'user.htm'; break; case 'user_login': $pre .= $default_pre .= 'user_login.htm'; break; case 'user_create': $pre .= $default_pre .= 'user_create.htm'; break; case 'user_resetpw': $pre .= $default_pre .= 'user_resetpw.htm'; break; case 'user_resetpw_complete': $pre .= $default_pre .= 'user_resetpw_complete.htm'; break; case 'user_comment': $pre .= $default_pre .= 'user_comment.htm'; break; case 'single_page': $pre .= $default_pre .= 'single_page.htm'; break; case 'search': $pre .= $default_pre .= 'search.htm'; break; case 'operate_sticky': $pre .= $default_pre .= 'operate_sticky.htm'; break; case 'operate_close': $pre .= $default_pre .= 'operate_close.htm'; break; case 'operate_delete': $pre .= $default_pre .= 'operate_delete.htm'; break; case 'operate_move': $pre .= $default_pre .= 'operate_move.htm'; break; case '404': $pre .= $default_pre .= '404.htm'; break; case 'read_404': $pre .= $default_pre .= 'read_404.htm'; break; case 'list_404': $pre .= $default_pre .= 'list_404.htm'; break; default: $pre .= $default_pre .= theme_mode_pre(); break; } if ($config['theme']) { $conffile = APP_PATH . 'view/template/' . $config['theme'] . '/conf.json'; $json = is_file($conffile) ? xn_json_decode(file_get_contents($conffile)) : array(); } !empty($json['installed']) and $path_file = APP_PATH . 'view/template/' . $config['theme'] . '/htm/' . ($id ? $id . '_' : '') . $pre; (empty($path_file) || !is_file($path_file)) and $path_file = APP_PATH . 'view/template/' . $config['theme'] . '/htm/' . $pre; if (!empty($config['theme_child']) && is_array($config['theme_child'])) { foreach ($config['theme_child'] as $theme) { if (empty($theme) || is_array($theme)) continue; $path_file = APP_PATH . 'view/template/' . $theme . '/htm/' . ($id ? $id . '_' : '') . $pre; !is_file($path_file) and $path_file = APP_PATH . 'view/template/' . $theme . '/htm/' . $pre; } } !is_file($path_file) and $path_file = APP_PATH . ($dir ? 'plugin/' . $dir . '/view/htm/' : 'view/htm/') . $default_pre; return $path_file; } function theme_mode_pre($type = 0) { global $config; $mode = $config['setting']['website_mode']; $pre = ''; if (1 == $mode) { $pre .= 2 == $type ? 'portal_category.htm' : 'portal.htm'; } elseif (2 == $mode) { $pre .= 2 == $type ? 'flat_category.htm' : 'flat.htm'; } else { $pre .= 2 == $type ? 'index_category.htm' : 'index.htm'; } return $pre; } ?>javascript - Split a CSV string by line skipping newlines contained between quotes - Stack Overflow
最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Split a CSV string by line skipping newlines contained between quotes - Stack Overflow

programmeradmin3浏览0评论

If the following regex can split a csv string by line.

var lines = csv.split(/\r|\r?\n/g);

How could this be adapted to skip newline chars that are contained within a CSV value (Ie between quotes/double-quotes)?

Example:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P
CB",,1,"Offsite",

If you don't see it, here's a version with the newlines visible:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P\r\nCB",,1,"Offsite",\r\n 

The part I'm trying to skip over is the newline contained in the middle of the "PCB" entry.

Update:

I probably should've mentioned this before but this is a part of a dedicated CSV parsing library called jquery-csv. To provide a better context I have added the current parser implementation below.

Here's the code for validating and parsing an entry (ie one line):

$.csvEntry2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;

  // build the CSV validator regex
  var reValid = /^\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*(?:S\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*)*$/;
  reValid = RegExp(reValid.source.replace(/S/g, separator));
  reValid = RegExp(reValid.source.replace(/D/g, delimiter));

  // build the CSV line parser regex
  var reValue = /(?!\s*$)\s*(?:D([^D\\]*(?:\\[\S\s][^D\\]*)*)D|([^SD\s\\]*(?:\s+[^SD\s\\]+)*))\s*(?:S|$)/g;
  reValue = RegExp(reValue.source.replace(/S/g, separator), 'g');
  reValue = RegExp(reValue.source.replace(/D/g, delimiter), 'g');

  // Return NULL if input string is not well formed CSV string.
  if (!reValid.test(csv)) {
    return null;
  }

  // "Walk" the string using replace with callback.
  var output = [];
  csv.replace(reValue, function(m0, m1, m2) {
    // Remove backslash from any delimiters in the value
    if (m1 !== undefined) {
      var reDelimiterUnescape = /\\D/g;              
      reDelimiterUnescape = RegExp(reDelimiterUnescape.source.replace(/D/, delimiter), 'g');
      output.push(m1.replace(reDelimiterUnescape, delimiter));
    } else if (m2 !== undefined) { 
      output.push(m2);
    }
    return '';
  });

  // Handle special case of empty last value.
  var reEmptyLast = /S\s*$/;
  reEmptyLast = RegExp(reEmptyLast.source.replace(/S/, separator));
  if (reEmptyLast.test(csv)) {
    output.push('');
  }

  return output;
};

Note: I haven't tested yet but I think I could probably incorporate the last match into the main split/callback.

This is the code that does the split-by-line part:

$.csv2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;
  var skip = 'skip' in meta ? meta.skip : $.csvDefaults.skip;

  // process by line
  var lines = csv.split(/\r\n|\r|\n/g);
  var output = [];
  for(var i in lines) {
    if(i < skip) {
      continue;
    }
    // process each value
    var line = $.csvEntry2Array(lines[i], {
      delimiter: delimiter,
      separator: separator
    });
    output.push(line);
  }

  return output;
};

For a breakdown on how that reges works take a look at this answer. Mine is a slightly adapted version. I consolidated the single and double quote matching to match just one text delimiter and made the delimiter/separators dynamic. It does a great job of validating entiries but the line-splitting solution I added on top is pretty frail and breaks on the edge case I described above.

I'm just looking for a solution that walks the string extracting valid entries (to pass on to the entry parser) or fails on bad data returning an error indicating the line the parsing failed on.

Update:

splitLines: function(csv, delimiter) {
  var state = 0;
  var value = "";
  var line = "";
  var lines = [];
  function endOfRow() {
    lines.push(value);
    value = "";
    state = 0;
  };
  csv.replace(/(\"|,|\n|\r|[^\",\r\n]+)/gm, function (m0){
    switch (state) {
      // the start of an entry
      case 0:
        if (m0 === "\"") {
          state = 1;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (/^\r$/.test(m0)) {
          // carriage returns are ignored
        } else {
          value += m0;
          state = 3;
        }
        break;
      // delimited input  
      case 1:
        if (m0 === "\"") {
          state = 2;
        } else {
          value += m0;
          state = 1;
        }
        break;
      // delimiter found in delimited input
      case 2:
        // is the delimiter escaped?
        if (m0 === "\"" && value.substr(value.length - 1) === "\"") {
          value += m0;
          state = 1;
        } else if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal state");
        }
        break;
      // un-delimited input
      case 3:
        if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\"") {
          throw new Error("Unquoted delimiter found");
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal data");
        }
          break;
      default:
        throw new Error("Unknown state");
    }
    return "";
  });
  if (state != 0) {
    endOfRow();
  }
  return lines;
}

All it took is 4 states for a line splitter:

  • 0: the start of an entry
  • 1: the following is quoted
  • 2: a second quote has been encountered
  • 3: the following isn't quoted

It's almost a plete parser. For my use case, I just wanted a line splitter so I could provide a more granual approach to processing CSV data.

Note: Credit for this approach goes to another dev whom I won't name publicly without his permission. All I did was adapt it from a plete parser to a line-splitter.

Update:

Discovered a few broken edge cases in the previous lineSplitter implementation. The one provided should be fully RFC 4180 pliant.

If the following regex can split a csv string by line.

var lines = csv.split(/\r|\r?\n/g);

How could this be adapted to skip newline chars that are contained within a CSV value (Ie between quotes/double-quotes)?

Example:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P
CB",,1,"Offsite",

If you don't see it, here's a version with the newlines visible:

2,"Evans & Sutherland","230-132-111AA",,"Visual","P\r\nCB",,1,"Offsite",\r\n 

The part I'm trying to skip over is the newline contained in the middle of the "PCB" entry.

Update:

I probably should've mentioned this before but this is a part of a dedicated CSV parsing library called jquery-csv. To provide a better context I have added the current parser implementation below.

Here's the code for validating and parsing an entry (ie one line):

$.csvEntry2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;

  // build the CSV validator regex
  var reValid = /^\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*(?:S\s*(?:D[^D\\]*(?:\\[\S\s][^D\\]*)*D|[^SD\s\\]*(?:\s+[^SD\s\\]+)*)\s*)*$/;
  reValid = RegExp(reValid.source.replace(/S/g, separator));
  reValid = RegExp(reValid.source.replace(/D/g, delimiter));

  // build the CSV line parser regex
  var reValue = /(?!\s*$)\s*(?:D([^D\\]*(?:\\[\S\s][^D\\]*)*)D|([^SD\s\\]*(?:\s+[^SD\s\\]+)*))\s*(?:S|$)/g;
  reValue = RegExp(reValue.source.replace(/S/g, separator), 'g');
  reValue = RegExp(reValue.source.replace(/D/g, delimiter), 'g');

  // Return NULL if input string is not well formed CSV string.
  if (!reValid.test(csv)) {
    return null;
  }

  // "Walk" the string using replace with callback.
  var output = [];
  csv.replace(reValue, function(m0, m1, m2) {
    // Remove backslash from any delimiters in the value
    if (m1 !== undefined) {
      var reDelimiterUnescape = /\\D/g;              
      reDelimiterUnescape = RegExp(reDelimiterUnescape.source.replace(/D/, delimiter), 'g');
      output.push(m1.replace(reDelimiterUnescape, delimiter));
    } else if (m2 !== undefined) { 
      output.push(m2);
    }
    return '';
  });

  // Handle special case of empty last value.
  var reEmptyLast = /S\s*$/;
  reEmptyLast = RegExp(reEmptyLast.source.replace(/S/, separator));
  if (reEmptyLast.test(csv)) {
    output.push('');
  }

  return output;
};

Note: I haven't tested yet but I think I could probably incorporate the last match into the main split/callback.

This is the code that does the split-by-line part:

$.csv2Array = function(csv, meta) {
  var meta = (meta !== undefined ? meta : {});
  var separator = 'separator' in meta ? meta.separator : $.csvDefaults.separator;
  var delimiter = 'delimiter' in meta ? meta.delimiter : $.csvDefaults.delimiter;
  var skip = 'skip' in meta ? meta.skip : $.csvDefaults.skip;

  // process by line
  var lines = csv.split(/\r\n|\r|\n/g);
  var output = [];
  for(var i in lines) {
    if(i < skip) {
      continue;
    }
    // process each value
    var line = $.csvEntry2Array(lines[i], {
      delimiter: delimiter,
      separator: separator
    });
    output.push(line);
  }

  return output;
};

For a breakdown on how that reges works take a look at this answer. Mine is a slightly adapted version. I consolidated the single and double quote matching to match just one text delimiter and made the delimiter/separators dynamic. It does a great job of validating entiries but the line-splitting solution I added on top is pretty frail and breaks on the edge case I described above.

I'm just looking for a solution that walks the string extracting valid entries (to pass on to the entry parser) or fails on bad data returning an error indicating the line the parsing failed on.

Update:

splitLines: function(csv, delimiter) {
  var state = 0;
  var value = "";
  var line = "";
  var lines = [];
  function endOfRow() {
    lines.push(value);
    value = "";
    state = 0;
  };
  csv.replace(/(\"|,|\n|\r|[^\",\r\n]+)/gm, function (m0){
    switch (state) {
      // the start of an entry
      case 0:
        if (m0 === "\"") {
          state = 1;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (/^\r$/.test(m0)) {
          // carriage returns are ignored
        } else {
          value += m0;
          state = 3;
        }
        break;
      // delimited input  
      case 1:
        if (m0 === "\"") {
          state = 2;
        } else {
          value += m0;
          state = 1;
        }
        break;
      // delimiter found in delimited input
      case 2:
        // is the delimiter escaped?
        if (m0 === "\"" && value.substr(value.length - 1) === "\"") {
          value += m0;
          state = 1;
        } else if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal state");
        }
        break;
      // un-delimited input
      case 3:
        if (m0 === ",") {
          value += m0;
          state = 0;
        } else if (m0 === "\"") {
          throw new Error("Unquoted delimiter found");
        } else if (m0 === "\n") {
          endOfRow();
        } else if (m0 === "\r") {
          // Ignore
        } else {
          throw new Error("Illegal data");
        }
          break;
      default:
        throw new Error("Unknown state");
    }
    return "";
  });
  if (state != 0) {
    endOfRow();
  }
  return lines;
}

All it took is 4 states for a line splitter:

  • 0: the start of an entry
  • 1: the following is quoted
  • 2: a second quote has been encountered
  • 3: the following isn't quoted

It's almost a plete parser. For my use case, I just wanted a line splitter so I could provide a more granual approach to processing CSV data.

Note: Credit for this approach goes to another dev whom I won't name publicly without his permission. All I did was adapt it from a plete parser to a line-splitter.

Update:

Discovered a few broken edge cases in the previous lineSplitter implementation. The one provided should be fully RFC 4180 pliant.

Share edited Oct 7, 2021 at 6:01 CommunityBot 11 silver badge asked May 2, 2012 at 4:25 Evan PlaiceEvan Plaice 14.2k6 gold badges78 silver badges94 bronze badges 2
  • You simply cannot do this using regex. You can create a regex that handles some or even most conditions. But there will always be some valid CSV which will not work with the regex. – James Anderson Commented May 2, 2012 at 4:41
  • Regex is for detection, not parsing. As you scan across the text, if you have to "remember" anything besides the characters (i.e. i'm "inside" a quoted literal), you are parsing it. Subtle difference, which is why everyone wants to use it to parse. – Jeff Meatball Yang Commented May 2, 2012 at 5:14
Add a ment  | 

3 Answers 3

Reset to default 2

As I have noted in a ment there is no plete solution just using single regex.

A novel method using several regexps by splitting on ma and joining back strings with embedded mas is described here:-

Personally I would use a simple finite state machine as described here

The state machine has more code, but the code is cleaner and its clear what each piece of code is doing. Longer term this will be much more reliable and maintainable.

It's not a good idea to use regex's to parse. Better to use it to detect the "bad" splits and then merge them back:

var lines = csv.split(/\r?\n/g);
var bad = [];

for(var i=lines.length-1; i> 0; i--) {
    // find all the unescaped quotes on the line:
    var m = lines[i].match(/[^\\]?\"/g);

    // if there are an odd number of them, this line, and the line after it is bad:
    if((m ? m.length : 0) % 2 == 1) { bad.push(i--); }
}

// starting at the bottom of the list, merge lines back, using \r\n
for(var b=0,len=bad.length; b < len; b++) {
    lines.splice(bad[b]-1, 2, lines[bad[b]-1]+"\r\n"+lines[bad[b]]);
}

(This answer is licensed under both CC0 and WTFPL.)

Be careful- That newline is PART of that value. It's not PCB, it's P\nCB.

However, why can't you just use string.split(',')? If need be, you can run through the list and cast to ints or remove the padded quotation marks.

发布评论

评论列表(0)

  1. 暂无评论