AWK:递归下降CSV分析器(AWK: Recursive Descent CSV Parser)

2019-10-16 16:56发布

在回答在BASH递归下降CSV解析器 ,我( 原作者两个职位的)已经提出了以下尝试把它翻译成AWK脚本,这些脚本语言的数据处理速度对比。 翻译不是1:1的翻译由于一些缓和因素,但对那些有兴趣谁,这个实现更快的字符串处理比其他。

本来我们有这样的已经全部撤销感谢乔纳森·莱弗勒几个问题。 虽然标题说CSV ,我们更新了代码DSV这意味着作为字段分隔符,你应该觉得有必要,可以指定任意单个字符。

此代码现在准备摊牌。

基本特点

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

[1]引字段是文字内容,因此没有转义序列解释是在引用内容执行。 然而,人们可以连接报价,纯文本和序列的解释在单个场以实现期望的效果。 例如:

one,two,three:\t"Little Endians," and one Big Endian Chief

是CSV的三场线,其中第三场相当于:

three:        Little Endians, and one Big Endian Chief

[2],因为它们不是由定义便携式的,或太暧昧是可靠的参考材料为“具体实施”,或具有“未定义的行为”中所描述的实施例将不被支持。 如果一个转义序列未在此处或在参考材料限定,反斜线将被忽略和单最后一个字符将被视为纯文本值。 整数值字符转义序列将不支持它是一种不跨越多个平台很好地扩展和不必要的,通过验证的代理增加了分析的复杂度不可靠的方法。

[3]八进制字符逃逸必须在3位八进制格式。 如果它不是一个3位八进制转义序列是一个单一的数字空转义序列。 十六进制转义序列必须在2位十六进制格式。 如果下面的转义序列标识符的前两个字符是无效的,不解释将发生和消息将在标准错误打印。 任何剩余的十六进制数字将被忽略。

[4]定界符定制输入iDelimiter必须是一个单个字符。 多行记录将不被支持,这种矛盾的使用应始终会让人不悦。 这减少了数据记录的使其具体到其位置和来源(该文件中)可能是未知文件的便携性。 例如, grep荷兰国际集团为内容的文件可能返回一个不完整的记录,因为内容可以在任何以前的行开始,限制了数据采集到数据库的全自上而下的分析。

[5]定界符定制输出oDelimiter可以是任何期望的字符串值。 脚本输出总是由单个换行终止。 这是正确的终端应用程序输出的特征。 否则,你解析CSV输出和终端提示将消耗同一行中创建一个混乱的局面。 此外,大多数口译,像控制台是基于线设备,谁想到一个换行符信号的I / O记录的结尾。 如果您发现尾随的换行符不可取的,修剪其关闭。

[6] 16位Unicode转义序列通过下面的符号是可用的:

 \uHHHH Unicode character with hex value HHHH (4 digits)

和32位的Unicode转义序列通过支持:

 \UHHHHHHHH Unicode character with hex value HHHHHHHH (8 digits)

特别感谢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表

未来的计划:

  • C库实现
  • 。c控制台应用程序的实现
  • 提交给互联网工程任务组可能的标准化

哲学

转义序列应始终使用在基于在线数据库中创建多行字段的数据,并引用应始终用来保存并串连记录字段的内容。 这是实现这种类型的记录分析器的最简单的(因此最有效的)的方式。 我鼓励所有的软件开发商和教育机构占用和信奉这个方向,以确保便携性和基于行分隔符分隔的记录准确采集。

CSV具有比其他没有正式规范RFC 4180 ,它并没有定义任何有用的便携式记录类型。 我希望与超过15年,这将成为便携式CSV / DSV纪录的官方认可标准经验的开发人员。

Answer 1:

有在原版本的代码,这使得它很难读太多的空行。 具有减少的空行的修改后的代码是更容易地读取; 相关线是可以被一起读出的块。 谢谢。

awk是像C; 它把0作为假,任何非零为真。 因此,任何大于0是真实的,但这样是任何低于0。

没有打印到直接的方法stderr标准awk 。 GNU AWK文档使用的print "message" > "/dev/stderr" (名称作为字符串!),并暗示它可能对系统甚至没有工作的实际设备。 它将与标准工作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之前上市。



文章来源: AWK: Recursive Descent CSV Parser