제품추천/IT

파워포인트 자동 보고서 만들기 feat. 파이썬

아보다 2023. 11. 7.
반응형

파워포인트 자동 보고서 만들기 를 위해서 파이썬을 활용하는 방법은 없을까?

엑셀로 데이터를 가지고 있고 정해신 양식대로 엑셀 값을 붙여넣어 PPT 슬라이드를 여러장 만들어야하는 사례가 많이 있을 것 같은데요, 복사 붙여넣기로 이루어진 단순 작업, 더 효율적으로 해치울수 있는 방법이 없을까 고민하셨던 분들을 위해  python-pptx 패키지 튜토리얼을 작성해보았습니다.

 

매년 발표하는 브랜드 가치 데이터를 활용하여 각 브랜드별 PPT 페이지를 만들어보는 예제를 소개하겠습니다.

 

파워포인트 자동 보고서 만들기 feat. 파이썬
파워포인트 자동 보고서 만들기 feat. 파이썬

 

파워포인트 자동 보고서 만들기 feat. 파이썬 (확인)


 

파워포인트 자동 보고서 만들기 feat. 파이썬
파워포인트 자동 보고서 만들기 feat. 파이썬
파워포인트 자동 보고서 만들기 feat. 파이썬
파워포인트 자동 보고서 만들기 feat. 파이썬

 

 


1. 필요 패키지 설치

  • python-pptx (pip install python-pptx)
  • pandas (pip install pandas)

 

2. 준비물

첨부된 파일을 다운받아 같은 폴더에 저장해주세요.

준비물

* 엑셀로 정리된 데이터값과 로고 이미지는 selenium 패키지를 활용하여 Interbrand 홈페이지에서 자동으로 수집하였고, 차트 이미지는 plotly 패키지를 활용하여 자동 생성하였습니다. 향후 포스트에서 웹크롤링과 차트 그리기 등의 내용도 다루어 볼 예정입니다.

 

3. PPT 템플릿, 엑셀 파일 및 그림파일 읽어오기

#.py 파일을 준비물들과 같은 폴더에 저장했다고 가정하겠습니다.
from pptx import Presentation
prs = Presentation('Best Global Brands 2022_interbrand_template.pptx')
# ppt파일을 읽어와 prs라는 변수명으로 저장합니다.

import pandas as pd
df = pd.read_excel("Best Global Brands 2022_interbrand.xlsx")
# 엑셀파일을 pandas dataframe으로 읽어옵니다.

import os
cwd = os.getcwd()
# cwd(current working directory)를 읽어옵니다.

logos = [file for file in os.listdir(f"{cwd}\logos") if os.path.isfile(f"{cwd}\logos\{file}")]
# logos 폴더 내에 있는 모든 파일명을 logos라는 변수명의 리스트로 저장합니다.

charts = [file for file in os.listdir(f"{cwd}\charts") if os.path.isfile(f"{cwd}\charts\{file}")]
# charts 폴더 내에 있는 모든 파일명을 charts라는 변수명의 리스트로 저장합니다.

check = f"{cwd}\images\check.png"
# images 폴더 내에 있는 체크모양 이미지의 경로값을 check라는 변수명으로 저장합니다.

 

4. 필요 함수 정의하기

① 원하는 text값을 가진 객체(그림, 도형, 텍스트박스, 표 등)를 읽어오는 함수

def select_shape_by_text(slide, text):
    for x in slide.shapes:
    # 선택한 슬라이드 내 모든 shape들 중에서
        if x.has_text_frame and x.text == text:
        # text_frame이 있고 해당 shape의 text값이 입력한 text값과 같으면
            return x
            # 해당 shape을 반환합니다.
    print('요청한 Shape를 찾을 수 없습니다.')

 

brand_name = select_shape_by_text(slide, 'Brand name')
brand_name.text = 'Hyundai'
prs.save('example.pptx')

위 코드를 실행하면 템플릿 내에서 "Brand name"라는 텍스트를 가진 박스가 선택되고 해당 박스의 텍스트값 Hyundai로 바뀌어 example.pptx라는 파일로 저장되는 것을 확인할 수 있습니다. 이때 기존 템플릿과 달리 폰트가 바뀌게 되는데 폰트를 유지하면서 텍스트만 바꾸는 방법은 뒤에서 소개하겠습니다.

