블로그 이미지
자유로운설탕

calendar

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Notice

2017. 4. 7. 23:30 프로그래밍

  이번 시간은 Windows GUI 자동화를 살펴보는 시간이다. 만들어 보려는 프로그램은 1) 메모장을 열어서, 2) 작은 python 소스를 입력 후, 3) 콤보 박스에서 인코딩과, 파일 형식을 선택하고 특정 폴더에 저장을 하는 프로그램이다. 이후 이전 시간에 잠시 언급했던 상용 GUI 자동화 툴인 unified functional testing 으로 구현된 코드와 비교해 보며, 오픈소스 모듈과 상용 모듈의 차이점에 대해서 얘기하려 한다.

 

 

[목차]

0. 왜 파이썬 공부에 구글을 이용하는게 좋은가?

1. 언어를 바라보는 방법. 파이썬을 어떻게 바라봐야 할까?

2. 파이썬 설치와 환경, 버전 선택 하기의 이유.

3. 만들고자 하는 기능을 모르는 조각으로 나눠 조사해 보기

4. 데이터 베이스에서 내용 가져와 출력하기

5. 암호화 모듈을 이용해 암복호화 해보기

6. 퍼즐 조각들을 합쳐보기

7. 엑셀 파일 사용해 보기 -> 부록 : fuction 을 이용해서, 코드 정리해 보기

8. 정규표현식을 왜 사용해야 할까? 언어속의 미니 언어 정규표현식 살펴보기

9. 입력과 결과를 GUI 화면과 연결해 보기

10. Whois API 이용해 보기

11. 웹페이지 호출해 내용 파싱 하기(BeautifulSoup 그리고 한계)

12. 자동화 - 웹 자동화(with Selenium)

13. 자동화 - 윈도우즈 GUI 자동화(with pywinauto)

14. 자동화 - 작업 자동화

15. 수학 라이브러리 살펴보기

16. 그래픽 라이브러리 살펴보기

17. 머신러닝에서의 파이썬의 역활

18. 웹 프로그래밍 - Legacy Web

19. 웹 프로그래밍 - Flask 살펴보기(feat. d3.js)

20. 웹 프로그래밍 - Django 살펴보기

21. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  굳이 파이썬 자동화 공부를 하면서 상용 자동화 툴을 언급하는 이유는, 첫째는 파이썬 언어 또는 연관된 selenium,  beaulifulsoup 같은 모듈들을 공부하는 이유가 효율적인 프로그래밍적 사고 및 구현 방식을 찾기 위한 것이라고 생각하기 때문이다. 그런 측면에서 향후 더 효율적인 언어나 개선된 모듈들이 나왔을 때, 현재 언어의 선입견에 갇혀서 고정된 관점에서 새로운 것들을 바라보는 것보다, 현재 사용하고 있는 언어의 장단점에 대해 객관적인 생각을 가지고 있는게 맞을 것 같기 때문이다. 둘째로는 뒤에서 보면 알겠지만, 상용툴은 사람들이 제품을 구입 하도록 어필할 수 있는 부분들이 있어야 하기 때문에, 오픈소스의 경우 수동으로 구축을 해야되는 설계, 구조적 측면 이나, 초창기 버전 들에서 흔히 간과되는 사용성 및 유지보수에 관련된 기능들이 제품 안에 기본으로 포함되어 있는 경우가 많다. 하지만 역으로 그러한 사용자를 돕는 기능들이 툴을 사용하는 프로세스를 고정시켜 버려서, 사용 범위의 제한을 가져오는 독이 될 수도 있다.

 

 

  앞의 selenuim 이나 beautifulsoup 을 보면 해당 모듈이 웹 페이지를 인식 할때, 태그(element)나 속성(attribute), css selector, xpath(이전 시간의 예제에는 사용을 안했지만, selenium은 xpath 를 지원한다. beautifulsoup 은 지원 안 하는 거 같지만, 최근 크롤링에서 많이 사용이 된다고 하는 scrapy 란 모듈도 xpath 를 지원한다). xpath 에 대해선 잘은 모르지만, xml 도큐먼트의 요소들을 정의할 때 사용된다고 하며, 왠지 정규 표현식 같은 스타일이여서, 문법에 익숙해지면 꽤 효율적일 것 같다. xpath 에 대한 상세 내용은 아래 링크를 참고한다.

  https://www.w3schools.com/xml/xpath_syntax.asp

 

 

  비슷한 맥락에서 gui 자동화 모듈은 원하는 개체(윈도우, 메뉴, 버튼, 리스트 박스 등)를 선택하고 조작하기 위해서, 위의 web 자동화의 element, attiribute 와 비슷한 기준 요소가 필요하게 되며, 그런 부분이 class 라든지 text 라든지, 좌표(position) 라든지 하는 속성들이다. 밑의 visual studio 로 gui 프로그램을 작성하는 화면을 보면, 폼 안에 위치한 버튼을 선택 했을때, 오른쪽 properies 창에서 해당되는 버튼 내의 text 등 버튼을 정의하는 많은 속성(property)들을 볼 수 있다. 모든 윈도우즈 gui 프로그램은 이 속성들을 기준으로 개체를 식별하고 메시지들을 교환한다. 이 부분이 바로 오늘 진행하는 내용의 핵심이다.

 

  그럼 앞에서 web 자동화를 구현하는데는 관련된 웹 기술들을 잘 알아야 유리하다고 얘기했듯이, gui 자동화의 구현에는 windows(또는 x-windows 든지 osx 든지) 및 그 환경에서 돌아가는 gui 프로그램 들의 구조에 대해서 잘 아는 것이 유리하다(하지만 잘 알게 되는게 쉬운일은 아니다 --; 이 부분은 저도 초보이다). 

 

 

 

 

 

