본문 바로가기
Programming/Python

Hex File Viewer 연습 - 001

by The Programmer 2025. 10. 28.

1. Introduction

 

중급 및 고급 예제입니다. Hex File Viewer를 만들어 볼까요? ^.^;

 

원리 이해를 목표로 가장 단순한 16진수 보기 앱을 만들어 보겠습니다.

 

정체 불명의 어마어마하게 큰 파일이 하나 있는데 이 파일의 형식이 어떤 형식의 파일인지 알 수 없다면? 일반적인 문서편집기 앱으로 열려면 꽤 곤란합니다. 보통은 파일의 앞 부분 일부만 읽어보아도 그 파일의 성격(?)을 대략 파악할 수 있는 경우도 꽤 흔합니다. 그래서 파일의 앞부분 일부분만 빠르게 읽어서 보여주는 앱이 필요합니다.

 

보통,  단순하게 구분할 경우, 파일은 텍스트 파일과 바이너리 파일(=이진 파일)로 구분하는데, 일반적인 문서 파일은 텍스트 파일, mp3 음악 파일이나 영화 동영상 파일과 같은 mov, mkv, mp4, wmv 같은 이름을 가진 파일들은 바이너리 파일로 분류됩니다.

 

일단 잘 작동하는 앱 코드부터 살펴보겠습니다.

 

 

2. Code

 

"""
Program Name : QuickHexView_Ex_001_.py
Version : 1.0.0. (rev. 032)
Date : 2025. 10. 28.
Copyright(C) 2025. James. All rights reserved.

도움말 문서는 QuickView_Ex_001_Doc_.txt 파일을 참고할 것.

""" 


myFile = r"D:\S5_MagPi_128_.rar"
# Raspberry Pi 에서 공식 발간하는 매거진임. 
# 파일 내용물이 뭔지 모른다고 가정하고.
# 이 파일의 앞 부분만 빠르게 로딩해서 보여주는 프로그램.
# 16진수로 보여주기.

 

print("\n", "- " * 72, "")    # 줄 바꿈, 구분용.

 

with open(myFile, "rb") as f:
    chunkBytes = f.read(1024 * 1024)  # 1MB
    # print(chunkBytes[:1024])  # 앞 200바이트만 출력
    print(chunkBytes[:200].hex())
    print("\n", "- " * 72, "")
    for hexCH in chunkBytes[:200].hex():
        print(hexCH, " ", end="")
    print()
    # 줄 바꿈, 구분용.

 

 

print("\n", "- " * 72, "")

 

 

with open(myFile, "rb") as f:
    chunk = f.read(64)
    hex_str = chunk.hex()
    # 2자리씩 끊어서 출력
    print(" ".join(hex_str[i:i+2] for i in range(0, len(hex_str), 2)))

 

 

print("\n", "- " * 72, "")

 

 

def hexdump(filename, length=256, width=16):
    with open(filename, "rb") as f:
        offset = 0
        while offset < length:
            chunk = f.read(width)
            if not chunk:
                break
            # HEX 부분 (대문자 16진수)
            hex_part = " ".join(f"{b:02X}" for b in chunk)     
# X는 대문자임. 주의.
            # ASCII 부분 (출력 가능한 문자만, 나머지는 '.')
            ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
            # 주소 + HEX + ASCII 출력
            print(f"{offset:08X}  {hex_part:<{width*3}}  {ascii_part}")
            offset += len(chunk)

 

 

# 위 함수 사용법 예시
hexdump(myFile, length=256, width=16)

 

 

print("\n", "- " * 72, "")

 

 

3. Result

대략 생김새는 다음과 같습니다. 최종 보기와 다른 몇 가지 서로 다른 방식으로 먼저 보여줍니다. 이들 모두가 코드에 포함되어 있습니다. 코드 학습용입니다.

 

[ QuickHexView v1.0.0. rev. 032 ]

 

 

4. Notes

 

원래는 딱 필요한 주석이나 해설만 포함해야 하지만, 이런 저런 노트 대신 제가 볼 용도로 학습하면서 참고로 정리한 내용들 모두를 두서없이 그냥 함께 올려둡니다. 정리는 나중에. ^.^;

 

