인프런 커뮤니티 질문&답변

yezi9733님의 프로필 이미지
yezi9733

작성한 질문수

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 (장고 4.2 기준)

엑셀 파일 생성 중에 cannot be used in worksheets 에러 발생

작성

·

229

·

수정됨

0

안녕하세요.

강의와는 무관한 질문이지만 본 강의 수강 완료 후 혼자서 프로젝트를 하고 있습니다.

현재 구글 리뷰 크롤링 & 스크랩중인데요. 해당 에러가 발생하는 이유를 도무지 찾을 수 가 없어서 질문 드립니다...

여러 사이트를 크롤링 해보고 엑셀을 생성 해 보았지만 왜 이런 에러가 발생하는지 로그를 봐도 제대로 표시가 안되니깐 찾기가 힘드네요.

구글 리뷰 사이트만 20여개 스크랩 했었고 엑셀도 제대로 생성 되었으니 스크랩 코드 자체에는 문제가 없는거 같습니다. 다만 이 부분에서만 문제가 생깁니다.



### 에러 발생 로그
[2024-06-12 09:42:59,954] [ERROR utils.py:179] >>

Traceback (most recent call last):

File "scraper\scrap_crawlers.py", line 1365, in get_review_details

File "scraper\utils.py", line 202, in create_xlsx_file

File "scraper\utils.py", line 181, in create_xlsx_file

File "pandas\util\_decorators.py", line 333, in wrapper

File "pandas\core\generic.py", line 2417, in to_excel

File "pandas\io\formats\excel.py", line 952, in write

File "pandas\io\excel\_openpyxl.py", line 490, in writecells

File "openpyxl\cell\cell.py", line 218, in value

File "openpyxl\cell\cell.py", line 197, in bindvalue

File "openpyxl\cell\cell.py", line 165, in check_string

openpyxl.utils.exceptions.IllegalCharacterError: 동생한테추천받았는데이렇게편한어플이있다니너무좋아요.현금비율은좋지않지만 신경많이안써도되서괜찮네요~ cannot be used in worksheets.

During handling of the above exception, another exception occurred:
###

cannot be used in worksheets. 이놈이 말썽이네요...

아래와 같이 테스트 케이스 만들어서 적용했을 때는 제대로 작동했었습니다.

import asyncio

from scraper.utils import create_xlsx_file, save_to_xlsx


DEFAULT_NAME = "test"


async def main():
    data = {
        "message": "동생한테추천받았는데이렇게편한어플이있다니너무좋아요.현금비율은좋지않지만 신경많이안써도되서괜찮네요~"
    }

    xlsx_file = await create_xlsx_file(
        data, file_name=DEFAULT_NAME, sheet_name=DEFAULT_NAME
    )
    await save_to_xlsx(xlsx_file, DEFAULT_NAME)


asyncio.run(main())




# utils.py

# 엑셀 가로 폭 조정하는 함수
async def calculate_dimension(worksheet: Worksheet) -> None:
    try:
        for column_cells in worksheet.iter_cols():
            length = max(len(str(cell.value)) for cell in column_cells)
            adjusted_width = (length + 2) * 1.2  # 조정된 폭 계산
            column_letter = get_column_letter(column_cells[0].column)
            worksheet.column_dimensions[column_letter].width = adjusted_width

    except Exception as e:
        message = f"엑셀 폭 조정 중에 예외 발생: '\n{e}"
        logger = await get_logger()
        logger.error(message)
        print(message)
        raise e


# 엑셀에 서식 스타일 지정하는 함수
async def cell_pattern_fill(
    df: pd.DataFrame,
    worksheet: Worksheet,
    head_fill_color: str = "4472C4",
    head_font_color: str = "FFFFFF",
    body_fill_color: str = "D9E1F2",
    body_font_color: str = "000000",
    head_border_color: str = "2E5C99",
    body_border_color: str = "B4C6E7",
    fill_type: fills = "solid",
) -> None:
    try:
        # Define border styles
        thin_border_head = Border(
            left=Side(border_style="thin", color=head_border_color),
            right=Side(border_style="thin", color=head_border_color),
            top=Side(border_style="thin", color=head_border_color),
            bottom=Side(border_style="thin", color=head_border_color),
        )
        thin_border_body = Border(
            left=Side(border_style="thin", color=body_border_color),
            right=Side(border_style="thin", color=body_border_color),
            top=Side(border_style="thin", color=body_border_color),
            bottom=Side(border_style="thin", color=body_border_color),
        )

        # Set header row style
        for row in worksheet.iter_rows(
            min_row=1, max_row=1, min_col=1, max_col=df.shape[1]
        ):
            for cell in row:
                cell.fill = PatternFill(
                    start_color=head_fill_color,
                    end_color=head_fill_color,
                    fill_type=fill_type,
                )
                cell.font = Font(color=head_font_color, bold=True)
                cell.border = thin_border_head

        # Set body row style
        for i, row in enumerate(
            worksheet.iter_rows(
                min_row=2, max_row=worksheet.max_row, min_col=1, max_col=df.shape[1]
            )
        ):
            for cell in row:
                if i % 2 == 0:
                    cell.fill = PatternFill(
                        start_color=body_fill_color,
                        end_color=body_fill_color,
                        fill_type=fill_type,
                    )
                    cell.font = Font(color=body_font_color)
                    cell.border = thin_border_body

    except Exception as e:
        message = f"엑셀 서식 지정 중에 예외 발생: '\n{e}"
        logger = await get_logger()
        logger.error(message)
        print(message)
        raise e