[GUI 코드 구현]

  그럼 본격적으로 코드 구현으로 들어가 원하는 기능을 만들기 위해 필요한 부분을 생각해 보자.

1) 어떤 자동화 모듈을 사용해야 되는지 결정해야 한다.

2) 메모장을 띄울 수 있어야 한다.

3) 메뉴장의 메뉴를 선택하거나, 글을 입력할 수 있어야 한다.

4) 저장 다이얼 로그에서, 경로를 지정하고, 인코딩 콤보 박스와, 확장자 콤보 박스에서 원하는 항목을 선택하고, 파일 이름을 넣은 후, 저장 버튼을 누르는 작업을 할 수 있어야 한다.

 

 

 

[메모장 실행과 메뉴 선택]

  우선 메모장을 조작할 수 있는 적절한 모듈을 찾기 위해서 구글에 'windows gui automation python notepad' 라고 검색한다.

  https://pywinauto.github.io/

 

  위의 'pywinauto' 라는 모듈의 홈페이지를 보면 원하는 코드가 다 있는 건 아니지만, 기본적으로 메모장을 실행하고, '도움말 > 메모장 정보' 메뉴를 선택해서 창을 띄운 후 닫고, 키를 입력하는 코드가 들어있다. 근데 소스 내용을 보니 영문 윈도우 기준 코드인거 같아서, 한글 윈도우에선 잘 돌아가려는지 확신이 안 든다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
from pywinauto.application import Application
 
# Run a target application
app = Application().start("notepad.exe")
 
# Select a menu item
app.UntitledNotepad.menu_select("Help->About Notepad")
 
# Click on a button
app.AboutNotepad.OK.click()
 
# Type a text string
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

 

  뭐 일단 좀더 정보를 수집하기 위해서 홈페이지 오른쪽에 있는 'Documentation' 링크를 눌러 보자.

  https://pywinauto.readthedocs.io/en/latest/index.html

 

  비교적 도움말이 잘되어 있다, 천천히 살펴보고 싶을 경우는, 맨 위의 'What is pywinauto' 부터 'Methods available to each different control type' 까지의 5개 정도의 설명들을 살펴 보면 될것 같다. 마지막 'Methods available to each different control type' 부분은 menu 나 textbox 등의 GUI 컨트롤 들을 어떻게 다룰수 있는지 설명하는 전반적인 레퍼런스 이다.

 

 

  도움말 첫 페이지의 내용을 보다보면 눈에 띄는 내용이 하나 있는데, 여러가지 오픈소스와 무료 및 상용 자동화 툴을 소개한 리스트가 있다. 몇 가지를 살펴 봤는데, 먼저 python 에서 쓸수 있는 winguiauto,  pyautogui 라는 툴은 메뉴얼이 상세하지 않은거 같아서 제외했고, 파이썬 모듈은 아니지만 독립적으로 돌아가는 무료 어플리케이션인 Autoit 은 윈도우즈 gui 자동화에서 유명하지만, 2015년 9월이후 더 이상 업데이트가 안되고 있어, win 7과 win 10의 지원이 명시되어 있지 않다(혹시 돌리면 돌아가는 지는 잘 모르겠지만 말이다...). 또 활발하게 버전업 되고 있는 다른 무료툴인 autohotkey 는 gui 컨트롤의 자동화 보다는 키보드와 마우스 동작 중심의 macro 프로그램(상용 프로그램으로 따지면 macro express 정도의 포지션)에 좀 더 가까운것 같다. 또 상용 gui 자동화 툴인 Winrunner 는 나중에 언급할 unified functional testing 의 과거 이름이다. silktest 는 예전엔 무척 독특하게 좋다고 생각 했었지만, 그 독특함 때문에 범용적인 winrunner 에게 시장을 많이 뺐긴 후, 오픈소스 와의 통합으로 방향을 틀었었는데 지금은 어찌 되고 있는지 잘 모르겠다. 웹이나 GUI 자동화 자체에 관심있는 분들은 해당 링크의 툴들을 찬찬히 살펴보면 괜찮을 듯 싶다.

 

 

  그럼 우선 도움말에 명시된 pip 명령어를 이용해 pywinauto 모듈을 설치해보자. 아래와 같이 명령어를 입력하면 잘 설치가 된다.

