10장 파일 다루기

감사의 글

유튜브 동영상 모음집 (나도코딩 C)에서 설명하는 코드를 활용합니다. 강의동영상을 공개한 나도코딩님께 감사드립니다.

핵심

  • 파일 생성 및 읽기/쓰기

(C 언어) 비밀일기

게임 설명

  • 파일을 생성하여 내용을 작성하여 저장한 후 저장된 내용 확인 및 수정하기

(C 언어) 파일 쓰기 1

  • 파일 열기: fopen() 함수 활용

    • 첫째 인자: 파일 저장 위치와 파일명 지정

      • 윈도우에서 파일경로를 지정할 때 아래와 같이 원(won) 기호를 두 번 사용해야 함.

        "C:₩₩test1.txt"

    • 둘째 인자: 파일을 열 때의 옵션 지정

      • r(읽기), w(쓰기), a(추가하기)
      • t(텍스트 문서), b(바이너리 문서)
        • 줄바꿈 기호(\n)처리하는 방식이 운영체제마다 다른데 이 문제를 피하려면 바이너리 방식 사용 추천
      • 참조: fopen() 옵션과 binary/text mode

// Visual Studio에서 오류 발생할 경우
// #define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main()
{
    // 파일에 쓰기: fputs
    // 파일에 특정 내용 저장

    FILE * file = fopen("test1.txt", "wb");  // 현재 디렉토리에 파일 생성 및 저장

    // 파일 열기 가능여부 확인
    if (file == NULL)
    {
        printf("파일 열기 실패\n");
        return 1;
    }

    fputs("fputs를 이용해서 글을 적어볼께요.\n", file);
    fputs("잘 적히는지 확인해주세요.\n", file);

    // 파일 닫기를 기본적으로 진행.
    // 그렇지 않으면 데이터 손실 발생 가능
    fclose(file);

    return 0;
}

Python 구현

  • 여기서는 open() 함수의 옵션으로 'b'옵션을 굳이 사용하지 않음.
    • 'b' 옵션을 사용하여 파일을 저장하려면 저장 내용을 바이너리로 전환시킨 후 저장해야 하기 때문임.
    • 바이너리 파일은 실행파일(exe), 음악파일(mp3) 등 일반 텍스트 용도가 아닌 경우에 사용함.
    • 파이썬에서 바이너리 파일을 다루는 일은 좀 더 복잡함.
In [1]:
# 파일에 쓰기: write() 메서드
# 파일에 특정 내용 저장

file = open("test1_py.txt", "w")  # 현재 디렉토리에 파일 생성 및 저장

file.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
file.write("잘 적히는지 확인해주세요.\n")

# 파일 닫기를 기본적으로 진행.
# 그렇지 않으면 데이터 손실 발생 가능
file.close()
  • 텍스트 파일을 바이너리 파일로 다루는 경우는 아래와 같이 함.
    • 내용을 저장할 때 문자열을 encode() 메서드를 활용하여 bytes 자료형으로 변환시켜야 함.
    • 바이너리 파일로 저장된 파일은 이후 계속 'b' 옵션을 함께 사용해야 함.
In [2]:
file = open("test1b_py.txt", "wb")

file.write("write() 메서드를 이용해서 글을 적어볼께요.\n".encode())
file.write("잘 적히는지 확인해주세요.\n".encode())

file.close()
  • 파일 다루기가 끝나면 자동으로 닫아주는 기능을 아래와 같이 활용할 수 있음.
In [3]:
with open("test1c_py.txt", "w") as file:

    file.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
    file.write("잘 적히는지 확인해주세요.\n")

    # 파일 닫기는 자동으로 진행됨.
  • 파일이 존재하지 않거나 파일 열기/닫기에 실패하는 경우에 대비하여 예외처리 활용

  • 아래 방식은 오류 종류에 맞추어 예외처리하는 기능 설명

In [4]:
try: 
    with open("test1d_py.txt", "r") as file:

        file.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
        file.write("잘 적히는지 확인해주세요.\n")

except IOError as err:
    print('파일 오류: ' + str(err))
    
파일 오류: [Errno 2] No such file or directory: 'test1d_py.txt'
  • 아래 방식은 임의의 오류를 처리하는 방식 설명
In [5]:
try: 
    with open("test1d_py.txt", "r") as file:

        file.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
        file.write("잘 적히는지 확인해주세요.\n")

except:
    print('파일 열기 실패')    
파일 열기 실패

(C 언어) 파일 읽기 1