코드 실행 결과

 

② 원하는 text값을 가진 표를 읽어오는 함수

def select_table_by_text(slide, text):
    for x in slide.shapes:
        if x.has_table and x.table.cell(0,0).text == text:
            return x.table
    print('요청한 Shape를 찾을 수 없습니다.')

비슷한 방식으로 정의한 slide 내에서 특정 text값을 가진 표를 읽어오는 함수입니다.

좌측 상단 셀의 text값이 입력한 text값과 같을 경우 해당 표를 읽어옵니다.

 

③ 선택한 PPT 파일에서 index번째 슬라이드를 복사하여 슬라이드를 만드는 함수

import copy           
def copy_slide(prs, index):
    template = prs.slides[index]
    # prs 내 slide 중 index번째 슬라이드를 선택하여 template라는 변수명으로 저장합니다.
    try:
        blank_slide_layout = prs.slide_layouts.get_by_name('빈 화면')
    except:
        blank_slide_layout = prs.slide_layouts[0]
    # 새 슬라이드를 만들때 활용할 layout을 선택합니다.
    # '빈 화면'이라는 레이아웃을 선택하되 에러가 날 경우 layout중 첫번째를 선택합니다.
    
    copied_slide = prs.slides.add_slide(blank_slide_layout)
    # '빈 화면' 레이아웃을 적용한 새 슬라이드를 만들어 copied_slide라는 변수명으로
    # 저장합니다.
    
    for shape in template.shapes:
        elem = shape.element
        new_elem = copy.deepcopy(elem)
        copied_slide.shapes._spTree.insert_element_before(new_elem, 'p:extLst')
    # template 슬라이드에서 모든 shapes의 element를 복사하여
    # copied_slide에 붙여넣습니다.
        
    return copied_slide
    
# 참고. https://stackoverflow.com/questions/50866634/how-to-copy-a-slide-with-python-pptx

 

5. 템플릿에 원하는 텍스트 정보 및 이미지 입력하기

① 텍스트박스에 원하는 정보 입력하기

copied_slide = copy_slide(prs, 0)
# 0번째 템플릿 슬라이드를 복사하여 새로운 슬라이드를 만들고
# copied_slide라는 변수명으로 저장합니다.

brand_name = select_shape_by_text(copied_slide, 'Brand name').text_frame
# 'Brand name'이라는 text를 가진 shape을 찾아서 해당 shape의 text_frame을 읽어옵니다.
p = brand_name.paragraphs[0]
# 선택된 text_frame 내 paragraph중에서 첫번째 문단을 선택하여 읽어옵니다.
run = p.runs[0]
# 선택된 문단의 첫번째 run을 선택하여 읽어옵니다.
run.text = 'Hyundai'
# 선택된 run 의 텍스트값을 원하는 값으로 변환합니다.

위 코드에서 paragraph 오브젝트는 말그대로 텍스트 프레임 내 문단을 의미하기 때문에 바로 이해가 되실텐데요, run의 개념은 생소하실 수 있을 것 같습니다. 같은 문단 내에서 폰트 등 서식이 바뀌게되는 경우 같은 서식을 갖고있는 부분까지가 같은 run에 해당합니다. 한 문단이 모두 같은 서식으로 작성되었다면 문단내 run의 갯수는 하나이며 중간에 서식이 바뀌게되면 run도 나뉘게 됩니다.

한 paragraph 내에 3개의 run

 

② 표에 원하는 정보 입력하기

table = select_table_by_text(copied_slide, 'Rank')
# 먼저 좌측 상단 셀의 텍스트가 Rank인 표를 선택합니다.

rank = table.cell(0,1).text_frame
p = rank.paragraphs[0]
run = p.runs[0]
run.text = '35'
# (0,1) 맨윗줄 왼쪽에서 두번째 셀에 원하는 값을 입력합니다.

 

③ 이미지 입력하기

from pptx.util import Cm
# 센티미터값 입력을 위한 모듈을 import합니다.