c:\Python\code>pip install pywinauto
Collecting pywinauto
  Running setup.py install for pywinauto ... done
Successfully installed pywinauto-0.6.2

 

 

  그럼 위의 샘플 소스 내용(좀 많이 위가 됬다)을 긁어서, c:\python\code 폴더에 utf-8 인코딩으로 notepad1.py 라고 저장 후, 실행해 보자(실행 부분을 잘 모르겠으면 2교시에서 복습을...)

c:\Python\code>python notepad1.py
Traceback (most recent call last):
  File "notepad1.py", line 7, in <module>
    app.UntitledNotepad.menu_select("Help->About Notepad")

.......
pywinauto.findbestmatch.MatchError: Could not find 'Help' in 'dict_keys(['서식(&O)', '보기(&V)', '도움말(&H)', '편집(&E)', '파일(&F)'])'

 

  그런데 메모장이 실행되긴 하지만, 위의 에러가 난다. 에러가 난 부분을 살펴보면 'Help->About Notepad' 메뉴를 선택하면서 에러가 난 것 이다. 위에서 보면 'menu_select' 를 실행하다 에러가 났고, 메뉴를 찾으려 하는데 'Help' 란 메뉴는 없고 자신이 알고 있는 상단 메뉴들은 (['서식(&O)', '보기(&V)', '도움말(&H)', '편집(&E)', '파일(&F)'] 밖에 없다고 한다. 대충 유추해 보면 'Help' 라는 부분을 pywinauto 가 인지하고 있는 '도움말(&H)' 로 바꿈 될거 같다. 그럼 소스를 수정해 보자(장황하겠지만 우선은 step by step 으로 진행한다--;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->About Notepad")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.AboutNotepad.OK.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

 

  저장 후 다시 실행해 본다.

c:\Python\code>python notepad1.py
Traceback (most recent call last):
  File "notepad1.py", line 8, in <module>
    app.UntitledNotepad.menu_select("도움말(&H)->About Notepad")
...

pywinauto.findbestmatch.MatchError: Could not find 'About Notepad' in 'dict_keys(['', '도움말 보기(&H)', '메모장 정보(&A)'])'

 

 

  위를 보면 앞에 난 에러와 비슷하게 'About Notepad' 부분에서 에러가 발생했다. 그럼 한번 해본거니 비슷하게 대응되는 한글 부분으로 수정한다. 이후 저장 후 다시 실행 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->메모장 정보(&A)")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.AboutNotepad.OK.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

c:\Python\code>python notepad1.py
Traceback (most recent call last):
...
pywinauto.findbestmatch.MatchError: Could not find 'OK' in 'dict_keys(['', 'Edit'])'

 

  그런데 또 에러가 난다. 이번엔 버튼을 클릭하는 부분이다(근데 슬슬 익숙해 지지 않는가?). 이번엔 힌트도 잘 표시되지 않지만, 이 경우는 실제 실행된 메모장 화면을 참고하면 된다.

 

  위의 화면을 보면 위쪽 타이틀엔 '메모장 정보' 가 아래쪽 버튼엔 '확인' 이라는 텍스트 들어가 있다. 여기 까지 오게 되면 메뉴얼을 찬찬하게 보진 않았지만, 조금은 pywinauto 가 gui 개체를 인식하는 방식을 알수 있을 것도 같다. 메뉴는 메뉴의 이름으로(단축키 기호 포함) 접근하고, 창은 타이틀로, 버튼은 버튼에 쓰여 있는 텍스트로 접근하나 보다. 최종으로 샘플 파일을 수정해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# '도움말' > '메모장 정보' 메뉴를 선택한다.
app.UntitledNotepad.menu_select("도움말(&H)->메모장 정보(&A)")
 
# '확인' 버튼을 눌러서 다이얼로그를 닫는다.
app.메모장_정보.확인.click()
 
# 메모장에 내용을 적는다.
app.UntitledNotepad.Edit.type_keys("pywinauto Works!", with_spaces = True)
cs

c:\Python\code>python notepad1.py

 

  실행하면 정상적으로 메모장이 실행되서, '메모장 정보' 창이 열렸다가 닫히고, 메모장에는 pywinauto Work! 라는 글자가 입력이 된다.

 

 

 

[파일 저장하기]

  그럼 이제 utf8 방식으로 인코딩을 선택해, c:\python\code 폴더에 samplecode.py 란 이름으로  저장하는 코드를 구현해 보자. 먼저 메모장에서 저장 메뉴를 선택했을 때 뜨는 '다른 이름으로 저장' 다이얼로그를 한번 살펴 보면서 고민을 해보자.

 

  위의 그림 상에서 원하는 대로 파일을 저장하려면 4가지 부분을 구현해야 하는데, 1) c:\python\code 로 폴더를 선택해야 하며, 2) 파일이름 텍스트 박스에 'samplecode.py' 라고 텍스트를 입력해야 하며, 3) 파일형식 콤보박스에서 '모든 파일' 로 선택해 바꿔 주어야 하고, 4) 인코딩을 ANSI 에서 'UTF-8' 로 변경해 준 후 저장 버튼을 클릭하면 된다. 일단 이렇게 되면 애매한 부분이 각각의 개체들을 어떤 '특성' 으로 접근해야 하는지 알수가 없다. 버튼처럼 타이틀이 있는 것도 아니고, 가리키는 텍스트가 있는 것도 아니다(텍스트 박스 옆에 있는 '파일이름(N):' 이라는 문장은 사실 텍스트 박스와 직접 관계는 없는 독립된 개체이다).

 

 

  일단 아직까진 힌트가 별로 없으니 구글에서 'pywinauto save as dialog' 라고 찾아보자. 그런데 파일을 save 하는 코드는 잘 보이진 않고, 아래의 2가지 스택오버플로우 글이 눈에 띈다. 

 

  http://stackoverflow.com/questions/9482019/how-do-i-select-a-folder-in-the-saveas-dialog-using-pywinauto 

  첫번째 글은 save 하며 폴더를 지정하고 싶은데, 어떻게 지정이 가능하냐는 문의다. 근데 답변 글은 대안을 제시하며, 파일 이름 텍스트 박스에 c:\python\code\samplecode.py 라고 풀 경로로 적으라는 것이다. 폴더를 선택 할수 있는 기능을 실제로 구현할수 있을지도 모르지만(이건 전적으로 pywinauto 를 만든 사람이 해당 컨트롤을 다룰 수 있게 기능을 구현해 넣었냐에 달려있다), 동일한 결과를 가지므로 폴더 선택 코드 부분은 이런 식으로 해결하자. 

 

 

  http://stackoverflow.com/questions/37027644/open-file-from-windows-file-dialog-with-python-automatically/37214623

  2번째 글은 파일을 save 하는 예제는 아니고 open 을 하는 코드이다. 근데 어차피 save 나 open 창이 비슷하기도 하고, 창을 열어 값을 입력하고, (콤보 박스 값을 선택하는 코드)만 넣음 비슷할 듯 하니 이 코드를 참고하자.

