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

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

Notice

2019. 7. 7. 19:32 보안

  이번 시간에는 자동화에 대해서 얘기하려 한다. 보안은 많은 부분이 자동화를 기반으로 구축되어 있는 분야기도 한것 같긴 하지만, 그러한 부분을 조망하는 얘기를 하려는 것은 아니고, 한 개인의 입장에서 생각 할 수 있는 부분이 어떤가에 대해서 이야기 하려 한다. 



[목차]

1. 보안을 바라보는 방법

2. 보안에서의 코드 읽기

3. 인젝션(Injection) 살펴보기 #1, #2

4. 암복호화

5. 클라이언트 코드

6. 업로드, 다운로드

7. 스크립트 문제

8. API

9. 하드닝(Hardening)
10. 보안 설계 문제
11. 스캐너 vs 수동 테스트

12. 자동화 잡

13. 리버싱과 포렌식

14. 모니터링 문제

15. 악성코드

16. 보안과 데이터

 


 


1. 들어가면서

  자동화에 대해서는 이전의 파이썬 글에서도 많이 언급이 된 주제이다. 웹 자동화에 대해서는 8교시 정규표현식, 11교시 웹 페이지 파싱, 12교시 웹 자동화편을 걸쳐서 얘기를 했고, 13교시에서 윈도우즈 자동화를, 14교시에서 작업에 대한 자동화를 이야기 했다. 10교시의 Whois API 이용도 API 를 호출해 자동화 한다는 측면에서 마찬가지이다(마음의 여유가 있다면 이번 시간을 보기전에 해당 글들을 가볍게 보고 왔음 한다)

 

  파이썬 글에서는 그렇게 나눠 놓긴 했지만, 지금와서 드는 생각은 실제 자동화는 저 부분들이 서로 섞여서 진행되는 것이라는 것이다. 조금 더 나아가면 우리가 프로그래밍 한 줄을 실행하는 시점 부터 자동화를 수행하고 있다고 봐도 될 듯 하다. 지금 이 글을 쓰고있는  컴퓨터 자체나, OS, 브라우저, 엑셀 포토샵, 일러스트레이트 같은 디자인, 편집 프로그램, 매일 재미있게 하고 있는 게임 등도 결국은 자동화의 결정체라고 보는게 맞지 않을까 싶다. 사람은 항상 효율성을 추구하는 방식으로 생각하기 때문에(어쩌면 사회적 훈련의 결과일지는 모르겠지만), 사람이 하는 많은 일들은 점점 성숙이 되다보면 자동화로 귀착되어 버리는 것 같다.

 

  보안쪽에서 흔히 솔루션이라고 얘기하는 많은 하드웨어(보통 소프트웨어가 기반이긴 하지만)와 소프트웨어도 결국은 보안 업무의 자동화가 구현된 결과라고 볼수 있다. 우리가 당연한게 사용하고 있는 백신이나, 각종 제어 솔루션, 모니터링 시스템, 방화벽, IPS, 여러 회사들의 보안 솔루션들도 결국은 보안적 지식을 자동화 하여(결과적인 판단 부분에서는 아직 사람들이 많이 관여해야 하겠지만) 구현한 결과라고 볼수 있다. 이 부분에 대해서는 파이썬 21교시의 "The five orders of ignorance" 에서 소개한 지식을 담는 매체의 관점으로 이해하면 어떨까 싶다.

 

 

 

 

 

 

 

