티스토리 뷰

반응형

파일 입출력 다루기

어떤 양의 정수들을 입력받고 (예를 들어 0이 입력될 때까지 계속 숫자를 입력 받음) 그 합을 구하는 코드를 작성했다고 생각해보자. 물론 이 프로그램은 매우 간단하게 작성이 될 것이다. 그런데 이 프로그램을 사용할 때를 상상해보자. 합산해야 하는 숫자가 7자리(수백만)숫자 100개 정도된다면, 이를 일일이 키보드로 하나하나 타이핑 하는 것은 매우 번거로운 일일 것이다. 게다가 엔터를 눌러 입력한 후에 숫자가 잘못됐다는 사실을 깨닫는 상황이라면 처음부터 새로 입력해야 하는 아픔이 있을 것이다.

많은 양의 데이터를 일괄적으로 처리하기에 가장 좋은 방법은 입력값을 파일에 저장해 놓은 다음, 이것을 읽어서 처리하는 것이다. 일반적으로 한 개의 레코드를 한 줄에 기록하고 한 줄씩 읽어서 처리하는 방식을 사용하는데, 오늘은 기본적인 파일 입출력에 대해서 살펴보도록 하자.

파일 핸들러

파일을 이용한다는 것은 어떤 데이터가 일회성으로 사용되었다가 버려지는 것이 아니라, 디스크에 기록된채로 추후에 사용되거나 여러 방법으로 다른 컴퓨터로 전송되어 사용될 수 있다는 것을 의미한다. 즉 모든 "이진데이터"가 파일로 만들어질 수 있다. 일단 여기서는 "텍스트 파일"을 다루는 것에 대해서만 먼저 생각하겠다.

파일을 액세스하기 위해서는 파일을 생성하거나 열고 그 파일에 대한 핸들러를 얻어야 한다. 이 핸들러를 통해서 파일로부터 데이터를 읽어들이거나 파일로 데이터를 쓰는 작업을 처리할 수 있다.

파일 핸들러를 만드는 함수는 바로 open() 이라는 내장함수이다. 이 함수는 기본적으로 다음과 같은 식으로 사용된다.

f = open('filename.txt', 'r', encoding='utf8')
lines = f.readlines()
...
f.close()

open() 함수는 파일의 이름(및 경로)과 모드 그리고 인코딩을 이용해서 파일을 열고 그 파일을 액세스할 준비를 하게 된다. 이 때 filename.txt는 파일의 이름이며, r은 모드, 그리고 utf8은 인코딩이다. 파일의 모드에는 다음과 같은 것이 있다.

  • r : read를 의미하며, '읽기 전용' 모드로 파일을 열어준다.
  • w : write를 의미하며 '쓰기 전용' 모드로 파일을 열어준다. 이 모드에서 파일에 기록을 시작하면 이전 내용은 모두 지워진다.
  • x : create를 의미한다. 기본적으로 쓰기 모드이나, 이미 존재하는 파일을 이 모드로 열게되면 예외를 발생시킨다.
  • + : rw로 쓸 수도 있다. 파일을 읽기 및 쓰기 모드로 열어준다.
  • a : 쓰기 모드이나, 파일의 원래 내용을 유지하고 파일의 끝 부분부터 내용을 추가한다.
  • b : 당장은 쓰지 않겠지만, 파일을 바이너리 데이터로 취급한다.

기본적으로 파일은 텍스트 파일이라고 가정된다. 만약 파일 내에 영문자와 숫자와 같은 ASCII 코드 범위 밖의 글자 (한글, 한자, 일본어 및 특수문자 들)가 포함되는 경우라면 대부분 어떠한 인코딩이 적용된 상태일 것이다. 이 인코딩값은 프로그래머가 미리 알고 있어야 하며, 정상적인 텍스트를 얻고 싶다면 정확한 인코딩을 명시해줄 필요가 있다. (이는 마치 mp3 파일을 jpg전용 이미지 뷰어에서 열어 볼 수 없는 것과 비슷한 이유이다.)

파일 읽기

텍스트 파일 핸들러는 TextIOWrapper 클래스의 인스턴스로 파일 입출력을 담당하는 API를 제공한다. 파일을 읽기 위해서 사용되는 메소드는 크게 세 가지 이다.

  1. read() / read(n) : 파일의 전체 내용을 읽거나 혹은 n 글자를 읽어 들인다.
  2. readline() : 파일에서 한줄을 읽어들인다.
  3. readlines() : 파일 전체의 내용을 읽은 후 각 행으로 나뉘어진 리스트를 반환한다.

파일 쓰기

파일을 wa 모드로 열었다면 파일에 문자열을 쓸 수 있다. .write()writelines()를 사용해서 한 문자열 혹은 여러 줄의 문자열을 파일에 쓴다. 참고로 write() 함수를 이용해서 기록할 때, 개행문자를 자동으로 넣어주지 않기 때문에 \n과 같이 개행문자를 반드시 붙여넣어주어야 한다.

그리고 텍스트 파일에는 반드시 텍스트를 써야한다.

 

Context Manager