1
2
3
4
5
6
7
8
9
from pywinauto import application
 
app = application.Application().start_('notepad.exe')
 
app.Notepad.MenuSelect('File->Open')
 
# app.[window title].[control name]...
app.Open.Edit.SetText('filename.txt')
app.Open.Open.Click()
cs

 

 

  그럼 그 다음은 콤보 박스를 어떻게 선택할 것이냐는 문제가 된다. 구글에서 다시 'pywinauto combobox select' 라고 검색하여 아래의 글을 참고한다.

  https://pywinauto.github.io/docs/code/pywinauto.controls.win32_controls.html

 

  해당 내용들을 pywinauto 에서 다룰수 있는 모든 컨트롤 들을 설명한 페이지인데, 중간 쯤에 보면 콤보 박스 관련 설명이 있다.

class pywinauto.controls.win32_controls.ComboBoxWrapper(hwnd)

   Bases: pywinauto.controls.HwndWrapper.HwndWrapper

....

Select(item)
Select the ComboBox item

item can be either a 0 based index of the item to select or it can be the string that you want to select

 

  콤보박스 컨트롤 뒤에 .select 로 호출해 숫자(순서)나, 이름을 넣음 된단다.

 

 

  근데 여기까지 오니 마지막 문제에 다다르게 된다. 위의 '다른 이름으로 저장' 다이얼로그 화면을 보면, 콤보 박스가 여러 개 있다. 각각의 콤보 박스의 이름을 어떻게 알아 낼 수 있을까? 상용 자동화 툴같이 레코딩 기능(레코딩 버튼을 누르고 사용자가 원하는 행동을 하면 해당 동작을 (완벽하진 않지만) 자동화 코드로 만들어 주는 기능)이라도 지원해 주면, 결과로 저장되는 코드에서 인식되는 개체 이름들을 파악하면 되지만, pywinauto 는 그러한 레코딩 기능도 없는것 같다.

 

  다시 pywinauto 메뉴얼 페이지를 하나씩 훝어보고, 구글을 검색하고 하다가 아래 페이지 들을 찾게 되었다.  
  https://pywinauto.readthedocs.io/en/latest/getting_started.html#attribute-resolution-magic

  http://stackoverflow.com/questions/5039642/how-to-access-the-control-identifiers-in-pywinauto

 

  해결책은 print_control_identifiers() 함수를 사용하게 되면, 각 창 안에 있는 모든 컨트롤 들의 속성들이 쭈루룩 나타난다는 거다. 그걸 보고 원하는 개체를 찾아서 이용해 코딩하면 된다고 한다(이건 윈도우 개발툴인 spy++의 텍스트 버전같다. --; spy++는 아래 글을 참고로...).

  http://happyguy81.tistory.com/51

 

  왠지 좀 노가다 일 것 같은 같은 불길한 예감이 들긴 했지만 뭐 시키는 데로 했다. --;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# 메모장에 code를 적는다.