2. 보안에서의 자동화 

  맨 처음 부분에서 "한 개인의 입장에서"라고 범위를 좁혀놓은 이유는 이젠 보안 쪽에 있는 많은 자동화의 부분이 개인이 감당하기에는 범위 및 정보가 너무 커졌다고 생각하기 때문이다. 백신 같은 분야만 봐도 예전에는 한명의 개인이 만들어 유지보수가 가능한 부분이였다면, 현재는 수많은 사람들이 해당 회사에 속해서 24시간 돌아가면서 새로운 악성코드 들에 대응을 해야되는 분야가 된것 같다. 또한 백신이 최신의 악성코드 공격을 따라기기 위해서 수집 해야하는 다양한 데이터의 양도 이제 한 개인이 수집하기 에는 너무 다양한 환경과 분석해야 될 데이터의 양이 존재한다(데이터를 준다해도 저장한 서버를 살 돈이 없어서라도 못할듯 싶다--;). 백신 회사들이 개인에게는 대부분 무료 백신을 제공하는 부분도 점유율을 높인다거나, 위험한 PC 들이 너무 늘어나게 되는 것을 기본적으로 방지하는 목적도 있겠지만, 다양한 사용자들의 환경과 그곳에서 발견되는 악성코드를 수집하기 위한 목적도 큰 부분을 차지할 것 같다. 그렇게 보면 세상에 공짜는 없어 보인다. 

 

  이러한 부분은 네트워크, 스캐너, 디비 접근 제어 및 모니터링, 매체제어 솔루션, 패치 등의 관리도 그렇고, 포렌식이나 리버싱 툴도 마찬가지인것 같다. 이젠 작은 개인이 이러한 부분에 대해서 무언가 의미있는 성과를 내기에는 관련된 환경과 데이터가 감당할 수준을 넘어버렸고 ROI와 계속적인 유지보수 측면에서 어려운 시기가 된것 같다. 또한 만들어 놓은 부분에 대해서 잘 동작하느냐를 증명하는 QA 부분도 더더욱 해당 부분을 어렵게 만든다. 또한 필요한 기술의 광범위 함도 한 몫 한다.

 

  예로서 만약 디비 접근 모니터링 솔루션을 만들고 싶다면 여러 데이터베이스의 통신 패킷들을 파싱하고 원하는 값들을 뽑을 수 있어야 한다. 물론 현재에 딱 맞는 오픈 라이브러리가 있을지는 모르지만 그 라이브러리가 적용 후, 새로운 데이터베이스 버전이 나왔을 때 더이상 업데이트가 안된다면 어떻게 하겠는가?(다른 많이 사용되는 오픈소스 라이브러리들도 마찬가지라고 생각될지 모르지만, 수많은 사람에게 필요한 데이터베이스에 쿼리를 날리는 범용의 목적을 가진 라이브러리와, 데이터베이스 패킷을 파싱하는 특수한 라이브러리는 유지보수가 계속 될 확률이 많이 차이가 난다고 본다). 물론 커다란 인력 풀을 가진 회사들에게는 직접 개발 및 유지보수에 대한 제약이 적을지는 모르겠지만, 일반적으로는 성숙된 분야의 툴을 스스로 만드는 것은 힘든 부분 같다.

 

 

 

  그렇다면 작은 개인은 무엇을 자동화 해야할까? 이 부분은 틈새 시장이라는 표현으로 생각함 어떨까 싶다. 아직 변동성이 크거나 시장성이 없어서 정식의 외부 솔루션들이 개발되지 않거나, 솔루션이 있더라도 많은 투자를 하지 않아서 어설픈 분야들이 있다. 또는 솔루션을 들여놓기에는 너무 부담이 되서 부족하게 나마 유사한 효과를 가진 프로그램을 만들고 싶을 때도 있을 것이다. 또는 솔루션의 기능이 부족해서, 뭔가 추가적인 보충을 해보고 싶을 때, 솔루션 개발사나 오픈 소스에서는 보통 해당 부분의 개선이 여러 이유로 쉽지가 않다(수많은 요청을 하나하나 들어주다가는 수 백개의 유지해야할 소스 트리가 생길 수도 있다). 아님 최악의 경우 예산이 없어서 몸으로 때울 수는 없고 비슷하게 라도 만들어 땜빵을 해야 할지도 모르겠다.

 

 

  또 다른 측면에서는 일상의 모든일이 해당된다고 봐도 괜찮을 듯 싶다. 엑셀 작업을 돕는 수많은 매크로 및 함수들이 있듯이, 파이썬 같은 간편한 언어를 가지고 일상의 작은 일들을 자동화 하는 것도 괜찮아 보인다.  처음부터 거창한 무언가를 만들려 하는 것보다는 이러한 작은 조각 조각들을 구현 하다보면, 언제가 꽤 큰 퍼즐에 대해서도 그 동안 모은 조각들을 기반으로 처음 생각보다 어렵지 않게 만들어 낼 수도 있다. 개인적으로 생각하기엔 이런 접근이 프로그램을 주 직업이 아닌 보조 기술로 접근하는 사람들에게는 현실적일 듯도 싶다.

 

  예를 들어 데이터베이스의 모든 테이블에 대해서 특정 쿼리를 날려서 원하는 정보를 가져오는 일을 해야 하는 경우, 쿼리 분석기에서 일일히 쿼리를 만들어낼 수도 있겠지만(SQL 이나 텍스트 편집기들을 잘 쓰는 사람들은 여러 팁을 통해서 모든 테이블에 대한 쿼리를 일괄로 만들어 낼수도 있긴 하겠지만), 파이썬으로 데이터베이스에서 테이블 목록을 얻어낸 후, 적당히 조건에 맞는 문법과 조합하여 쿼리를 만든 후, 각각의 쿼리로 조회하여 결과를 엑셀 등에 정리하면 좀더 편할 것이다. 추가로 해당 작업이 매일매일 일어나거나, 쿼리가 자주 특정 패턴으로 변경 되거나, 대량으로 일어난다면 시간을 아끼게 된다고 느껴 매우 만족하게 될 수도 있다.

 

 

  여러 오픈 소스로 제공되는 대시보드 같은 프로그램 들도 결국은 이러한 자동화에 대한 장벽을 해당 분야에 익숙한 전문가들이 낮춰주려 하는 노력인것 같다. 그렇게 보면 Redis 나 Elastic Search 같은 데이터베이스도 키, 값을 쉽고, 효율적으로 저장하게 해주는 많은 노력들이 자동화의 산물로 만들어져 제공 된다고 보면 된다. 파이썬 같은 프로그래밍 언어나 거기에 속해져 전세계 사람들에 의해 무료로 개발되어 제공되고 있는 모듈들도 마찬가지 인것 같다. 우리 중 일부는 가끔 자동화의 1차 생산자가 되기도 하지만, 대부분의 사람들은 2차 생산자나 3차 생산자인 경우가 많은 것 같다.

 

  이렇게 보면 특정 분야의 자동화라는 말은 별 의미는 없는것 같기도 하다. 적용되는 형태는 다를지 몰라도, 적용되는 기술의 배경은 많은 부분들이 서로 겹치게 되는 것 같다. 결국은 자동화는 프로그래밍 또는 프로그래밍으로 감싸진 좀더 사용자 친화적인 2차 기술 들을 이용하여 일을 하는 방식 같다.

 

 

 

 

