2

为了响应BASH 中的递归下降 CSV 解析器,我(两篇文章的原作者)尝试将其翻译成 AWK 脚本,以比较这些脚本语言的数据处理速度。由于一些缓解因素,翻译不是 1:1 翻译,但对于那些感兴趣的人来说,这种实现在字符串处理方面比其他实现更快。

最初,由于 Jonathan Leffler,我们有几个问题都被取消了。虽然标题是CSV,但我们已经更新了代码,DSV这意味着您可以将任何单个字符指定为字段分隔符,如果您认为有必要的话。

这段代码现在可以摊牌了。

基本功能

  • 对输入长度、字段长度或字段计数没有强加限制
  • 通过双引号的文字引用字段"
  • 在第 1.1.2 [1][2][3]节中定义的ANSI C 转义序列
  • 自定义输入分隔符:UNIX 编程艺术(DSV) [4]
  • 自定义输出分隔符[5]
  • UCS-2 和 UCS-4 转义序列[6]

[1]引用的字段是文字内容,因此不会对引用的内容执行转义序列解释。然而,可以在单个字段中连接引号、纯文本和解释序列以实现所需的效果。例如:

一、二、三:\t“Little Endians”和一个 Big Endian Chief

是 CSV 的三字段行,其中第三字段相当于:

三:Little Endian 和一个 Big Endian Chief

[2]参考资料中描述为“特定于实现”或具有“未定义行为”的示例将不受支持,因为它们在定义上不可移植,或者过于模棱两可而不可靠。如果此处或参考资料中未定义转义序列,则反斜杠将被忽略,最后面的单个字符将被视为纯文本值。不支持整数值字符转义序列,这是一种不可靠的方法,不能很好地跨多个平台扩展,并且不必要地增加了通过验证代理解析的复杂性。

[3]八进制字符转义必须是 3 位八进制格式。如果它不是 3 位八进制转义序列,则它是单个数字空转义序列。十六进制转义序列必须采用 2 位十六进制格式。如果转义序列标识符后面的前两个字符无效,则不会进行任何解释,并且将在标准错误上打印一条消息。任何剩余的十六进制数字都将被忽略。

[4]自定义输入分隔符iDelimiter必须是单个字符。不支持多行记录,并且应始终不赞成使用这种矛盾。这降低了数据记录的可移植性,使其特定于位置和来源(在该文件中)可能未知的文件。例如,grep为内容生成文件可能会返回不完整的记录,因为内容可能从任何前一行开始,从而将数据获取限制为对数据库进行完全自上而下的解析。

[5]自定义输出分隔符oDelimiter可以是任何所需的字符串值。脚本输出总是由一个换行符终止。这是正确终端应用程序输出的一个特征。否则,您解析的 CSV 输出和终端提示将使用同一行,从而造成混乱的情况。此外,大多数解释器(如控制台)都是基于行的设备,它们希望换行符表示 I/O 记录的结束。如果您发现尾随换行符不受欢迎,请将其剪掉。

[6] 16 位 Unicode 转义序列可通过以下符号获得:

\uHHHH 十六进制值 HHHH 的 Unicode 字符(4 位)

和 32 位 Unicode 转义序列通过以下方式支持:

\UHHHHHHHH 十六进制值 HHHHHHHH 的 Unicode 字符(8 位)

特别感谢 SO 社区的所有成员,他们的经验、时间和投入使我创建了这样一个非常有用的信息处理工具。

代码清单:dsv.awk

#!/bin/awk -f
#
###############################################################
#
# ZERO LIABILITY OR WARRANTY LICENSE YOU MAY NOT OWN ANY
# COPYRIGHT TO THIS SOFTWARE OR DATA FORMAT IMPOSED HEREIN 
# THE AUTHOR PLACES IT IN THE PUBLIC DOMAIN FOR ALL USES 
# PUBLIC AND PRIVATE THE AUTHOR ASKS THAT YOU DO NOT REMOVE
# THE CREDIT OR LICENSE MATERIAL FROM THIS DOCUMENT.
#
###############################################################
#
# Special thanks to Jonathan Leffler, whose wisdom, and 
# knowledge defined the output logic of this script.
#
# Special thanks to GNU.org for the base conversion routines.
#
# Credits and recognition to the original Author:
# Triston J. Taylor whose countless hours of experience,
# research and rationalization have provided us with a
# more portable standard for parsing DSV records.
#
###############################################################
#
# This script accepts and parses a single line of DSV input
# from <STDIN>.
#
# Record fields are seperated by command line varibale
# 'iDelimiter' the default value is comma.
#
# Ouput is seperated by command line variable 'oDelimiter' 
# the default value is line feed.
#
# To learn more about this tool visit StackOverflow.com:
#
# http://stackoverflow.com/questions/10578119/
#
# You will find there a wealth of information on its
# standards and development track.
#
###############################################################

