Csv biçimi bir normal ifade ile tanımlanabilir mi?


19

Bir meslektaşım ve ben son zamanlarda saf bir normal ifadenin csv formatını tam olarak kapsülleyebildiğini tartıştık, böylece tüm dosyaları herhangi bir kaçış karakteri, alıntı karakteri ve ayırıcı karakterle ayrıştırabilsin.

Normal ifadenin, oluşturulduktan sonra bu karakterleri değiştirmesi gerekmez, ancak başka bir kenar durumunda başarısız olmamalıdır.

Bunun sadece bir belirteç için imkansız olduğunu iddia ettim. Bunu yapabilen tek regex, sadece tokenleştirmenin ötesine geçen çok karmaşık bir PCRE stili.

Ben hatları boyunca bir şey arıyorum:

... csv formatı bağlamsız bir gramerdir ve bu nedenle, sadece regex ile ayrıştırmak imkansızdır ...

Yoksa yanılıyor muyum? Csv'yi sadece POSIX normal ifadesiyle ayrıştırmak mümkün mü?

Örneğin, hem escape char hem de quote char ise ", bu iki satır geçerli csv'dir:

"""this is a test.""",""
"and he said,""What will be, will be."", to which I replied, ""Surely not!""","moving on to the next field here..."

hiçbir yerde yuvalama olmadığı için bir CSV değil (IIRC)
cırcır ucube

1
ama uç durumlar nelerdir? belki CSV'de düşündüğümden daha fazlası var?
c69

