개발 · 자동화기초 · 16분 읽기

Python 파일·인코딩·JSON과 경로

파일 경로와 문자 인코딩, 자료 형식을 명확히 지정하고 자원 수명과 입력 크기를 관리합니다.

개념부터 찾아보기

정의·역할·동작 원리를 독립 페이지에서 확인하세요. 본문의 밑줄 용어에도 설명이 있습니다.

이 노트에서 설명할 수 있게 되는 것

  1. 경로·바이트·텍스트·파싱 결과·업무 검증의 층을 구분한다.
  2. with와 반복 읽기로 파일 자원과 입력 크기를 관리한다.
  3. 파일 열기·디코딩·구문·자료형 오류를 별도로 진단한다.

01open의 모드가 생성·덮어쓰기·표현을 결정한다

  • r은 기존 파일 읽기, w는 생성하거나 기존 파일을 잘라 쓰기, a는 끝에 추가, x는 파일이 이미 있으면 실패하는 새 생성이다.
  • +는 읽기와 쓰기를 함께 허용하고 b는 bytes 중심 바이너리 모드다.
  • 텍스트 모드는 인코딩을 통해 str과 바이트 사이를 변환하므로 을 명시하면 시스템 기본값 차이로 한글이 잘못 읽히는 상황을 줄일 수 있다.
  • with open(...) as f에서 f는 파일 경로 문자열이 아니라 열린 파일 객체다.
  • open 자체가 권한·경로·장치 문제로 실패하면 with 본문에 들어가지 않는다.
  • 모드를 w로 잘못 선택하면 읽기 전에 기존 내용이 없어질 수 있으므로 작업 목적과 객체 수명을 먼저 정한다.
  • 아래 예시는 이미 존재하는 로그를 읽는 동작만 설명한다.
from pathlib import Path
path = Path("lesson.log")
with path.open("r", encoding="utf-8") as stream:
    first_line = stream.readline()
print(first_line.rstrip("\n"))

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

02read·readline·반복과 현재 파일 위치

  • read()는 남은 내용을 한 번에 읽고 read(n)은 정해진 크기만큼 읽는다.
  • 텍스트 모드의 n은 문자 단위이고 바이너리 모드에서는 바이트 단위다.
  • readline은 한 줄을 읽으며 for line in f는 파일을 줄 단위로 반복한다.
  • 읽은 만큼 현재 위치가 진행하므로 같은 객체에서 read를 다시 부르면 이미 읽은 내용을 자동으로 처음부터 주지 않는다.
    • 파일 끝에서 빈 문자열 또는 빈 bytes를 반환한다.
  • tell은 현재 위치를 나타내지만 텍스트 모드에서는 인코딩·줄바꿈 변환 상태를 복원하기 위한 불투명한 위치값일 수 있어 임의 문자 인덱스로 계산하지 않는다.
  • 바이너리 모드는 바이트 오프셋 기준 seek가 명확하다.
  • 큰 로그는 read().splitlines()로 전부 메모리에 올리기보다 줄 단위로 처리하면 메모리 사용량을 줄일 수 있다.
with open("lesson.log", "r", encoding="utf-8") as stream:
    for number, line in enumerate(stream, start=1):
        if "ERROR" in line:
            print(number, line.rstrip("\n"))

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

03버퍼 flush·close와 with의 예외 처리

  • 쓰기 데이터는 버퍼와 운영체제 캐시를 거칠 수 있다.
  • flush는 쪽 버퍼를 아래 계층으로 밀어내고 close는 파일 객체를 닫으면서 관련 정리를 한다.
  • 이것만으로 모든 저장 장치의 전원 장애 내구성이 같은 수준으로 보장되는 것은 아니며 필요하면 fsync와 파일 시스템·장치의 보장 범위를 별도로 다룬다.
  • context manager의 __enter__가 성공적으로 끝나면 with 본문이 정상 종료되거나 예외로 빠져나갈 때 __exit__를 호출한다. __enter__ 자체가 실패하면 그 관리자의 __exit__가 무조건 호출되는 것은 아니다.
  • __exit__는 예외를 받아 정리할 수 있고 참 값을 반환하면 해당 예외를 억제할 수 있다.
  • 일반 파일 객체의 with는 예상 가능한 블록 수명 안에서 close를 보장하는 데 유용하지만 강제 종료·전원 장애까지 마법처럼 처리하는 것은 아니다.