3. 자동화 예제 구현해 보기 - 데이터베이스에서 개인정보 찾기

  보안 쪽에서 가장 많이 쓰인다고 생각되는 자동화 프로그래밍 요소중 하나는 패턴에 대한 해석인것 같다. 그 중에서도 으뜸은 파이썬 8교시에 얘기한 정규 표현식 이라고 본다. 나중에 얘기할 모니터링과도 연결될 내용이긴 하지만, 결국 데이터를 기반으로 모든걸 자동으로 검사할 수 밖에 없기 때문에, 결국 통계든 Raw 데이터 자체든 패턴을 찾아야 한다. 물론 특정한 데이터베이스 안의 숫자의 변화를 체크하는 경우도 있긴 하겠지만, 그 숫자가 만들어지는 배경을 살펴 보게 되면 특정한 조건이 발생할때 쌓이는 경우라고 볼 수 있고, 그런 특정한 조건이 주어지는 상황 또한 패턴이라고 볼수 있다고 본다.

 

  여하튼 현재 보여줄 예제는 데이터베이스 안의 모든 테이블에 대해서 10건씩 데이터를 조회해서, 모든 컬럼에서 개인정보를 검사한 후, 결과를 반환하는 프로그램이다.

 

1) 데이터베이스내의 테이블을 가져오는 쿼리는 구글에서 "mssql get tables" 로 조회해 얻었다.

 

2) 기본적으로 테이블에 조회 쿼리를 만들고 행을 가져오는 부분은 파이썬 7교시의 내용을 pyodbc 모듈을 사용하는 것으로 변환해 적절히 만들었다(책에는 pyodbc 로 진행되게 됬어서 해당 샘플을 가져왔다). 예전 댓글이 생각나서 가능한 인자가 호출되는 부분은 parameterized query 로 만들었긴 했는데, 사실 어플리케이션 관점에서 보면 작업의 시작인 테이블 리스트를 가져오는 함수에 인자 자체가 없고, 테이블 이름이나, 컬럼 이름은 데이터베이스 내부에서 가져온 값이기 때문에 조작 가능성은 거의 없다고 봐도 될듯하다.

 

