728x90

fastapi 엑셀 파일 불러와서 뿌려주기 #3 - Refactoring


 

"""
main.py
@제목: 메인 실행 파일
@설명: 메인 실행 파일

    작성일자        작성자
-----------------------
    2024.03.14    hiio420

"""
import math

import pandas as pd
from fastapi import FastAPI,Depends
from db.initDB import SessionLocal,get_user
from sqlalchemy.orm import sessionmaker,declarative_base,relationship,Session
# 엑셀 파일 불러오기
xlsx = pd.ExcelFile("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx")

# 각 시트 별 DataFrame 생성
df_cmStdTrm = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                            sheet_name=xlsx.sheet_names[0]).fillna("")
df_cmStdWd = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                           sheet_name=xlsx.sheet_names[1]).fillna("")
df_cmStdDmn = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                            sheet_name=xlsx.sheet_names[2]).fillna("")
df_cmStdTmplt = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                              sheet_name=xlsx.sheet_names[3]).fillna("")

# DataFrame을 Value로 하고 키가 int인 딕셔너리 생성
df_dict = {
    0: df_cmStdTrm,
    1: df_cmStdWd,
    2: df_cmStdDmn,
    3: df_cmStdTmplt
}

app = FastAPI()


@app.get("/")
def read_root(data_id: int = 0, page: int = 1, limit: int = 10,db: Session = Depends(get_db)):
    if page < 1:
        page = 0
    if limit < 10:
        limit = 10
    df = df_dict[data_id]
    first_idx = (page - 1) * limit
    last_idx = first_idx + limit - 1
    total_size = df.shape[0]
    first_page = 1
    last_page = math.ceil(total_size / limit)
    df_sliced = df[first_idx:last_idx + 1].copy()

    df_sliced["번호"] = [i for i in range(total_size - (page - 1) * limit, total_size - (page - 1) * limit - df_sliced.shape[0], -1)]
    data = []

    if df_sliced.shape[0] != 0:
        data = df_sliced.to_dict("records")
  
    
    return {"sheetName": xlsx.sheet_names[data_id], "data_id": data_id, "page": page, "limit": limit,
            "first_page": first_page, "last_page": last_page, "firstIdx": first_idx, "lastIdx": last_idx,
            "totalSize": total_size,
            "data": data}


if __name__ == "__main__":
    print(xlsx.sheet_names)
    print([(key, value.size) for key, value in df_dict.items()])

 

현재 까지 코드는 위와 같이 되어 있다 . 파일을 불러와서 쿼리스트링에 맞춰서 제공하는데 기능은 잘 작동하지만 코드가 너무 지저분한 느낌

 

파일을 읽어와서 딕셔너리로 변환하는 부분까지 우선 class 로 만들어서 사용하면 좋겠다는 생각을 했다.

 

1. 디렉토리 만들기


서버에서 사용하는 유틸성 있는 class나 함수들을 libs 디렉토리 안에 utils 디렉토리를 만들어서 관리하면 어떨까 한다.

 

pycharm에서 Python Package 만들기로 만들어 주자

 

 

 

 

2. FileUtils Class 만들기


먼저 excel 파일을 만들기 전에 파일을 읽어와서 파일에 대한 데이터를 가지고 있는 공통 class를 만들면 어떨까 싶어서

FileUtils Class를 만들어 보려고 한다.

 

1. 파일을 읽고

2. 파일 정보를 멤버 변수에 할당합니다.

3. class를 dict 와 json으로 변환 할 수 있도록 합니다.

4. 파일 사이즈를 Byte,KB,MB,GB 등로 변환합니다.

 

파일 정보는 

1. 파일이름

2. 파일 경로

3. 파일 확장자

4. 파일 사이즈

5. 파일 사이즈 + 용량단위

6. 파일 마지막 사용 날짜

7. 파일 생성 날짜

8. 파일 마지막 수정 날짜

입니다.

 

"""
FileUtils.py
@제목: 파일 정보 파일
@설명: 파일 정보를 읽어오는 Class 파일

    작성일자        작성자
-----------------------
    2024.03.15    hiio420

"""
import os
import pathlib
from datetime import datetime
import json
from typing import Any


