2012-03-09 44 views
10

Tôi đang xem các tệp phân tách của tôi (ví dụ: CSV, tab tách biệt, v.v.) dựa trên ngăn xếp MS nói chung và .net cụ thể. Công nghệ duy nhất tôi loại trừ là SSIS, bởi vì tôi đã biết nó sẽ không đáp ứng được nhu cầu của tôi.Tùy chọn phân tích cú pháp CSV với .NET

Vì vậy, lựa chọn của tôi xuất hiện là:

  1. Regex.Split
  2. TextFieldParser
  3. OLEDB CSV Parser

Tôi có hai tiêu chí tôi phải đáp ứng. Thứ nhất, cho các tập tin sau đây, trong đó có hai hàng logic của dữ liệu (và năm hàng vật lý hoàn toàn):

101, Bob, "Keeps his house ""clean"".
Needs to work on laundry."
102, Amy, "Brilliant.
Driven.
Diligent."

Các kết quả phân tích phải nhường hai logic "hàng", gồm ba dây (hoặc cột) mỗi . Chuỗi hàng/cột thứ ba phải lưu giữ các dòng mới! Nói cách khác, trình phân tích cú pháp phải nhận ra khi các dòng "tiếp tục" trên hàng vật lý tiếp theo, do bộ định dạng văn bản "không được tiết lộ".

Tiêu chí thứ hai là dấu phân cách và dấu phân cách văn bản phải được định cấu hình, cho mỗi tệp. Dưới đây là hai chuỗi, lấy từ các tập tin khác nhau, mà tôi phải có khả năng phân tích:

var first = @"""This"",""Is,A,Record"",""That """"Cannot"""", they say,"","""",,""be"",rightly,""parsed"",at all"; 
var second = @"~This~|~Is|A|Record~|~ThatCannot~|~be~|~parsed~|at all"; 

Một phân tích thích hợp của chuỗi "đầu tiên" sẽ là:

  • này
  • là, A, Ghi
  • That "không thể", họ nói,
  • _
  • _
  • được
  • đúng
  • phân tích cú pháp
  • ở tất cả

Các '_' chỉ đơn giản có nghĩa là một trống bị bắt - Tôi không muốn có một thanh dưới đen xuất hiện.

Một giả thiết quan trọng có thể được thực hiện về các tệp phẳng được phân tích cú pháp: sẽ có một số cột cố định cho mỗi tệp.

Bây giờ, hãy đi sâu vào các tùy chọn kỹ thuật.

REGEX

Thứ nhất, nhiều phản ứng nhận xét rằng regex "không phải là cách tốt nhất" để đạt được mục tiêu.Tôi đã, tuy nhiên, hãy tìm một commenter who offered an excellent CSV regex:

var regex = @",(?=(?:[^""]*""[^""]*"")*(?![^""]*""))"; 
var Regex.Split(first, regex).Dump(); 

Kết quả, áp dụng cho chuỗi "đầu tiên" là khá tuyệt vời:

  • "Đây"
  • "Có, A, Record"
  • "That "" không thể" "họ nói,"
  • ""
  • _
  • "b e"
  • đúng
  • 'phân tích'
  • ở tất cả

Nó sẽ được tốt đẹp nếu có dấu ngoặc kép được dọn dẹp, nhưng tôi có thể dễ dàng đối phó với điều đó như một bước sau quá trình. Nếu không, phương pháp này có thể được sử dụng để phân tích cả hai chuỗi mẫu "đầu tiên" và "thứ hai", miễn là regex được sửa đổi cho dấu ngã và ký hiệu ống cho phù hợp. Xuất sắc!

Nhưng vấn đề thực sự liên quan đến tiêu chí nhiều dòng. Trước khi có thể áp dụng regex cho một chuỗi, tôi phải đọc toàn bộ "hàng" hợp lý từ tệp. Thật không may, tôi không biết có bao nhiêu hàng vật lý cần đọc để hoàn thành hàng logic, trừ khi tôi có máy regex/state.

Vì vậy, điều này sẽ trở thành vấn đề "gà và trứng". Lựa chọn tốt nhất của tôi là đọc toàn bộ tệp vào bộ nhớ dưới dạng một chuỗi khổng lồ và để regex phân loại nhiều dòng (tôi không kiểm tra liệu regex ở trên có thể xử lý điều đó) hay không. Nếu tôi đã có một tập tin 10 gig, điều này có thể là một chút bấp bênh.

Bật tùy chọn tiếp theo.

TextFieldParser

Ba dòng mã sẽ làm cho các vấn đề với tùy chọn này rõ ràng:

var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser(stream); 
reader.Delimiters = new string[] { @"|" }; 
reader.HasFieldsEnclosedInQuotes = true; 

Cấu hình ký tự phân cách có vẻ tốt. Tuy nhiên, "HasFieldsEnclosedInQuotes" là "trò chơi kết thúc". Tôi choáng váng rằng các dấu phân tách có thể được định cấu hình tùy ý, nhưng ngược lại, tôi không có tùy chọn định danh nào khác ngoài các trích dẫn. Hãy nhớ rằng, tôi cần cấu hình trên vòng loại văn bản. Vì vậy, một lần nữa, trừ khi ai đó biết một thủ thuật cấu hình TextFieldParser, đây là trò chơi kết thúc.

