들어가며
AI교육 3주차에는 PyPDF2를 활용하여 pdf에 있는 텍스트를 읽어오는 방법과 easyocr로 이미지에 있는 텍스트를 읽어오는 방법을 배웠다.
이에 배운 내용과 과제를 하면서 추가적으로 배운 것들을 정리해보려고 한다.
PyPDF2를 활용하여 PDF의 text를 가져와 화면에 출력하는 것과, easyocr를 활용하여 image에 있는 text만 가져와 화면에 출력하는 실습을 해봤다.

우선 위와 같은 화면을 다음 코드를 활용하여 구현했다.
with pdf_col1:
st.subheader("📤 PDF 파일 업로드")
# PDF 파일 업로더
pdf_file = st.file_uploader(
"PDF 파일을 선택하세요",
type=['pdf'],
key="pdf_uploader"
)
if pdf_file is not None:
st.success(f"✅ 파일 선택됨: {pdf_file.name}")
st.info(f"파일 크기: {pdf_file.size / 1024:.2f} KB")
# 파싱 버튼
if st.button("📋 PDF 파싱 시작", key="parse_pdf_btn", use_container_width=True):
with st.spinner("PDF 파싱 중..."):
# FastAPI로 파일 전송
files = {
"file": (pdf_file.name, pdf_file.getvalue(), "application/pdf")
}
########################################
pdf_parse_url = FASTAPI_URL + "/parse-pdf"
resp = requests.post(pdf_parse_url, files = files)
st.session_state.pdf_result = resp.json()
########################################
file_uploader 함수는 사용자가 파일을 업로드할 수 있는 UI 컴포넌트를만들어주는 Streamlit 함수이다.
넣을 문구, 파일 타입, Key를 인자로 받는데, 이때 Key는 Streamlit이 UI컴포넌트를 구분하기 위해 붙이는 고유 ID이다.
이러한 file_uploadere함수는 UploadFile 객체를 만들어준다.
이 객체를 통해, 파일명과 파일크기, MIME 타입, 파일전체 bytes 등을 알 수 있다.
나는 객체에서 "파일명"과 "파일내용(bytes)"만 꺼내서 files (multipart방식)로 만든 후, PyPDF2 처리를 해 줄 fastAPI 서버로 넘겼다.
@app.post("/parse-pdf")
async def parse_pdf(file: UploadFile = File(...)):
try:
# 파일 확장자 검증
if not file.filename.endswith('.pdf'):
raise HTTPException(status_code=400, detail="PDF 파일만 업로드 가능합니다.")
# PDF 파일 읽기
contents = await file.read()
pdf_file = io.BytesIO(contents)
# PyPDF2로 텍스트 추출
pdf_reader = PyPDF2.PdfReader(pdf_file)
extracted_text = ""
page_texts = []
total_pages = len(pdf_reader.pages)
for page_num, page in enumerate(pdf_reader.pages):
page_text = page.extract_text()
page_texts.append({
"page_number": page_num + 1,
"text": page_text
})
extracted_text += f"\n--- 페이지 {page_num + 1} ---\n"
extracted_text += page_text
return {
"success": True,
"filename": file.filename,
"total_pages": total_pages,
"extracted_text": extracted_text,
"pages": page_texts,
"text_length": len(extracted_text)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"PDF 파싱 중 오류 발생: {str(e)}")
서버에서는 이를 UploadFile 객체로 받아서, file.read()를 통해 파일 스트림을 끝까지 읽고, 순수한 bytes로 가져온다.
이때, io.BytesIO(contents)를 해주는 이유는, contents는 단순히 bytes이기에 PyPDF2가 바로 읽을 수 없고, 이를 읽을 수 있는 형태인 "파일"처럼 다룰 수 있게 해주는 객체(BytesIO)이다.
PyPDF2는 PDF 파일을 읽고, 일부 수정하고, 분리/병합할 수 있는 파이썬 라이브러리이다.
이 라이브러리를 통해, pdf 텍스트를 추출한다던가, 페이지 수를 확인하거나 메타데이터 등을 읽을 수 있다.
또한,
PyPDF2는 파일 경로 또는 파일 객체를 받을 수 있다. 따라서, 위에도 언급했듯이, bytes가 아닌 BytesIO객체로 바꾼 파일 객체를 집어넣어야한다.
파일 경로를 받아서 처리하는 방식은
with open(pdf_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
위와 같은 코드로 표현할 수 있다. 이때 pdf_path는 실제 파일이 저장된 경로를 지정해줘야한다.
다만, 이 방식을 하려면, 해당 파일이 서버 디스크에 먼저 저장되어있어야하고, 그 경로를 읽어와야하는 것이다보니 디스크에 저장해야하는 과정이 추가되어야한다.
# PyPDF2로 텍스트 추출
pdf_reader = PyPDF2.PdfReader(pdf_file)
extracted_text = ""
page_texts = []
total_pages = len(pdf_reader.pages)
for page_num, page in enumerate(pdf_reader.pages):
page_text = page.extract_text()
page_texts.append({
"page_number": page_num + 1,
"text": page_text
})
extracted_text += f"\n--- 페이지 {page_num + 1} ---\n"
extracted_text += page_text
return {
"success": True,
"filename": file.filename,
"total_pages": total_pages,
"extracted_text": extracted_text,
"pages": page_texts,
"text_length": len(extracted_text)
}
여기서 PdfReader는 "PDF를 읽어서 구조를 파싱하는 객체"이다.
즉, PDF 파일 전체를 읽고, 페이지 구조와 객체 트리, 글자 정보 등을 파싱하며, pdf_reader.pages라는 페이지 목록을 생성해준다.
따라서 이를 for 반복문으로 돌면서 각각의 page에서 extract_text()를 통해 텍스트 객체들을 읽어서 문자열로 반환한다.
이때 텍스트 추출은 "페이지 단위"로만 가능하다 !

즉 위와 같은 과정으로 변환이 이루어진다고 할 수 있다.
이렇게 추출된 페이지 개수와 페이지 내용들을 다시 클라이언트로 return

# 세션 스테이트에 저장된 결과 표시
if 'pdf_result' in st.session_state:
result = st.session_state.pdf_result
# 정보 표시
col_a, col_b, col_c = st.columns(3)
with col_a:
st.metric("총 페이지 수", result['total_pages'])
with col_b:
st.metric("추출된 문자 수", result['text_length'])
with col_c:
st.metric("파일명", result['filename'][:20] + "..." if len(result['filename']) > 20 else result['filename'])
st.markdown("---")
# 탭으로 전체 텍스트와 페이지별 보기 구분
tab1, tab2 = st.tabs(["📄 전체 텍스트", "📑 페이지별 보기"])
with tab1:
# 전체 텍스트 표시
st.text_area(
"추출된 전체 텍스트",
value=result['extracted_text'],
height=400,
key="pdf_full_text"
)
with tab2:
########################################
### 필수과제 1-(3): 페이지별 텍스트 표시
pages = result["pages"]
for page_num, page in enumerate(pages):
with st.expander(f"{page_num + 1} 페이지"):
st.text_area(
"추출된 텍스트",
value=page,
height=400,
key="pdf_page_text"
)
else:
st.info("👈 왼쪽에서 PDF 파일을 업로드하고 파싱을 시작하세요.")
텍스트를 위 코드로 페이지별, 전체 텍스트 구역에 출력 해줬다.

이번엔 이미지를 업로드하여, 이미지 내의 텍스트를 추출할 수 있는 실습을 했다.
위 화면은 다음과 같은 코드로 구현했다.
with ocr_col1:
st.subheader("📤 이미지 파일 업로드")
# 이미지 파일 업로더
image_file = st.file_uploader(
"이미지 파일을 선택하세요",
type=['jpg', 'jpeg', 'png', 'bmp', 'gif', 'webp'],
key="image_uploader"
)
if image_file is not None:
# 이미지 미리보기
image = Image.open(image_file)
st.image(image, caption=f"업로드된 이미지: {image_file.name}", use_container_width=True)
st.success(f"✅ 파일 선택됨: {image_file.name}")
st.info(f"파일 크기: {image_file.size / 1024:.2f} KB")
# OCR 버튼
if st.button("🔍 이미지 OCR 시작", key="ocr_image_btn", use_container_width=True):
with st.spinner("이미지 OCR 처리 중... (처음 실행 시 모델 다운로드로 시간이 걸릴 수 있습니다)"):
# 파일 포인터를 처음으로 되돌리기
image_file.seek(0)
# FastAPI로 파일 전송
files = {
"file": (image_file.name, image_file.getvalue(), f"image/{image_file.type}")
}
########################################
img_parse_url = FASTAPI_URL + "/ocr-image"
resp = requests.post(img_parse_url, files = files)
st.session_state.ocr_result = resp.json()
이미지 파일도 마찬가지로 file_uploader를 통해 파일을 받았고, 이 name과 파일 내용을 포함한 files를 만들어 서버로 넘겨주었다.
@app.post("/ocr-image")
async def ocr_image(file: UploadFile = File(...)):
try:
# 파일 확장자 검증
allowed_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']
if not any(file.filename.lower().endswith(ext) for ext in allowed_extensions):
raise HTTPException(
status_code=400,
detail="이미지 파일만 업로드 가능합니다. (jpg, jpeg, png, bmp, gif, webp)"
)
# 이미지 파일 읽기
contents = await file.read()
image = Image.open(io.BytesIO(contents))
# numpy array로 변환
img_array = np.array(image)
########################################
먼저 file.read로 클라이언트에서 받아온 이미지 파일의 내용을 bytes로 읽어온다.
다음으로, Image.open(io.BytesIO(contenst)) 로 이미지 객체를 만들어주는데, Image.open은 "파일 경로" 또는 "파일처럼 동작하는 객체"를 인자로 기대하기에, BytesIO를 통해 파일 객체로 만들어준다.
이렇게 만들어진 image는 이미지 크기, 색상 모드, 픽셀 접근 등이 가능해진다.
np.array(image)를 통해 NumPy 배열로 바꾸는 이유는,
EasyOCR은 내부적으로 OpenCV, PyTorch, NumPy 기반으로 동작한다.
즉, 픽셀 데이터를 숫자 행렬 형태로 받는 걸 전제로 설계되어있기에 이미지를 변환해주는 작업이 필요하다.

즉 위 그림과 같은 과정을 통해, EasyOCR이 받을 수 있는 형태로 변환되고, EasyOCR로 텍스트 추출이 일어난다.
reader = easyocr.Reader(["ko", "en"])
extracted_data = reader.readtext(img_array, detail=1, paragraph=False)
result_simple = [item[1] for item in extracted_data]
# 모든 텍스트를 하나로 합치기
full_text = " ".join(result_simple)
detailed_results = []
for bbox, text, conf in extracted_data:
bbox_py = [[int(x), int(y)] for x, y in bbox]
detailed_results.append({
"bbox": bbox_py,
"text": str(text),
"confidence": float(conf)
})
return {
"success": True,
"filename": file.filename,
"extracted_text": full_text,
"detailed_results": detailed_results,
"total_detections": len(detailed_results)
}
Reader는 OCR 엔진 + 언어 모델을 로딩한 객체로, Reader에게 NumPy 이미지 배열을 주면 readText() 가 글자영역,텍스트,신뢰도를 추출해준다.
즉, OCR 모델(딥러닝) + 언어 설정 + 전처리/후처리 로직을 모두 포함한 OCR엔진 객체라고 할 수 있다.
Reader(["ko", "en"])는 한국어와 영어 OCR을 할 수있는 상태가 된 엔진을 가져오는 것이다.
그리고 이 엔진을 통해 readText로 이미지를 읽는다.
이때, readText는
detail = 1을 주게 되면 bbox, 신뢰도 등을 포함한 상세결과를 받을 수 있는데, 이때 반환되는 형태는 리스트 형태로
[
[bbox, text, confidence],
[bbox, text, confidence],
...
]
위와 같은 형태를 띈다
** 반대로 detail=0인 경우 text만 출력된다.
만약 text를 추려서 출력하려는 경우에는 해당 추출된 데이터 (extracted_data)리스트의 각 요소 (작은 리스트)에서 text만 뽑아내야하는데, 이때 사용한 코드가
result_simple = [item[1] for item in extracted_data]
위 코드이다.
리스트 내에 리스트 형태로 존재하고 있는 결과값이라, 타고타고 들어가서.. 추출해야한다.
마지막으로
detailed_results = []
for bbox, text, conf in extracted_data:
bbox_py = [[int(x), int(y)] for x, y in bbox]
detailed_results.append({
"bbox": bbox_py,
"text": str(text),
"confidence": float(conf)
})
나는 return 전에 bbox(글자 좌표)와 conf (신뢰도) 를 int,float 형태로 변환해주는 과정을 거쳤는데,
이 이유는 easyocr과정에서 반환된 bbox와 conf 값은, numpy.int32, numpy.float32 와 같은 Numpy객체라 json 변환이 안되어, 반환하는 과정에서 에러가 발생하게된다.
따라서 json 변환 가능한 형태로 가공하기 위해 추가해주었다.

이미지에서 텍스트 추출하여 출력한 결과 이미지이다 ~
오늘은 비교적 어려운 부분은 없었지만, 위에 실습에서 사용한 것말고도 여러가지 방법이 있었는데 그것도 실습 외에 따로 연습해보며 익히는 게 필요할 듯하다..!!
재밌고 신기한 기술이 많ㄷ ㅏ ...........