// Visual Studio에서 오류 발생할 경우
// #define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#define MAX 10000 // 상수 선언

int main()
{
    // 파일 읽기 fgets
    // 저장된 파일 내용 읽어오기

    char line[MAX];

    // 읽기 전용으로 열기
    // 바이너리 파일로 저장한 경우 바이너리 파일로 읽어와야 함.
    FILE * file = fopen("test1.txt", "rb");

    // 파일 열기 가능여부 확인
    if (file == NULL)
    {
        printf("파일 열기 실패\n");
        return 1;
    }

    // 한 줄씩 읽어오기
    while(fgets(line, MAX, file) != NULL)
    {
        printf("%s", line);
    }

    // 파일 닫기를 기본적으로 진행.
    // 그렇지 않으면 데이터 손실 발생 가능
    fclose(file);

    return 0;
}

Python 구현

  • 저장된 파일 내용 읽어오기

readline() 함수 활용

  • readline() 메서드: 한 줄씩 읽기
In [6]:
with open("test1_py.txt", "r") as file:

    while 1:
        line = file.readline()
        if not line: break
        print(line)
write() 메서드를 이용해서 글을 적어볼께요.

잘 적히는지 확인해주세요.

  • 주의사항: 파이썬의 print() 함수는 줄바꿈을 자동으로 추가함.
  • strip() 메서드 활용하면 문자열의 양 끝에 있는 줄바꿈 기호('\n'), 스페이스(' ', 뛰어쓰기) 등의 소위 화이트 스페이스(white space) 등을 제거함.
In [7]:
with open("test1_py.txt", "r") as file:

    while 1:
        line = file.readline().strip()
        if not line: 
            break
        print(line)
write() 메서드를 이용해서 글을 적어볼께요.
잘 적히는지 확인해주세요.

readlines() 함수 활용

  • readlines() 메서드: 모든 줄을 한꺼번에 리스트로 읽어오기. 각각의 줄을 리스트의 항목으로 사용.
In [8]:
with open("test1_py.txt", "r") as file:
    for line in file.readlines():
        if not line: 
            break
        print(line.strip())
write() 메서드를 이용해서 글을 적어볼께요.
잘 적히는지 확인해주세요.
In [9]:
try:
    with open("test1d_py.txt", "r") as file:
        for line in file.readlines():
            if not line: 
                break
            print(line.strip())
except:
    print("파일 열기 실패")
파일 열기 실패

(C 언어) 파일 쓰기 2

  • fprintf() 함수: 서식 문자열을 파일에 저장할 때 사용

// Visual Studio에서 오류 발생할 경우
// #define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int main()
{
    // 파일 쓰기 fprintf
    // 서식을 지원하는 문자열 저장 함수

    // 예제: 로또 번호 저장

    FILE * file = fopen("test2.txt", "wb");

    if (file == NULL)
    {
        printf("파일 열기 실패!\n");
        return 1;
    }

    // 로또 추첨번호 저장
    fprintf(file, "%s %d %d %d %d %d %d\n", "추첨번호", 1, 2, 3, 4, 5, 6);
    fprintf(file, "%s %d\n", "보너스번호", 7);

    fclose(file);

    return 0;
}

Python 구현

  • 문자열 서식은 기본으로 지원함.
In [10]:
with open("test2_py.txt", "w") as file:
    file.write("%s %d %d %d %d %d %d\n" % ("추첨번호", 1, 2, 3, 4, 5, 6))
    file.write("%s %d\n" % ("보너스번호", 7))

(C 언어) 파일 읽기 2

  • fscanf() 함수: 파일에 저장된 문자열을 서식에 맞추어 읽어올 때 사용

// Visual Studio에서 오류 발생할 경우
// #define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

#define MAX 10000 // 상수 선언

int main()
{
    // 파일 읽기 fscanf
    // 서식을 지원하는 문자열 저장 함수

    // 예제: 저장된 로또 번호 읽어오기
    int num[6] = { 0, 0, 0, 0, 0, 0 }; // 추첨번호 저장용
    int bonus = 0; // 보너스 번호 저장용

    char str1[MAX];
    char str2[MAX];

    FILE * file = fopen("test2.txt", "rb");

    if (file == NULL)
    {
        printf("파일 열기 실패!\n");
        return 1;
    }

    // 저장된 로또 추첨번호 읽어오기
    fscanf(file, "%s %d %d %d %d %d %d\n", str1, 
            &num[0], &num[1], &num[2], &num[3], &num[4], &num[5]);
    printf("%s %d %d %d %d %d %d\n", str1,
            num[0], num[1], num[2], num[3], num[4], num[5]);

    fscanf(file, "%s %d\n", str2, &bonus);
    printf("%s %d\n", str2, bonus);

    fclose(file);

    return 0;
}