app.UntitledNotepad.Edit.type_keys("print ('test')", with_spaces = True)
 
# '파일' > '저장' 메뉴 실행
app.UntitledNotepad.menu_select("파일(&F)->저장(&S)")
 
# '다른 이름으로 저장' 창의 속성을 리스트업 한다.
app.다른_이름으로_저장.print_control_identifiers()
cs

 

  위의 내용을 c:\python\code 폴더에 utf-8 인코딩으로 notepad2.py 라고 저장해서 실행해 보자. 헐 그런데 위의 마지막 코드에 의해 '다른 이름으로 저장' 창에 있는 컨트롤 특성들이 쭈르륵 나오는데 거의 100페이지쯤은 된다.(정말 저 함수를 무슨 생각으로 저렇게 나이브하게 만들었는지 싶다. 찾는 사람 입장도 생각해야지...)

 

c:\Python\code>python notepad2.py
Control Identifiers:

Dialog - '다른 이름으로 저장'    (L770, T191, R1709, B800)
['다른 이름으로 저장', 'Dialog', '다른 이름으로 저장Dialog']
child_window(title="다른 이름으로 저장", class_name="#32770")
   |
   | DUIViewWndClassName - ''    (L781, T287, R1698, B697)
   | ['DUIViewWndClassName', '다른 이름으로 저장DUIViewWndClassName']
   | child_window(class_name="DUIViewWndClassName")
   |    |
   |    | DirectUIHWND - ''    (L781, T287, R1698, B697)
   |    | ['DirectUIHWND1', '다른 이름으로 저장DirectUIHWND1', 'DirectUIHWND0', '다른 이름으로 저장DirectUIHWND0', '다른 이름으로 저장DirectUIHWND', 'DirectUIHWND']
   |    | child_window(class_name="DirectUIHWND")

.... 이런 식으로 100페이지쯤 됨

 

 

  내용을 위의 다이얼로그 화면과 비교해 몇번의 시행 착오를 거쳐서, 아래의 항목들을 찾아내서 한땀한땀(정말로 이런 기분으로) 코드를 작성했다.

 

   |    |    |    | Edit - '*.txt'    (L972, T562, R1661, B589)
   |    |    |    | ['다른 이름으로 저장Edit1', 'Edit', '다른 이름으로 저장Edit', 'Edit1', 'Edit0', '다른 이름으로 저장Edit0']

 

ComboBox - '텍스트 문서(*.txt)'    (L969, T595, R1690, B628)
   |    |    |    | ['ComboBox2', '다른 이름으로 저장ComboBox2']
   |    |    |    | child_window(title="텍스트 문서(*.txt)", class_name="ComboBox")

 

   | ComboBox - 'ANSI'    (L1147, T738, R1372, B771)
   | ['ComboBox3', '인코딩(&E):ComboBox']

 

   |    |    |    | Button - ''    (L781, T287, R781, B287)
   |    |    |    | ['Button0', 'Button', '다른 이름으로 저장Button', 'Button1']
   |    |    |    | child_window(class_name="Button")

 

 

-> 불편한 걸 직접 느껴봤음 이제부턴 맨 밑의 '보충 내용'에 있는 AutoHotKey 의 spy++를 써서 이름을 알아내보자!

 

 