function NextSymbol() {

    strIndex++;
    symbol = substr(input, strIndex, 1);

    return (strIndex < parseExtent);
    
}

function Accept(query) {
    
    #print "query: " query " symbol: " symbol
    if ( symbol == query ) {
        #print "matched!"        
        return NextSymbol();         
    }
    
    return 0;
    
}

function Expect(query) {

    # special case: empty query && symbol...
    if ( query == nothing && symbol == nothing ) return 1;

    # case: else
    if ( Accept(query) ) return 1;
    
    msg = "dsv parse error: expected '" query "': found '" symbol "'";
    print msg > "/dev/stderr";
    
    return 0;
    
}

function PushData() {
    
    field[fieldIndex++] = fieldData;
    fieldData = nothing;
    
}

function Quote() {

    while ( symbol != quote && symbol != nothing ) {
        fieldData = fieldData symbol;
        NextSymbol();
    }
    
    Expect(quote);
    
}

function GetOctalChar() {

    qOctalValue = substr(input, strIndex+1, 3);
    
    # This isn't really correct but its the only way
    # to express 0-255. On unicode systems it won't
    # matter anyway so we don't restrict the value
    # any further than length validation.
    
    if ( qOctalValue ~ /^[0-7]{3}$/ ) {
    
        # convert octal to decimal so we can print the
        # desired character in POSIX awks...
        
        n = length(qOctalValue)
        ret = 0
        for (i = 1; i <= n; i++) {
            c = substr(qOctalValue, i, 1)
            if ((k = index("01234567", c)) > 0)
            k-- # adjust for 1-basing in awk
            ret = ret * 8 + k
        }
        
        strIndex+=3;
        return sprintf("%c", ret);
        
        # and people ask why posix gets me all upset..
        # Special thanks to gnu.org for this contrib..
        
    }
    
    return sprintf("\0"); # if it wasn't 3 digit octal just use zero
    
}
             
function GetHexChar(qHexValue) {
    
    rHexValue = HexToDecimal(qHexValue);
    rHexLength = length(qHexValue);
    
    if ( rHexLength ) {
            
        strIndex += rHexLength;
        return sprintf("%c", rHexValue);
                
    }
    
    # accept no non-sense!
    printf("dsv parse error: expected " rHexLength) > "/dev/stderr";
    printf("-digit hex value: found '" qHexValue "'\n") > "/dev/stderr";
    
}
  
function HexToDecimal(hexValue) {

    if ( hexValue ~ /^[[:xdigit:]]+$/ ) {
    
        # convert hex to decimal so we can print the
        # desired character in POSIX awks...
        
        n = length(hexValue)
        ret = 0
        for (i = 1; i <= n; i++) {
        
            c = substr(hexValue, i, 1)
            c = tolower(c)
            
            if ((k = index("0123456789", c)) > 0)
                k-- # adjust for 1-basing in awk
            else if ((k = index("abcdef", c)) > 0)
                k += 9

            ret = ret * 16 + k
        }
        
        return ret;
        
        # and people ask why posix gets me all upset..
        # Special thanks to gnu.org for this contrib..
        
    }
    
    return nothing;
    
}
  
function BackSlash() {

    # This could be optimized with some constants.
    # but we generate the data here to assist in
    # translation to other programming languages.
    
    if (symbol == iDelimiter) { # separator precedes all sequences
        fieldData = fieldData symbol;
    } else if (symbol == "a") { # alert
        fieldData = sprintf("%s\a", fieldData);
    } else if (symbol == "b") { # backspace
        fieldData = sprintf("%s\b", fieldData);
    } else if (symbol == "f") { # form feed
        fieldData = sprintf("%s\f", fieldData);
    } else if (symbol == "n") { # line feed
        fieldData = sprintf("%s\n", fieldData);
    } else if (symbol == "r") { # carriage return
        fieldData = sprintf("%s\r", fieldData);
    } else if (symbol == "t") { # horizontal tab
        fieldData = sprintf("%s\t", fieldData);
    } else if (symbol == "v") { # vertical tab
        fieldData = sprintf("%s\v", fieldData);
    } else if (symbol == "0") { # null or 3-digit octal character
        fieldData = fieldData GetOctalChar();
    } else if (symbol == "x") { # 2-digit hexadecimal character 
        fieldData = fieldData GetHexChar( substr(input, strIndex+1, 2) );
    } else if (symbol == "u") { # 4-digit hexadecimal character 
        fieldData = fieldData GetHexChar( substr(input, strIndex+1, 4) );
    } else if (symbol == "U") { # 8-digit hexadecimal character 
        fieldData = fieldData GetHexChar( substr(input, strIndex+1, 8) );
    } else { # symbol didn't match the "interpreted escape scheme"
        fieldData = fieldData symbol; # just concatenate the symbol
    }

    NextSymbol();
    
}

