Flutter - 파일 처리 ③: UTF-8·CP949 디코딩, CSV Converter

3 분 소요


목차

1: Flutter - 파일 처리 ① 파일 들고 오기(file_picker)

  • file_picker 패키지를 사용하여 파일 들고 오기

2: Flutter - 파일 처리 ② drag & drop으로 파일 입력 받기(desktop_drop)

  • desktop_drop 패키지를 사용하여 웹에서 drag & drop으로 파일 입력 받기

3: Flutter - 파일 처리 ③: UTF-8·CP949 디코딩, CSV Converter <현재>

  • 읽어 온 UTF-8 또는 CP949로 인코딩된 파일을 디코딩하기


파일 디코딩

utf8

import 'dart:convert' show utf8;

UTF-8 인코딩/디코딩을 위해 convert에서 utf8 부분을 들고 온다.

cp949

CP949는 Microsoft Windows의 한글 인코딩 방식으로, EUC-KR의 확장이다.

cp949 패키지로 CP949를 인코딩, 디코딩할 수 있다.

  cp949: ^1.2.1

dependencies에 추가

// ignore: import_of_legacy_library_into_null_safe
import 'package:cp949/cp949.dart' as cp949;

조금 아쉽지만 cp949 패키지는 Null safety가 아니더라

빌드할 때는 --no-sound-null-safety를 추가해야 한다.

안드로이드 스튜디오를 쓰는 경우 Run - Edit Configurations의 Additional arguments에 --no-sound-null-safety를 추가하도록 하자


파일 디코딩 하기

PlatformFile file;
Uint8List fileBytes = file.bytes!;
XFile file;
Uint8List fileBytes = await file.readAsBytes();

이전 포스트에서 알아봤듯이 각 파일에서 Uint8List 타입으로 파일 바이트를 읽어올 수 있었다.

import 'dart:convert' show utf8;
import 'package:cp949/cp949.dart' as cp949;

String utf8Str = utf8.decode(fileBytes);
String cp949Str = cp949.decode(fileBytes);

해당 파일 바이트를 각 패키지를 통해 디코딩해서 문자열을 얻을 수 있다.

인코딩 방식을 모를 경우

CP949로 인코딩 된 것을 UTF-8로 디코딩할 경우 내용이 깨지고 에러가 난다.

  static String convertBytesToString(Uint8List fileBytes){
    String ret = "";
    try{
      ret = utf8.decode(fileBytes);
      debugPrint("decode: UTF8");
    }
    catch (e) {
      ret = cp949.decode(fileBytes);
      debugPrint("decode: CP949");
    }
    return ret;
  }

try-catch문으로 이런 걸 방지하자


CSV Converter

번외로 CSV Converter를 만들어 보자

csv 패키지가 이미 있으므로 그걸 쓰면 되지만, 나는 엑셀이나 워드에서 복사 붙여넣기로 표를 만들어야 할 때도 있어서 따로 컨버터를 만들어보기로 했다.

내용 보기

CSV는 Comma Separated로, ,로 값이 나눠져 있으며 다음 행으로는 \n으로 구분된다.

내용,내용,내용\n
내용,내용,내용\n
내용,내용,내용\n

참고: 엑셀이나 워드에서 표를 복사할 경우 ,가 아닌 \t로 구분된다.


문제점

그렇다면 \n으로 split하고, ,로 또 그것들을 split해주면 될까??

되지 않았다!!

뭔가 싶어 아스키코드로 글자마다 다 출력해보니, csv의 각각 줄의 마지막마다 코드 13, 즉 CR이 항상 마지막에 있더라

CR은 Carriage Return, LF는 Line Feed인데,
한 줄을 내릴 때
Windows에서는 CRLF로, 즉 \r\n,
Unix-Like에서는 그냥 LF, \n을 사용한다.

csvStr = csvStr.replaceAll('\r\n', '\n');

// or
csvStr = csvStr.replaceAll('\r', '');

로 한 번 CRLF를 LF로 바꿔주자


그러고도 문제가 있다.