3) 개인정보 패턴을 찾는 정규 표현식은 구글에서 찾다가 행정 자치부에서 배포한 "홈페이지 개인정보 노출 가이드라인"의 83페이지를 참조했다(개인적으로 언어별로 최신 패턴으로 유지보수 되는 라이브러리를 제공하면 더 좋을 것 같다).

https://www.privacy.go.kr/nns/ntc/selectBoardArticle.do?nttId=5952

 

4) 마지막으로 어떻게 테이블, 컬럼, 10개 행을 어떤 자료구조에 넣어서 적절히 루프를 돌리면서 개인정보를 찾은 후 결과를 정리할 수 있을까 생각하다보니, Pandas의 Dataframe 을 사용하면 어떨까 싶었다. 개인적으로 dataframe 은 메모리에 떠있는 디비라고 생각하며 사용하고 있는데, 파이썬에서 간단하게 세로 열을 순서대로 가져와 검사할 수 있기 때문이다(예: A 테이블의 10개 행 중에 메모 컬럼에 해당하는 10개 데이터). 왠지 RDB 에서 가져온 결과를 담은 결과 리스트를 인덱스를 통해 조작하는 것보다는, openpyxl 같은 엑셀 형태의 객체나 pandas 같은 데에 넣는 것이 좀 더 편하게 원하는 값을 선택을 할 수 있을 것 같았다.

 

 

  위에 얘기한 것을 그림으로 정리하면 아래와 같다. 

 

 

  코드를 보기전에 개인정보가 담긴 테이블이 있어야 결과가 나올 것이기 때문에, 우선 MSSQL Management Studio 를 실행하여 기존 데이터베이스에 테이블을 2개 추가하고 데이터를 넣자)쿼리를 실행할 줄 모른다면 3교시를 다시 참고한다)

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
create table Secret_A
(
    MemberNo int,
    MobileTel varchar(20),
)
 
go
 
insert into Secret_A
values (10'010-1111-2222')
 
insert into Secret_A
values (20'010-3333-4444')
 
go
 
create table Secret_B
(
    MemoNo int,
    Memo varchar(100),
)
 
go
 
insert into Secret_B
values (2000'제 전화번호는 011-1234-5678입니다.')
 
insert into Secret_B
values (2001'이건 개인정보가 아니예요 010-2222')
cs

 

 

  테이블을 생성하고 조회하면 아래와 같이 조회 데이터가 나온다. 내용을 보게 되면 Secret_A 테이블에는 2개의 행에 모두 핸드폰 번호가 있고, Secret_B 테이블에는 1개의 핸드폰 번호가 있다. 파이썬과 보안 글을 모두 실습했다면 이 두개의 테이블을 포함해 7개의 잡다한 테이블이 있을 것이다. 

 

  그럼 파이썬으로 만든 코드를 봐보자. 위에 그린 그림과 비슷하게 get_table_names 함수에서 테이블 이름들을 가져오고, get_column_names, make_column_query 함수를 이용해 make_dataframe 함수가 데이터베이스에서 조회한 내용을 dataframe 에 담는다. 이후 check_personal_pattern 를 이용해 각 컬럼별 데이터들에 대해서 루프를 돌리면서 개인정보를 찾아 반환한다.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import pyodbc
import pandas as pd
import re
 
 
# 연결 문자열을 세팅
server = 'localhost'
database = 'mytest'
username = 'pyuser'
password = 'test1234'
 
# 데이터베이스에 연결
conn = pyodbc.connect('DRIVER={ODBC Driver 13 for SQL Server};SERVER='+server+' \
;PORT=1433;DATABASE='+database+';UID='+username+';PWD='+ password)
 
# 커서를 만든다.
cursor = conn.cursor()
 
 
# 테이블 이름을 얻어옴
def get_table_names():
    table_names = []
    
    sql_get_tables = "SELECT name FROM sysobjects WHERE xtype='U'"
    cursor.execute(sql_get_tables)        
    rows = cursor.fetchall()
    
    for row in rows:
        table_names.append(row[0])
 
    return table_names   
    
 
# 테이블의 컬럼이름을 얻어옴
def get_column_names(table_name):
    column_names = []
    
    sql_get_columns = "SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"
    cursor.execute(sql_get_columns, table_name)        
    rows = cursor.fetchall()
    
    for row in rows:
        column_names.append(row[0])
 
    return column_names
 
 
