BASHの再帰下降CSVパーサーに対応して、私(両方の投稿の元の作成者)は、データ処理とこれらのスクリプト言語の速度を比較するために、次のようにAWKスクリプトに変換しようとしました。いくつかの緩和要因があるため、翻訳は1:1の翻訳ではありませんが、興味のある人にとっては、この実装は他の実装よりも文字列処理が高速です。
もともと、ジョナサン・レフラーのおかげですべてが潰されたいくつかの質問がありました。タイトルには「」と記載CSV
されていますが、コードが更新されましたDSV
。つまり、必要に応じて、フィールド区切り文字として任意の1文字を指定できます。
これで、このコードは対決の準備ができました。
基本的な機能
- 入力長、フィールド長、またはフィールド数に制限はありません
- 二重引用符によるリテラル引用符フィールド
"
- ここでセクション1.1.2 [1][2][3]で定義されているANSICエスケープシーケンス
- カスタム入力区切り文字:UNIXプログラミングの技術(DSV)[4]
- カスタム出力区切り文字[5]
- UCS-2およびUCS-4エスケープシーケンス[6]
[1]引用符で囲まれたフィールドはリテラルコンテンツであるため、引用符で囲まれたコンテンツに対してエスケープシーケンスの解釈は実行されません。ただし、引用符、プレーンテキスト、および解釈されたシーケンスを1つのフィールドに連結して、目的の効果を実現することができます。例えば:
1、2、3:\t「リトルエンディアン」と1人のビッグエンディアンチーフ
CSVの3つのフィールド行であり、3番目のフィールドは次のものと同等です。
3つ:リトルエンディアンと1つのビッグエンディアンチーフ
[2]参考資料で「実装固有」または「未定義の動作」を持っていると説明されている例は、定義上移植性がないか、あいまいすぎて信頼できないため、サポートされません。エスケープシーケンスがここまたは参考資料で定義されていない場合、円記号は無視され、最後から1番目の文字がプレーンテキスト値として扱われます。整数値の文字エスケープシーケンスはサポートされません。これは信頼性の低い方法であり、複数のプラットフォーム間で適切に拡張できず、不必要に検証のプロキシによる解析の複雑さが増します。
[3] 8進数の文字エスケープは、3桁の8進数形式である必要があります。3桁の8進数のエスケープシーケンスでない場合は、1桁のnullエスケープシーケンスです。16進エスケープシーケンスは、2桁の16進形式である必要があります。エスケープシーケンス識別子に続く最初の2文字が無効な場合、解釈は行われず、メッセージが標準エラーで出力されます。残りの16進数は無視されます。
[4]カスタム入力区切り文字iDelimiter
は1文字である必要があります。複数行のレコードはサポートされないため、このような矛盾の使用は常に眉をひそめる必要があります。これにより、データレコードの移植性が低下し、(そのファイル内の)場所と出所が不明な可能性のあるファイルに固有になります。たとえば、grep
コンテンツのファイルをingすると、コンテンツが前の行から始まる可能性があるため、不完全なレコードが返される可能性があり、データ取得がデータベースの完全なトップダウン解析に制限されます。
[5]カスタム出力区切り文字oDelimiter
は、任意の望ましい文字列値にすることができます。スクリプト出力は常に単一の改行で終了します。これは、正しい端末アプリケーション出力の機能です。そうしないと、解析されたCSV出力とターミナルプロンプトが同じ行を消費し、混乱を招く状況になります。また、コンソールのようなほとんどの通訳者は、改行がI/Oレコードの終了を通知することを期待する回線ベースのデバイスです。末尾の改行が望ましくない場合は、切り取ります。
[6] 16ビットUnicodeエスケープシーケンスは、次の表記法で使用できます。
\ uHHHH 16進値のUnicode文字HHHH(4桁)
および32ビットUnicodeエスケープシーケンスは、次の方法でサポートされます。
\ UHHHHHHHH 16進値のUnicode文字HHHHHHHH(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チャートを参照してください。
今後の計画:
- Cライブラリの実装
- Cコンソールアプリケーションの実装
- 標準化の可能性のためのインターネットエンジニアリングタスクフォースへの提出
哲学
エスケープシーケンスは、行ベースのデータベースに複数行のフィールドデータを作成するために常に使用する必要があり、引用符は、レコードフィールドのコンテンツを保持および連結するために常に使用する必要があります。これは、このタイプのレコードパーサーを実装するための最も簡単な(したがって最も効率的な)方法です。私は、すべてのソフトウェア開発者と教育機関がこの方向性を採用し、公言して、ラインベースの区切り文字で区切られたレコードの移植性と正確な取得を確保することをお勧めします。
CSVには、 RFC 4180以外の公式の仕様はなく、有用なポータブルレコードタイプは定義されていません。15年以上の経験を持つ開発者として、これがポータブルCSV/DSVレコードの公式に認められた標準になることを願っています。