Python 구현

  • 비슷한 기능의 함수는 없지만 구현은 아래와 같이 가능.
In [11]:
with open("test2_py.txt", "r") as file:
    print("%s %s %s %s %s %s %s" % tuple(file.readline().split()))
    print("%s %s" % tuple(file.readline().split()))
추첨번호 1 2 3 4 5 6
보너스번호 7

(C 언어) 프로젝트

  • 비밀읽기 작성하기

    • 비밀번호 맞는 경우: 비밀일기를 읽어와서 보여주고, 계속 작성하도록 함.
    • 비밀번호 틀린 경우: 경고 메시지를 표시하고 종료함.
  • fopen() 함수의 a+ 옵션

    • 파일이 존재하면 파일 끝에 문장 추가
    • 파일이 존재하지 않으면 새로 생성 후 문장 추가

주의사항

  • getchar()getch()의 차이점

    • getchar() 함수: 엔터가 입력될 때까지 기다린 후 처리
    • getch() 함수: 키 입력 시 바로바로 처리
  • 하지만 getch() 함수는 윈도우에서만 지원됨.

  • Repl.it 사이트 등 처럼 리눅스/맥 환경에서 사용하는 gcc 컴파일러에서는 지원하지 않음.

  • 하지만 아래와 같이 동일한 기능을 하는 함수를 구현할 수 있음.


#include <termios.h>
#include <stdio.h>

static struct termios old, current;

// 새로운 터미널의 i/o 세팅 초기화
void initTermios(int echo) 
{
  tcgetattr(0, &old); // 이전 터미널의 i/o 세팅 저장
  current = old; // 이전 i/o 세팅을 그대로 활용
  current.c_lflag &= ~ICANON; // 버퍼를 사용하는 i/o 끄기
  if (echo) {
      current.c_lflag |= ECHO; // 메아리용 버퍼를 사용하는 모드. 입력한 문자열을 화면에 보여주는 기능.
  } else {
      current.c_lflag &= ~ECHO; // 메아리용 버퍼를 사용하지 않는 모드. 입력한 문자열을 화면에 보여주지 않음.
  }
  tcsetattr(0, TCSANOW, &current); // 지정한 새로운 i/o 세팅 사용
}

/* 이전 i/o 세팅 복구 */
void resetTermios(void) 
{
  tcsetattr(0, TCSANOW, &old);
}

/* 한 개의 문자열 읽기 - echo 인자는 버퍼 사용여부 지정 */
char getch_(int echo) 
{
  char ch;
  initTermios(echo);
  ch = getchar();
  resetTermios();
  return ch;
}

/* 메아리 없이 한 개의 문자 읽기. 즉, 입력내용 화면에 보여주지 않음. */
char getch(void) 
{
  return getch_(0);
}

/* 메아리 사용하며 한 개의 문자 읽기. 즉, 입력내용 화면에 보여줌. */
char getche(void) 
{
  return getch_(1);
}

프로젝트 구현

  • 동영상과 동일하게 코드 구현 가능

  • 단, 위 코드를 동일 프로젝트 내의 다른 c 파일에 저장했다고 가정함.

    • 다른 c 파일에 저장된 함수를 불러오려면 extern 지정자를 이용하여 함수를 선언해야 함.
  • #include <string.h>strcmp()를 사용하기 위해 필요함.

  • 비밀일기 작성을 'Exit' 뿐만 아니라 'exit'를 입력해도 종료하도록 수정됨.
    • '또는'(or)에 해당하는 논리연산자 || 활용하였음.

// Visual Studio에서 오류 발생할 경우
// #define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <string.h>

// 다른 c 파일에 저장된 getch() 함수 불러오기
// 동일한 프로젝트의 폴더에 포함된 파일이어야 함.
extern char getch(void);

// 비밀번호를 입력 받아 맞는 경우는 내용 읽어와서 보여주고, 계속 작성하도록 함수
// 비밀번호가 틀린 경우 경고 메시지 출력 후 종료함.

#define MAX 10000 // 상수 선언

