Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby로 CSV 파일 읽기와 파싱 완벽 가이드

CSV는 "Comma-Separated Values(쉼표로 구분된 값)"의 약자입니다.

각 행의 값이 쉼표로 구분되어 있는 매우 흔한 데이터 형식으로, 데이터를 내보내거나(export) 가져올(import) 때 널리 활용됩니다.

예를 들어, Gmail 연락처를 CSV 파일로 내보낼 수 있고, 같은 형식으로 다시 가져올 수도 있습니다.

CSV 파일은 다음과 같은 형태입니다:

id,name
1,chocolate
2,bacon
3,apple
4,banana
5,almonds

이제 Ruby CSV 라이브러리를 사용해 CSV 파일을 읽고 쓰는 방법을 알아보겠습니다.

Ruby CSV 파싱

Ruby에는 CSV 라이브러리가 기본적으로 내장되어 있어 별도 설치 없이 바로 사용할 수 있습니다.

파일을 직접 읽는 방법은 다음과 같습니다:

require 'csv'

CSV.read("favorite_foods.csv")

또는 CSV 데이터가 담긴 문자열을 파싱할 수도 있습니다:

require 'csv'

CSV.parse("1,chocolate\n2,bacon\n3,apple")

그럼 결과는 어떨까요? 테이블의 각 행이 하나의 요소가 되는 2차원 배열을 얻게 됩니다.

결과는 다음과 같습니다:

[
  ["id", "name"],
  ["1", "chocolate"],
  ["2", "bacon"],
  ["3", "apple"],
  ["4", "banana"],
  ["5", "almonds"]
]

data[1][1]처럼 배열 인덱스를 사용해 이 데이터를 다룰 수도 있습니다.

하지만 더 좋은 방법이 있습니다!

CSV 옵션 활용하기

파일에 헤더가 포함되어 있다면 CSV 파서에게 헤더를 사용하라고 지시할 수 있습니다.

다음과 같이 말이죠:

table = CSV.parse(File.read("cats.csv"), headers: true)

이렇게 하면 다차원 배열 대신 CSV::Table 객체를 얻게 됩니다.

공식 문서의 설명은 다음과 같습니다:

"CSV::Table은 CSV 문서를 표현하기 위한 2차원 데이터 구조입니다. 테이블을 사용하면 행(row) 또는 열(column) 단위로 데이터를 다루고 조작할 수 있으며, 결과를 다시 CSV로 변환하는 것도 가능합니다."

테이블 객체가 있다면 원하는 행에서 필요한 데이터를 손쉽게 꺼낼 수 있습니다.

예시:

table[0]["id"]
# "1"

table[0]["name"]
# "chocolate"

여기서 0은 첫 번째 행을 의미하고, idname은 열 이름입니다.

테이블 모드는 두 가지가 있습니다:

  • by_col
  • by_row

테이블 모드(기본값은 row)를 변경하면 서로 다른 관점에서 데이터를 바라볼 수 있습니다.

예를 들어:

table.by_col[0]
# ["1", "2", "3", "4", "5"]

table.by_col[1]
# ["chocolate", "bacon", "apple", "banana", "almonds"]

여기서 0은 첫 번째 열, 1은 두 번째 열입니다.

by_colby_row 메서드는 테이블의 복사본을 반환합니다. 원본 테이블을 직접 수정하고 싶다면 by_col!by_row! 메서드를 사용하세요. 복사본을 생성하지 않기 때문에 메모리 측면에서 훨씬 효율적입니다.

CSV 컨버터(Converters) 사용법

앞선 예제에서 id 열이 문자열 배열로 반환된 것을 눈치채셨을 겁니다.

정수(Integer) 타입이 필요하다면 어떻게 해야 할까요?

각 문자열에 to_i를 호출해도 되지만… 더 간편한 방법이 있습니다!

Ruby CSV 라이브러리에는 컨버터(converters)라는 기능이 내장되어 있습니다. 컨버터는 값을 자동으로 변환해 주는 역할을 합니다.

예시:

CSV.parse("1,2,3,4,5")
# [["1", "2", "3", "4", "5"]]

CSV.parse("1,2,3,4,5", converters: :numeric)
# [[1, 2, 3, 4, 5]]

내장 컨버터는 총 6가지입니다:

  • Integer
  • Float
  • Numeric (Float + Integer)
  • Date
  • DateTime
  • All

물론 자신만의 커스텀 컨버터를 만들 수도 있습니다.

방법은 다음과 같습니다:

CSV::Converters[:symbol] = ->(value) { value.to_sym rescue value }

새로 만든 컨버터는 이렇게 사용합니다:

CSV.parse("a,b,c", headers: false, converters: :symbol)

# [[:a, :b, :c]]

새로운 CSV 파일 만들기

다양한 방식으로 CSV를 읽고 파싱하는 것 외에도, 처음부터 CSV 데이터를 생성할 수 있습니다.

가장 간단한 방법은 다음과 같습니다:

cats = [
  [:blue, 1],
  [:white, 2],
  [:black_and_white, 3]
]

cats.map { |c| c.join(",") }.join("\n")

generate 메서드를 사용할 수도 있습니다:

CSV.generate do |csv|
  csv << [:blue, 1]
  csv << [:white, 2]
  csv << [:black_and_white, 3]
end

이렇게 하면 데이터가 올바른 CSV 형식으로 준비됩니다.

실제 파일에 쓰려면 File.write("cats.csv", data)를 사용하거나, generate 대신 파일 이름과 쓰기 모드("w")를 지정해 open을 호출하면 됩니다.

다음과 같이 말이죠:

CSV.open("cats.csv", "w") do |csv|
  csv << [:white, 2]
end

이제 새로운 CSV 파일이 완성되었습니다!

CSV 젬(Gem)과 성능 비교

내장 라이브러리만으로도 충분히 작업을 처리할 수 있습니다.

하지만 서로 다른 기능을 제공하는 CSV 파싱 젬도 몇 가지 존재합니다.

예를 들어 smarter_csv 젬은 CSV 데이터를 해시(hash)의 배열로 변환해 줍니다.

예시:

require 'smarter_csv'

IntegerConverter = Object.new

def IntegerConverter.convert(value)
  Integer(value)
end

SmarterCSV.process('testing.csv', value_converters: { id: IntegerConverter })

# [{:id=>1, :name=>"a"}, {:id=>2, :name=>"b"}, {:id=>3, :name=>"c"}]

성능 벤치마크 비교 결과는 다음과 같습니다:

Comparison:
       CSV:      112.9 i/s
Smarter CSV:     21.7 i/s - 5.21x  slower
   Tabular:      17.3 i/s - 6.52x  slower

순수 속도 면에서는 내장 CSV 라이브러리가 가장 빠르다는 점을 확인할 수 있습니다.

마무리

지금까지 Ruby에서 CSV 파일을 읽고 쓰는 방법을 배웠습니다! 컨버터 활용법과 CSV 데이터를 처리할 수 있는 대체 젬에 대해서도 살펴보았습니다.

대용량 CSV 파일(10MB 초과)을 처리해야 한다면 블록과 함께 CSV.foreach(file_name) 메서드를 사용하는 것이 좋습니다. 이 메서드는 한 번에 한 행씩 읽기 때문에 메모리 사용량을 크게 줄일 수 있습니다.

이 글이 도움이 되었다면 더 많은 사람들이 볼 수 있도록 공유해 주세요!