# select 컬럼 쿼리 제작(a, b, c 이런 식으로 만들어진다)
def make_column_query(column_names):
    column_query = ''
 
    for name in column_names:
        column_query = column_query + name + ','
    column_query = column_query[:-1]
    return column_query
 
# 테이블의 내용을 읽어서 Panda Dataframe 에 저장한다.
def make_dataframe(table):
    columns = get_column_names(table)
    column_query = make_column_query(columns)
    
    query1 = "SELECT top 10 " + column_query + " FROM " + table + "(nolock)"
    df = pd.read_sql(query1, conn)
 
    return df
 
# 핸드폰 패턴을 찾아서 반환한다
def check_personal_pattern(table, column, check_items):
    # 결과를 반환할 배열
    detected_list = []
    # 핸드폰 정규 표현식
    mobile_tel_pattern = "(01[016789][-~.\s]?[0-9]{3,4}[-~.\s]?[0-9]{4})"
 
    for check_item in check_items:
        check_item = str(check_item)
        match_mobile_tel = re.search(mobile_tel_pattern, check_item)
 
        if  match_mobile_tel:
            matched_word = match_mobile_tel.group(1)
            matched_type = "핸드폰번호"    
            
            # 이미 다른 row가 걸린게 있다면 발견한 정보를 추가해 업데이트 하자 
            if detected_list:
                detected_list[0][3= detected_list[0][3+ " / " + matched_word
            # 처음 걸린 거라면 그냥 넣자
            else:
                detected_list.append([table, column, matched_type, matched_word])
             
    return detected_list
 
 
################
# 메인코드 시작
################
 
# 데이터베이스에서 테이블 이름을 얻어온다
tables = get_table_names() 
 
# 테이블 하나하나에 대해서, 10개의 row 를 불러와 dataframe에 넣은 후,
# 개인정보 검사를 하여 결과를 반환 한다.
for table in tables:
    print("[" + table + "]"
    
    my_dataframe = make_dataframe(table)
    
    # 컬럼 이름을 가져와서
    for column in my_dataframe.columns: 
        # 컬럼 이름에 해당하는 수직 row 데이터를 가져온다.
        lists = my_dataframe[column].tolist()
        
        # 개인정보가 있는지 검사한다
        for item in lists:
            personal_infos = check_personal_pattern(table, column, lists)
        
        # 찾은 개인정보를 출력한다
        for personal_info in personal_infos:
            print(personal_info)
cs

 

 

  c:\python\code 폴더에 인코딩은 UTF-8 로 해서 dbsearch.py 란 이름으로 저장 후 실행해 본다.

c:\Python\code>python dbsearch.py
[supermarket]
[order_record]
[escape_test]
[play]
[sms_cert]
[Secret_A]
['Secret_A', 'MobileTel', '핸드폰번호', '010-1111-2222 / 010-3333-4444']
[Secret_B]
['Secret_B', 'Memo', '핸드폰번호', '011-1234-5678']

 

  결과를 보면 개인정보 패턴이 없는 위의 supermarket 같은 테이블들은 그냥 넘어갔고, 개인정보가 들어있는 Secret_A, B 테이블에서만 개인정보를 찾아서 결과를 보여준다.

 

 

 

 

4. POC 이상의 것

  위의 코드는 어찌보면 POC(Proof of Concept) 레벨이라고 볼수 있다. 실제 환경에서는 몇가지 추가적인 확장점들이 필요하다.

 

  첫 째, 위에는 핸드폰 번호 하나밖에 없지만, 여러 개인정보 패턴을 넣도록 해야한다. 가이드에서 제시된 패턴이 실제 필드의 환경에 맞는 적절한 패턴인지도 검증도 해야된다. 특정 필드에서 쓰는 개인정보 패턴은 가이드에도 없을 수 있으므로, 정규 표현식으로 찾을 수 있는지 고민하고 구현을 해야할지도 모른다. 다른 개인정보 패턴을 넣게 되면 핸드폰 패턴을 검사하는 코드가 아마 더 복잡하게 되어버리게 될것 이다. 추가로 파이썬에서 쓰는 정규표현식 표현으로 일부 조정해야할지도 모른다.

 

  둘 째, 범위의 확장도 해야된다. 지금은 데이터베이스 하나밖에 없다고 가정하지만, 수많은 서버가 있고 해당 서버내에 데이터베이스가 복수개 있다면, 서버 목록을 관리하고, 해당 서버에서 데이터베이스를 찾는 부분이 확장되서 만들어져야 한다.

 

  셋 째, 아마 실제 필드에서 위의 프로그램을 돌리게 되면 수많은 false postive 들이 나올 것이다. 해당 부분이 패턴으로 찾는 방식의 맹점중 하나인데 그러한 부분들을 감소시켜 최종적으로 봐야할 데이터를 줄이는 여러 노력들이 필요하다. 안전한 컬럼명의 제외라든지, 특정 패턴의 예외라든지, 패턴들의 통계적 분포 라든지 등등, 패턴이 찾았지만 실제 개인정보가 아닌 데이터들을 자동으로 예외처리하는 여러가지 로직이 있어야 해당 툴을 사용하는 사람이 현실적으로 스트레스를 안 받고 유용하게 쓸수 있게 된다.

 

  넷 째, 결과를 어떻게 관리하고 보여줄지도 고민해 봐야 된다. 엑셀로 담을지 데이터 베이스로 담아 웹 페이지에서 관리하게 할지 등등, 히스토리나 결과에 대한 관리 및 검증에 대해서도 생각해 보면 좋을 것이다.

 

  다섯 째, 오라클, MySQL 같은 RDB 라면 위의 MSSQL 에 대한 코드를 비슷하게 수정해 쓸 수 있겠지만, Redis, MongoDB 같은 NoSQL 데이터베이스라면 앞의 데이터를 가져와 개인정보 검출 함수에 던지는 부분에 대한 정리를 해당 데이터베이스의 형태에 맞게 고민해 봐야한다. 쿼리 형태도 다를 뿐아니라, 넘어오는 데이터도 Json 베이스의 키, 값 데이터가 여러 깊이로 쌓여있는 경우가 많기 때문에 어떻게 파싱을 해야할지도 고민해야 한다. 또한 위의 테이블, 컬럼, 값에 대한 개념에 NoSQL 데이터베이스의 요소들을 어떻게 매칭 시킬지, 아니면 분리해 다룰지를 고민해야 한다. 인코딩, 디코딩에 관련된 데이터 깨짐 문제도 발생해 해결해야 할 수 있다.

 

  여섯 째, 검사해야 되는 대상자체를 유사도에 따라서 제외해야 할수도 있다. Elestic Search 의 날짜 별로 쌓이는 인덱스 라든지, Redis 의 캐시 성격의 키 같은 경우는 모든 것을 검사안하고 샘플링 하여 검사하는게 현실적일 수도 있다.

 

  일곱 째, 만들어진 프로그램에 대해서 어떻게 동작을 보장할 수 있을지에 대해서 검증 계획도 세워야 한다. 버그가 있을지도 모르는 프로그램이라면 안심하고 있다 뒷통수를 맞게 되거나, 프로그램의 효과를 자신하기 힘들게 된다.

 

  이렇게 열거하고 보면 할일이 엄청 많이 보이지만, 기본이 되는 POC코드가 만들어 졌으므로 하나하나 살을 붙여 보는 것도 꽤 재밌는 과정이 되리라 생각한다. 파이썬 글에서 얘기했던 여러 잡다한 주제들이 이런 과정에서 다들 현실화가 되서 하나의 툴로 동작하게 되는 것을 볼 수 있을지도 모른다.

 

 

 

 

5. 마무리 하면서

  자동화라는 망망한 바다 같은 주제를 짧은 예제하나로 요약해 소개해 좀 그렇긴 하지만, 앞에서 얘기했듯이 프로그래밍 자체가 자동화 이고, 이전 파이썬 글들도 결국은 자동화에 대해서 얘기한 것이라고 봐도 될듯 하다. 무언가 대단한 결과를 꿈꾸기 보다는 자신 주변에 있는 작은 자동화 요소들을 찾아서 조금씩 연습을 하면서 자동화의 씨앗을 키워보는게 어떨까 싶다. 

 

 

 

2019.7.13 by 자유로운설탕
cs

 

 

 

posted by 자유로운설탕
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 자유로운설탕
prev 1 next