[최종 코드]

  결과적으로 아래와 같이 최종 코드를 만들어 내게 됬다.(하지만 솔직히 이런 방식으로 개발하게 되면, 메모장 말고 다른 프로그램을 대상으로 하게 되면 잘 될련지 자신은 없다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#-*- coding: utf-8 -*-
from pywinauto.application import Application
 
# 메모장를 띄운다.
app = Application().start("notepad.exe")
 
# 메모장에 code를 적는다.
app.UntitledNotepad.Edit.type_keys("print ('test')", with_spaces = True)
 
# '파일' > '저장' 메뉴 실행
app.UntitledNotepad.menu_select("파일(&F)->저장(&S)")
 
# '다른 이름으로 저장' 창의 속성을 리스트업 한다.
# app.다른_이름으로_저장.print_control_identifiers()
 
# 파일 full 경로 입력
app.다른_이름으로_저장.Edit1.SetEditText("c:\python\code\samplecode.py")
 
# '파일이름' 콤보박스에서 파일 종류 선택
app.다른_이름으로_저장.ComboBox2.Select("모든 파일")
 
# '파일형식' 콤보박스에서 인코딩 선택
app.다른_이름으로_저장.ComboBox3.Select("UTF-8")
 
# 바로 저장 버튼을 누르면 미처 콤보 박스가 안 바뀌어 에러가 나서 1초 시간 줌
import time
time.sleep(1.0)
 
# 저장 버튼 누름
app.다른_이름으로_저장.Button1.click()
 
 
cs

 

  최종 완성된 소스를 저장하고, 실행해 보자. 아래와 같이 해당 폴더에 정상적으로 문서가 저장이 된다.

c:\Python\code>python notepad2.py

 

 

 

 

[최종 코드의 유지보수 문제점]

  위의 코드의 유지보수 문제를 하나 생각해 보자(약간 이건 테스팅 관점이다). 저 코드는 메모장 프로그램이 대상이라 사실 변경될 일이 없지만, 만약 메모장이 아니라 자주 변경되는 프로그램에 대한 코드라 가정하면 '다른 이름으로 저장' 창의 타이틀이 바뀌면 어떻게 될까? 저 코드 내의 모든 '다른_이름으로_저장' 문자열을 모두 바뀐 이름으로 치환해 주어야 한다. 그런데 만약 해당 다이얼로그를 언급하는 파이썬 파일이 수십, 수백개라면 바꾸는 작업은 카오스가 될 것이다(파일이 많아지다보면, 이름들이 일부 겹치기도 하기(저장, 다른이름으로저장, 저장하기 등등) 때문에 일괄로 바꾸게 되면 경험상 분명 예상치 못한 오류가 날수 있다). 그래서 원래 저런 변화될수 있는 값들은 따로 빼놓아 관리하면 좋은데(마치 프로그램에서 하드코딩 하지 않고, 상수 등으로 빼는 것과 비슷하다고 보면 될 듯 하다), pywinauto 의 경우 아쉽게도 아래와 같이 유지보수에 조금이라도 도움을 주는 코딩 방식를 지원하지 않아 에러가 난다.

1
2
3
4
5
6
# 관리를 위해 변수로 뺌
saveAs = '다른_이름으로_저장'
 
# 파일 full 경로 입력
#app.다른_이름으로_저장.Edit1.SetEditText("c:\python\code\samplecode.py")
app.saveAs.Edit1.SetEditText("c:\python\code\samplecode.py")
cs

 

pywinauto.findbestmatch.MatchError: Could not find 'saveAS' in 'dict_keys(['Dialog', 'Notepad', '제목 없음 - 메모장', ' 다른 이름으로 저장', '제목 없음 - 메모장Notepad', '다른 이름으로 저장Dialog'])' <- saveAS 에 대응되는 컨트롤을 못 찾아 이런 에러가 난다.

 

  ※ 아마 내부적으로 이미 '다른_이름으로_저장' 이 컨트롤 이름으로 강제로 취급되어 관리되는 듯 하다(뭐 이 부분은 제가 pywinauto 를 제대로 이해 못해 그럴 수도 있으니 방법이 있을 지도 모른다고 꼬리를 남기는게 날 것 같다).

 

 

 

 

[한 걸음 더 - 상용툴(unified function testing)과의 비교]

  처음에 얘기했듯이, 위의 pywinauto 의 부족한 부분들을 이해하고, 혹시나 더 좋은 모듈이 나온다면 선택 할 수 있게 하기 위해서, 상용툴의 기능과 함 비교해 보겠다. 다만 현재 글의 목적에 맞게 테스팅 관점은 배제하고, 유지보수와 코드 구현의 관점에서만 살펴보려 한다.

 

  해당툴은 회사가 2번인가 바뀌면서 이름까지 바뀌었지만 지금도 왠지 자리를 못잡은 느낌도 나긴 한다(왜냐하면 현재 최신 버전이 윈도우7 밖에 지원 안한다). 그래도 예전의 무거운 느낌의 사용감은 요즘의 SSD 의 힘 때문인지 부드럽게 동작한다. 

 

  지금은 UI 자동화 업무는 명시적으로 안하기 때문에 해당 툴이 없으므로(예전 기준으로 몇 백만원 이상 하기 때문에 이런저런 잡무에 쓰면 좋겠다고 생각은 하지만...) trial 버전을 찾아봤다. 다행히 다운로드 방식의 트라이얼을 제공하고, 트라이얼 기간도 60일로 늘어난듯 해서, 옛 향수를 떠올리며 다운하여 설치를 했다. 뭐 파이썬 강좌이기 때문에 구현 과정을 생략하고 결과만 얘기한다.

 

 

  해당 툴의 레코딩 기능을 이용해서 pywinauto 코드를 비슷하게 구현한 코드가 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 윈도우 시작메뉴에서 notepad 실행함
Window("시작 메뉴").WinObject("항목 보기").WinList("항목 보기").Select "notepad"
 
# 메모장에 test 라고 입력 함
Window("메모장").WinEditor("Edit").Type "print ('test')"
 
# 메뉴에서 파일 > 저장 선택
Window("메모장").WinMenu("Menu").Select "파일(F);저장(S)    Ctrl+S"
 
# 다이얼 로그의 디렉토리 창에서 c:\python\code 로 이동 
Window("메모장").Dialog("다른 이름으로 저장").WinObject("항목 보기").WinList("항목 보기").Activate "python"
Window("메모장").Dialog("다른 이름으로 저장").WinObject("항목 보기").WinList("항목 보기").Activate "code"
 
# 파일 형식과 인코딩 선택
Window("메모장").Dialog("다른 이름으로 저장").WinComboBox("파일 형식:").Select "모든 파일"
Window("메모장").Dialog("다른 이름으로 저장").WinComboBox("인코딩(E):").Select "UTF-8"
 
# 이름 저장
Window("메모장").Dialog("다른 이름으로 저장").WinEdit("파일 이름:").Set "testscript.py"
 
# 저장 버튼 
Window("메모장").Dialog("다른 이름으로 저장").WinButton("저장(S)").Click
cs

 

  위의 코드를 찬찬히 보게되면, notepad 실행도 실제 시작메뉴의 메뉴 리스트를 '선택해' 실행하고, 디렉토리 선택도 실제 디렉토리 리스트를 '선택'해서 이동 했다. 나머지 부분은 거의 비슷해 보인다. 하지만 'object repository' 라는 인식된 컨트롤을 관리해 주는 메뉴로 가면 하나의 중요한 차이가 있다. 아래의 그림을 보면 코드에서 쓰는 이름과 속성들이 트리 형태로 구조적으로 정의되어 있다. 그럼 1) 코드상에 쓰이는 이름을 수정할 수도 있고(아마 관련된 코드에 자동 반영되는 걸로 안다). 2), 4)  속성이 여러개 조합될 수 있고, 추가 삭제도 할수 있다. 그리고 전체적인 윈도우와 그 안의 컨트롤 들에 대한 트리구조를 왼편에 보여준다. 아래 기능은 pywinauto의 print_control_identifiers() 함수의 결과를 시각화해서 관리하는 모드라고 볼 수있다. 이 기능은 실제 써보면 꽤 직관적이고, 메이저 GUI 상용툴은 거의 이런 식으로 컨트롤들의 실제 속성들을 개념적으로 코드와 분리해 관리한다.

 

 

  또 하나는 print_control_identifiers() 의 GUI 버전으로 아까 소개한 spy++ 와 비슷한 object spy 라는 기능이다. print_control_identifiers() 를 매번 코드에 넣어 디버깅 하듯 특성을 확인하는 것보다 스파이형태로 직접 윈도우 창을 클릭해 확인하는게 확실히 효율적이 아닐까 싶다.

 

 

  마지막으로 특정 이미지 영역을 커스텀 컨트롤로 지정해서 인식가능한 기능이 최근 추가됬다고 한다(이 기능은 정말 있었음 하던 기능이다). 이 기능으로 인해서 비표준적인 컨트롤 영역에 대한 인식을 좌표 방식과 이미지 방식 사이에서 취사선택 할 수 있는 선택점이 생긴 것 같다.