1
@ c69 Kaçış ve alıntı karakter her ikisi de ". O zaman aşağıdakiler geçerlidir:"""this is a test.""",""
Spencer Rathbun

Buradan regexp denedin mi?
dasblinkenlight

1
Edge vakalarına dikkat etmeniz gerekir, ancak bir normal ifade csv'yi tanımladığınız şekilde tokenize edebilmelidir. Normal ifadenin keyfi sayıda tırnak sayması gerekmez - yalnızca düzenli ifadelerin yapabileceği 3'e kadar saymak gerekir. Diğerlerinin de belirttiği gibi, bir csv belirtecinin olmasını beklediğiniz şeyin iyi tanımlanmış bir temsilini
yazmaya çalışmalısınız

Yanıtlar:


20

Teoride güzel, pratikte korkunç

By CSV ben açıklandığı gibi kongre demek varsaymak gidiyorum RFC 4180 .

Temel CSV verilerini eşleştirmek önemsiz olsa da:

"data", "more data"

Not: BTW, bunun gibi çok basit ve iyi yapılandırılmış veriler için bir .split ('/ n'). Split ('"') işlevi kullanmak çok daha verimlidir. Normal İfadeler NDFSM (Deterministik Olmayan Sonlu Durum Makinesi), kaçış karakterleri gibi uç durumlar eklemeye başladığınızda çok fazla zaman geri izleme harcar.

Örneğin, bulduğum en kapsamlı düzenli ifade eşleme dizesi:

re_valid = r"""
# Validate a CSV string having single, double or un-quoted values.
^                                   # Anchor to start of string.
\s*                                 # Allow whitespace before value.
(?:                                 # Group for value alternatives.
  '[^'\\]*(?:\\[\S\s][^'\\]*)*'     # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*"     # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*    # or Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Allow whitespace after value.
(?:                                 # Zero or more additional values
  ,                                 # Values separated by a comma.
  \s*                               # Allow whitespace before value.
  (?:                               # Group for value alternatives.
    '[^'\\]*(?:\\[\S\s][^'\\]*)*'   # Either Single quoted string,
  | "[^"\\]*(?:\\[\S\s][^"\\]*)*"   # or Double quoted string,
  | [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*  # or Non-comma, non-quote stuff.
  )                                 # End group of value alternatives.
  \s*                               # Allow whitespace after value.
)*                                  # Zero or more additional values
$                                   # Anchor to end of string.
"""

Tek ve çift tırnaklı değerleri makul bir şekilde işler, ancak değerlerde, kaçan tırnak işaretlerinde vb.

Kaynak: Yığın Taşması - Bir dizeyi JavaScript ile nasıl ayrıştırabilirim

Ortak uç durumlar tanıtıldığında kabus haline geliyor ...

"such as ""escaped""","data"
"values that contain /n newline chars",""
"escaped, commas, like",",these"
"un-delimited data like", this
"","empty values"
"empty trailing values",        // <- this is completely valid
                                // <- trailing newline, may or may not be included

Yalnızca newline değeri olarak kenar durumu, vahşi doğada bulunan RegEx tabanlı ayrıştırıcıların% 99.9999'unu kırmak için yeterlidir. Tek 'makul' alternatif, daha yüksek seviyeli analiz için kullanılan bir durum makinesiyle eşleştirilmiş temel kontrol / kontrol dışı karakter (yani terminal vs terminal olmayan) belirteçleri için RegEx eşleşmesini kullanmaktır.

Kaynak: Başka türlü kapsamlı ağrı ve ıstırap olarak bilinen deneyim.

Dünyadaki javascript tabanlı, tam RFC uyumlu CSV ayrıştırıcısı olan jquery- CSV'nin yazarıyım. Ayları bu sorunla başa çıkmak, birçok akıllı insanla konuşmak ve çekirdek ayrıştırıcı motorunun 3 tam yeniden yazımı da dahil olmak üzere farklı uygulamalar varsa bir ton denemek için harcadım.

tl; dr - Hikayenin ahlakı, PCRE tek başına en basit ve katı düzenli (Ie Type-III) dilbilgisi dışında bir şeyi ayrıştırmak için berbat. Yine de, terminal ve terminal olmayan dizeleri tokenize etmek için yararlıdır.


1
Evet, bu benim deneyimim de oldu. Çok basit bir CSV modelinden daha fazlasını tam olarak kapsülleme girişimleri bu şeylere girer ve daha sonra büyük bir regex'in hem verimlilik sorunlarına hem de karmaşıklık sorunlarına karşı koyarsınız. Düğüm-csv kütüphanesine baktınız mı ? Bu teoriyi de doğrulıyor gibi görünüyor. Önemsiz olmayan her uygulama dahili olarak bir ayrıştırıcı kullanır.
Spencer Rathbun

@SpencerRathbun Yep. Eminim daha önce düğüm-csv kaynağına bir göz attım. İşlem için tipik bir karakter belirteç durumu makinesi kullanıyor gibi görünüyor. Jquery-csv ayrıştırıcı, terminal / terminal olmayan belirteçleri için regex kullanmam dışında aynı temel kavram üzerinde çalışır. Chargex, char-by-char bazında değerlendirmek ve birleştirmek yerine, bir seferde birden fazla terminal olmayan karakteri eşleştirebilir ve bunları bir grup (yani dize) olarak döndürebilir. Bu, gereksiz birleştirmeyi en aza indirir ve verimliliği 'artırmalıdır'.
Evan Plaice

20

Regex herhangi bir normal dili ayrıştırabilir ve özyinelemeli dilbilgisi gibi süslü şeyleri ayrıştıramaz. Ancak CSV oldukça düzenli gibi görünüyor, bu yüzden düzenli bir ifadeyle çözümlenebilir.

Tanımdan çalışalım : izin verilen dizi, seçim formu alternatifleri ( |) ve tekrarlama (Kleene star, the *).

  • Sıralanmamış bir değer normal: [^,]*# virgül hariç herhangi bir karakter
  • Verilen bir değer normaldir: "([^\"]|\\\\|\\")*" Alıntı "veya kaçan alıntı \"veya kaçan kaçış dışında herhangi bir şey dizisi\\
    • Bazı formlar, tırnak işaretleri içeren bir tırnak işareti içerebilir ("")*" yukarıdaki ifadeye .
  • İzin verilen bir değer normaldir: <unquoted-value> |<quoted-value>
  • Tek bir CSV satırı normaldir: <değer>(, <değer>)*
  • İle ayrılmış satırlar dizisi \n de açıktır.

Bu ifadelerin her birini titizlikle test etmedim ve asla yakalama grupları tanımlamamıştım. Ben de yerine kullanılabilecek bir karakter varyantları gibi bazı technicalities arkasına gizleyen ,, "ya o satırı: Bu sadece birkaç biraz farklı dilleri olsun, düzenlilik bozmazlar.

Bu kanıtta bir sorun tespit ederseniz, lütfen yorum yapın! :)

Ancak buna rağmen , CSV dosyalarının saf düzenli ifadelerle pratik olarak ayrıştırılması sorunlu olabilir. Ayrıştırıcıya hangi varyantların beslendiğini bilmeniz gerekir ve bunun için bir standart yoktur. Biri başarılı olana kadar veya her nasılsa biçim yorum yorumlarını böldüğünüzde her satıra karşı birkaç ayrıştırıcı deneyebilirsiniz. Ancak bu, verimli bir şekilde veya hiç yapmak için düzenli ifadeler dışında araçlar gerektirebilir.


4
Pratik nokta için kesinlikle +1. Eminim bir şey var, derin bir yerde sadece ne olduğunu bilmiyorum alıntı değer sürümünü kıracak bir (contrived) değeri örneğidir. Birden fazla ayrıştırıcı ile 'eğlenceli' "bu iki iş, ama farklı cevaplar vermek" olacaktır

1
Açıkça ters eğik çizgiden kaçan tırnaklar için iki katına çıkan alıntılardan kaçan tırnaklar için farklı regex'lere ihtiyacınız olacaktır. Eski csv alanı türü için bir regex gibi bir şey olmalı [^,"]*|"(\\(\\|")|[^\\"])*"ve ikincisi böyle bir şey olmalıdır [^,"]*|"(""|[^"])*". (Bunlardan hiçbirini test etmediğim için dikkatli olun!)
comingstorm

Standart olabilecek bir şey için avlanmak , kaçırılan bir durum var - bir kayıt sınırlayıcısının kapalı olduğu bir değer. Bu, bununla başa çıkmak için birden fazla farklı yol olduğunda pratik

Ben çalıştırırsanız Güzel cevap, ancak perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'içinde ve boru "I have here an item,\" that is a test\""ardından sonuç bir test \ "dir` yay" dir. Normal ifadenizin kusurlu olduğunu düşünüyor.
Spencer Rathbun

@SpencerRathbun: Daha fazla zamanım olduğunda normal olarak normal ifadeleri test edeceğim ve muhtemelen testleri geçen bazı kavram kanıtlarını yapıştıracağım. Üzgünüz, iş günü devam ediyor.
9000

5

Basit cevap - muhtemelen hayır.

İlk sorun bir standart eksikliğidir. Bir csv kesinlikle tanımlanmış bir şekilde tanımlamak olabilir, ancak kesinlikle tanımlanmış csv dosyaları almak için sabırsızlanıyorum. "Yaptığınız işte muhafazakar olun, başkalarından kabul ettiğiniz şeyde liberal olun" -Jon Postal

Birinin kabul edilebilir bir standart testi olduğu varsayılarak, kaçış karakterleri ve bunların dengelenmesi gerekip gerekmediği sorusu vardır.

Birçok csv biçimindeki bir dize olarak tanımlanır string value 1,string value 2. Ancak, bu dize virgül içeriyorsa, şimdi dizedir "string, value 1",string value 2. Bir teklif içeriyorsa olur "string, ""value 1""",string value 2.

Bu noktada imkansız olduğuna inanıyorum. Sorunun kaç tırnak okuduğunuzu ve virgülün değerin çift tırnaklı kipinin içinde mi yoksa dışında mı olduğunu belirlemeniz gerekir. Parantezleri dengelemek imkansız bir normal ifade problemidir. Bazı genişletilmiş düzenli ifade motorları (PCRE) bununla başa çıkabilir, ancak o zaman normal bir ifade değildir.

Https://stackoverflow.com/questions/8629763/csv-parsing-with-a-context-free-grammar bulabilirsiniz .


Değişik:

Kaçış karakterleri için biçimlere bakıyordum ve keyfi sayım gerektiren herhangi bir şey bulamadım - bu muhtemelen sorun değil.

Ancak, kaçış karakteri ve kayıt sınırlayıcısının (başlangıç ​​olarak) ne olduğu ile ilgili sorunlar vardır. http://www.csvreader.com/csv_format.php , vahşi doğadaki farklı biçimlerde iyi bir okumadır.

  • Alıntılanan dizenin kuralları (tek tırnaklı bir dize veya çift tırnaklı bir dize ise) farklıdır.
    • 'This, is a value' vs "This, is a value"
  • Kaçış karakterleri için kurallar
    • "This ""is a value""" vs "This \"is a value\""
  • Katıştırılmış kayıt sınırlayıcının ({rd}) işlenmesi
    • (ham gömülü) "This {rd}is a value"vs (kaçtı) "This \{rd}is a value"vs (çevrildi)"This {0x1C}is a value"

Buradaki anahtar şey, her zaman birden fazla geçerli yorumu olacak bir dizeye sahip olmanın mümkün olmasıdır.

İlgili soru (uç durumlar için) "kabul edilen geçersiz bir dize olabilir mi?"

Bazı uygulamalar tarafından oluşturulan her geçerli CSV ile eşleşebilecek ve ayrıştırılamayan her csv'yi reddedebilecek düzenli bir ifade olduğundan hala şüpheliyim.


1
Tırnak içindeki tırnakların dengelenmesi gerekmez. Bunun yerine, belli ki düzenli gömülü bir alıntı, önce tırnak çift sayıda olması gerekir: ("")*". Değerin içindeki teklifler dengede değilse, zaten bizim işimiz değildir.
9000

Bu benim konumum, geçmişte "veri aktarımı" için bu korkunç bahanelerle karşılaşmak. Onları doğru şekilde işleyen tek şey, ayrıştırıcı, saf regex birkaç haftada bir kırıldı.
Spencer Rathbun

2

Öncelikle CSV'niz için dilbilgisini tanımlayın (alan sınırlayıcıları metinde görünüyorlarsa bir şekilde kaçar mı veya kodlanır mı?) Ve sonra normal ifade ile ayrıştırılıp ayrıştırılamadığını belirleyebilir. Önce gramer: ayrıştırıcı ikinci: http://www.boyet.com/articles/csvparser.html Bu yöntemin bir belirteç kullandığına dikkat edilmelidir - ancak tüm kenar durumlarıyla eşleşen bir POSIX normal ifadesi oluşturamıyorum. CSV formatlarını kullanımınız normal değilse ve bağlamdan bağımsızsa ... o zaman cevabınız sorunuz olacaktır. Burada iyi bir genel bakış: http://nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html


2

Bu normal ifade, RFC'de açıklandığı gibi normal CSV'yi tokenleştirebilir:

/("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/

Açıklama:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) - alıntılanmış veya alıntılanmamış bir CSV alanı
    • "(?:[^"]|"")*" - belirtilen alan;
      • [^"]|""- her karakter ya değil ", ya "olduğu gibi kaçtı""
    • [^,"\n\r]* - içermeyen, sıralanmamış bir alan , " \n \r
  • (,|\r?\n|\r)- aşağıdaki ayırıcı ,veya bir satırsonu
    • \r?\n|\r - yeni satır, biri \r\n \n \r

Bu regexp tekrar tekrar kullanılarak tüm bir CSV dosyası eşleştirilebilir ve doğrulanabilir. Daha sonra alıntı yapılan alanları sabitlemek ve ayırıcılara dayalı olarak satırlara ayırmak gerekir.

Normal ifadeye dayalı olarak Javascript'teki bir CSV ayrıştırıcısının kodu:

var csv_tokens_rx = /("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/y;
var csv_unescape_quote_rx = /""/g;
function csv_parse(s) {
    if (s && s.slice(-1) != '\n')
        s += '\n';
    var ok;
    var rows = [];
    var row = [];
    csv_tokens_rx.lastIndex = 0;
    while (true) {
        ok = csv_tokens_rx.lastIndex == s.length;
        var m = s.match(csv_tokens_rx);
        if (!m)
            break;
        var v = m[1], d = m[2];
        if (v[0] == '"') {
            v = v.slice(1, -1);
            v = v.replace(csv_unescape_quote_rx, '"');
        }
        if (d == ',' || v)
            row.push(v);
        if (d != ',') {
            rows.push(row)
            row = [];
        }
    }
    return ok ? rows : null;
}

Bu cevabın argümanınızı çözmenize yardımcı olup olmayacağına siz karar verirsiniz; Küçük, basit ve doğru bir CSV ayrıştırıcıya sahip olduğum için mutluyum.

Bence bir lex program aşağı yukarı büyük bir düzenli ifadedir ve bunlar C programlama dili gibi çok daha karmaşık formatları belirtebilir.

RFC 4180 tanımlarına referansla :

  1. satır sonu (CRLF) - Normal ifade daha esnektir, CRLF, LF veya CR'ye izin verir.
  2. Dosyadaki son kayıt bir bitiş çizgisi kesmesi içerebilir veya içermeyebilir.
  3. İsteğe bağlı bir başlık satırı olabilir - Bu ayrıştırıcıyı etkilemez.
  4. Her satır dosya boyunca aynı sayıda alan içermelidir - zorunlu değil
    Alanlar bir alanın parçası olarak kabul edilir ve yok sayılmalıdır - tamam
    Kayıttaki son alanı virgülle takip etmemeli - zorunlu değil
  5. Her alan çift tırnak içine alınabilir veya alınmayabilir ... - tamam
  6. Satır sonu (CRLF), çift tırnak ve virgül içeren alanlar çift tırnak içine alınmalıdır - tamam
  7. bir alanın içinde görünen bir çift tırnak, başka bir çift tırnak ile önüne geçilmeli - tamam

Normal ifade, RFC 4180 gereksinimlerinin çoğunu karşılar. Diğerleriyle aynı fikirde değilim, ancak ayrıştırıcıyı bunları uygulayacak şekilde ayarlamak kolaydır.


1
bu, sorulan soruya cevap vermekten daha çok kendini tanıtmaya benziyor, bkz. Nasıl Cevap
gnat

1
@gnat, cevabımı daha fazla açıklama yapmak, RFC 4180'e karşı düzenli ifadeyi kontrol etmek ve daha az kendi kendini tanıtmak için düzenledim. Excel ve diğer e-tablolar tarafından kullanılan en yaygın CSV biçimini belirtebilecek test edilmiş bir normal ifade içerdiğinden bu cevabın bir değeri olduğuna inanıyorum. Bunun soruyu çözdüğünü düşünüyorum. Küçük CSV ayrıştırıcı, bu normal ifadeyi kullanarak CSV'yi ayrıştırmanın kolay olduğunu gösterir.
Sam Watkins

Kendimi aşırı derecede tanıtmak istemeden, küçük bir e-tablo uygulamasının parçası olarak kullandığım tam küçük csv ve tsv kütüphanelerim (Google sayfaları benim için çok ağır geliyor). Bu, yayınladığım tüm şeyler gibi açık kaynak / kamu malı / CC0 kodudur. Umarım bu başka biri için yararlı olabilir. sam.aiki.info/code/js
Sam Watkins
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.