function Line() {

    if ( Accept(quote) ) {
        Quote();
        Line();
    }
    
    if ( Accept(backslash) ) {
        BackSlash();
        Line();        
    }
    
    if ( Accept(iDelimiter) ) {
        PushData();
        Line();
    }
    
    if ( symbol != nothing ) {
        fieldData = fieldData symbol;
        NextSymbol();
        Line();
    } else if ( fieldData != nothing ) PushData();
    
}

BEGIN {

    # State Variables
    symbol = ""; fieldData = ""; strIndex = 0; fieldIndex = 0;
    
    # Output Variables
    field[itemIndex] = "";

    # Control Variables
    parseExtent = 0;

    # Formatting Variables (optionally set on invocation line)
    if ( iDelimiter != "" ) {
        # the algorithm in place does not support multi-character delimiter
        if ( length(iDelimiter) > 1 ) { # we have a problem
            msg = "dsv parse: init error: multi-character delimiter detected:";
            printf("%s '%s'", msg, iDelimiter);
            exit 1;
        }
    } else {
        iDelimiter = ",";
    }
    if ( oDelimiter == "" ) oDelimiter = "\n";
    
    # Symbol Classes
    nothing = "";
    quote = "\"";
    backslash = "\\";
    
    getline input;
    
    parseExtent = (length(input) + 2);
    
    # parseExtent exceeds length because the loop would terminate
    # before parsing was complete otherwise.
    
    NextSymbol();
    Line();
    Expect(nothing);
    
}

END {

    if (fieldIndex) {
    
        fieldIndex--;
        
        for (i = 0; i < fieldIndex; i++)
        {
             printf("%s", field[i] oDelimiter);
        }

        print field[i];
        
    } 
      
}

**如何“像专业人士一样”运行脚本**
# Spit out some CSV "newline" delimited:
echo 'one,two,three,AWK,CSV!' | awk -f dsv.awk

# Spit out some CSV "tab" delimited:
echo 'one,two,three,AWK,CSV!' | awk -v oDelimiter=$'\t' -f dsv.awk

# Spit out some CSV "ASCII Group Separator" delimited:
echo 'one,two,three,AWK,CSV!' | awk -v oDelimiter=$'\29' -f dsv.awk

如果您需要一些自定义输出控制分隔符但不确定使用什么,您可以参考这个方便的 ASCII 图表

未来的计划:


哲学

转义序列应始终用于在基于行的数据库中创建多行字段数据,并且应始终使用引用来保留和连接记录字段内容。这是实现这种类型的记录解析器的最简单(因此也是最有效)的方法。我鼓励所有软件开发人员和教育机构接受并表明这一方向,以确保可移植性和精确获取基于行的分隔符分隔的记录。

CSV 没有除RFC 4180之外的官方规范,也没有定义任何有用的可移植记录类型。作为一名拥有超过 15 年经验的开发人员,我希望这将成为官方认可的便携式 CSV/DSV 记录标准。

4

1 回答 1

1

原始代码中的空行太多,难以阅读。减少空行的修改后的代码更容易阅读;相关行位于可以一起阅读的块中。谢谢。

awk就像C;它将 0 视为假,将任何非零视为真。所以,任何大于 0 的东西都是正确的,但任何小于 0 的东西也是如此。

stderr在标准中没有直接的打印方式awk。GNU AWK 记录了print "message" > "/dev/stderr"(name as string!) 的使用,并暗示它甚至可以在没有实际设备的系统上工作。它也适用awk于带有该/dev/stderr设备的系统上的标准。

awk处理数组中每个索引的习惯用法是for (i in array) { ... }. 但是,由于您有一个索引,itmIndex告诉您数组中有多少项,您应该使用

for (i = 0; i < itmIndex; i++) { printf("%s%s", item[i], delim); }

然后在最后输出一个换行符。这让我的思维方式太多了一个分隔符,但这是bash代码正在做什么的转录。我通常的技巧是:

pad = ""
for (i = 0; i < itmIndex; i++)
{
     printf("%s%s", pad, item[i])
     pad = delim
}
print "";

-v var=value您可以使用(或省略)将变量传递到脚本中-v。请参阅前面列出的 POSIX URL。

于 2012-05-14T07:32:26.137 回答