chart = [x for x in charts if 'Hyundai' in x][0]
# charts 폴더 내에 있는 모든 파일명 중에서 Hyundai라는 이름의 파일명을 읽어옵니다.

chart = f"{cwd}\charts\{chart}"
# 해당 파일의 경로를 정의합니다.

copied_slide.shapes.add_picture(chart, Cm(16.93), Cm(2.77), width=Cm(16), height=Cm(8))
# 복사된 슬라이드에 그림을 추가합니다.
# add_picture(파일경로, 가로위치_왼쪽 위 모서리 기준, 세로위치_왼쪽 위 모서리 기준, 넓이, 높이)
# 위치값을 모르겠다면 PPT파일을 열어서 그림을 원하는 위치에 맞추고 우클릭, 크기 및 위치를 눌러
# 확인할 수 있습니다.

 

④ 읽어온 엑셀파일의 모든 열에 대해서 slide 만들기

for i, r in df.iterrows():
    # pandas dataframe의 iterrows함수를 이용해 모든 row를 iterate 합니다.
    copied_slide = copy_slide(prs, 0)
    
    brand_name = select_shape_by_text(copied_slide, 'Brand name').text_frame
    p = brand_name.paragraphs[0]
    run = p.runs[0]
    run.text =  r['브랜드명']
    
    comment = select_shape_by_text(copied_slide, 'comment').text_frame
    p = comment.paragraphs[0]
    run = p.runs[0]
    run.text =  r['코멘트']
    
    
    table = select_table_by_text(copied_slide, 'Rank')
    
    rank = table.cell(0,1).text_frame
    p = rank.paragraphs[0]
    run = p.runs[0]
    run.text =  str(r['순위'])
    
    value = table.cell(1,1).text_frame
    p = value.paragraphs[0]
    run = p.runs[0]
    run.text = f"{r['브랜드가치']:,} $m"
    # 엑셀에는 브랜드가치가 정수값으로 저장되어있기 때문에 ,로 구분한 포맷으로 바꾸어줍니다.
    
    growth = table.cell(2,1).text_frame
    p = growth.paragraphs[0]
    run = p.runs[0]
    run.text = f"{r['성장률']:.0%}"
    # 엑셀에는 성장률이 소숫점으로 저장되어있기 때문에 % 포맷으로 바꾸어줍니다.

    copied_slide.shapes.add_picture(check, Cm(18.05), Cm(4.05), width=None, height=None)
    # 체크모양 이미지를 입력해줍니다.

    chart = [x for x in charts if r['브랜드명'] in x][0]
    chart = f"{cwd}\charts\{chart}"
    copied_slide.shapes.add_picture(chart, Cm(16.93), Cm(2.77), width=Cm(16), height=Cm(8))
    # 차트 이미지를 입력해줍니다.
    
    
    logo = [x for x in logos if r['브랜드명'] in x][0]
    logo = f"{cwd}\logos\{logo}"
    inserted_logo = copied_slide.shapes.add_picture(logo, Cm(2.38), Cm(4.81))
    # 로고 이미지를 입력하여 inserted_logo라는 변수명으로 저장합니다.
    # 로고 이미지별로 크기가 제각각이기 때문에 이후 크기를 조정하기 위함입니다. 
    
    
    if inserted_logo.height>Cm(2):
        inserted_logo.width = int(inserted_logo.width * (Cm(2)/inserted_logo.height))
        inserted_logo.height = Cm(2)
        # 높이가 2cm보다 크면 2cm로 줄이고 넓이도 비율에 맞게 줄여줍니다.
        
    if inserted_logo.width>Cm(6):
        inserted_logo.height = int(inserted_logo.height * (Cm(6)/inserted_logo.width))
        inserted_logo.width = Cm(6)
        # 넓이가 6cm보다 크면 6cm로 줄이고 높이도 비율에 맞게 줄여줍니다.
        
    if inserted_logo.height < Cm(2) :
        inserted_logo.top = int(Cm(4.81) + (Cm(2)-inserted_logo.height)*0.5)
        # 크기 보정 후 높이가 2cm 보다 작아지면 이미지 위치가 위로 쏠리기 때문에
        # 아래로 조금 내려줍니다.
        
    