QuickHexView_v1_0_0_rev_032_Hex_File_Viewer
: 파일 앞 부분만 빠르게 읽어서 16진수(Hex)로 보여주기

====================================================
리눅스/유닉스 계열 유틸리티로 처리할 경우 :: 
====================================================

1) head : 텍스트 파일의 앞부분을 빠르게 확인 :
예) head -n 50 filename.txt    // 앞 50줄 출력.

2) xxd 또는 hexdump : 바이너리 파일의 앞부분을 16진수/ASCII로 확인.

3) less : 파일 전체를 열지 않고 스트리밍 방식으로 탐색 가능


====================================================
Windows의 경우 ::
====================================================

1) 파워쉘, 

# 비교적 작은 파일
Get-Content -TotalCount 50 filename.txt    // 앞 50줄만 읽기, 텍스트 파일의 경우임.

# 주의 : 
PowerShell의 Get-Content는 기본적으로 파일 전체를 메모리에 로드하려는 성향이 있어서, 
대용량 파일에서는 OutOfMemory 오류가 쉽게 납니다. 그래서 단순히 -TotalCount 옵션을 붙여도
내부적으로는 전체를 스캔하려고 해서 문제가 생길 수 있어요.

# 약간 큰 파일이어서 파워쉘에서 메모리 부족 오류가 날 경우의 처리 :
$reader = [System.IO.StreamReader]::new("C:\path\bigfile.txt")
for ($i=0; $i -lt 100; $i++) {
    if ($reader.EndOfStream) { break }
    $line = $reader.ReadLine()
    Write-Output $line
}
$reader.Close()
// 이렇게 하면 앞 100줄만 읽고 종료하므로 메모리 부담이 거의 없습니다.

# 다른 방법으로, 파워쉘에서 Get-Content에 -ReadCount 옵션 활용
Get-Content "C:\path\bigfile.txt" -TotalCount 100 -ReadCount 1
// -ReadCount 1을 주면 한 줄씩 스트리밍 방식으로 읽습니다

// 텍스트가 아니라 바이너리 파일이라면 Get-Content는 적합하지 않습니다. 
// 대신, 파워쉘 코드:
$fs = [System.IO.File]::OpenRead("C:\path\bigfile.bin")

$buffer = New-Object byte[] 1024  # 앞 1KB만
$fs.Read($buffer, 0, $buffer.Length) | Out-Null
$fs.Close()
[System.BitConverter]::ToString($buffer)
// 앞부분을 16진수로 확인할 수 있습니다.

파워쉘에서의 방법 정리 : 
- PowerShell 기본 Get-Content는 대용량 파일에 비효율적 -->
 StreamReader나 FileStream을 직접 쓰는 게 안전합니다.
- 텍스트라면 ReadLine(), 바이너리라면 Read(byte[]) 방식으로 앞부분만 확인하세요.

====================================================

2) fc /b 같은 명령어로 바이너리 비교 가능. 또는, 

====================================================

3) Notepad++ 같은 에디터는 대용량 파일 일부만 로딩해 보여줌

====================================================

4) 파이썬 프로그램으로 처리하는 경우, 이하 전부 파이썬 예제.

# 16진수 표기법 기초 연습:
data = b"\x41\x42\x43\x00\x44"
print(data)        # b'ABC\x00D'
print(list(data))  # [65, 66, 67, 0, 68]

====================================================

# 순수한 16진수 값만 보고자 할 때
with open("bigfile.bin", "rb") as f:
    chunk = f.read(64)  # 앞 64바이트만 읽기
    print(chunk.hex())  # 16진수 문자열 출력

# 출력 결과 예시 : 000102ff41424300444546...

====================================================

# 사람이 보기 좋게 포맷 정리해서 보여주기

with open("bigfile.bin", "rb") as f:
    chunk = f.read(64)
    hex_str = chunk.hex()
    # 2자리씩 끊어서 출력
    print(" ".join(hex_str[i:i+2] for i in range(0, len(hex_str), 2)))

# 출력 결과 예시 : 00 01 02 ff 41 42 43 00 44 45 46 ...

====================================================

# 파이썬 hexdump 스타일 코드