class FileUtils:

    def __init__(self):
        self.filename: str | None = None
        self.path: str | None = None
        self.ext: str | None = None
        self.size: int | None = None
        self.size_unit: str | None = None
        self.access_dt: datetime | None = None
        self.created_dt: datetime | None = None
        self.modified_dt: datetime | None = None

    def read_file(self, path):
        self.path = path
        self.size = os.path.getsize(path)
        self.size_unit = self.calc_size_unit(self.size)
        self.access_dt = datetime.fromtimestamp(os.path.getatime(path))
        self.created_dt = datetime.fromtimestamp(os.path.getctime(path))
        self.modified_dt = datetime.fromtimestamp(os.path.getmtime(path))
        self.ext = pathlib.Path(path).suffix
        self.filename = pathlib.Path(path).stem

    def to_json(self) -> Any:
        return json.dumps(self.__dict__, default=str, indent=4)

    def to_dict(self) -> dict[str,Any]:
        return self.__dict__

    @staticmethod
    def calc_size_unit(size) -> str:
        byte_units = ["Bytes", "KB", "MB", "GB", "TB"]
        size_unit = ""
        for unit in byte_units:
            if size < 1024.0:
                size_unit = f"{round(size, 2)} {unit}"
                break
            else:
                size /= 1024.0
        return size_unit


if __name__ == '__main__':
    fileUtil = FileUtils()
    fileUtil.read_file('F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx')
    print(fileUtil.to_dict())

 

 

3. ExcelUtils class 만들기


이제 위에서 만든 FileUtils를 상속 받는 ExcelUtils class를 생성해 줍니다.

 

ExcelUtils는 파일 정보와 함께 엑셀 파일에 대한 간단한 정보를 멤버 변수에 저장합니다.

 

1. 엑셀파일 객체

2. 엑셀 시트 리스트 객체

3. 엑셀 시트 이름 리스트

4. DataFrame dictionary

 

"""
ExcelUtils.py
@제목: 엑셀 정보 파일
@설명: 엑셀 정보 Class 파일

    작성일자        작성자
-----------------------
    2024.03.15    hiio420

"""
from typing import List

import pandas as pd
from openpyxl.worksheet._read_only import ReadOnlyWorksheet
from pandas import ExcelFile, DataFrame

from libs.utils.FileUtils import FileUtils


class ExcelUtils(FileUtils):
    def __init__(self):
        super().__init__()
        self.xlsx_file: ExcelFile | None = None
        self.sheets: List[ReadOnlyWorksheet] | None = None
        self.sheet_names: List[str] | None = None
        self.df_dict: dict[int, DataFrame] | None = None

    def read_excel(self, file_path) -> dict[int, DataFrame]:
        self.read_file(file_path)
        self.xlsx_file = ExcelFile(file_path)
        self.sheets = self.xlsx_file.book.worksheets
        self.sheet_names = [sheet.title for sheet in self.sheets if sheet.sheet_state == "visible"]
        self.df_dict = {i: pd.read_excel(self.path, sheet_name=self.sheet_names[i]).fillna("") for i, n in
                        enumerate(self.sheet_names)}

        return self.df_dict


if __name__ == '__main__':
    excelUtils = ExcelUtils()
    excelUtils.read_excel('F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx')
    print(excelUtils.to_dict())

 

 

이전과 달라진 점이 있다면 숨긴 처리된 시트이름도 모두 가져왔으나, sheet_state를 통해서 보이도록 설정된 시트들 이름만 가져와 dictionay를 만든다는 점입니다.

 

4. main.py 적용


이제 기본에 있던 코드를 삭제하고 새로 만든 ExcelUtils class로 대체합니다.

"""
main.py
@제목: 메인 실행 파일
@설명: 메인 실행 파일

    작성일자        작성자
-----------------------
    2024.03.14    hiio420

"""
import math

from fastapi import FastAPI

from libs import ExcelUtils

# 엑셀 파일 불러오기

file_path = "F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx"
excel_utils = ExcelUtils()
df_dict = excel_utils.read_excel(file_path)

app = FastAPI()


@app.get("/")
def read_root(data_id: int = 0, page: int = 1, limit: int = 10):
    if page < 1:
        page = 0
    if limit < 10:
        limit = 10
    df = df_dict[data_id]
    first_idx = (page - 1) * limit
    last_idx = first_idx + limit - 1
    total_size = df.shape[0]
    first_page = 1
    last_page = math.ceil(total_size / limit)
    df_sliced = df[first_idx:last_idx + 1].copy()

    df_sliced["번호"] = [i for i in range(total_size - (page - 1) * limit, total_size - (page - 1) * limit - df_sliced.shape[0], -1)]
    data = []

    if df_sliced.shape[0] != 0:
        data = df_sliced.to_dict("records")


    return {"sheetName": excel_utils.sheet_names[data_id], "data_id": data_id, "page": page, "limit": limit,
            "first_page": first_page, "last_page": last_page, "firstIdx": first_idx, "lastIdx": last_idx,
            "totalSize": total_size,
            "data": data}


