본문 바로가기

코딩

파이썬으로 PPT 노가다 자동화 하기

 

Best Global Brands 2022_interbrand.xlsx
0.04MB
Best Global Brands 2022_interbrand_template.pptx
15.66MB
images.zip
6.54MB

대부분의 HR 시스템에서 인사 프로필 출력기능을 제공하지만, 인사담당자로 일하다 보면 직원들의 프로필을 PPT로 직접 만들어야 할 때가 자주 있습니다. 승진 심사, 해외 주재원 선발, 임원 평가 등 다양한 목적을 위해 인사 프로필을 작성하게 되는데 기본적으로 시스템을 통해 제공되는 양식에는 필요로 하는 정보가 빠져있는 경우가 많기 때문에 어쩔 수 없이 담당자는 노가다의 늪에 빠지게 됩니다.

 

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

 

실제 인사 데이터를 활용할 수는 없기 때문에 인사 프로필 만들기 대신 인터브랜드에서 매년 발표하는 브랜드 가치 데이터를 활용하여 각 브랜드별 PPT 페이지를 만들어보는 예제를 소개하겠습니다.

 

활용할 데이터. Best Global Brands 2022
템플릿
결과물 예시. 꼭 내가 팔면 오르는 현대차


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')

 

 

추가. 연도별 브랜드가치 차트를 ppt에서 직접 그리는 방법은 아래 게시글을 참고해주세요.

https://jgws.tistory.com/8

 

파이썬으로 예쁜 PPT 차트 그리기

본 포스트에서는 Python-pptx 패키지를 활용하여 차트를 그리고 스타일을 적용하는 방법에 대해 소개하겠습니다. python으로 차트를 그리는 방법을 다루는 블로그들은 많지만 디자인 변경까지 알려

jgws.tistory.com