def hexdump(filename, length=256, width=16):
    with open(filename, "rb") as f:
        offset = 0
        while offset < length:
            chunk = f.read(width)
            if not chunk:
                break
            # HEX 부분 (2자리씩 공백 구분)
            hex_part = " ".join(f"{b:02x}" for b in chunk)
            # ASCII 부분 (출력 가능한 문자만, 나머지는 '.')
            ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
            # 주소 + HEX + ASCII 출력
            print(f"{offset:08x}  {hex_part:<{width*3}}  {ascii_part}")
            offset += len(chunk)

# 사용 예시
hexdump("bigfile.bin", length=256, width=16)

# 출력 결과 예시 :
00000000  48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00 ff 10 20  Hello World!... 
00000010  41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50  ABCDEFGHIJKLMNOP

- 왼쪽: 파일 오프셋(주소)
- 가운데: 16진수 값
- 오른쪽: ASCII 문자 (출력 불가능한 값은 . 처리)

length 값을 조절하면 파일 앞부분을 원하는 만큼만 확인할 수 있고, 
width를 바꾸면 한 줄에 표시할 바이트 수를 조절할 수 있습니다.

- f.read(64) --> 64바이트 읽기
- f.read(1024) --> 1KB 읽기
- f.read(1024*1024) --> 1MB 읽기


# Notes 1. for b in chunk 부분

- chunk는 bytes 객체입니다.
- bytes는 반복(iterable) 가능하므로, for b in chunk를 하면
각 바이트 값(0~255 정수)이 하나씩 b에 들어옵니다.

chunk = b"\x41\x42\x43"
for b in chunk:
    print(b)

# 출력 결과 예시 :
65
66
67

# (즉, 'A', 'B', 'C'의 아스키 코드 값)


Notes 2. f"{b:02x}" 부분 

- f-string 포맷팅 구문입니다.
- :02x의 의미:
- x --> 정수를 16진수 소문자로 변환
- 02 --> 최소 2자리, 빈 자리는 0으로 채움

# 예 : 
b = 5
print(f"{b:02x}")   # "05"
b = 255
print(f"{b:02x}")   # "ff"

 

# 소문자 대신 대문자로 포맷팅하려면 :

정답 : 소문자(ff) 대신 대문자(FF)로 출력하려면 포맷 문자열에서 x 대신 X를 쓰면 됩니다.


Notes 3. " ".join(...) 부분, 주의 사항.

 

- 위 본문 코드에서는 join() 함수가 2회 등장하는데 서로 구분해야 합니다.

 

# 파이썬 쉘창에서 이어붙이기 기초 연습. ^.^;

 

>>> myList = [ "A", "B", "C", "D", "E", "F" ]

 

# 데시(-) 문자로 이어붙이기

>>> print("-".join(myList))
A-B-C-D-E-F

# 빈 칸 한 칸으로 이어붙이기

>>> print(" ".join(myList))
A B C D E F

 

# 직접 이어붙이기. '없음Empty' 이어붙이기.

>>> print("".join(myList))
ABCDEF

 

- join() 함수 앞에 붙은 큰 따옴표 부분에 주의해야 합니다.

- " ".join([...])은 리스트의 문자열 요소들을 공백으로 연결합니다.
- 즉, 각 바이트를 2자리 16진수 문자열로 바꾼 뒤, " "로 이어붙이는 것.

 

비교.

1) " ".join([...])은 '공백 문자(=Space 문자)'으로 리스트 내용을 이어붙인다. --> 리스트 내용 문자들 사이사이에 공백 문자가 추가되어 이어짐. 공백 문자도 하나의 정식 문자임. A B C ....

2) "".join([...])은 '없음(=Empty)'으로 리스트 내용을 이어붙인다. --> 리스트 내용 문자들 사이사이에 아무 문자도 없이 문자들이 이어짐. Empty는 문자가 아님. 텅 빈 그 어떤 것, '아무 것도 없음'이라는 개념만 존재함. 따라서 리스트 내용 문자들을 이을 때 문자들이 직접 이어짐. ABC...

# 예:
chunk = b"\x41\x42\x43"
hex_part = " ".join(f"{b:02x}" for b in chunk)
print(hex_part)

# 결과 :
41 42 43

 