728x90
728x90

fastapi 엑셀 파일 불러와서 뿌려주기 #2


이전 포스팅에서 파일을 불러와 sheetNames와 data 전체를 응답하는 것을 만들었습니다.

이제 페이징을 할 수 있게끔 page 번호와 표시할 목록 개수를 요청해서 응답 받는 것을 해보겠습니다.

 

 

1. Pagination


페이징 처리를 하기위해서 기본적으로 요청 받는 파라미터는 페이지 번호와 출력 갯수입니다.

 

저는 페이지 번호를 page 출력 갯수를 limit으로 해서 쿼리스트링을 포함시켜 주겠습니다.

@app.get("/")
def read_root(id: int = 0,page:int = 1, limit:int = 10):
    data = df_dict[id].to_dict("records")
    return {"sheetName": xlsx.sheet_names[id], "data": data}

page의 기본값은 1 limit은 10입니다.

 

두 값을 통해서 DataFrame을 slicing 해야합니다.

그러기 위해서 첫 index값과 마지막 index값을 정해야합니다.

 

@app.get("/")
def read_root(data_id: int = 0, page: int = 1, limit: int = 10):
    if page < 1:
        page = 0
    if limit < 10:
        limit = 10
    df = df_dict[data_id]
    first_idx = (page - 1) * limit
    last_idx = first_idx + limit - 1
    total_size = df.shape[0]
    first_page = 1
    last_page = math.ceil(total_size / limit)
    df_sliced = df[first_idx:last_idx + 1].copy()

    df_sliced["번호"] = [i for i in range(total_size - (page - 1) * limit, total_size - (page - 1) * limit - df_sliced.shape[0], -1)]
    data = df_sliced.to_dict("records")
    return {"sheetName": xlsx.sheet_names[data_id], "data_id": data_id, "page": page, "limit": limit,
            "first_page": first_page, "last_page": last_page, "firstIdx": first_idx, "lastIdx": last_idx,
            "totalSize": total_size,
            "data": data}

 

기본값 미만인 값이 들어올경우 기본값으로 바꾸는 조건문을 작성해 줍니다.

 

첫 index 값은 (page-1) * limit 

마지막 index값은 첫index + limit -1 입니다.

새로운 번호를 매기기 위해서 total size를 구해줍니다.

 

id 라는 변수는 python에 이미 지정된 함수가 있기때문에 되도록 사용하지 않도록 하기 위해 data_id 라는 값으로 변경했습니다.

 

response 시에 data,sheetName 이외에도 전달받은 값과 함께 first_page,last_page,first_idx,last_idx,total_size를 추가해 주었습니다.

 

 

2. Swagger로 데이터 요청 응답


 

FastAPI Swagger를 이용한 API 문서를 제공합니다.

 

 

3. Pycharm Http request 기능


728x90
728x90

엑셀 파일 불러와서 뿌려주기 #1


파일로 저장되어 있는 공공표준용어 데이터를 읽어와서 url로 접속시 해당 내용을 뿌려주는 작업을 진행해보자!!

 

먼저 , 파일을 불러오기 위한 방법으로는 pandas를 사용한다.

진행 순서는 아래와 같다.

 

1. pandas ExcelFile 로 엑셀 파일을 불러와서 sheet 이름들을 가져온다.

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.ExcelFile.html

 

pandas.ExcelFile — pandas 2.2.1 documentation

A file-like object, xlrd workbook or openpyxl workbook. If a string or path object, expected to be a path to a .xls, .xlsx, .xlsb, .xlsm, .odf, .ods, or .odt file.

pandas.pydata.org

2. 각 시트 별로 read_excel로 데이터 프레임을 만들어 준다.

3. 데이터를 정렬 시키고 새 index를 부여한다.

4. 각 데이터프레임을 api query string에 맞춰 return 시킨다.

5. 최소 출력 row 수는 10개이다.

 

 

1. 파일 불러오기


 

pandas에서 엑셀 파일을 불러오기 위해서는 openpyxl 패키지가 필요합니다.

 

requirements.txt에 추가하고 설치합니다.

 

openpyxl==3.1.2

 

프로젝트 디렉토리에 upload 디렉토리를 만들고 파일을 넣어줍니다.

 