https://www.itcentralstation.com/product_reviews/hpe-uft-qtp-review-33718-by-don-ingerson

 

  뭐 업체에서 광고하는 여러가지 다른 요소들도 있겠지만, 제가 느끼기엔 위의 3가지 정도가 현재의 오픈소스 gui 자동화 툴과 상용툴의 가장 큰 차이인 것 같다. 반대로 다양한 언어를 지원하는 것은 보통 상용툴은 한가지 언어(이 툴의 경우 vbscript)만 주로 지원하고, 보통 자바나 .net 정도만 플러그인 같은 형식으로 추가적으로 지원 하기 때문에, 오픈소스 쪽이 휠씬 유리한 것 같다. 혹시 트라이얼 버전을 사용해보고 싶으면 아래의 링크에서 'free trial' 버튼을 클릭해 정보를 넣고, 메일 인증을 받은 후, 로그인해 다운 받으면 된다.

https://saas.hpe.com/en-us/software/uft 

 

 

 

 

[마치면서]

  개인적으로 pywinauto 같은 오픈 소스 gui 자동화 툴은 selenium 의 완성도에 비해서는 아직은 부족한 듯한 느낌이 있다. 하지만 위의 한계들을 인지하고, 구글에 있는 여러 레퍼런스 글들을 참조하여, 다른 좋은 파이썬 모듈들과 결합하여 사용한다면 해당 장점이 단점을 상쇄 할듯 싶다. 개인적으로 상용툴 만큼의 편리성을 지니도록 발전되었음 하는 바램을 가지면서, 다음 시간에는 자동화의 마지막 시간으로 작업 자동화 부분을 진행하려고 한다.

 

 

 

 

[보충내용]

<지나가는나그네님 문의 답변>

1) 브라우저에서 뜬 업로드 팝업을 어떻게 다루느냐에 대해서 autoit, pywinauto, win32 함수를 이용해서 3가지 해결책을 보여줌