csv는 \n,으로 각 셀들이 구분이 되어지는데, 셀 안에 이미 이 두 기호가 들어있는 경우에는 어떡할까??


케이스

1. \n

자 이제 이런 경우를 생각해 보자

abc
abc
abc abc
asdd abc abcd
a b c

csv 파일 내에서 두 줄이 띄어진 경우다(엑셀에서는 Alt+Enter로 셀 내에서 줄 구분을 할 수 있다)

이 경우 raw 파일은 다음과 같다.

"abc
abc",abc,abc
a,b,c
a,b,c

보면 문자열 처리되어 있고, "abc\nabc"로 입력되어 있는 걸 알 수 있다.

따라서 이 경우 그냥 \n으로 split을 하면 이상하게 돌아가게 된다.

아하 그럼 뭔가 문자열을 따로 구분할 일이 있을 때 "를 사용하는구나
예를 들어 셀 안에 ,이 들어있을 때에도 "가 사용된다.

셀 abc => "ab,c"


2. ""

그렇다면 이번엔 이런 경우를 한 번 보자

abc
abc
abc” “abc”
a b c
a b c

셀 자체에 이미 "이 들어있다면 어떻게 될까

이 경우 raw 파일은 다음과 같다.

"abc
abc",abc"","""abc"""
a,b,c
a,b,c

"""로 표현하는 것을 알 수 있다.


3. ,

마지막으로

abc,   abc
a b c
a b c”c

셀 안에 ,가 들어 있는 경우는?

이 경우 raw 파일은

"abc,",,abc
a,b,c
a,b,"c""c"

당연하지만 "로 감싸지게 된다.


코드

처리 로직은

  • "으로 시작하면
    • , 직전의 " 찾으면 그게 한 셀
      • 셀 내에 ""가 있다면 "로 치환
  • "로 시작하지 않으면
    • 그냥 , 직전까지가 한 셀
      • 셀 내에 ""가 있다면 "로 치환


보통 문자열 파싱 알고리즘 문제를 풀 때는 스택 자료구조로 풀었는데, 플러터에는 따로 없어서 그냥 구현했다.

  static List<List<String>> splitCSV(String csvStr){
    csvStr = csvStr.replaceAll('\r\n', '\n'); // convert CRLF to LF
    List<List<String>> list = [[]];
    int now = 0;
    for(int i = 0; i < csvStr.length; i++){
      if( csvStr[i] == '"' ){
        String s = "";
        int j = i+1;
        for( ; j < csvStr.length; j++){
          if( csvStr[j] == '"' ){
            j++;
            if( j >= csvStr.length ){
              debugPrint("splitCSV error");
              return [];
            }
            bool breakSign = false;
            switch (csvStr[j]) {
              case '"': // just "
                s += '"';
                break;
              case ',': // cell ending
                list[now].add(s);
                breakSign = true;
                break;
              case '\n':  // cell ending and line ending
                list[now].add(s);
                list.add([]);
                now++;
                breakSign = true;
                break;
              default:
                debugPrint("splitCSV Error");
                return [];
            }
            if( breakSign ) break;
          }
          else{
            s += csvStr[j];
          }
        }
        i = j;
      }
      else{
        String s = "";
        int j = i;
        for( ; j < csvStr.length; j++){
          bool breakSign = false;
          switch (csvStr[j]) {
            case '"':
              j++;
              if( j >= csvStr.length ){
                debugPrint("splitCSV Error");
                return [];
              }
              if( csvStr[j] == '"' ){ // just "
                s += '"';
              }
              else{
                debugPrint("splitCSV Error");
                return [];
              }
              break;
            case ',': // cell ending
              list[now].add(s);
              breakSign = true;
              break;
            case '\n':  // cell ending and line ending
              list[now].add(s);
              list.add([]);
              now++;
              breakSign = true;
              break;
            default:
              s += csvStr[j];
          }
          if( breakSign ) break;
        }
        i = j;
      }
    }
    if( list.last.isEmpty ) list.removeAt(list.length-1);
    return list;
  }



cyj893.github.io/markdown_table_generator/에 적용함


댓글남기기