# 헥사 파트, 이 부분에 주의 해야 함. 

hex_part = " ".join(f"{b:02x}" for b in chunk)

위, " ".join(...) 부분, 주의.

 

- 큰따옴표 안에 " " --> 공백 한 칸이 들어 있습니다.

- 의미: 리스트의 요소들을 공백으로 구분해서 이어붙인다는 뜻입니다.

- 예시:

- " ".join(["41", "42", "43"])

- 결과 : 41 42 43

- 41 42 43

 - --> 각 바이트가 "41", "42", "43"으로 변환된 뒤, 그 사이에 공백 하나가 들어갑니다.

즉, HEX 부분은 '41 42 43'처럼 바이트마다 띄어쓰기가 들어가도록 만든 겁니다.

 

# 아스키 파트 조인 부분도 주의해야 함.

"".join(chr(b) if 32 <= b < 127 else "." for b in chunk)

- 여기서는 큰따옴표 안이 '""' --> 아무 문자도 없음.  
- 의미: 리스트의 요소들을 구분자 없이 그대로 이어붙인다는 뜻입니다.  
- 예시:

"".join(["A", "B", "C"])
출력:

ABC

 

- 그래서 ASCII 부분은 'ABC...'처럼 연속된 문자열로 붙습니다.  

 

Notes 정리 :


hex_part = " ".join(f"{b:02x}" for b in chunk)

 

--> chunk 안의 각 바이트를 2자리 16진수 문자열로 변환하고,
그것들을 공백으로 구분해 이어붙인 문자열을 만든다.

 

====================================================

 

ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)

 

1) for b in chunk

- chunk는 bytes 객체이므로, 반복하면 각 바이트 값(0~255 정수)이 b에 들어옵니다.
- 예: chunk = b"\x41\x42\x10" --> 반복 시 65, 66, 16

 

2) chr(b)

- 정수 b를 유니코드 문자로 변환합니다.

- 예: chr(65) → 'A', chr(66) → 'B'

 

3) if 32 <= b < 127 else "."

- ASCII 코드에서 32~126 범위는 사람이 읽을 수 있는 문자(스페이스, 알파벳, 숫자, 기호 등)입니다.

- 126은 포함, 127은 제외.

- 그 외 값(예: 제어문자, NULL, 0xFF 등)은 화면에 출력하면 깨지거나 보이지 않으므로 "."으로 대체합니다.

- 즉, 출력 가능한 문자는 그대로, 나머지는 점(.)으로 표시하는 규칙입니다.

 

4) "".join(...)

- 위 조건으로 변환된 문자들을 하나의 문자열로 이어붙임.

 

- 예:

chunk = b"\x41\x42\x10\x43"
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
print(ascii_part)

 

- 결과: 

AB.C

 

정리

이 구문은 바이너리 덤프의 오른쪽 ASCII 영역을 만드는 부분입니다.

- 사람이 읽을 수 있는 문자는 그대로 보여주고

- 읽을 수 없는 값은 .으로 표시해서

- \x10 은 아스키 코드표 참고할 것. 아스키 코드 32번 아래 코드들은 읽기 어려움. ^.^; 왜?

- xxd나 hexdump처럼 직관적인 출력이 가능해집니다.

 

 

====================================================

print(f"{offset:08x}  {hex_part:<{width*3}}  {ascii_part}")

 

1) f"..."

- f-string: 문자열 안에서 중괄호 {}로 변수를 직접 넣고, 뒤에 포맷 옵션을 붙일 수 있습니다.

 

2) {offset:08x}

- offset은 현재 파일에서의 위치(주소, 바이트 단위).

- 08x의 의미:

-예: offset = 32 --> "00000020"

즉, 왼쪽 열에 8자리 16진수 주소를 찍는 부분입니다.

 

3) {hex_part:<{width*3}}

- hex_part는 "41 42 43" 같은 16진수 문자열.

- :<{width*3}의 의미:

- < --> 왼쪽 정렬
- {width*3} --> 출력 칸의 폭을 width*3으로 지정
- width는 한 줄에 출력할 바이트 수 (예: 16)
- 바이트마다 "HH " (2자리 + 공백 = 3칸) 차지하므로 width*3

