0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PL/SQLでRFC4180の仕様を満たすCSVの1行をパースしたい

Last updated at Posted at 2024-05-11

概要

  • フィールドは,で区切る
  • フィールドは"..."で囲んでも囲まなくてもよい
  • フィールドが,を含む場合はフィールドを"..."で囲む
  • フィールドが"を含む場合はフィールドを"..."で囲む。また""としてエスケープする

上記のような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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?