이제 기존에 있던 소스 코드를 지우고 새로 작성해 보겠습니다.

 

pandas를 import 하고 ExcelFile의 인자값으로 파일이 위치한 경로를 절대경로로 넣어줍니다.

 

import pandas as pd

xlsx = pd.ExcelFile("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx")

 

이제 ExcelFile에서는 sheet_names를 통해서 이 엑셀 파일의 sheet들의 이름 정보를 리스트로 가져올 수 있습니다.

if __name__ == "__main__": 을 통해서 print 찍어 보겠습니다.

 

import pandas as pd

xlsx = pd.ExcelFile("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx")



if __name__ == "__main__":
    print(xlsx.sheet_names)
    

 

 

실제 파일과 비교해 보겠습니다.

실제 파일에서 보이는 sheet는 총 3개이지만 sheet_names로 표시되는 element 수는 4개입니다.

엑셀 파일에서 숨기기 취소를 보면 '공통표준도메인 초안 1' 숨기기 처리 된것을 볼 수 있습니다.

 

 

이제 이 파일에 들어있는 시트별로 데이터프레임을 만들어 주고

딕셔너리를 만들어 줍니다.

"""
main.py
@제목: 메인 실행 파일
@설명: 메인 실행 파일

    작성일자        작성자
-----------------------
    2024.03.14    hiio420

"""

import pandas as pd

# 엑셀 파일 불러오기
xlsx = pd.ExcelFile("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx")

# 각 시트 별 DataFrame 생성
df_cmStdTrm = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",sheet_name=xlsx.sheet_names[0])
df_cmStdWd = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",sheet_name=xlsx.sheet_names[1])
df_cmStdDmn = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",sheet_name=xlsx.sheet_names[2])
df_cmStdTmplt = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",sheet_name=xlsx.sheet_names[3])


# DataFrame을 Value로 하고 키가 int인 딕셔너리 생성
df_dict = {
    0:df_cmStdTrm,
    1:df_cmStdWd,
    2:df_cmStdDmn,
    3:df_cmStdTmplt
}



if __name__ == "__main__":

    print(xlsx.sheet_names)
    print([(key,value.size) for key,value in df_dict.items()])

 

 

 

2. Get Routing


이제 df_dict에 있는 데이터를 Routing 하는 함수를 만들어 줍니다.

@app.get("/")
def read_root(id: int = 0):
    data = df_dict[id].to_dict("records")
    return {"sheetName": xlsx.sheet_names[id], "data": data}

아직 API에 대한 정확한 정보를 작성하지는 않았습니다.

 

쿼리 스트링으로 id 값을 받고 기본값으로는 0을 입력합니다.

 

응답으로는 data와 함께 sheet 명을 추가적으로 전달합니다.

 

서버를 실행하고 http://127.0.0.1:4882/ 접속해보면

 

정상적으로 출력 되는 것을 볼 수 있습니다.

 

그럼 query String 을 url에 추가로 입력에 id 값을 1로 하여 보내봅니다.

 

 

서버 에러가 나버렸습니다.

서버 로그를 보면

ValueError: Out of range float values are not JSON compliant 에러가 발생했습니다.

Nan이나 null 값이 Dataframe에 있기때문에 결측치 값 처리를 해줘야 할거 같습니다.

 

Pandas fillna를 이용해 결측치를 모두 빈문자열로 만들어 줍니다.

https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.fillna.html

 

pandas.DataFrame.fillna — pandas 2.2.1 documentation

If method is specified, this is the maximum number of consecutive NaN values to forward/backward fill. In other words, if there is a gap with more than this number of consecutive NaNs, it will only be partially filled. If method is not specified, this is t

pandas.pydata.org

# 각 시트 별 DataFrame 생성
df_cmStdTrm = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                            sheet_name=xlsx.sheet_names[0]).fillna("")
df_cmStdWd = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                           sheet_name=xlsx.sheet_names[1]).fillna("")
df_cmStdDmn = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                            sheet_name=xlsx.sheet_names[2]).fillna("")
df_cmStdTmplt = pd.read_excel("F:\\[01]project\\commonStandardTerm\\upload\\(붙임)공공데이터 공통표준용어(2022.7월).xlsx",
                              sheet_name=xlsx.sheet_names[3]).fillna("")

 

이후 다시 서버를 reload 하고 

 

접속해 봅니다.

 

이제 정상적으로 출력 되는 것을 볼 수 있습니다.

 

 

728x90

+ Recent posts