OLEDB

Một đồng nghiệp nói với tôi tùy chọn này có hai nhược điểm lớn. Đầu tiên, nó có hiệu suất khủng khiếp đối với các tệp lớn (ví dụ: 10 gig). Thứ hai, vì vậy tôi đã nói, nó đoán các kiểu dữ liệu đầu vào thay vì cho phép bạn chỉ định. Không tốt.

TRỢ GIÚP

Vì vậy, tôi muốn biết sự thật tôi đã sai (nếu có), và các tùy chọn khác mà tôi bỏ qua. Có lẽ ai đó biết một cách để ban giám khảo-rig TextFieldParser sử dụng một dấu phân cách tùy ý. Và có lẽ OLEDB đã giải quyết các vấn đề đã nêu (hoặc có lẽ không bao giờ có chúng?).

Điều gì xảy ra?

+0

Bạn đã thử các tùy chọn được liệt kê tại http://stackoverflow.com/questions/316649/csv-parsing chưa? – TrueWill

+0

Tôi đồng ý với @ Appleman1234, Người trợ giúp phải là tất cả những gì bạn cần – Kane

+0

[Người trợ giúp tệp] (http://www.filehelpers.com/) có đáp ứng các yêu cầu của bạn không? – Appleman1234

Trả lời

4

Bạn đã thử tìm kiếm một .NET đã sẵn có CSV parser chưa? This one xác nhận quyền sở hữu để xử lý các bản ghi nhiều dòng nhanh hơn đáng kể so với OLEDB.

+0

FastCSV là một thư viện được chấp nhận khá tốt. –

+0

Ya, tôi đã làm một số tìm kiếm xung quanh - đó là lý do tại sao tôi đã đề cập đến ba lựa chọn. Vấn đề là có nhiều lựa chọn hơn, nhưng liệu chúng có đáp ứng tiêu chí của tôi hay không là một quá trình rất chậm. Tôi hy vọng ai đó đã biết sự lựa chọn đúng đắn. –

1

Hãy nhìn vào đoạn code tôi gửi cho câu hỏi này:

https://stackoverflow.com/a/1544743/3043

Nó bao gồm nhất các yêu cầu của bạn, và nó sẽ không mất nhiều để cập nhật nó để hỗ trợ các dấu phân tách hoặc văn bản thay thế.

4

Tôi đã viết điều này một thời gian trở lại làm trình phân tích cú pháp CSV độc lập, gọn nhẹ. Tôi tin rằng nó đáp ứng tất cả các yêu cầu của bạn. Hãy thử với kiến ​​thức rằng nó có lẽ không phải là chống đạn.

Nếu nó hoạt động cho bạn, hãy thay đổi không gian tên và sử dụng mà không bị giới hạn.

namespace NFC.Portability 
{ 
    using System; 
    using System.Collections.Generic; 
    using System.Data; 
    using System.IO; 
    using System.Linq; 
    using System.Text; 

    /// <summary> 
    /// Loads and reads a file with comma-separated values into a tabular format. 
    /// </summary> 
    /// <remarks> 
    /// Parsing assumes that the first line will always contain headers and that values will be double-quoted to escape double quotes and commas. 
    /// </remarks> 
    public unsafe class CsvReader 
    { 
     private const char SEGMENT_DELIMITER = ','; 
     private const char DOUBLE_QUOTE = '"'; 
     private const char CARRIAGE_RETURN = '\r'; 
     private const char NEW_LINE = '\n'; 

     private DataTable _table = new DataTable(); 

     /// <summary> 
     /// Gets the data contained by the instance in a tabular format. 
     /// </summary> 
     public DataTable Table 
     { 
      get 
      { 
       // validation logic could be added here to ensure that the object isn't in an invalid state 

       return _table; 
      } 
     } 

     /// <summary> 
     /// Creates a new instance of <c>CsvReader</c>. 
     /// </summary> 
     /// <param name="path">The fully-qualified path to the file from which the instance will be populated.</param> 
     public CsvReader(string path) 
     { 
      if(path == null) 
      { 
       throw new ArgumentNullException("path"); 
      } 

      FileStream fs = new FileStream(path, FileMode.Open); 
      Read(fs); 
     } 

     /// <summary> 
     /// Creates a new instance of <c>CsvReader</c>. 
     /// </summary> 
     /// <param name="stream">The stream from which the instance will be populated.</param> 
     public CsvReader(Stream stream) 
     { 
      if(stream == null) 
      { 
       throw new ArgumentNullException("stream"); 
      } 

      Read(stream); 
     } 

     /// <summary> 
     /// Creates a new instance of <c>CsvReader</c>. 
     /// </summary> 
     /// <param name="bytes">The array of bytes from which the instance will be populated.</param> 
     public CsvReader(byte[] bytes) 
     { 
      if(bytes == null) 
      { 
       throw new ArgumentNullException("bytes"); 
      } 

      MemoryStream ms = new MemoryStream(); 
      ms.Write(bytes, 0, bytes.Length); 
      ms.Position = 0; 

      Read(ms); 
     } 

     private void Read(Stream s) 
     { 
      string lines; 

      using(StreamReader sr = new StreamReader(s)) 
      { 
       lines = sr.ReadToEnd(); 
      } 

      if(string.IsNullOrWhiteSpace(lines)) 
      { 
       throw new InvalidOperationException("Data source cannot be empty."); 
      } 

      bool inQuotes = false; 
      int lineNumber = 0; 
      StringBuilder buffer = new StringBuilder(128); 
      List<string> values = new List<string>(); 

      Action endSegment =() => 
      { 
       values.Add(buffer.ToString()); 
       buffer.Clear(); 
      }; 

      Action endLine =() => 
      { 
       if(lineNumber == 0) 
       { 
        CreateColumns(values); 
        values.Clear(); 
       } 
       else 
       { 
        CreateRow(values); 
        values.Clear(); 
       } 

       values.Clear(); 
       lineNumber++; 
      }; 

      fixed(char* pStart = lines) 
      { 
       char* pChar = pStart; 
       char* pEnd = pStart + lines.Length; 

       while(pChar < pEnd) // leave null terminator out 
       { 
        if(*pChar == DOUBLE_QUOTE) 
        { 
         if(inQuotes) 
         { 
          if(Peek(pChar, pEnd) == SEGMENT_DELIMITER) 
          { 
           endSegment(); 
           pChar++; 
          } 
          else if(!ApproachingNewLine(pChar, pEnd)) 
          { 
           buffer.Append(DOUBLE_QUOTE); 
          } 
         } 

         inQuotes = !inQuotes; 
        } 
        else if(*pChar == SEGMENT_DELIMITER) 
        { 
         if(!inQuotes) 
         { 
          endSegment(); 
         } 
         else 
         { 
          buffer.Append(SEGMENT_DELIMITER); 
         } 
        } 
        else if(AtNewLine(pChar, pEnd)) 
        { 
         if(!inQuotes) 
         { 
          endSegment(); 
          endLine(); 
          pChar++; 
         } 
         else 
         { 
          buffer.Append(*pChar); 
         } 
        } 
        else 
        { 
         buffer.Append(*pChar); 
        } 

        pChar++; 
       } 
      } 

      // append trailing values at the end of the file 
      if(values.Count > 0) 
      { 
       endSegment(); 
       endLine(); 
      } 
     } 

     /// <summary> 
     /// Returns the next character in the sequence but does not advance the pointer. Checks bounds. 
     /// </summary> 
     /// <param name="pChar">Pointer to current character.</param> 
     /// <param name="pEnd">End of range to check.</param> 
     /// <returns> 
     /// Returns the next character in the sequence, or char.MinValue if range is exceeded. 
     /// </returns> 
     private char Peek(char* pChar, char* pEnd) 
     { 
      if(pChar < pEnd) 
      { 
       return *(pChar + 1); 
      } 

      return char.MinValue; 
     } 

     /// <summary> 
     /// Determines if the current character represents a newline. This includes lookahead for two character newline delimiters. 
     /// </summary> 
     /// <param name="pChar"></param> 
     /// <param name="pEnd"></param> 
     /// <returns></returns> 
     private bool AtNewLine(char* pChar, char* pEnd) 
     { 
      if(*pChar == NEW_LINE) 
      { 
       return true; 
      } 

      if(*pChar == CARRIAGE_RETURN && Peek(pChar, pEnd) == NEW_LINE) 
      { 
       return true; 
      } 

      return false; 
     } 

     /// <summary> 
     /// Determines if the next character represents a newline, or the start of a newline. 
     /// </summary> 
     /// <param name="pChar"></param> 
     /// <param name="pEnd"></param> 
     /// <returns></returns> 
     private bool ApproachingNewLine(char* pChar, char* pEnd) 
     { 
      if(Peek(pChar, pEnd) == CARRIAGE_RETURN || Peek(pChar, pEnd) == NEW_LINE) 
      { 
       // technically this cheats a little to avoid a two char peek by only checking for a carriage return or new line, not both in sequence 
       return true; 
      } 

      return false; 
     } 

     private void CreateColumns(List<string> columns) 
     { 
      foreach(string column in columns) 
      { 
       DataColumn dc = new DataColumn(column); 
       _table.Columns.Add(dc); 
      } 
     } 

     private void CreateRow(List<string> values) 
     { 
      if(values.Where((o) => !string.IsNullOrWhiteSpace(o)).Count() == 0) 
      { 
       return; // ignore rows which have no content 
      } 

      DataRow dr = _table.NewRow(); 
      _table.Rows.Add(dr); 

      for(int i = 0; i < values.Count; i++) 
      { 
       dr[i] = values[i]; 
      } 
     } 
    } 
} 
Các vấn đề liên quan