원리 심화

04바이트 버퍼와 텍스트 디코더는 위치 단위가 다르다

  • 일반 텍스트 파일 객체는 바이트 스트림 위에 버퍼링과 디코딩·줄바꿈 처리를 얹는다. 디스크에서 읽은 바이트 수와 호출자가 받은 문자 수가 항상 같지는 않다.
  • 문자 하나가 여러 바이트일 수 있어 바이너리 청크 끝이 문자 중간일 수 있다. 각 청크를 독립 문자열처럼 디코딩하면 올바른 전체 자료도 오류가 날 수 있다.
  • incremental decoder는 아직 완성되지 않은 바이트를 보관하고 다음 청크와 함께 해석한다. 최종 청크임을 알릴 때 남은 불완전 입력을 오류로 판단할 수 있다.
  • TextIOWrapper의 tell 결과는 디코더 상태를 복원하는 불투명한 위치값이다. 임의로 1을 더해 다음 문자로 이동한다는 방식은 지원되는 일반 규칙이 아니다.
  • newline=None의 텍스트 읽기는 CRLF·CR·LF를 LF로 통일할 수 있다. 따라서 원본 바이트 길이·줄바꿈 형태를 보존해야 하는 검증은 바이너리 관찰과 분리한다.
예제로 추적

05가A의 4바이트가 두 문자로 복원되는 과정

  1. 교육용 문자열 가A의 은 EA B0 80 41이다. 가는 3바이트, A는 1바이트이므로 바이트 길이는 4이고 문자열 길이는 2개다.
  2. 첫 청크 EA B0에는 가의 마지막 바이트가 없다. 이것만 완전한 입력으로 strict 디코딩하면 불완전한 시퀀스 오류가 된다.
  3. incremental decoder에 첫 청크를 final=False로 주면 아직 문자가 완성되지 않아 빈 문자열을 반환하고 필요한 바이트를 보관한다.
  4. 80 41을 final=True로 주면 저장했던 EA B0와 80이 합쳐져 가가 되고 뒤 41이 A가 된다. 두 번째 반환값은 가A다.
  5. 뒤 청크 없이 끝났다면 불완전 입력을 보고해야 한다. errors="ignore"로 버리면 전송이 잘린 사실과 사라진 문자를 놓칠 수 있다.
import codecs
raw = bytes.fromhex("EA B0 80 41")
decoder = codecs.getincrementaldecoder("utf-8")()
print(repr(decoder.decode(raw[:2], final=False)))  # ''
print(repr(decoder.decode(raw[2:], final=True)))   # '가A'
print(len(raw), len(raw.decode("utf-8")))          # 4 2

실행하지 않은 교육 예시. 주석의 출력은 언어 규칙으로 추적한 예상값이다.

검증·예외

06파일 경로·바이트·문법·업무값의 실패 위치

  • 같은 텍스트 객체에서 재개할 위치는 tell 값을 보관하고 seek에 돌려주는 방식으로 다룬다. 여러 파일의 문자 수를 더해 임의 seek 위치로 쓰지 않는다.
  • UnicodeDecodeError가 청크 끝에서 반복되면 실제 손상인지 문자 경계를 잘라 읽는 코드인지 나눈다. 오류의 byte 위치와 청크 합성 방식을 함께 살펴본다.
  • 인용 필드에는 줄바꿈이 들어갈 수 있어 물리적인 한 줄이 레코드 한 개와 같지 않을 수 있다. 줄 수만으로 행 수나 누락 건수를 계산하지 않는다.
  • with가 끝나면 파일은 닫히지만 이미 읽은 str·dict는 계속 존재할 수 있다. 닫힌 파일에서 다시 read하는 오류와 기존 자료 객체의 사용은 별개다.
  • 의 "port":"22"는 파싱 가능해도 port가 정수라는 계약과 다르다. 형식 파싱 성공 뒤 자료형·허용 범위·필수 키를 검증해야 오류 계층이 분명해진다.

