概要
- フィールドは
,
で区切る - フィールドは
"..."
で囲んでも囲まなくてもよい - フィールドが
,
を含む場合はフィールドを"..."
で囲む - フィールドが
"
を含む場合はフィールドを"..."
で囲む。また""
としてエスケープする
上記のようなRFC4180の仕様を満たすCSVの1行が与えられたとき、これをパースする関数をPL/SQLで作成したいとします。
アルゴリズム
このようなCSV文字列をパースするアルゴリズムはさまざまあると思いますが、個人的に好きな方法をここでは紹介します。
まず、与えられた文字列を,
でスプリットします。ただし、これではフィールドに,
が含まれている場合、うまくパースできません。たとえばaaa,"bbb,ccc",ddd
という文字列は以下のようにパースされてしまいます。
aaa
"bbb
ccc"
ddd
このようなケースに対応するため、スプリットしたあとの各要素の"
の数を数えます。"
の数が偶数ならOK。"
の数が奇数なら、対応が取れていないので、後続の要素と連結します。
一連の作業が終わったら、エスケープされている"
をアンエスケープし、フィールドを囲む"..."
を削除します。
これをPythonで実装すると、以下のようになります。
def csv_split(line):
import re
tokens = []
for token in line.split(','):
if len(tokens)==0 or tokens[-1].count('"')%2==0:
tokens.append(token)
else:
tokens[-1] += "," + token
return [re.sub(r'^"(.*)"$', r"\1", token).replace(r'""', '"') for token in tokens]
if __name__ == '__main__':
line = ',abc,,"abc","a,b,c","""a"",""b"",""c""","a b c",'
print(csv_split(line))
#=> ['', 'abc', '', 'abc', 'a,b,c', '"a","b","c"', 'a b c', '']
PL/SQL実装
上記のPythonコードをPL/SQLに翻訳します。ただし、Pythonのstr.split
に対応する関数がPL/SQLには用意されていないので、自前で実装する必要があります。今回はraw_split
という名前で、Pythonのstr.split
によく似た関数を準備しました。
CREATE OR REPLACE TYPE CSV_ARRAY AS TABLE OF VARCHAR2(32767);
CREATE OR REPLACE FUNCTION raw_split(line VARCHAR2) RETURN CSV_ARRAY
IS
cur_pos INT;
del_pos INT;
tokens CSV_ARRAY;
BEGIN
cur_pos := 1;
tokens := CSV_ARRAY();
LOOP
del_pos := INSTR(line, ',', cur_pos);
IF del_pos = 0 THEN
tokens.extend();
tokens(tokens.last) := SUBSTR(line, cur_pos);
EXIT;
END IF;
tokens.extend();
tokens(tokens.last) := SUBSTR(line, cur_pos, del_pos - cur_pos);
cur_pos := del_pos + 1;
END LOOP;
RETURN tokens;
END;
CREATE OR REPLACE FUNCTION csv_split(line VARCHAR2) RETURN CSV_ARRAY
IS
tokens1 CSV_ARRAY;
tokens2 CSV_ARRAY;
BEGIN
tokens1 := raw_split(line);
tokens2 := CSV_ARRAY();
FOR i IN tokens1.FIRST..tokens1.LAST LOOP
IF tokens2.count = 0 OR MOD(COALESCE(REGEXP_COUNT(tokens2(tokens2.last), '"'), 0), 2) = 0 THEN
tokens2.extend();
tokens2(tokens2.last) := tokens1(i);
ELSE
tokens2(tokens2.last) := tokens2(tokens2.last) || ',' || tokens1(i);
END IF;
END LOOP;
FOR i IN tokens2.first..tokens2.last LOOP
tokens2(i) := REPLACE(REGEXP_REPLACE(tokens2(i), '^"(.*)"$', '\1'), '""', '"');
END LOOP;
RETURN tokens2;
END;
実際に動かしてみましょう。
SET SERVEROUTPUT ON;
DECLARE
tokens CSV_ARRAY;
BEGIN
tokens := CSV_SPLIT(',abc,,"abc","a,b,c","""a"",""b"",""c""","a b c",');
FOR i IN tokens.first..tokens.last LOOP
DBMS_OUTPUT.PUT_LINE('tokens[' || i || '] --> ' || tokens(i));
END LOOP;
END;
これを実行すると、コンソールに以下のような結果が表示されるはずです。
tokens[1] -->
tokens[2] --> abc
tokens[3] -->
tokens[4] --> abc
tokens[5] --> a,b,c
tokens[6] --> "a","b","c"
tokens[7] --> a b c
tokens[8] -->
環境情報
SELECT * FROM V$VERSION;
BANNER | BANNER_FULL | BANNER_LEGACY | CON_ID |
---|---|---|---|
Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production | Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production Version 21.3.0.0.0 |
Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production | 0 |