파일을 열어서 사용한 후에는 반드시 파일을 닫아야 한다. 초보가 가장 많이 하는 실수 중 하나가 (사실 그런다고 치명적인 문제가 생기는 경우가 드물긴 하지만) 쓰고난 파일을 닫지 않는 것이다. 하지만 많은 파이썬 코드들은 f = open() 과 같은 구문을 직접 쓰는 일은 별로 없다. 대신에 with 구문을 쓴다. with 문을 사용하면 파일을 닫는 처리를 하지 않아도 자동으로 닫는 처리가 된다.

이제 처음 이야기했던 파일을 열어서 숫자값을 읽고 그 합을 구해서 다른 파일에 쓰는 과정을 코드로 작성해보자.

먼저 처리 절차는 다음과 같다.

  1. 데이터가 들어있는 파일을 연다.
  2. 결과를 쓸 파일을 연다. (쓰기모드)
  3. 원본 파일에서 각 행을 읽어서 정수값으로 변환한다.
  4. 그 결과를 합산한다.
  5. 결과값을 문자열로 변환하여 결과 파일에 쓴다.
  6. 두 파일을 닫는다.

그리고 이는 더 간단한 코드로 귀결된다.

with open('data.txt') as inFile:  ## 원본 파일을 열고
  with open('result.txt', 'w') as outFile:  ## 결과 파일을 또 열고 
    result = sum(int(x) for x in inFile.readlines())  ## 원본 파일의 각 행을 정수로 변환하고 더한다.
    outFile.write(str(result))  ## 결과는 

 

한줄씩 처리하기

텍스트 타입의 파일 핸들러는 한 번에 한 행씩 읽어서 문자열을 리턴하는 반복자(iterator)처럼 행동한다. 따라서 각 라인을 한 번에 하나씩 읽어서 처리한다면 for문을 이용해서 돌릴 수 있다.

다음은 한 행에 여러 개의 정수값이 쓰여 있고, 각 행별 합계와 평균을 구해서 파일에 쓰는 코드이다.

def sum_and_avg(alist:[int]) -> (int, int): ## 1
  s = sum(alist)
  return s, s / len(alist)

with open('data.txt') as inFile:
  with open('result.txt', 'w') as outFile:
    for line in inFile: ## 2
      ns = [int(w) for w in line.split()]  ## 3
      outFile.write("{0} {1:0.2f}\n".format(*sum_and_avg(ns)))  ## 4

 

  1. sum_and_avg()는 정수 리스트를 받아서 정수들의 합과 평균값을 구해서 리턴하는 함수이다.
  2. 원본 파일에서 한 줄씩 처리하기 위해 for 문을 사용한다.
  3. ns는 원본 파일을 공백으로 끊은 단어들을 int 형으로 변환한 리스트이다.
  4. ns의 합계와 평균값을 구하고, 이중 평균값은 소수점 아래 2자리까지 출력하도록 포맷팅하여 결과 파일에 기록한다. 이 때 쓰는 내용에는 \n을 써서 개행 문자를 포함해주어야 한다.

결과를 기록하는 부분의 코드는 writelines()를 사용하면 아래와 같이 좀 더 축약해서 쓸 수 있다.

outFile.writelines(["{0} {1:0.2f}".format(*sum_and_avg([int(w) for w in line.split()]))\
                    for line in inFile.readlines()]

 

좀 더 고급 정보

파일 핸들러는 내부적으로 현재 액세스중인 파일 내에서의 바이트 오프셋 값을 가지고 있다. readline()의 경우 현재 위치로부터 EOF나 개행문자를 만날 때까지 읽은 결과를 리턴하게 된다. 만약 f.read(4) 명령을 통해서 첫 4바이트를 읽은 후, 다시 readline()을 호출하면, 이미 읽어들인 분량을 지나치고, 현재 오프셋으로부터 한줄을 읽게 된다.

그리고 EOF까지 읽었다 하더라도 파일이 닫히지는 않는다. .tell() 메소드를 통해서 현재 위치가 어디인지를 알 수 있으며, .seek() 메소드를 통해서 원하는 위치로 커서를 이동시킬 수 있다.

seek(cookie, whence=0) 를 통해서 원하는 위치로 커서를 옮긴다. cookie는 이동해야 하는 오프셋값이며, whence는 출발 지점을 정의한다. 0인 경우에는 파일의 첫부분이며, 1인 경우에는 현재위치, 2인 경우에는 파일의 끝부분이다. (따라서 오프셋값인 cookie는 음의 정수가 될 수 있다.) 이는 바이너리 파일을 편집하는 경우에 효과적으로 사용할 수 있다.

다음 예제는 time.sleep을 이용해서 리눅스의 tail 명령을 흉내낸다. 파일을 읽기 모드로 열어놓고 0.2초마다 추가된 내용이 있는지를 검사하여 한 줄씩 출력한다.

import sys, time

def watch(filename):
  with open(filename) as f:
    while True:
      c = f.read()
      if c:
        print(c)
        c = 0
      time.sleep(0.2)
      
def main():
  filename = sys.argv[1]
  watch(filename)
  
if __name__ == '__main__':
  main()

이렇게 작성된 스크립트를 다음과 같이 실행한다.

$ touch a.txt
$ python pytail.py a.txt

처음에 a.txt는 빈 파일이기 때문에 아무런 값도 출력되지 않지만, $ echo 'hello' >> a.txt 명령을 이용해서 a.txt 에 라인을 하나 추가하면 (매우 짧은 딜레이 후에) 해당 내용이 출력된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함