07경로 결합·작업 디렉터리·인코딩 오류를 분리하기

  • 상대 경로는 현재 작업 디렉터리에 대해 해석되며 스크립트 파일이 놓인 디렉터리와 반드시 같지 않다.
  • Path(__file__).resolve().parent는 일반 파일 모듈에서 자신의 위치를 기준으로 자료를 찾을 때 쓸 수 있지만 대화형 환경에는 __file__이 없을 수 있다.
  • 경로를 문자열 덧셈으로만 만들기보다 의 / 연산으로 요소를 연결하면 구분자와 가독성을 관리하기 쉽다.
  • 존재하지 않음, 읽기 권한 없음, 디렉터리를 파일처럼 열기, 디코딩 실패는 서로 다른 예외다.
  • errors="ignore"로 모든 깨진 바이트를 버리면 로그 증거가 사라질 수 있다.
  • 텍스트로 가정할 수 없는 입력은 바이너리로 읽고 형식을 확인하거나 오류 대체 정책을 명시한다.
  • 외부 입력의 경로를 다룰 때는 ../와 심볼릭 링크 등이 의도한 디렉터리 경계를 벗어나는지도 별도 문제로 고려한다.
from pathlib import Path
base = Path.cwd()
print(base)
print(base / "data" / "lesson.json")
# 읽기 실패의 원인과 디코딩 실패를 같은 오류로 합치지 않는다.

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

08JSON·CSV의 구조와 쓰기 완료의 기준

  • 은 객체·배열·문자열·수·논리값·을 표현한다.
  • dict/를 저장하고 복원할 수 있지만 임의 객체 전체를 그대로 복제하는 형식은 아니다.
  • .load는 파일 객체에서, .loads는 문자열에서 읽는다.
  • 는 구분자가 문자열 내부에 들어가는 경우와 줄바꿈 인용 규칙이 있으므로 단순 split(",")보다 모듈을 사용하는 편이 구조에 맞다.
  • 설정 파일을 갱신할 때 중간 내용이 보이면 읽는 프로그램이 불완전한 파일을 파싱할 수 있다.
  • 같은 파일 시스템의 임시 파일에 완성된 내용을 쓰고 적절히 닫은 뒤 os.replace로 바꾸는 패턴은 이름 교체의 원자성을 활용한다.
    • 다만 전원 장애 지속성, 권한·소유권 보존, 두 작성자의 경쟁까지 모두 해결하는 것은 아니다.
  • 공개 예시는 실제 운영 설정을 덮어쓰지 않는 독립 실습 경로로 제한한다.
  • 신뢰할 수 없는 pickle 자료는 읽는 과정에서 코드가 실행될 수 있어 일반 외부 입력 형식으로 사용하지 않는다.
  • 파일 크기와 중첩 깊이, 필수 필드도 제한해 잘못된 입력으로 자원이 과도하게 소비되지 않도록 한다.
import json
from pathlib import Path

path = Path("study-example.json")
data = {"service": "demo", "ports": [8000, 8001]}
# 독립 실습 파일에만 적용하는 쓰기 예시
with path.open("w", encoding="utf-8") as stream:
    json.dump(data, stream, ensure_ascii=False, indent=2)
with path.open("r", encoding="utf-8") as stream:
    restored = json.load(stream)

코드 동작을 설명하는 교육 예시. 주석의 결과는 언어 규칙으로 설명한 예상값이며 이 페이지 작성 중 실행하지 않았다.

09자원 수명과 스트리밍

  • with 문은 컨텍스트 관리 규칙에 따라 파일 정리를 수행하게 해 예외가 발생해도 닫는 처리를 빠뜨리지 않도록 돕는다.
  • 큰 파일은 전체를 한 번에 읽기보다 줄이나 적절한 단위로 처리할 수 있다.
    • 읽기 성공과 내용의 의미가 올바르다는 사실은 다르다.
  • 아래 예시는 실제 파일을 변경하지 않고 메모리 안의 문자열을 읽는다.
import json

text = '{"service": "web", "enabled": true}'
record = json.loads(text)
print(record["service"], record["enabled"])

문자열을 자료로 변환해 필드 값을 조회한다.