https://sqa.stackexchange.com/questions/12851/how-can-i-work-with-file-uploads-during-a-webdriver-test

 

 

2) 위의 글에 있는 pywinauto 예제가 connect 를 안해 에러나서 찾음

https://github.com/pywinauto/SWAPY/issues/45

 

 

3) visual studio 설치시 제공하는 spy++ 를 이용해서 pywinauto 코드를 만드는 설명 - 이 글은 pywinauto 의 아쉬운 부분을 spy++로 보강할 수 있는 꼭 한번 읽어볼만한 글 인거 같음. 요즘은 visual studio 도 개인 개발자에겐 무료인듯 하니 설치해 같이 사용해 보시길... 설치시 옵션에서 공통 툴을 선택해야 깔리다고 함) --> 그리고 그것보다 본문에서 잠시 언급했던 autohotkey 설치할때 같이 깔리는 Active Window Info 툴이 제일 좋다고 쓰여 있다. (밑에 '다른 이름으로 저장' 창에 대한 정보를 보여주는 해당 툴의 그림을 추가했다-> print_control_identifiers 쓰지 말고, autohotkey 에서 깔리는 이 프로그램을 쓰자!!

http://stackoverflow.com/questions/42213490/pywinauto-how-to-select-this-dialog-which-spying-tool-to-use-what-information

 

 

  위의 세 개를 조합하여 만든 코드. 네이버 메일에서 팝업 창이 띄워져 있다는 가정하에서, 아래 코드를 실행 시키면 팝업 창의 파일 이름에 test.txt 라고 쓰여지고 열기 버튼 클릭.(물론 해당 폴더에 test.txt 파일이 없다고 에러 메시지는 날거임. IE 에서 확인)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#-*- coding: utf-8 -*-
from pywinauto.application import Application
import pywinauto
 
# 열려진 다이얼로그 창에 컨넥트 함
app = pywinauto.application.Application()
app.Connect(title="업로드할 파일 선택"
 
# 다이얼로그 창 정의
mainWindow = app['업로드할 파일 선택'# main windows' title
 
# 파일 이름 입력하는 창에가서 'test.txt' 라고 입력
ctrl=mainWindow['Edit'
mainWindow.SetFocus()
ctrl.ClickInput()
ctrl.TypeKeys("test.txt")
 
# 열기 버튼 클릭
mainWindow.Button1.click()
cs

 

 

<비주얼스튜디오_자동화님 문의 답변>

4) 일단 메뉴 아이콘을 인식하여 클릭하는 방법을 찾긴 어려울거 같고, 비주얼 스튜디오 프로그램 특성상 화면의 변경이 없이 매번 일정할 것이기 때문에, 화면 좌표를 이용한 매크로 방식으로 접근하기로 결정.

 

  a) 전체 윈도우 창을 기준으로 클릭 하긴 뭔가 예상못한 일이 생길 수도 있을 듯 해서, 일단 비주얼 스튜디오 윈도우를 인지하고, 그 창 내부의 상대 좌표에서 클릭하기 위해서 'pywinauto click within window' 로 찾아서 아래 페이지를 찾음.

https://stackoverflow.com/questions/28665941/python-click-by-coordinate-inside-a-window

 

  b) Open 아이콘을 클릭하기 위해서, 위에 언급했던, Active Window Info 로 아이콘 위치를 파악.

 

  c)  위의 코드를 적당히 맞춰 변경해서, Open 다이얼로그가 뜨는 것까지 확인 했음. 창이 넘 작으면 위의 메뉴가 겹쳐져서 좌표가 틀려질 수 있으니 주의 할것. 이번에 알았는데 타이틀로 창 이름 지정할시, '공백'이나 '-' 등은 생략해도 인지한다('Start Page - Microsoft Visual Studio' ->StartPageMicrosoftVisualStudio,  '-' 같은 경우는 생략을 안하면 에러가 난다. 아마 공백이나 특수문자는 모두 빼버려도 무방한듯 싶다). --;

1
2
3
4
5
6
7
8
9
10
from pywinauto.application import Application
import pywinauto
 
# 창 타이틀로 프로그램을 연결
app = pywinauto.Application().connect(title='Start Page - Microsoft Visual Studio')
# 메인 윈도우 창을 찾고, (200, 100) 상대 좌표를 클릭함
app.StartPageMicrosoftVisualStudio.ClickInput(coords=(200100))
 
# 최대 창으로 하고 그냥 윈도우 좌표로 클릭하고 싶을 경우는 아래의 코드로
# pywinauto.mouse.click(button='left', coords=(200, 100))
cs

 

 

 

[추가]

파이썬 3.7에서는 pywinauto 설치시 에러가 날텐데, 링크의 설명대로 wheels 를 업데이트 해주면 됩니다^^

https://stackoverflow.com/questions/14296531/what-does-error-option-single-version-externally-managed-not-recognized-ind

 

 

2017.4.8 by 자유로운설탕
cs

 

posted by 자유로운설탕