Какой самый надежный способ эффективного анализа CSV с использованием awk?

Цель этого вопроса - предоставить канонический ответ.

Учитывая CSV, который может быть сгенерирован Excel или другими инструментами со встроенными новыми строками, встроенными двойными кавычками и пустыми полями, например:

$ cat file.csv
"rec1, fld1",,"rec1"",""fld3.1
"",
fld3.2","rec1
fld4"
"rec2, fld1.1

fld1.2","rec2 fld2.1""fld2.2""fld2.3","",rec2 fld4

Какой наиболее надежный способ эффективно использовать awk для идентификации отдельных записей и полей:

Record 1:
    $1=<rec1, fld1>
    $2=<>
    $3=<rec1","fld3.1
",
fld3.2>
    $4=<rec1
fld4>
----
Record 2:
    $1=<rec2, fld1.1

fld1.2>
    $2=<rec2 fld2.1"fld2.2"fld2.3>
    $3=<>
    $4=<rec2 fld4>
----

поэтому он может использоваться как те записи и поля внутри остальной части awk script.

Действительным CSV будет тот, который соответствует RFC 4180 или может быть сгенерирован MS-Excel.

Решение должно терпеть конец записи, просто являющийся LF (\n), что характерно для файлов UNIX, а не CRLF (\r\n), как этого требует стандарт, и Excel или другие инструменты Windows будут генерировать. Он также переносит некотируемые поля, смешанные с указанными полями. В частности, не нужно допускать экранирование " с предыдущей обратной косой чертой (т.е. \" вместо ""), как позволяют некоторые другие форматы CSV - если у вас есть, то добавление перед ним gsub(/\\"/,"\"\"") будет обрабатывать и пытаться чтобы автоматически обрабатывать оба механизма экранирования в одном script, сделало бы script излишне хрупким и сложным.

Ответ 1

Если ваш CSV не может содержать символы новой строки или экранированные двойные кавычки, тогда все, что вам нужно, это (с GNU awk для FPAT):

$ echo 'foo,"field,with,commas",bar' |
    awk -v FPAT='[^,]*|"[^"]+"' '{for (i=1; i<=NF;i++) print i, "<" $i ">"}'
1 <foo>
2 <"field,with,commas">
3 <bar>

В противном случае, однако, более общее, надежное, портативное решение, которое будет работать с любым современным awk:

$ cat decsv.awk
function buildRec(      i,orig,fpat,done) {
    $0 = PrevSeg $0
    if ( gsub(/"/,"&") % 2 ) {
        PrevSeg = $0 RS
        done = 0
    }
    else {
        PrevSeg = ""
        gsub(/@/,"@A"); gsub(/""/,"@B")            # <"[email protected]""bar"> -> <"[email protected]@Bbar">
        orig = $0; $0 = ""                         # Save $0 and empty it
        fpat = "([^" FS "]*)|(\"[^\"]+\")"         # Mimic GNU awk FPAT meaning
        while ( (orig!="") && match(orig,fpat) ) { # Find the next string matching fpat
            $(++i) = substr(orig,RSTART,RLENGTH)   # Create a field in new $0
            gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i)  # <"[email protected]@Bbar"> -> <"[email protected]"bar">
            gsub(/^"|"$/,"",$i)                    # <"[email protected]"bar">   -> <[email protected]"bar>
            orig = substr(orig,RSTART+RLENGTH+1)   # Move past fpat+sep in orig $0
        }
        done = 1
    }
    return done
}

BEGIN { FS=OFS="," }
!buildRec() { next }
{
    printf "Record %d:\n", ++recNr
    for (i=1;i<=NF;i++) {
        # To replace newlines with blanks add gsub(/\n/," ",$i) here
        printf "    $%d=<%s>\n", i, $i
    }
    print "----"
}

,

$ awk -f decsv.awk file.csv
Record 1:
    $1=<rec1, fld1>
    $2=<>
    $3=<rec1","fld3.1
",
fld3.2>
    $4=<rec1
fld4>
----
Record 2:
    $1=<rec2, fld1.1

fld1.2>
    $2=<rec2 fld2.1"fld2.2"fld2.3>
    $3=<>
    $4=<rec2 fld4>
----

Выше предполагается, что окончания строки UNIX \n. В Windows \r\n окончания строк это намного проще, так как "новые строки" внутри каждого поля будут фактически просто переводами строк (то есть \n s), и поэтому вы можете установить RS="\r\n" а затем \n пределах поля не будут обрабатываться как окончания строк.

Он работает путем простого подсчета количества " присутствующих" в текущей записи всякий раз, когда он встречает RS - если это нечетное число, тогда RS (предположительно \n но не обязательно) находится в середине поля, и поэтому мы продолжаем строит текущую запись, но если это даже тогда, это конец текущей записи, и поэтому мы можем продолжить с остальной частью сценария, обрабатывающей теперь завершенную запись.

gsub(/@/,"@A"); gsub(/""/,"@B") gsub(/@/,"@A"); gsub(/""/,"@B") преобразует каждую пару двойных кавычек, пересекающих всю запись (имейте в виду, что эти пары "" могут применяться только в полях в кавычках) в строку @B, которая не содержит двойных кавычек, поэтому что когда мы разделяем запись на поля, match() не срабатывает из-за кавычек, появляющихся внутри полей. gsub(/@B/,"\"",$i); gsub(/@A/,"@",$i) восстанавливает кавычки внутри каждого поля по отдельности, а также преобразует "" в " они действительно представляют,

См. Также Как использовать awk в cygwin для печати полей из таблицы Excel? о том, как создавать CSV из таблиц Excel.