10파일을 읽는 것과 의미를 얻는 것은 다른 단계다

  • 설정 파일을 읽는 과정을 나누면 먼저 경로를 해석해 파일을 열고 바이트를 얻는다.
  • 텍스트 읽기에서는 지정한 인코딩으로 문자열을 만들고, 파서는 그 문자열을 dict··str·숫자 등의 객체로 바꾼다.
  • 마지막으로 애플리케이션은 필수 키와 허용된 값·자료형을 확인한다.
  • 문법이 맞다는 사실은 업무 설정이 올바르다는 뜻이 아니다.
    • 예를 들어 {"port":"443"}는 유효한 이지만 port를 정수로 기대하는 코드에는 문자열이 들어온다.
  • 파일이 없으면 경로 단계에서, 읽기 권한이 없으면 파일 열기 단계에서, 인코딩이 다르면 문자열 변환 단계에서 실패할 수 있다.
  • 의 따옴표나 쉼표가 잘못되면 파싱 단계에서, 필수 키가 빠지면 업무 검증 단계에서 문제가 생긴다.
  • 이 실패를 모두 파일 읽기 오류로 묶으면 사용자가 무엇을 고쳐야 할지 알기 어렵다. 오류 메시지는 문제의 층과 필요한 위치를 알려 주되 파일 전체나 비밀 설정값을 그대로 로그로 출력하지 않도록 한다.
  1. 기준 디렉터리와 읽기 권한을 확인한다.
  2. 바이트를 명시된 인코딩으로 해석한다.
  3. 문법에 따라 객체로 바꾼다.
  4. 정수라는 계약에 맞지 않으면 명시적으로 거부하거나 정해진 변환을 수행한다.

설명과 함께 보는 보조 도식

처리 과정

바이트에서 검증된 설정까지

모의 JSON {"port":"443"}를 정수 포트 설정으로 읽으려는 사례다.

전체 단계를 한 번에 읽기
  1. 01

    대상 열기

    기준 디렉터리와 읽기 권한을 확인한다.

    오류 예
    없음·권한 거부
  2. 02

    텍스트 변환

    바이트를 명시된 인코딩으로 해석한다.

    구분
    문자열과 바이트
  3. 03

    JSON 파싱

    문법에 따라 Python 객체로 바꾼다.

    port의 자료형
    str
  4. 04

    업무 검증

    정수라는 계약에 맞지 않으면 명시적으로 거부하거나 정해진 변환을 수행한다.

    결론
    파싱 성공 ≠ 설정 유효
학습 모식도. 선택한 입력과 조건에서 발생하는 처리·상태 변화를 보여 주며 실제 명령은 실행하지 않는다.

11자원·크기·형식을 함께 관리하기

  • with 안에서 파일을 다루면 블록을 벗어날 때 컨텍스트 관리 규칙에 따라 닫힌다.
  • 읽은 내용을 계속 사용하려면 파일 객체의 수명과 읽어서 얻은 문자열·객체의 수명을 구분한다.
  • 큰 파일의 read 전체 호출은 메모리에 전체 문자열을 만들 수 있으므로 줄 단위 반복이나 형식에 맞는 스트리밍을 검토한다.
  • 줄별 과 하나의 큰 배열은 파싱 단위가 다르고, 도 따옴표 안 줄바꿈이 가능하므로 아무 텍스트나 줄별 분리만으로 처리할 수 없다.
  • 쓰기 작업을 설계할 때는 기존 파일을 자르는 모드와 끝에 추가하는 모드를 구분하고 실패 도중 중간 파일이 남는 상황도 고려한다.
  • 중요한 설정은 새 파일을 준비하고 검증한 뒤 교체하는 방식처럼 일관된 갱신 절차가 필요할 수 있다.
  • 이 문서의 예시는 실제 파일을 바꾸지 않는 읽기·메모리 처리만 사용한다.
  • 재현을 위해 경로 기준, 인코딩, 자료 형식과 크기를 기록하고 입력 손상을 숨기는 무조건적인 오류 무시보다 실패 위치를 남기는 처리를 우선한다.
구성·관찰 대상확인하는 값구분할 문제
파일 경로기준 디렉터리·모드·권한잘못된 파일과 접근 거부
텍스트 변환인코딩·오류 위치바이트 해석 실패
형식 파서· 문법구문 오류와 단순 분리 실패
업무 자료필수 키·자료형·범위형식은 맞지만 의미는 틀림
학습 확인

이해했는지 확인하기

  • 상대 경로의 기준과 읽기 모드를 확인한다.
  • 인코딩과 구조화된 입력의 자료형을 확인한다.
  • 파일 크기와 자원 정리 방식을 검토한다.

함께 확인할 공식 자료