int main()
{
    // fgets, fputs 활용

    char line[MAX]; // 파일에서 불러온 내용 저장
    char contents[MAX]; // 일기장에 입력할 내용
    char password[20]; // 비밀번호 입력할
    char c; // 비밀번호가 입력될 때 한 캐릭터씩 처리하는 용도

    printf("'비밀일기'에 오신 것을 환영합니다!\n");
    printf("비밀번호를 입력하세요:");

    // 비밀번호 입력 처리하기
    int i = 0;
    while (1)
    {
        c = getch();

        // if (c == 13) // 동영상에서처럼 getch()가 지원되는 윈도우에서 실행하는 경우. 
                        // 13은 '\n'의 아스키 코드
        if (c == '\n') // Enter를 가리키는 번호, 즉, 비밀번호 입력 종료 의미
        {
            password[i] = '\0'; // 비밀먼호 문자열 끝에 추가되는 널 문자 지정
            break; // 비밀번호 입력 종료
        }
        else // 비밀번호 입력 중
        {
            printf("*"); // 입력되는 비밀번호 마스킹 처리 용도
            password[i] = c;
        }

        i++;
    }

    // 입력된 비밀번호 확인하기
    printf("\n\n === 비밀번호 확인 중 ... ===\n\n");

    if (strcmp(password, "skehzheld") == 0) // 비밀번호 = (영어)나도코딩
    {
        printf("=== 비밀번호 확인 완료 ===\n\n");
        char * fileName = "secretdiary.txt";
        FILE * file = fopen(fileName, "a+b"); 

        if (file == NULL) 
        {
            printf("파일 열기 실패!\n");
            return 1;
        }

        // 파일 내용 읽어오기
        while (fgets(line, MAX, file) != NULL)
        {
            printf("%s", line);
        }

        // 내용 추가하기
        printf("\n\n 내용을 계속 작성하세요. 종료하려면 EXIT 또는 exit를 입력하세요\n\n");

        while (1)
        {
            scanf("%[^\n]", contents); // 줄바꿈이 사용되기 직전까지 저장. 즉, 한 문단씩 저장.
            getchar(); // Enter 키 입력을 flush 처리하기

            if (strcmp(contents, "EXIT") == 0 || strcmp(contents, "exit") == 0)
            {
                printf("비밀일기 입력을 종료합니다.\n\n");
                break;
            }

            fputs(contents, file);
            fputs("\n", file); // 줄바꿈을 인위적으로 진행
        }
        fclose(file);
    }
    else
    {
        printf(" === 비밀번호가 틀렸어요. ===\n\n");
        printf(" 꺅!! 당신 누가야?!! 감히 내 일기장을 !!!\n\n\n");
    }

    return 0;
}

Python 구현

  • 파이썬에서는 getch()에 해당하는 함수 없지만 아래와 같이 구현할 수 있음.

  • 주의사항

    • 아래 코드에서 정의된 getch() 함수는 주피터 노트북에서 작동하지 않음.
    • 이유: 주피터 노트북의 입출력 장치가 제대로 지원되지 않아 보임.
    • repl.it 사이트 등에서는 작동함.
  • 참조: OS별 처리


try:
    # Windows용 코드
    import msvcrt

    def getch():
        """단일키 누르는 것을 받아옴"""
        return msvcrt.getch()
except ImportError:
    # Linux & Mac 용 코드
    import sys
    import tty
    import termios

    def getche():
        """단일키 누르는 것을 받아옴
        입력된 값도 출력함"""
        fd = sys.stdin.fileno()
        original_attributes = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, original_attributes)
        print(ch)
        return ch

    def getch():
        """단일키 누르는 것을 받아옴"""
        fd = sys.stdin.fileno()
        original_attributes = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, original_attributes)
        return ch

프로젝트 구현

  • 주의사항:
    • 위 코드가 getch_termios.py 파일에 저장되어 있다고 가정함.
    • 엔터 키 입력여부 확인은 '\r' 문자열과 비교함. C 언어의 경우와 다름.
    • 파이썬의 문자열은 불변자료형임. 따라서 키가 입력될 때마다 매번 새로 생성해야 함.

from getch_termios import getch

print("'비밀일기'에 오신 것을 환영합니다!")
print("비밀번호를 입력하세요:")

password = ''
while True:
    c = getch()
    if (c == '\r'):
        break
    else:
        print("*", end='', flush=True)
        password += c

print("\n=== 비밀번호 확인 중 ... ===\n")