- 이렇게 하면 HEX 부분이 항상 일정한 폭을 차지해서, 오른쪽 ASCII 열이 줄마다 깔끔하게 정렬됩니다.

 

 

4) {ascii_part}

- 사람이 읽을 수 있는 ASCII 문자열.

- 출력 가능한 문자는 그대로, 나머지는 .으로 표시.

- 예: "Hello..."

 

5) 최종 출력 예시:

 

00000000  48 65 6c 6c 6f 20 57 6f 72 6c 64 21 00 ff 10 20  Hello World!... 
00000010  41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50  ABCDEFGHIJKLMNOP

 

- 왼쪽: 주소 (8자리 16진수)
- 가운데: HEX 값 (폭 고정, 정렬됨)
- 오른쪽: ASCII 문자

 

 

 

====================================================

# 응용 연습 : 위 코드를 응용하여 대문자 Hex 코드로 보여주기
# 전용 함수를 하나 만들고 이 함수를 사용하는 방식으로.

def hexdump(filename, length=256, width=16):
    with open(filename, "rb") as f:
        offset = 0
        while offset < length:
            chunk = f.read(width)
            if not chunk:
                break
            # HEX 부분 (대문자 16진수)
            hex_part = " ".join(f"{b:02X}" for b in chunk)
            # ASCII 부분 (출력 가능한 문자만, 나머지는 '.')
            ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
            # 주소 + HEX + ASCII 출력
            print(f"{offset:08X}  {hex_part:<{width*3}}  {ascii_part}")
            offset += len(chunk)

# 사용 예시
hexdump("bigfile.bin", length=256, width=16)

# 결과 예시 :
00000000  48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 FF 10 20  Hello World!... 
00000010  41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50  ABCDEFGHIJKLMNOP

====================================================

# 이하, 기타 사항 정리.

 

# 텍스트 파일일 때와 바이너리 파일일 때를 구분해서 처리하는 것이 좋다.

# Text File, 앞 100줄만 읽기 예제입니다.
with open("bigfile.txt", "r", encoding="utf-8", errors="ignore") as f:
    for i in range(100):
        line = f.readline()
        if not line:
            break
        print(line.strip())

====================================================

# Binary File, 앞 부분 1MB만 읽기 예제.
with open("bigfile.bin", "rb") as f:
    chunk = f.read(1024 * 1024)  # 1MB
    print(chunk[:200])  # 앞 200바이트만 출력

====================================================

# 메모리 매핑 (mmap) 활용
# 매우 파일을 다룰 때는 mmap을 쓰면
# OS가 필요한 부분만 메모리에 매핑해준다.
import mmap

with open("bigfile.txt", "r+b") as f:
    mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
    print(mm[:1024])  # 앞 1KB만 출력
    mm.close()

====================================================
전체 정리 코드 : QuickHexView_Ex_001_.py
====================================================

전체 코드는 위 본문 코드와 같습니다.

하단 첨부 파일을 참고하시거나.

 


■ 응용 연습 과제

1) Hex View와 Hex Editor는 천지 차이.. 가장 어떤 기능이 필요할까요?

2) 평범한 보통 텍스트 에디터 만들기부터 많은 연습이 필요합니다.

 

 

5. Files

 

 

QuickHexView_Ex_001_.py
0.00MB

 

 

6. Ref.

1) 강환수, 신용현. [ 파이썬으로 배우는 누구나 코딩 ] 홍릉.

2) 우재남. [ 파이썬 ] 한빛.

3) 김영탁. [ 파이썬 ] 홍릉.

4) 박상현. [ 파이썬 3 ] 한빛미디어.

5) Mark Lutz. [ Learning Python ] 5th. Ed. O'Reilly.

6) Mark Lutz. [ Programming Python ] 4th. Ed. O'Reilly.

 

 

Happy Programming!

^.^;

 

 

 

'Programming > Python' 카테고리의 다른 글

SR 만들기 - 002  (0) 2025.11.15
Python Intro. - 001  (0) 2025.11.02
신생 언어 Mojo 소개 - 001  (0) 2025.10.06
딕셔너리 연습 - 1  (0) 2025.09.28
파이썬 계산기 - 001 - Version 1.0.2 Update  (0) 2025.09.15