# 본 강의 drf 엑셀 생성 파트를 참고해서 만든 엑셀 생성 함수
async def create_xlsx_file(
    data: Union[Dict, List],
    file_name: str = DEFAULT_DIR_NAME,
    sheet_name: str = DEFAULT_DIR_NAME,
) -> BytesIO:
    df = pd.json_normalize(data)

    io = BytesIO()
    io.name = file_name
    try:
        writer = pd.ExcelWriter(io, engine="openpyxl")  # noqa
        df.to_excel(
            writer,
            index=False,
            engine="openpyxl",
            sheet_name=sheet_name,
        )
        workbook = writer.book
        worksheet = workbook.active

        tasks = [
            calculate_dimension(worksheet),
            cell_pattern_fill(df, worksheet),
        ]
        await tqdm.gather(*tasks, desc=f" 엑셀 파일 생성중")
        writer._save()  # noqa

    except Exception as e:
        message = f"엑셀 생성 중에 예외 발생: '\n{e}"
        logger = await get_logger()
        logger.error(message)
        print(message)
        raise e

    io.seek(0)
    return io


# 엑셀 저장 함수
async def save_to_xlsx(
    xlsx_file: BytesIO,
    dirname: str = DEFAULT_DIR_NAME,
):
    output_path = BASE_DIR / "스크랩_결과" / "엑셀" / dirname
    output_path.mkdir(parents=True, exist_ok=True)

    now = datetime.datetime.now()
    timestamp = now.strftime("%Y-%m-%d_%H_%M")

    filename = f"{xlsx_file.name}_{timestamp}"
    extension = ".xlsx"

    file_path = output_path / (filename + extension)

    try:
        async with aiofiles.open(file_path, "wb") as f:
            await f.write(xlsx_file.getvalue())

    except Exception as e:
        message = f"엑셀 파일 저장 중에 예외 발생: '{filename}'\n{e}"
        logger = await get_logger()
        logger.error(message)
        print(message)
        raise e


전체적인 함수는 위와 같으며 엑셀 생성 중에 에러가 발생하였으니 create_xlsx_file 함수 부분에서 해결을 해보아야 할것 같습니다.

아니면 혹시 엑셀의 행을 생성 중에 에러가 발생하였을 때 해당 행은 스킵하고 이어서 진행하게 하는 방법이 있을까요?? "raise e"을 발생 시키지 않아도 엑셀 생성 작업 스킵이 되지않고 작업 자체에 문제가 생기네요



답변 1

0

이진석님의 프로필 이미지
이진석
지식공유자

안녕하세요.

async 처리 상의 이슈가 아닐까 추측이 됩니다. 일단 async를 제거하시고 동기적으로 먼저 구현하신 후에, async 적용을 검토해보시면 어떨까요?

yezi9733님의 프로필 이미지
yezi9733
질문자

ILLEGAL_CHAR_PATTERN = r"[\000-\031]"  # 제어 문자 정규식

def clean_data(data):
    if isinstance(data, str):
        return re.sub(ILLEGAL_CHAR_PATTERN, "", data)
    elif isinstance(data, dict):
        return {key: clean_data(value) for key, value in data.items()}
    elif isinstance(data, list):
        return [clean_data(item) for item in data]
    else:
        return data

async def create_xlsx_file(
    data: Union[Dict, List],
    file_name: str = DEFAULT_DIR_NAME,
    sheet_name: str = DEFAULT_DIR_NAME,
) -> BytesIO:
    # 데이터에서 불법 문자를 제거
    cleaned_data = clean_data(data)

    df = pd.json_normalize(cleaned_data)

    # 데이터프레임에서 불법 문자를 제거
    df = df.replace(ILLEGAL_CHAR_PATTERN, "", regex=True)
    ...

image
제어 문자가 문제인가 싶어서 제거해봤는데 이 경우 제대로 작동합니다.
그런데 해당 텍스트에서는 그럴만한 요소가 없었는데 왜인지 찝찝하네요...

yezi9733님의 프로필 이미지
yezi9733

작성한 질문수

질문하기