if (password == "skehzheld"):
    print("\n=== 비밀번호 확인 완료 ===\n")
    fileName = "secretdiary.txt"
    with open(fileName, "a+") as file:
        for line in file.readlines():
            print(line)

        print("내용을 계속 작성하세요. 종료하려면 EXIT 또는 exit를 입력하세요\n")

        while True:
            contents = input()
            if (contents == "EXIT" or contents == "exit"):
                print("비밀일기 입력을 종료합니다.\n")
                break
            file.write(contents+'\n')
else:
    print("=== 비밀번호가 틀렸어요. ===\n")
    print("꺅!! 당신 누가야?!! 감히 내 일기장을 !!!")

부록

불변 자료형 vs. 가변 자료형

  • 파이썬에서 문자열은 한 번 생성하면 절대로 내용을 수정할 수 없음.
  • 반면에 C 에서의 문자열은 문자들의 어레이로 정의되었기 때문에 수정이 가능함.
In [12]:
try: 
    password = "skehzheld" # 나도코딩
    password[0] = "S"
except:
    print("문자열은 수정불가!")
문자열은 수정불가!

오프셋(offset) 활용

  • fgets() 함수 또는 readline() 함수 등으로 파일 내용을 읽으면 어디까지 읽었는지를 기억하는 장치가 있음.
    • 읽기 위치를 오프셋(offset)이라 부름.
  • 따라서 파일 끝까지 읽으면 더 이상 읽을 내용이 없어서 빈 문자열을 되돌려줌.

파이썬

  • 예를 들어, 아래와 같이 두 줄을 포함하고 있는 파일에 readline() 함수를 세 번 적용하면 빈 줄이 반환됨.
In [13]:
with open("test3_py.txt", "w") as f:
    f.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
    f.write("잘 적히는지 확인해주세요.\n")

with open("test3_py.txt", "r") as f:
    print(f.readline().strip(), 1)
    print(f.readline().strip(), 2)
    print(f.readline().strip(), 3)
write() 메서드를 이용해서 글을 적어볼께요. 1
잘 적히는지 확인해주세요. 2
 3
  • 오프셋의 현재 위치는 tell() 메서드로 확인 가능
In [14]:
with open("test3_py.txt", "w") as f:
    f.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
    f.write("잘 적히는지 확인해주세요.\n")

with open("test3_py.txt", "r") as f:
    print(f.readline().strip(), 1)
    print(f.readline().strip(), 2)
    print("현재 오프셋 위치:", f.tell())
    print(f.readline().strip(), 3)
write() 메서드를 이용해서 글을 적어볼께요. 1
잘 적히는지 확인해주세요. 2
현재 오프셋 위치: 95
 3
  • 파일을 다시 처음부터 읽으려면 파일을 새로 열어야 함.
  • 아니면 seek() 메서드를 통해 오프셋 값을 바이트 단위로 지정할 수 있음.
    • 주의사항: 오프셋의 값을 정확하게 지정하기 어려움.
In [15]:
with open("test3_py.txt", "w") as f:
    f.write("write() 메서드를 이용해서 글을 적어볼께요.\n")
    f.write("잘 적히는지 확인해주세요.\n")

with open("test3_py.txt", "r") as f:
    print(f.readline().strip(), 1)
    print(f.readline().strip(), 2)
    
    print("다시 처음부터...")
    f.seek(0)
    print("현재 오프셋 위치:", f.tell())
    print(f.readline().strip(), 1)
    print(f.readline().strip(), 2)
write() 메서드를 이용해서 글을 적어볼께요. 1
잘 적히는지 확인해주세요. 2
다시 처음부터...
현재 오프셋 위치: 0
write() 메서드를 이용해서 글을 적어볼께요. 1
잘 적히는지 확인해주세요. 2

C 언어

#include <stdio.h>

#define MAX 10000

int main()
{
    char line[MAX];

    FILE * file = fopen("test1.txt", "rb");

    fgets(line, MAX, file);
    printf("%s", line);

    fgets(line, MAX, file);
    printf("%s", line);

    printf("%ld\n", ftell(file));
    // 파일의 현재위치부터 4바이트 뒤로 이동
    fseek(file, 4, 1);
    printf("%ld\n", ftell(file));
    // 파일의 현재위치부터 4바이트 앞으로 이동
    fseek(file, -4, 1);
    printf("%ld\n", ftell(file));
    // 파일의 처음위치부터 0바이트 떨어진 곳으로 이동
    // 즉, 파일의 처음으로 이동
    fseek(file, 0, 0);
    printf("%ld\n", ftell(file));

    fgets(line, MAX, file);
    printf("%s", line);

    fgets(line, MAX, file);
    printf("%s", line);

    fclose(file);

    return 0;
}