prs.save('Best Global Brands 2022_interbrand.pptx')
# 저장해보면 100개의 슬라이드가 생성된것을 확인할 수 있습니다.

최종 결과물

6. 전체 코드 정리

앞에서 소개한 코드를 정리하며 오늘의 포스트를 마치겠습니다. 감사합니다.

from pptx import Presentation
from pptx.util import Cm
import pandas as pd
import os
import copy

prs = Presentation('Best Global Brands 2022_interbrand_template.pptx')
slide = prs.slides[0]

df = pd.read_excel("Best Global Brands 2022_interbrand.xlsx")

cwd = os.getcwd()
logos = [file for file in os.listdir(f"{cwd}\logos") if os.path.isfile(f"{cwd}\logos\{file}")]
charts = [file for file in os.listdir(f"{cwd}\charts") if os.path.isfile(f"{cwd}\charts\{file}")]
check = f"{cwd}\images\check.png"

def select_shape_by_text(slide, text):
    for x in slide.shapes:
        if x.has_text_frame and x.text == text:
            return x
    print('요청한 Shape를 찾을 수 없습니다.')
    
def select_table_by_text(slide, text):
    for x in slide.shapes:
        if x.has_table and x.table.cell(0,0).text == text:
            return x.table
    print('요청한 Shape를 찾을 수 없습니다.')
     
def copy_slide(prs, index):
    template = prs.slides[index]
    try:
        blank_slide_layout = prs.slide_layouts.get_by_name('빈 화면')
    except:
        blank_slide_layout = prs.slide_layouts[0]
    copied_slide = prs.slides.add_slide(blank_slide_layout)
    
    for shape in template.shapes:
        elem = shape.element
        new_elem = copy.deepcopy(elem)
        copied_slide.shapes._spTree.insert_element_before(new_elem, 'p:extLst')
    return copied_slide

for i, r in df.iterrows():
    copied_slide = copy_slide(prs, 0)
    
    brand_name = select_shape_by_text(copied_slide, 'Brand name').text_frame
    p = brand_name.paragraphs[0]
    run = p.runs[0]
    run.text =  r['브랜드명']
    
    comment = select_shape_by_text(copied_slide, 'comment').text_frame
    p = comment.paragraphs[0]
    run = p.runs[0]
    run.text =  r['코멘트']
    
    table = select_table_by_text(copied_slide, 'Rank')
    
    rank = table.cell(0,1).text_frame
    p = rank.paragraphs[0]
    run = p.runs[0]
    run.text =  str(r['순위'])
    
    value = table.cell(1,1).text_frame
    p = value.paragraphs[0]
    run = p.runs[0]
    run.text = f"{r['브랜드가치']:,} $m"
    
    growth = table.cell(2,1).text_frame
    p = growth.paragraphs[0]
    run = p.runs[0]
    run.text = f"{r['성장률']:.0%}"
    
    copied_slide.shapes.add_picture(check, Cm(18.05), Cm(4.05), width=None, height=None)

    chart = [x for x in charts if r['브랜드명'] in x][0]
    chart = f"{cwd}\charts\{chart}"
    copied_slide.shapes.add_picture(chart, Cm(16.93), Cm(2.77), width=Cm(16), height=Cm(8))
    
    logo = [x for x in logos if r['브랜드명'] in x][0]
    logo = f"{cwd}\logos\{logo}"
    inserted_logo = copied_slide.shapes.add_picture(logo, Cm(2.38), Cm(4.81))
    
    if inserted_logo.height>Cm(2):
        inserted_logo.width = int(inserted_logo.width * (Cm(2)/inserted_logo.height))
        inserted_logo.height = Cm(2)
        
    if inserted_logo.width>Cm(6):
        inserted_logo.height = int(inserted_logo.height * (Cm(6)/inserted_logo.width))
        inserted_logo.width = Cm(6)
        
    if inserted_logo.height < Cm(2) :
        inserted_logo.top = int(Cm(4.81) + (Cm(2)-inserted_logo.height)*0.5)

prs.save('Best Global Brands 2022_interbrand.pptx')

 

 

728x90
반응형

댓글

💲 추천 글