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

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. 3. 26. 20:29 프로그래밍

  이번 시간은 웹 자동화에 대해서 얘기하려 한다. 자동화에 대한 몇 가지 생각들을 얘기 한 후, 셀레늄(selenium)의 구조를 설명 후, 구글에서 특정 키워드를 검색하여, 상위 5개의 결과 링크를 새로운 탭들에  로딩하는 예제를 구현 후, 이를 다시 보이지 않는 유령 웹브라우저인 PhantomJS를 이용해 호출하여, 몇 가지 생각해 볼 만한 주제들에 관해서 이야기를 하려한다. 이후 부록으로 IE 를 통해서 같은 구현을 하는 부분을 얘기한 후 마무리 하려 한다. 

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  '자동화'란 무엇일까? 자동화의 반대말에 해당하는 '수동화'를 먼저 생각해 보자. '수동화'란 결국 사람이 무언가를 하는 것, 또는 사물이 사람의 도움을 통해서 움직이는 것을 얘기한다고 볼 수 있다. 그럼 자동화는 사람의 손을 거치지 않고도 사람이 원하는 동작을 하는 것을 의미하는 단어라고 볼수 있다. 사실상 자동화는 우리가 사용하는 프로그래밍 언어나, 윈도우즈 같은 운영체제 자체도 자동화가 구현된 예라고 봐도 무방할 듯하다. 운영체제는 예전의 기계식 컴퓨터와 비교한다면 전원만 넣어주면, 사람의 행동에 따라서 반응하여 동작을 해주는 인터렉티브한 자동화 구현체라고 볼 수 있다. 더 확장하면 우리가 현실의 어떤 일을 하는데 사용하는 패턴이나 노하우 같은 부분도 실체화 되있지는 않지만 자동화라고 볼 수 있을것 같다.

 

  그렇다면 자동화를 구현하는데 있어서 가장 필수적이면서도 어려운 요소들는 무얼까? 일반적으로 자동화는 초기화 단계에서는 '스스로 움직이는 것'이 가장 중요하다. (잘은 모르지만) 처음 로봇이나 자율주행 자동차를 만든다면, 사람이 수동으로 운전하는 것처럼 자동으로 움직이고, 뛰고, 코너링을 돌고, 멈추고 하는 기능들이 필요할 것이다. 그런데 해당 부분의 요소가 어느정도 해결나게 되는 순간 새로운 차원의 어려운 문제가 발생하게 된다. 그것은 '환경'과의 상호동작 이라는 요소이다. 만약 자율주행 자동차 라면 도로의 커브라든지, 앞차와의 간격, 도로 노면 상태, 기후 상태, 보행자 상태, 교통 신호 등등 여러가지 주행 중 만날 수 있는 환경적인 부분의 차이를 인지하여, 해당 부분에 대해서 자동차 스스로 가지고 있는 여러가지 자동화 능력(전진, 후진, 회전, 브레이크 등등..)을 적용할지를 결정해야 한다(이런 부분에서 요즘 한참 유행하는 딥러닝 같은 요소들이 끼어 들수도 있을 듯하다). 그럼 그러한 환경을 인지하기 위해서는 어떻게 해야 할까? 우리가 현재 개발해 놓은 센서, 또는 그러한 환경을 인지하기 위한 새로운 타입의 센서가 있어서 원하는 환경 요소를 구조적인 소프트웨어적인 정보로 변환하여 제공하여야 할 것 이다.

 

  그럼 위의 가정에서 자동화를 위한 소프트웨어에서 필요한 부분을 유추해 보면, 스스로 동작하게 하는 기능들, 환경을 인지해서 필요한 동작을 판단하게 할 수 있는 센서들이 필요하다. 전자는 셀레늄 같은 자동화 모듈의 API 들과, 파이썬, 자바 같은 범용적인 언어의 프로그램 로직으로서 커버되고, 두번째는 순수하게 자동화 모듈에서 제공하는 여러가지 센서와 동작 API 로 이루어지게 된다. 아래는 지금까지 얘기한 내용을 도시한 것이다. 

 

 

 

 

[자동화의 종류]

  개인적인 의견으로 자동화는 몇가지 타입으로 나누어 볼수 있다.

 

  1 번째는 가장 원초적인 '화면 좌표를 기준으로 한 자동화' 이다. 온라인 RPG 게임의 레벨업, 스킬업 노가다 같은 부분을 macro express 나, auto mouse 같은 프로그램으로 자동화 하는 것 같은 작업이다. 특정 프로그램 창 내의 특정한 위치를 반복적으로 클릭한다든지, 키보드 이벤트 명령을 이용해서 원하는 키를 입력한다든지, 화면의 특정한 좌표의 칼라를 기준으로 판단하여 캐릭 및 몹들의 위치를 판단하여 특정 이벤트를 일으킨다는지 하는 부분 말이다. 이런 방식의 단점은 UI의 변경이 일어나면, 화면 배치상의 좌표와 칼라 등이 달라질 가능성이 높기 때문에, 그에 따라 전체적인 자동화 코드의 수정이 필요해 질 수도 있다는 것이다. 다만 많은 변경이 없거나 변경 주기가 긴 프로그램등을 대상으로 했을 때는 적절히 정확한 동작을 보여주게 된다. 게임 패킷 등을 직접 조작하여 자동화를 구성하는 경우도 있는 것은 같지만, 그 부분은 일반적인 자동화의 영역이라기 보다는 해킹 작업에 가까운것 같다.

 

 

  2 번째는 셀레늄과 같은 자동화 모듈을 이용하는 '웹 자동화' 이다. 웹 자동화의 구현은 이전에 소개한 html, xml 파싱 모듈인 beautifulsoup 의 구현 방식과 많이 유사하다. 해당 페이지 내의 태그를 인지하거나, 원하는 값을 가져오는 방식은 거의 비슷하고, 추가적으로 특정한 행동을 해당 태그 개체에 일으킬 수 있다(텍스트 박스에 검색어를 넣거나, 특정 버튼을 누른다든지 - 실제 사람이 해당 버튼을 누를때 브라우저에서 일어나는 이벤트와 비슷하게 구현한다고 보면 될듯 하다). 또한 브라우저와 상호 작용하여 움직이기 때문에, 사람이 실제 사용하는 것과 거의 비슷하게 돌아간다고 볼 수 있어, UI 자동화 테스팅 같은 영역에서 시작하게 되었다. 

 

  좌표 방식의 자동화와는 달리 UI 가 조금 달라진다고 해도, 내부 페이지의 속성 등만 잘 유지되면, 코드의 수정이 필요 없을 가능성이 높은 반면, 화면의 모양은 같아도 내부 코드 구조가 달라져 안 돌아 갈수도 있다. 또 웹의 특성상 네트워크나 시스템 부하에 따라 응답 속도 차이나, 타 어플리케이션 또는 사용자의 마우스, 키보드 사용에 의한 이벤트 방해 등으로 동작이 멈춰버리는 등 미묘하고 골치아픈 문제들을 종종 만날 수 있다. 또한 호환이 안되는 웹 확장 컴포넌트나 플래시 컴포넌트 등이 자동화의 대상일 경우에는 표준 적인 접근이 안되어 최후의 수단인 좌표로 해결할 수 밖에 없게되서, 좌표 기준 구현과 비슷한 레벨의 유지보수 문제가 생길수도 있다. 또 단순한 숫자 입력을 벗어나 점점 어려워지고 있는 캡챠 화면이나 은행에서 사용하는 랜덤한 번호키, 보안 프로그램 등과 연관되어 인식이 힘든 문제가 발생할 수 있다. 실제 화면을 봐야될 필요가 없는 경우는 뒤에 소개할 PhantomJS 같이 화면이 표시되지 않는 브라우저를 이용하는 방식도 있는 듯 하다.

 

 

  3 번째는 '윈도우즈 자동화' 이다. 윈도우즈 자동화도 어찌보면 웹 자동화와 거의 비슷한데, 근본적인 차이라면 웹 자동화에서 웹의 여러 요소(태그, 어트리뷰트, DOM 구조)를 기준으로 인식하던 부분을, 윈도우 창들의 여러 특성(타이틀, 캡션, 클래스 등등)을 기준으로 인식한다는 점만 틀리다. 아무래도 브라우저에 비해서 응답 지연에 의한 타이밍 문제가 생길 가능성이 낮으나, 비슷한 통신을 하는 FTP 프로그램의 자동화와 같이, 껍데기는 앱 형태긴 하지만 실제 뒷단에서서의 실제적인 동작은 서버 클라이언트 통신인 경우엔 비슷하거나 더 어려운 문제가 발생할 수도 있다.

 

 

  4 번째는 가장 범용적인 부분으로 '작업의 자동화' 이다. 소스를 특별한 주기로 백업 및 압축하여 FTP, NAS 등에 올린다든지 하는 수동으로 하다보면 잊어버릴 수도 있고, 귀찮은 작업들을 해결하는 부분이다. 공장 자동화도 넓게보면 같은 영역에 포함될 것이다. 해당 부분은 1, 2, 3 번의 자동화와 연동 하여 동작할 수도 있고, OS 명령어나, 7-zip 과 같은 외부 프로그램, 외부 API, 다른 여러가지 모듈들을 조합하여 동작 될 수 있다. 4번 영역은 만들때는 귀찮거나 난이도가 어려울 순 있지만, 한번 만들어 놓으면 특별한 변경 작업 없이 계속 잘 돌아가 자동화에 대한 ROI 가 잘 증명되는 영역인 것 같긴하다. 어찌보면 머신러닝이나 딥러닝 같은 분야도 약간 고급 버전의 데이터 기반의 행동 자동화라고 봐도 되지 않을까 싶다. 결론적으로 처음에 얘기한 것 같이 우리가 PC나 스마트폰 등의 OS 상에서 행하는 모든 작업은, 또 어쩌면 우리의 사고 조차도 자동화의 요소들을 어느 정도 담고 있다고 생각한다.

 

 

  뭐 이외에도 성능 테스트 툴 등에서 쓰는 URL 기준의 자동화나, 유닛 테스트 등 포함되지 않은 다른 영역이 있을진 모르지만, 나름 4가지 분류에 대충 일반적인 자동화는 언급 되었다고 생각한다. 괜찮은 느낌을 받은 아래 쿠키런 자동화 슬라이드를 봐보자.
https://speakerdeck.com/sgonv/pythoneuro-kukireon-unyeonghagi

 

 

 

 

[Selenium 개론]

  그럼 파이썬 웹 자동화 이야기에서 단골로 등장하는 셀레늄(selenium)이란 모듈은 뭘까? 개인적으로 7~8년 전 쯤에 테스트 자동화 업무에 조금 발 담그고 있는 편이였는데, 그때는 셀레늄 같은 오픈 소스 보다는 QTP(Quick Test Professional - 현재는 unified functional testing 으로 제품명 변경)나 SilkTest 같은 상용 자동화 솔루션을 사용해서 많이 진행 했었다. 그 당시의 셀레늄은 거의 java 베이스로 사용하는게 대세였고, 딱히 브라우저 호환성도 적절하진 않아서, 내장된 자바스크립트 베이스의 API 를 포기하고 webdriver 라는 현재 통합된 오픈소스 모듈과 통합을 진행 하던 시기였다. 그때는 사실 해당 모듈을 지금 같은 일반적인 웹 접근 용도로 쓴다는 것은 상상을 못했었고, 상용툴 만큼 잘 동작 하지 못해서 아쉬울 뿐 이였다. 그런 연유로 python 공부를 하면서 다시 selenium 이라는 툴을 만나게 되서 많이 반가웠다. 셀레늄 변화에 대한 자세한 히스토리는 아래의 링크를 참조한다.    

http://www.seleniumhq.org/docs/01_introducing_selenium.jsp#brief-history-of-the-selenium-project

 

  현재 셀레늄은 우리나라 에서 주로 테스트 자동화 툴로 소개되거나, 크롤링 중 특정 문제(로그인)에 대해서 PhantomJS 브라우저 등을 이용하여 해결하는 용도로 소개되는 듯 하지만, 조금 더 범용적으로 접근하면 브라우저에서 반복적으로 하는 일을 자동화 할 수 있는 킬러 툴이 되는것 같다. 윈도우즈 쪽 자동화 모듈은 아직 살펴 보지 못했지만, 괜찮은 윈도우즈 자동화 모듈이 있다면 셀레늄과 조합해서 파이썬을 통해 사용하면 좋은 시너지가 날 듯 싶다.

 

 

 

  그럼 셀레늄 코드를 만들기 전에, 셀레늄을 어떻게 바라보면 좋은지에 대해서 간단히 얘기하려 한다(개인적으로 이렇게 생각하는 거고, 각자의 뷰에 따라 다르게 해석해도 괜찮다). 셀레늄은 크게 3개의 부분으로 나눠진다고 보면 된다. 

 

  1 번째는 원래 의도된 용도였던 UI 자동화 테스트를 위해서 Testcase 및 Testsuite(테스트 케이스를 그룹 지은 것) 관리 기능이 있다. UI 자동화 테스트를 제작하는 사람들은 해당 라이브러리들을 이용하여 케이스를 관리하고 실행한다. NUnit 프레임윅과 같은 역활이라고 보면 된다.

 

 

  2 번째는 검증(verification) 부분이다. 실행 결과로 나온 값이 예상했던 값과 같은지 체크하는 것이다. 1, 2 번 기능은 테스팅 영역에 가까우며 관련된 문서 경로는 아래와 같다.(단 해당 기능을 잘 이해 한다면 자동화 작업들을 관리하고 검증하는데 잘 사용할 수도 있을 것이다)

http://www.seleniumhq.org/docs/

 

 

  3 번째인 웹 드라이버(webdriver) 부분이 파이썬 자동화에서 보통 관심을 가지고 이용하는 부분이다. 웹 드라이버는 위에서 설명한 센서 API 및 기능 API 의 집합이라고 보면 되며, 전반적인 사용법은 이전 시간에 소개했던 beautifulsoup 과 유사하다(문법이 같다라기 보다 개념이 같다). 이 얘기는 결국 웹 드라이브를 잘 쓰려면, 웹 구조에 대해서 전반적으로 잘 이해하고 있어야 한다는 말이다. 웹 드라이버는 윈도우 장치 드라이버랑 유사하게, 브라우저에 상관없이 같은 코드로 돌아가게 되어있다. 즉 최초 웹 드라이버를 불러오는 부분에서 특정 브라우저를 지정하면 이후 나머지 코드는 (이상적으로는) 공통 적으로 사용할 수 있다. 윈도우에서 웹 드라이브는 보통 exe 형태의 실행파일로 제공되어 셀레늄과 브라우저를 연결 해주는 역활을 한다. 아래의 코드와 같이 초기 브라우저 지정만 선언하면 뒤의 코드는 모두 공통되게 돌아갈 수 있다는 개념이다. 그래서 web 'driver' 라고 하나 보다.

1
2
3
4
5
6
7
8
9
#-*- coding: utf-8 -*-
from selenium import webdriver
browser = webdriver.Firefox()
#browser = webdriver.PhantomJS()
#browser = webdriver.Ie()
 
type(browser)
 
browser.get("https://www.google.com")
cs

 

 

  4 번째로 이러한 자동화 라이브러리 들을 사용해 호출하는 언어가 파이썬 이다. 파이썬은 문법 요소를 기술할 뿐만 아니라, 자동화 모듈 이외의 다른 여러가지 유틸 모듈들을 조합하여, 자동화 구성을 풍성하게 만들어주는 기능 API 역활도 지원하게 된다(이건 나중에 얘기하겠지만 머신러닝에서도 같은 역활을 한다). 결국 셀레늄이라는 것은 자동화를 위한 라이브러리 묶음이다. 조금 더 공식적으로 얘기하면 UI 자동화 테스팅용 프레임윅이라고 볼수 있다. firefox 에서 selenium IDE 를 설치하게 되면 사용자의 액션을 자동으로 레코딩 하는 기능도 가지고 있는데, 큰 기대는 하지말고, 초기 코드를 만드는 용도로 사용해도 된다. 이번 시간에는 수동으로 코드를 만들며 레코딩 기능은 아래의 블로그를 참조하자. 

http://jeen.tistory.com/entry/Selenium-%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%90%EB%8F%99%ED%99%94%EC%99%80%EB%8A%94-%EB%B3%84-%EC%83%81%EA%B4%80%EC%97%86%EB%8A%94-%EC%9E%91%EC%97%85%EC%9D%84-%EC%9C%84%ED%95%B4

 

 

  위에서 얘기한 그림을 정리해 보면 아래와 같다.

 

 

 

 

 

[Firefox 를 이용한 구글 검색 후 Top 5 링크 새 탭에 열기]

 

  먼저 가장 셀레늄하고 궁합이 잘 맞는다고 여겨지는 파이어폭스 브라우저를 설치해서 자동화 코드를 만들어 보려 한다.  만들려는 기능은 첨에도 얘기했지만, "구글에서 특정 키워드를 검색하여, 상위 5개의 검색 결과의 링크를 새로운 탭들을 열어 각각 로딩" 하려고 한다. 먼저 우리가 해야하거나 모르는 작업들을 나열해 보자

  1) 파이어 폭스 설치

  2) 셀레늄 설치

  3) 파이썬에서 셀레늄을 이용해 동작하는 샘플 검증

  4) 구글을 열어 검색어 날리기

  5) 검색 결과 링크 중 상위 5개를 얻기

  6) 브라우저에 탭을 열어서 얻은 링크 5개를 각각 로딩하기

 

 

 

[Firefox 설치하기]

  먼저 firefox 를 설치해 보자. 구글에서 'firefox' 라고 검색해 본다. 제일 위의 파이어 폭스 링크로 이동한다.

https://www.mozilla.org/ko/firefox/new/

 

 

  '무료 다운로드' 버튼를 클릭하여 설치를 진행한다. 설치 진행 중 아래의 '가져오기 마법사' 항목이 나오면 굳이 IE 설정을 가져올 필요는 없으니 '아무 것도 가져오기 않기' 를 선택한다.

 

 

  다음에 "기본 브라우저" 설정도 굳이 취향이 아님 firefox 를 기본 브라우저로 쓸 필요는 없으니, 시작할 때마다 확인 체크 박스를 풀고, '나중에' 버튼을 누른다. 그럼 firefox 설치는 완료됬다.

 

 

 

[셀레늄 설치하기]

  다음은 셀레늄 설치 이다. pip 설치가 가능한듯 하니 아래와 같이 pip 설치를 시도해 본다. 정상적으로 설치가 된다.

c:\Python\code>pip install selenium
Collecting selenium
  Downloading selenium-3.3.1-py2.py3-none-any.whl (930kB)
    100% |################################| 931kB 297kB/s
Installing collected packages: selenium
Successfully installed selenium-3.3.1

 

 

 

[샘플 코드 구현]

  그럼 셀레늄을 이용해서 firefox 를 한번 호출해 보자. 구글에서 'python selenium firefox' 라고 검색해서, 맨 위의 공식 문서의 샘플을 참고하여 본다.

http://selenium-python.readthedocs.io/getting-started.html 

 

  해당 예제는 firefox 로 python.org 페이지를 열고 검색어를 입력해서 페이지가 잘 열렸는지 검증을 한다. 전체 코드는 굳이 필요 없으니 python.org 페이지를 여는 부분만 잘라서 가져오자. 호출하는 사이트도 구글로 바꾼다.

1
2
3
4
5
6
#-*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
 
driver = webdriver.Firefox()
driver.get("https://www.google.com")
cs

 

  c:\python\code 폴더에 selenium_1st.py 라고 저장하고 실행을 해본다(2교시 참고)

c:\Python\code>python selenium_1st.py
Traceback (most recent call last):
... 생략

os.path.basename(self.path), self.start_error_message)
selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.

 

 

  그런데 불행하게도 에러가 난다. geckodriver 라는 파일이 패스 경로에 없다고 한다. 해당 파일은 위에서 얘기한 webdriver 기능을 사용하기 위한 exe 형태의 실행 파일이 없어서이다. 구글에 'Message: 'geckodriver' executable needs to be in PATH.' 라고 검색한다. 아래 스택 오버 플로우 링크를 보면 파일을 다운받아 path 가 지정된 경로에 복사하라는 안내가 있다. 리눅스 기준 설명이지만, 해당 파일이 결국 path 경로 안에만 들어있음 셀레늄에서 알아서 참조해 실행 하나 보다.

http://stackoverflow.com/questions/40208051/selenium-using-python-geckodriver-executable-needs-to-be-in-path

 

this steps SOLVED for me on ubuntu firefox 50.

1.Download geckodriver
2.Copy geckodriver in /usr/local/bin

 

 

  링크된 깃허브 경로로 이동하여(https://github.com/mozilla/geckodriver/releases) 제공된 여러 파일 중 현재 환경(윈도우10, 64bit)에 맞는 'geckodriver-v0.15.0-win64.zip' 파일을 다운 받는다. 해당 압축 파일안에는 'geckodriver.exe' 파일이 하나 달랑 들어 있는데, 뭐 특정 폴더를 만들어 넣고 시스템 path 등을(path 에 대해선 파이썬 설치하는 부분에서 설명했다) 걸거나, 기존 패스가 지정되 있는 c:\windows\system32 폴더 같은데 복사해도 되지만, 일을 단순하게 만들기 위해서 파이썬 소스가 있는 경로인 c:\python\code 폴더에다 압축을 푼다.

 

  이후 아까 에러난 코드를 다시 실행해 본다.

c:\Python\code>python selenium_1st.py

 

  조금은 신기하게도 아래와 같이 파이어 폭스가 뜬 후 구글 페이지를 로딩해 준다.

 

 

 

 

 

[구글 검색해 상위 5개 링크 가져오기]

  그럼 구글에서 상위 5개 링크를 가져오는 코드를 만들어 보자. 검색 결과에서 링크를 얻기 위해서 구글에 'selenium get google results python' 라고 검색 하자. 아래의 스택 오버플로우 코드를 참고하면, 결과 중 첫번째 항목(results[0])을 참고하여 링크를 가져온다.

http://stackoverflow.com/questions/35241230/how-to-extract-a-google-links-href-from-search-results-with-selenium
1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver
 
driver = webdriver.PhantomJS()
driver.get("https://www.google.com/search?q=test")
 
results = driver.find_elements_by_css_selector('div.g')
link = results[0].find_element_by_tag_name("a")
href = link.get_attribute("href")
 
import urlparse
print(urlparse.parse_qs(urlparse.urlparse(href).query)["q"])
cs

 

 

 

  해당 코드를 firefox 웹 드라이브를 사용하도록 앞에서 만든, selenium_1st.py 파일과 적절히 합치면, 아래의 코드가 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
 
# 파이어 폭스 드라이버 로딩
driver = webdriver.Firefox()
 
# 구글 페이지에서 '파이썬 공부' 라고 검색해 옴.
driver.get("https://www.google.com/search?q=파이썬 공부")
 
# 검색 된 div 태그 안에 들은 링크 들을 가져옴
results = driver.find_elements_by_css_selector('div.g')
 
# 그중 첫번째 링크 안에서 a 태그를 찾음
link = results[0].find_element_by_tag_name("a")
 
# a태그 안에서 href 라는 인자를 가져옴 
href = link.get_attribute("href")
 
# 인자안에서 q 에 해당되는 값을 가져옴.
import urlparse
print(urlparse.parse_qs(urlparse.urlparse(href).query)["url"])
cs

 

 

  위의 링크를 가져오는 코드의 내용을 이해하기 위해서, IE의 개발자 도구 기능을 한번 사용해 보도록 하자(선호하는 다른 브라우저의 개발자 도구를 이용해도 된다). 아래의 화면에 있는 대로 구글에서 '파이썬 공부' 라고 검색 후,  첫번째 링크를 기준으로 요소 검사를 해서, 위에 있는 python 코드와 개발자 도구에서 보여주는 소스코드 내용을 비교해 놓았다. 자세히 보면 class 속성이 'g' 인 'div' 태그들을 찾은 후(페이지 전체를 분석해 보면 이 'g' 속성을 가진 'div' 태그가 각 링크 결과를 하나씩 감싸고 있을 것이다), 이후 해당 'div' 태그 안에서 다시 'a' 태그를 찾은 후, 이후 찾은 'a' 태그 안에서 다시 'href' 속성을 찾은 후 해당 url 형태의 결과 값에 대해 'urlparse' 라는 라이브러리를 이용하여, 'url' 이라는 인자 요소만 추출하게 되는 구조이다. 그 추출된 값이 바로 실제 링크된 페이지인 'http://ngee.tistory.com/263' 일 것이다. (개발자 도구를 사용하는 방법은 11교시에 설명했다)

 

  그럼 해당 파일을 c:\python\code 폴더에 selenium_2nd.py 로 저장하여 실행해 본다.

c:\Python\code>python selenium_2nd.py
  File "selenium_2nd.py", line 8
SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xc6 in position 0: invalid continuation byte

 

 

  근데 예전에 whois api 구현하면서 한번 본 적이 있는 유니코드 에러가 난다. 구글 호출 인자에 한글 검색어('파이썬 공부')를 명시한 부분 때문에 그런가 보다. 그 때 해결했던 것과 동일하게 파일을 다른 이름으로 저장 하면서 utf-8 형식으로 저장한다. 이후 다시 실행 한다.  

c:\Python\code>python selenium_2nd.py
Traceback (most recent call last):
  File "selenium_2nd.py", line 16, in <module>
    import urlparse
ImportError: No module named 'urlparse'

 

 

  이젠 구글 페이지까진 잘 뜨는데 urlparse 라는 모듈이 없다고 에러가 난다. 아마도 코드가 파이썬 2.x 대 기준이라 그런 듯 싶다. 파이썬 3 에서 사용하는 url parsing 방식을 찾기 위해서 구글에 'get some argument from url urlparse python' 이라고 찾는다. 첫번째 스택오버 플로우 페이지를 보는데, 아래와 같은 눈에 띄는 항목이 있다.

http://stackoverflow.com/questions/5074803/retrieving-parameters-from-a-url

 

There is a new library called furl. I find this library to be most pythonic for doing url algebra. To install:
pip install furl

 

Code:
from furl import furl
f = furl("/abc?def='ghi'")
print f.args['def']

 

 

  코드를 보니 전체 적인 기능은 어떨지 모르지만 현재 사용하려는 목적으로는 urlparse 보다 간략해 보인다. 그럼 위에 적힌 데로, 모듈을 설치한다.

c:\Python\code>pip install furl
Collecting furl
  Downloading furl-0.5.7.tar.gz
  Running setup.py install for furl ... done
Successfully installed furl-0.5.7 orderedmultidict-0.7.11

 

  정상적으로 설치가 됬다. 그럼 해당 코드를 이용해서 아래와 같이 기존 코드를 수정하여 다시 실행해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#-*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
 
# 파이어 폭스 드라이버 로딩
driver = webdriver.Firefox()
 
# 구글 페이지에서 '파이썬 공부' 라고 검색해 옴.
driver.get("https://www.google.com/search?q=파이썬 공부")
 
# 검색 된 div 태그 안에 들은 링크 들을 가져옴
results = driver.find_elements_by_css_selector('div.g')
 
# 그중 첫번째 링크 안에서 a 태그를 찾음
link = results[0].find_element_by_tag_name("a")
 
# a태그 안에서 href 라는 인자를 가져옴 
href = link.get_attribute("href")
 
# 인자안에서 q 에 해당되는 값을 가져옴.
from furl import furl
= furl(href) 
print (f.args['url'])
cs

 

c:\Python\code>python selenium_2nd.py
Traceback (most recent call last):
... 생략
    raise KeyError(key)
KeyError: 'url'

 

 

  그런데 이상하게도, 아까 개발자 도구상으로는 분명이 존재했던 'url' 인자가 없다고 나온다. 왜 그런지 원인을 찾기 위해서 아래와 같이 소스를 수정해 href 값을 화면에 출력해 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#-*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
 
# 파이어 폭스 드라이버 로딩
driver = webdriver.Firefox()
 
# 구글 페이지에서 '파이썬 공부' 라고 검색해 옴.
driver.get("https://www.google.com/search?q=파이썬 공부")
 
# 검색 된 div 태그 안에 들은 링크 들을 가져옴
results = driver.find_elements_by_css_selector('div.g')
 
# 그중 첫번째 링크 안에서 a 태그를 찾음
link = results[0].find_element_by_tag_name("a")
 
# a태그 안에서 href 라는 인자를 가져옴 
href = link.get_attribute("href")
 
# 프린트 해봄
print(href)
cs

 

c:\Python\code>python selenium_2nd.py
http://ngee.tistory.com/263

 

 

  헐 근데 특이 하게도, 개발자 도구로 소스상에서 본 href 는 분명 '/url?.........' 하는 구글을 기준으로 한 긴 링크 문장이였는데, 실제 firefox 웹드라이브에서 참조해 온 값은 이미 해당 url 이 실행되어 해석된, 최종 url 경로를 가져오게 된다(나중에 PhantomJS 에서도 보겠지만 거기는 또 가져오는 값이 조금 틀리다). 이 부분이 예전의 beautifulsoup 을 사용할 때와 틀린 부분 중 하나이다. 해당 beautifulsoup은 정적인 페이지를 대상으로 했기 때문에, 항상 소스에 기반해 일정한 값이 나오지만, 웹 드라이브의 경우 드라이브를 만든 곳에 따라서(ie, firefox, chrome, phantomjs) 구현 방식이 틀릴 수 있기 때문에, 각각 다른 동적인 시점에 따라 상이한 결과 값을 리턴해 줄수도 있다. 이게 어찌보면 'one source multi use' 를 지향하는 셀레늄의 현실적인 한계 부분 일지도 모른다(웹 드라이버는 만드는 단체가 각각 다를 수 있기 때문에 입출력 인터페이스는 동일 하지만 실제 얻어오는 값이나, 동작에는 조금씩 차이가 날 수 있다. 브라우저 특성도 영향을 줄 것이고 말이다). 여튼 이렇게 되면 굳이 설치한 furl 을 이용해서 한번더 주소를 가져올 필요가 없어진다. (나중에 PhantomJS 에 사용할 테니 굳이 삭제 하진 말길...)

 

 

 

[새로운 탭에 링크 URL 로딩하기]

  여튼 그럼 마지막으로 새로운 탭을 열어 특정 웹 주소를 여는 방식을 알아보자. 구글에 'selenium open link in new tab python' 이라고 검색한다. 아래 스택오버플로우 페이지를 보면 ctrl+t 키를 눌러서 새탭을 열고, 웹 주소를 해당 탭에 로딩하는 코드가 있는데, 마지막에 간단한 코드가 하나 눈에 뜨인다. 

http://stackoverflow.com/questions/28431765/open-web-in-new-tab-selenium-python

 

browser.execute_script('''window.open("http://bings.com","_blank");''')

 

  위의 코드가 정상적으로 돌아 간다면 좋을 것 같다(확인 결과 다행히 firefox 는 기본 동작 옵션이 _blank 로 새창을 열면 새 탭으로 열어준다. 넘 시행 착오 코드가 많아서 줄임 차원에서...). 실제 최종 확인 결과 어떤 사유인지는 모르지만 최근 브라우저 들에서는 아래 제시된 예제들과 같이 웹드라이버에서 ctrl + t 키를 body 태그에 날려도 새 탭이 안 생기는 거 같다. 그래서 결국 위의 코드를 쓰게 되었다.

https://gist.github.com/lrhache/7686903

 

 

 

[최종 코드 완성하기]

   앞에서 해결된 코드들을 합쳐, 제어 로직을 추가하여 최종 완성된 코드는 아래와 같다.

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
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# firefox 웹 드라이버를 연다 
browser = webdriver.Firefox()
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com/search?q=파이썬 공부")
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 화면에 그냥 프린트 해봄
for href in hrefs:
   print(href)
 
# 각 링크에 대해서 새 탭에서 연다
for href in hrefs:
   browser.execute_script('window.open("' + href + '","_blank");')
cs

 

  그럼 해당 파일을 c:\python\code 폴더에 selenium_firefox.py 로 저장하여 실행해 본다.

c:\Python\code>python selenium_firefox.py
http://ngee.tistory.com/263
https://nolboo.kim/blog/2014/08/10/the-best-way-to-learn-python/
http://analyticsstory.com/1
http://www.boxnwhis.kr/2016/03/25/how_to_be_a_developer_as_a_statistician.html
https://wikidocs.net/43

 

  아래와 같이 파이어 폭스 브라우저가 뜨면서 5개의 결과가 탭들에 담긴다. 

 

 

 

 

 

 

 

[팬텀JS(PhantomJS) 브라우저로 같은 작업 하기]

  위에 firefox 웹 드라이버로 했던 부분을 비교해보는 의미가 있을 것 같아서, 이번엔 PhantomJS 브라우저를 이용해 해보려고 한다. PhantomJS 는 화면에는 없고 메모리 상에만 존재하는 브라우저 같다. 웃긴건 스크린 캡쳐는 가능하다(다행히 이거라도 있어서 에러가 발생할 때 확인이 수월하다). 대신에 실제로 브라우저가 화면 상에 표시되어 움직이지 않기 때문에, 자동화 코드를 실행하면서도 비교적 해당 컴퓨터에서 다른 작업을 자유롭게 할수 있을 것 같고(기존 브라우저 베이스로 자동화 코드를 실행해 본 사람들은, 해당 실행이 끝나기 전에 컴퓨터를 사용 못하는 경험을 겪어봤을 것이다), 실제 화면에 모든 개체를 표시되지 않기 때문에 부하나 외부 환경에 대한 에러 요소도 좀 적을 것 같다. 단점은 해당 브라우저로 만든 코드가 다른 브라우저와 호환성을 100% 보장한다고는 할수 없을 것 같다. webkit 베이스 이기 때문에 사파리나 스마트폰 용 브라우저와는 어느정도 호환성이 있다고 볼 수 있을 거 같다.

 

 

[샘플 코드 구현]

  그럼 구글에 'python use PhantomJS' 라고 검색한다. 아래의 스택 오버플로우 페이지를 확인해 보면, 다른 부분은 동일하고, 처음에 웹 드라이브 정의 하는 부분만 아래와 같이 바꾸면 되는 것 같아 보인다.

http://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python

 

1
driver = webdriver.PhantomJS()
cs

 

 

  그럼 해당 방식으로 아까 만든 코드를 가져와 실행해 본다. 탭을 띄우는 부분만 굳이 의미 없을것 같으니 제외 시키고, url 만 프린트 하고 종료되게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# firefox 웹 드라이버를 연다 
browser = webdriver.PhantomJS()
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com/search?q=파이썬 공부")
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 화면에 그냥 프린트 해봄
for href in hrefs:
   print(href)
cs

 

  그럼 해당 파일을 c:\python\code 폴더에 selenium_phantom.py 로 저장하여 실행해 본다.

c:\Python\code>python selenium_phantom.py

Traceback (most recent call last):  .....
      os.path.basename(self.path), self.start_error_message)
selenium.common.exceptions.WebDriverException: Message: 'phantomjs.exe' executable needs to be in PATH.

 

 

  역시 한번에 될리가 없고 무언가 에러가 난다. 하지만 이미 firefox 일때 웹드라이버 파일이 경로에 필요했던 부분을 겪어 봤으니, 이번엔 좀더 쉬울듯 하다. 구글에서 "Message: 'phantomjs' executable needs to be in PATH windows" 로 검색한다.

http://stackoverflow.com/questions/37903536/phantomjs-with-selenium-error-message-phantomjs-executable-needs-to-be-in-pa

 

  아래의 내용이 눈에 띈다

you need to download the DRIVER

after that session = webdriver.PhantomJS("c:\driverPath")

 

 

  해당 글에 명시된 http://phantomjs.org/ 페이지로 이동하여 'Download Ver 2.1(버전은 틀려질 수 있을 듯) 버튼을 클릭 하여, 'phantomjs-2.1.1-windows.zip' 파일을 다운 받는다. 이전같이 같은 폴더에 복사하거나, 또는 path가 지정되 있는 폴더에 복사하거나, 또는 특정 폴더에 복사후 시스템 path를 지정해도 좋지만, 조금 다른 방식을 보여주기 위해서 c:\python 폴더에 압축을 풀고(phantomjs-2.1.1-windows 폴더로 풀린다), Phantomjs 호출하는 코드 부분을 아래와 같이 조금 수정한다(어느 페이지에서 본 코드 인지는 잘 생각 안난다;).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# 수정된 코드 
phantomjs_path = r'c:\Python\phantomjs-2.1.1-windows\bin\phantomjs.exe'
browser = webdriver.PhantomJS(phantomjs_path)    
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com/search?q=파이썬 공부")
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 화면에 그냥 프린트 해봄
for href in hrefs:
   print(href)
cs

 

  해당 내용을 selenium_phantom.py 에 엎어쓰고 다시 실행해 본다.

c:\Python\code>python selenium_phantom.py
Traceback (most recent call last):
  File "selenium_phantom.py", line 19, in <module>
    link = results[i].find_element_by_tag_name("a")
IndexError: list index out of range

 

 

  ※ 아마 실행 전에 아래와 같은 윈도우즈 방화벽 허용 창이 뜰것이다. 허가되지 않은 프로그램이 외부랑 통신을 할까봐 그러는 것으로 같다. 실행시 필수이니 '엑세스 허용'하고, 다시 프로그램을 실행 시켜야 한다.  

 

 

 

[에러 원인 확인 하기]

  근데 브라우저 실행까진 잘 됬는데 엉뚱하게 'a' 태그가 없다고 나온다. 화면이 보이지도 않으니 어떤 일이 일어났는지 알수 없으니 에러가 나기 전 코드 위치에서 스크린 샷을 찍어 본다.

 

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
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# firefox 웹 드라이버를 연다 
phantomjs_path = r'c:\Python\phantomjs-2.1.1-windows\bin\phantomjs.exe'
browser = webdriver.PhantomJS(phantomjs_path)    
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com/search?q=파이썬 공부")
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# 디버그용 스크린 샷
browser.save_screenshot('screen.png')
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 화면에 그냥 프린트 해봄
for href in hrefs:
   print(href)
cs

 

 

  소스와 같은 폴더에 저장된 screen.png 파일을 열어보니, 글자가 ??? ?? 나오는거 보면 PhantomJS 는 url 에 한글을 포함해 보내면 제대로 처리를 못해주는 것 같다. --;

 

 

  그럼 어떻게 내부에서 인코딩을 처리하는지 모르는 상태에서(물론 홈 페이지에서 전체 소스를 제공해 주긴 하지만) 뭔가 인코딩 부분을 건드리긴 무섭고, url 에 넣는 방식이 아닌 한글 검색어를 입력 후 한글 검색어를 넣고 검색버튼을 누르는 방향으로 전환해 보며 한글 처리를 잘 하길 기대해 보자. 구글에 'selenium google search python' 라고 검색한다. 처음 링크로 나오는 github 페이지를 참고한다.

https://gist.github.com/azam-a/32b89944b98a3fd79d44ebfdac16b63d#file-google-py

http://stackoverflow.com/questions/24598648/searching-google-with-selenium-and-python

 

 

  해당 움직이는? 방식으로 코드를 수정하면 아래와 같다.

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
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# firefox 웹 드라이버를 연다 
phantomjs_path = r'c:\Python\phantomjs-2.1.1-windows\bin\phantomjs.exe'
browser = webdriver.PhantomJS(phantomjs_path)    
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com")
input_element = browser.find_element_by_name("q"
input_element.send_keys("파이썬 공부"
input_element.submit() 
 
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# 디버그용 스크린 샷
# browser.save_screenshot('screen.png')
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 화면에 그냥 프린트 해봄
for href in hrefs:
   print(href)
cs

 

c:\Python\code>python selenium_phantom.py
https://www.google.co.kr/url?q=http://ngee.tistory.com/263&sa=U&ved=0ahUKEwi8sJba-_bSAhVEzbwKHQfuD5oQFggZMAA&usg=AFQjCNFNP7xOQT4RepCgxW-lSoAIucugwA
https://www.google.co.kr/url?q=https://nolboo.kim/blog/2014/08/10/the-best-way-to-learn-python/&sa=U&ved=0ahUKEwi8sJba-_bSAhVEzbwKHQfuD5oQFgggMAE&usg=AFQjCNFtcAIvPG3wUHOowvFHpgBjf7XQsw
.. 뒤에 3개는 생략

 

 

[웹 자동화의 타이밍 문제]

  흠 그런데 PhantomJS 코드를 처음 만들어 실행 했을 때 났던 에러가 지금은 안 나서 조금 당황스러운 상태다. --;

 

  어떤 일이 발생 할  수 있냐면, 코드 중에서 처음 find_elements_by_css_selector('div.g') 찾는 부분에서 원래 에러가 났었다. 그 이유는 browser.get() 함수는 아마 페이지 전체가 로딩될때 까지 대기 후, 다음 코드가 실행되지만, 위와 같이 submit 버튼으로 전송한 경우는 버튼을 누른 순간 코드가 끝났기 때문에 다음 코드가 실행 됬을때, 페이지가 아직 로딩 중일 수 있기 때문에, find_elements_by_css_selector('div.g') 코드에서 아직 페이지에 div.g 태그가 로딩되지 않아서 에러가 발생할 수 있다. 그래서 그것을 보여주며 타이밍 이슈에 대해서 얘기하려 했는데 재현이 안 된다. 보통 사용의 UI 자동화 솔루션들이 이러한 태그나 속성 요소를 찾는 함수에 기본으로 지정가능한 '대기시간'이 있어서, 해당 부분의 요소가 없더라도 '대기시간'동안 계속 해당 요소를 반복해서 찾아 웹페이지에 로딩에 의한 타이밍 에러를 줄여준다.

 

 

  셀레늄에도 그러한 타이밍 이슈를 위한 기능들이 있는데, 보통 2가지 방식으로 해결한다. 1 번째는 쉬는 시간을 무조건 지정하는 것이다. 하지만 경험상 시간을 충분히 주더라도 특정 경우에 타이밍 에러가 날 수도 있도,  또 이미 해당 부분이 로딩 됬더라도 무조건 기다리게 되어 실행 시간이 많이 걸리기 때문에 그다지 바람직한 방식은 아니다.  

 

  2 번째로 페이지 로딩이 충분이 됬는지를 특정 엘리먼트를 계속 확인하여 기다리는 것이다. 이 경우 해당 엘리먼트를 찾게되면 바로 다음 단계로 넘어가고, 아니면 지정된 시간이 지나서 에러를 내게 된다. 이런 코드를 만들 경우 현재 참조 하려는 태그나, 페이지에서 가장 마지막에 로딩 될것 같은 태그를 페이지 구조를 분석하여 분석하여 지정해야 한다. 이것 역시 쉬운 작업은 아니지만, 이렇게 구현하면 빠르고, 정확한 코드가 된다. 그런 면에서 상용 솔루션이 이런 부분에서 조금 더 유연성은 있을 것 같다.  아마 7년전의 지식 기준이니 지금은 좀 더 해당 부분이 강화되었을 것 같긴 한다. 두 가지 방식에 대한 상세한 얘기는 밑의 링크를 참고하고, 여기서는 쉽게 가려고 sleep 함수를 2초간 사용하려 한다. (firefox 같은 실제 브라우저를 사용하는 코드에서는 조금 더 시간을 주는게 좋다)

http://stackoverflow.com/questions/26566799/selenium-python-how-to-wait-until-the-page-is-loaded 

1
2
import time
time.sleep(2)
cs

 

 

  또 해당 결과에서 하나 걸리는 것은 firefox 와 비교되는 최종 url 을 얻는 부분인데, firefox 에서는 'href' 에 '최종 url' 이 나왔는데, PhantomJS 에서는 아래와 같이 정적 소스와도 좀 상이한(원본 소스는 'url' 인자에 링크가 있었는데 여기 'q' 인자임) 최종 url 이전의 링크가 나온다.

https://www.google.co.kr/url?q=http://ngee.tistory.com/263&sa=U&ved=0ahUKEwi8sJba-_bSAhVEzbwKHQfuD5oQFggZMAA&usg=AFQjCNFNP7xOQT4RepCgxW-lSoAIucugwA

 

  이 링크를 띄워도 최종 url 이 표시되긴 하지만, 굳이 구글을 거쳐서 실행 되는게 찜찜해서 firefox 와 동일하게 나오게 하기 위해서 아까 사용하려다 만 furl 을 사용하려 한다.  

 

Code:
from furl import furl
f = furl("/abc?def='ghi'")
print f.args['def']

 

 

 

[최종 코드]

  모듈 설치는 이미 했으니 해당 코드를 반영하여 최종 PhantomJS 코드를 만들면 아래와 같다.

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
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# firefox 웹 드라이버를 연다 
phantomjs_path = r'c:\Python\phantomjs-2.1.1-windows\bin\phantomjs.exe'
browser = webdriver.PhantomJS(phantomjs_path)    
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com")
input_element = browser.find_element_by_name("q"
input_element.send_keys("파이썬 공부"
input_element.submit() 
 
import time
time.sleep(2)
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# 디버그용 스크린 샷
# browser.save_screenshot('screen.png')
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# href 에서 q 인자를 추출해 옴   
from furl import furl
for href in hrefs:
   f = furl(href) 
   print (f.args['q'])
cs

 

  해당 내용을 selenium_phantom.py 에 엎어쓰고 마지막으로 실행해 본다. 원하는 결과가 나온다.

c:\Python\code>python selenium_phantom.py
http://ngee.tistory.com/263
https://nolboo.kim/blog/2014/08/10/the-best-way-to-learn-python/
... 뒤의 3개 생략

 

 

 

 

 

[부록 - IE (Internet explorer) 로 같은 작업 하기]

  마지막으로 자주 사용하는 IE 브라우저를 무시하긴 그래서 IE 코드도 하나 넣으려 한다. 결론 부터 말하자면 생각보다 실행을 위해 해야 될 귀찮은 일들이 좀 많고, 실행 해 보면 firefox 보다 상당히 느리게 동작한다(물 안에서 움직이는 느낌이라고 할까?). 그래서 가능한 동작을 꼭 IE 브라우저에서 할 필요가 없으면 firefox 나 해보진 않았지만 크롬에서 해보는게 어떨까 싶다(구글에서 webdriver 를 만드는 것 같으니 최적화는 잘되지 않았을까 싶어서). 아니면 화면을 꼭 봐야될 필요가 없다면 PhantomJS 도 괜찮고 말이다. 앞에서 같이 경험 목적으로 반복해서 시행착오 과정을 굳이 보여줄 필요는 없을 것 같아서, 실행에 필요한 세팅들을 안내 후, 실행 결과를 보여주고 마무리를 하려 한다.

 

 

[웹드라이버 세팅]

http://stackoverflow.com/questions/24925095/selenium-python-internet-explorer

http://selenium-release.storage.googleapis.com/2.42/IEDriverServer_x64_2.42.0.zip

  다운로드 후 c:\python 폴더에 c:\Python\IEDriverServer_x64_2.42.0\ 로 압축 해제를 하면 된다. 코드내에 경로를 반영하기가 귀찮다면 c:\python\code 에 직접 exe 파일을 복사후, browser = webdriver.Ie() 로 소스를 바꾸어 호출해도 된다.  

 

 

[IE 보안 설정]

  Unexpected error launching Internet Explorer. Protected Mode settings are not the same for all zones 에러 발생함.

 

  인터넷 옵션 > 보안 탭 > 아래 4개 영역에 대해서 보호 모드 사용이 꺼져 있어야 한다. 테스트 완료 후 인터넷 쪽은 꼭 원래대로 복원하길 바란다.

 

 

[IE 확대/축소 사이즈 설정]

  Message: Unexpected error launching Internet Explorer. Browser zoom level was set to 125%. It should be set to 100% 에러 발생.

 

  확대 설정이 100% 여야만 동작한다(IE 브라우저는 왜 이런 설정까지 맞춰야 하는지 싶다 --;)

 

 

[방화벽 설정]

  PhantomJS 와 비슷하게 방화벽 혀용을 묻는 창이 뜨면 허용을 해주고 다시 코드를 실행 해주어야 한다.

 

 

 

[최종 코드]

  세팅은 좀 달랐지만 최종 정리한 코드는 브라우저 호출 부분만 바뀌었다.

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
#-*- coding: utf-8 -*-
from selenium import webdriver
 
# IE 웹 드라이버를 연다 
ie_path = r'c:\Python\IEDriverServer_x64_2.42.0\IEDriverServer.exe'
browser = webdriver.Ie(ie_path)    
 
# 구글에 '파이썬 공부'로 검색어 조회
browser.get("https://www.google.com")
input_element = browser.find_element_by_name("q"
input_element.send_keys("파이썬 공부"
input_element.submit() 
 
# 시간 좀 늘임
import time
time.sleep(5)
 
# 결과 div 태그를 클래스 기준으로 검색해 다 가져옴.
results = browser.find_elements_by_css_selector('div.g')
 
# 인자를 담을 리스트
hrefs = []
 
# 디버그용 스크린 샷
# browser.save_screenshot('screen.png')
 
# div 중 최초 5개를 가져와서, 그 안에서 a 태그를 찾고, a 태그 안의 href 속성을 찾는다. 
for i in range(05):
   link = results[i].find_element_by_tag_name("a")
   hrefs.append(link.get_attribute("href"))
 
# 링크 출력 해보기 
for href in hrefs:
   print(href)
 
# 새 창에 띄움
for href in hrefs:
   browser.execute_script('window.open("' + href + '","_blank");')
cs

 

  selenium_ie.py 로 저장하여 실행하면 아래와 같이 새창으로 뜬다. (firefox 처럼 탭으로 뜨게 하고 싶으면, 현재 버전의 브라우저 들은 코드가 지원하는지 불분명하니 아래 설명한 바와 같이 브라우저 옵션을 바꾸는게 현실적일 것 같다 -> 밑에 코맨트 단 것과 같이 IE 웹 드라이버에서는 ctrl+t, ctrl+click, 팝업 옵션을 바꾸어도 모두 탭으로 열리진 않는다. 현재로서는 새창으로 밖에 안 열리는 것 같다는 결론을 내렸다.)

http://meaningone.tistory.com/52

c:\Python\code>python selenium_ie.py

 

 

 

  마지막으로 웹 자동화 연습용 페이지 링크를 소개한다(사실 실제 사이트 들에서 하면 되서 효율성은 잘 모르겠지만, 구조가 쉬워 연습할때 도움이 되지 않을까도 싶다). 다음 시간에는 윈도우즈 UI 자동화 부분에 대해서 간단한 프로그램 대상으로 진행해 보려 한다.

http://www.techbeamers.com/websites-to-practice-selenium-webdriver-online/

 

 

 

 

2017.3.29 by 자유로운설탕
cs

 

posted by 자유로운설탕
2017. 3. 19. 21:03 프로그래밍

  금번 시간은 html 을 쉽게 파싱(parsing) 할 수 있는, BeautifulSoup 이라는 모듈을 사용하는 방법에 대해서 살펴보고, 지난 시간에 API 를 사용해 호출했던 WhoIS 사이트를 웹 호출을 통해 결과를 받아 파싱하여 결과를 얻는 부분을 간단히 시연하면서 파싱을 할때 생기는 현실적인 이슈들에 대해 얘기를 하려 한다. 크롤링 이라고 부르기엔 좀 조촐한 범위라서 파싱이라고 주제를 정했다.

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  먼저 파싱이란 어떤 작업일까? 원래 언어학에서 먼저 나온 용어 같고, 간단히 얘기하면 문장의 구조를 분석해서, 문법 요소를 뽑아서 구조를 체계화 한다고 할 수 있다. 그렇게 체계화 하면 무엇이 좋아질까? 특히 컴퓨터 분야에서는 주어진 문구들을 체계화 하고 나면 해당 문법요소에 따라서, 원하는 내용들을 추출 할 수 도 있게 된다. 뭐 잘은 모르지만 자동 번역이나, 음성 인식 등 에서도 그렇게 문장 요소를 분류하는 게 먼저 일 거 같다. 

https://ko.wikipedia.org/wiki/%EA%B5%AC%EB%AC%B8_%EB%B6%84%EC%84%9D

 

  자 그럼 html 을 파싱하려면 어떻게 해야 할까? 만약 beautifulsoup 같은 파서가 없다면, 아마 정규 표현식 같은 방법들을 별 수 없이 이용해야 할 것 이다. 그러기 위해 우리는 우선 html 이 어떤 문법 요소를 가지고 있는지 이해해야 한다. 어떤 태그들을 가지고 있고, 어떤 속성을 가지고 있고, 각 태그와 속성의 위치, 상대적 관계, 문서에서의 역활 등에 대해서 이해해야 한다. 그래야 html 문서안에 있는 어떤 필요한 정보 값에 대해서, 그러한 외부 요소들의 관계를 이용해서 콕 지정해서 가져올 수 있을 것이다.

 

  해당 부분은 beautifulsoup 같은 파서를 이용한다고 해도 마찬가지다. 기본적으로 html 구조에 대한 지식이 있어야 브라우저의 개발자 도구 같은 유틸리티 들을 이용해서 살펴 보면서 페이지도 분석하고, 원하는 값을 가져오는 코드도 만들 수 있다. 결국 예전에 얘기했던 프로그램의 외적 요소를 사용하기 쉽게 해주는 모듈들은 많지만, 해당 모듈들을 잘 사용하기 위해서 해당 외적 요소에 대해서 포괄적인 이해를 하는게 유리하다는 것이다.

 

  html 공부에 대해서는 아래 링크의 w3school 같은데서, 하나씩 항목들을 살펴보거나, head first html(헤드 퍼스트 html) 같은 쉽게 설명해 주는 책들을 한 권 정도 보게 되면 해당 모듈을 사용하는 기본 소양은 갖추게 된다고 볼 수 있다. 이 후는 실제 여러 다양한 웹 페이지를 분석해 살펴보거나, 만들어 보는 것이(특히 만들면서 생각한 데로 구현이 안 되서 헤메다 보면, 좀 더 깊이 이해되고 잘 잊혀지지 않게 된다) 최선의 방법이다. 많이 경험하면 할수록 모르면 10분 걸리는 분석이 1분 만에 끝날 수도 있으며, 해당 지식은 앞으로 만나는 어떤 언어, 어떤 모듈에도 비슷한 주제와 형태라면 마찬가지로 사용할 수 있다. 또한 xml 같이 비슷하지만 좀더 엄격한 언어를 공부하거나, javascript 같이 웹의 이런 저런 요소들에 모두 문어발 관계를 가지는 언어를 배울 때 허들을 낮추게 해준다.

https://www.w3schools.com/html/default.asp

 

 

 

 

[Beautiful Soup]

  그럼 BeatutifulSoup 에 대해서 어떻게 접근하면 되는지 간단한 브리핑과 샘플을 실행해 보도록 하겠다. 먼저 'soup' 이란 영어 단어의 뜻 대로 '스프'는 아니고 아마 'soap' 의 패러디 같은게 아닌가 싶다--; 확실히는 모르지만 soap은 http 프로토콜을 이용해서 원격에 있는 서비스를 호출하여 사용하자는 규격이다. 전송을 위한 데이터는 xml 이나 그 사촌 정도인 html 이 사용된다. 그래서 이 모듈을 만든 사람은 html 이나 xml 을 아름답게 잘 파싱하여 원하는 결과를 만들 수 있다고 해서 이름을 붙이지 않았을까 추측한다. 여기서는 순수 웹과 연관된 html 쪽만 살펴본다. 뭐 전문적인 수준의 얘기는 아니니 전체적인 이야기의 맥락만을 이해하고, 위에 얘기한 책이나, 여러 웹 자료들에서 자세한 내용들은 살펴보길 바란다.

https://ko.wikipedia.org/wiki/SOAP 

 

  html 이란 HyperText Markup Language 의 약자이다. 이것은 링크를 가진 태그 형식의 언어라는 의미가 된다. 그럼 html 의 문법 요소는 무엇 일까? 크게 Element(tag), Attribute(id, class, name) 로 나눌 수 있다.  먼저 엘리먼트는 html 모든 태그를 얘기한다고 보면 될 듯 하다. attribute(속성)은 여러가지가 있는데 태그안에 들어간 어려가지 요소로 id, class, name, height, width 등으로 태그의 identity 나 표시 방법을 나타내 준다고 보면 된다. 

 

 

  처음 html 을 쓰다보면 id, class, name 이 조금 헷깔리는데(보통 자바스크립트나, 웹 프로그래밍, css 를 다룰때야 비로소 고민된긴 하지만..), id 는 고유의 태그를 표시하는 용도로 써서 같은 p 태그라도 id="myp" 같이 지정하면 고유한 p 태그가 되는 것이다(어린왕자의 장미라 그럴까?). 중복되는 경우 javascript 등에서 접근시 에러난다곤 하는데(확인 안 해봤다), 중복 된다고 html 의 관대한 특성상 페이지에서 당장 에러가 나는 경우는 없다(beautifulsoup 에서도 id 를 지정할때 중복되도 에러가 안 나고 그냥 다른 attribute 처럼 모두 결과로 가져온다). 이러한 값들은 나중에 beautifulsoup 이나 javascript, css 나아가 여러가지 자동화 라이브러리에서도 사용하게 된다.

 

  name 이 좀 id 와 개념이 헷깔리긴 하나(누가 이름을 지었는지...), 보통 name 은 form 태그 안에서 각 폼 요소들의 종류를 가리킬때 쓰며, 복수 지정이 가능하다(예를 들어 radio 버튼 한 그룹은  type=radio 이고 name 이 myradio 같이 같을 수 있다. 그럼 브라우저나, 자바스크립트는 해당 구조를 해석해서 동일한 그룹의 라디오 버튼이라는 것을 판단해서, 클릭 시 같은 동작시 서로 배타적으로 선택되게 만든다). class 는 보통 css(Cascading Style Sheets) 에서 많이 사용하게 되는데, 같은 이름의 class 를 가진 td 태그를 css 에 지정해서 자동으로 같은 배경색으로 표시한다는지 하는 식으로 디자인 적으로 많이 사용되게 된다. 뭐 하지만 프로그래머가 얼마나 해당 개념들을 헷깔려 하느냐에 따라서, 두서 없이 섞어 쓰는 경우도 종종 있는 듯 하다. id, name, class 의 차이는 아래 링크를 참고한다.

https://css-tricks.com/the-difference-between-id-and-class/
http://stackoverflow.com/questions/1397592/difference-between-id-and-name-attributes-in-html

 

 

  이러한 html 의 많은 요소는 DOM(document object model) 이라는 체계로 표현되고 관리된다. 이것은 html 이나 xml 을 트리형태로 구성하고, 해당 요소들에 대해서 프로그래밍 적으로 접근하거나 조작하는 것을 가능하게 해준다. 우리가 보는 html 문서는 dom 형태로 정리되어 javascript 나 beautifulsoup 같은 모듈에서 접근 할수 있다고 봐도 무방하다.

https://en.wikipedia.org/wiki/Document_Object_Model

 

 

  그럼 beautifulsoup 을 가지고 페이지에서 원하는 개체를 어떻게 찾을까? 구글에서 'beautifulsoup doc' 을 찾아보면 아래의 공식 메뉴얼이 나오는데, 해당 페이지내에 설명된 방식이 사실상 전부라고 보면 된다.

https://www.crummy.com/software/BeautifulSoup/bs4/doc/

 

  우선 아까 얘기한 특정 요소들로 찾을 수 있다. id, class, name, tag name 등을 기준으로 찾아 배열에 담아서 이용할수도 있고(findall), css selector 란 방식으로도 찾을 수 있다(search). 자세한 내용은 아래 링크를 참고 한다.

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#find-all

https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-by-css-class

 

  find_all 은 find_all('a') (<a> 태그를 전부 찾아라), find_all(id="mylink") (id가 mylink 인 태그를 찾아라) 같이 명시적으로 접근하는 편이고, css select 는 css 라는 html 디자인을 나타내는 스타일 언어의 개체 접근 방식을 사용해서 select('a'), select('#mylink') 같이 약간 약어 같이 쓴다.(정규 표현식 필이 조금 난다). 그래서 css selector 형태의 경우 좀더 간결한 듯 하다. 해당 부분은 밑의 링크를 참조하길 바란다.

https://www.w3schools.com/cssref/css_selectors.asp

 

  또한 상대적 위치 또한 이용할 수 있다. 현재 id 가 'mytd' 인 <td> 태그의 부모 역활을 하는 <tr> 태그를 찾는다던지, 특정 <tr> 태그 바로 밑에 있는 동일한 레벨의 <tr> 태그(sibling)를 찾을 수도 있다. 이런 것들은 beautifulsoup 모듈과 html 코드에 대한 전체적인 개념이 잡혔으면, 메뉴얼에서 필요한 부분을 훝어 보면서 어떤 접근 방식이 가능한지 보는게 좋다.(자바스크립트나 css 의 접근 방식과 많이 비슷하다. 어차피 비슷한 목적으로 접근하기 때문에 말이다) 

 

 

 

 

[Beautiful Soup 으로 샘플 파싱 해보기]

  그럼 예제를 하나 보면서 실제 코드를 만들어 보도록 하자. 실제 웹에 있는 페이지 하나를 봐도 좋지만, 그런 페이지들은 보통 복잡한 요소들이 막 믹스되어 있으므로, 주제에 집중을 위해서 하나의 html 코드를 만들어서 원하는 요소를 선택하는 부분을 시연하려 한다. 파일을 읽어 올까도 했지만, beaufilsoup 샘플을 보다보니 변수에 문서 내용을 지정해 시연하는게 좋은 듯 해서, 우선 html 코드를 담을 변수를 만들기 위해서 구글에 'python multi line string' 이라고 검색 한다. 아래의 페이지를 보면 """ 세개로 스트링 앞 뒤를 감싸면 멀티라인을 가진 스트링이 되나 보다.

http://stackoverflow.com/questions/10660435/pythonic-way-to-create-a-long-multi-line-string

 

  그럼 해당 우리가 웹에서 가져오는 데이터와 비슷한 데이터를 담은 (이상적인) 소스를 하나 만들어 본다. 해당 소스는 하나의 테이블로 이루어져 있고, 위 줄에 <th> 태그(테이블 제일 상단 표시) 2개와, 아랫 줄에 <td> 태그 2개와 있다. <tr> 태그는 각각의 행을 나타낸다. <th> 태그는 'choco', 'cookie' 라는 두개의 id 를 각각 가지고 있고, <td> 태그는 위쪽 태그만 'candy' 라는 name 속성을 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
html_doc = """
<table> 
  <tr>     
     <th id = "choco">초콜릿</th>    
     <th id = "cookie">과자</th>  
  </tr>  
  <tr>    
     <td name = "candy">사탕</td>
     <td>오렌지</td>
  </tr>  
</table>
"""
cs

 

  브라우저로 보면 아래와 같은 모양일 것이다. 이해를 돕기 위해 대충 tr, td 태그의 관계적 위치와 id, name 의 위치도 표시했다. (원래 크기가 크진 않고 잘 보이라고 확대 했다 --;)

 

 

  그럼 위의 공식 문서에 안내된 대로 아래와 같이 입력해서 beautifulsoup 를 설치해 보자. 성공적으로 잘 설치 됬다.

c:\Python\code>pip install beautifulsoup4
Collecting beautifulsoup4
  Downloading beautifulsoup4-4.5.3-py3-none-any.whl (85kB)
    100% |################################| 92kB 178kB/s
Installing collected packages: beautifulsoup4
Successfully installed beautifulsoup4-4.5.3

 

 

  예제에는 아래 5가지의 접근 방식을 소개하려 한다. 아래 예를 보고 나머지는 구글이나 공식 문서를 참조해서 이것 저것 해보다 보면 자기만의 감이 올 것 이다.

   1) td 태그를 모두 찾아서 출력 한다. -> '사탕', '오렌지' 가 출력될 것이다.

   2) id 가 'choco' 인 태그를 찾아서 출력한다 -> '초콜릿' 이 출력될 것이다.

   3) td 태그이며, name 이 'candy' 인 태그의 옆에 있는 td 태그를 출력한다 -> '오렌지' 출력.

   4), 5) 앞의 2, 3번과 같지만 css selector 방식을 이용한다 -> 초콜릿, 오렌지 가 출력 된다.

 

  태그에서 내용만 뽑아내는 방법은 'beautifulsoup find_all get text'  로 구글에서 찾았다.

http://stackoverflow.com/questions/16835449/python-beautifulsoup-extract-text-between-element

 

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
#-*- coding: utf-8 -*-
from bs4 import BeautifulSoup
 
html_doc = """
<table border=1> 
  <tr>     
     <th id = "choco">초콜릿</th>    
     <th id = "cookie">과자</th>  
  </tr>  
  <tr>    
     <td name = "candy">사탕</td>
     <td>오렌지</td>
  </tr>  
</table>
"""
 
# html 파서를 이용해서 html 형태의 데이터를 읽어온다.
soup = BeautifulSoup(html_doc, 'html.parser')
 
# 1. td 태그를 모두 찾아서 td_list 담은 후, 루프를 돌리면서 각 td 태그 내용을 출력한다.
td_list = soup.find_all('td')
for td_item in td_list:
  print(td_item.string)
 
print ('\n')
 
# 2. id 가 choco 인 항목을 찾아서 해당 태그 내용을 출력한다. 
id_list = soup.find(id='choco')
print(id_list.string)
 
# 3. td 태그이면서 name 속성이 candy 인 항목('캔디')을 찾아서 
#    그 다음에 있는 같은 td 속성을 찾아서 태그 내용을('오렌지') 출력한다. 
td_list = soup.find('td', {'name':'candy'})
print(td_list.find_next_sibling().string)
 
print ('\n')
 
# 4. 2번과 동일하지만 css selector 방식으로 사용한다.
td_list = soup.select('#choco')
print(td_list[0].string)
 
# 5) 3번과 동일하지만 css selector 방식으로 사용한다.
td_list = soup.select('td[name="candy"]')
print(td_list[0].find_next_sibling().string)
cs

 

  해당 코드를 c:\python\code 폴더에 beautiful.py 라고 저장한다(역시 방법을 모르면 2교시 때 자세히 설명한 게 있으니 본다). 이후 실행 아래와 같이 시켜본다. 그런데 에러가 났다.

c:\Python\code>python beautiful.py
  File "beautiful.py", line 14
    """
SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte

 

  작성한 내용이 utf-8 코드가 아니라고 난다. 첨에는 python 3 버전도 뭔가 한글 처리 문제가 있어서 변환 방법을 찾아봐야 하나 하다가, 혹시나 해서 py 문서의 저장 방식을 살펴 보았더니 메모장에서 저장할때 ansi 로 인코딩 되어 저장되어 있다. 아래와 같이 메모장에서 열어 "파일 > 다른 이름으로 저장..."을 통해서 utf-8로 다시 저장해 본다.

 

  이후 다시 실행해 보면 아래와 같이 정상으로 동작이 된다.

c:\Python\code>python beautiful.py
사탕
오렌지


초콜릿
오렌지


초콜릿
오렌지

 

 

  이렇게 다 해놓고 보면 별거 아닌거 같지만, 사실 별거 아닌게 맞다--; 다만 전제 조건은 html 문법에 대해서 충분히 이해하여 소스를 잘 볼 수 있고, 복잡한 html 소스(javascript, css 가 섞여 있고, 코드가 꼬여 있을 수도 있다)를 만나도 개발자 도구 정도만 있으면(최악의 경우 메모장으로 소스를 보더라도 화면하고 비교해 가면서 찾아서) 충분히 원하는 태그의 특징과 위치를 지정 할 수 있는 방법만 파악할 수만 있음 된다. 그럼 이정도로 beautifulsoup 사용법 부분은 마치고, 그래도 짧아 아쉬운 느낌이 든다면 해당 내용을 기반으로 구글에서 크롤링에 대한 다른 블로그들을 보면서 파싱 부분을 어떻게 만든건지 하나씩 소화해 보자. 

 

 

  부록으로 개발자 도구를 사용하는 방법은 구글을 예로 들면, 구글 검색 페이지로 간후 'F12' 키를 누르면 하단에 개발자 도구가 생긴다(물론 다시 F12 를 누르면 사라진다). 이후 구글 검색 박스를 클릭해 포커스를 주고, 마우스 오른쪽 버튼을 눌러서 컨택스트 메뉴를 띄워서 '요소 검사' 항목을 선택한다.

 

  그럼 아래에 해당 되는 요소(엘리먼트, 태그)에 해당되는 소스 위치가 하이라이트 되서 나타난다. 이 소스 부분 구조를 분석해서, 해당 태그에 접근하는 방법을 beautifulsoup 이든 정규식이든 이용해서 설계하는 것이다. 이게 단순하게 보이지만 웹 파싱의 전부다(파싱 난이도는 대상인 소스가 어떤가에 의해 결정난다).

 

 

 

 

 

[WhoIS 사이트 웹을 통해서 읽어오기] 

  그럼 10교시에서 API 를 이용해서 호출했던 WhoIS 페이지를 웹을 통해서 파싱해서 값을 가져와 보자. 문제를 단순해 하기 위해서 여러 IP 를 가져온다든지 하는 기능은 다 제쳐 두고 1개의 주소만 검색해 가져오려 한다(1개의 주소 호출만 해결나면 나머지 코드는 10장의 소스를 참고해서 적당히 머지해 만들면 된다). 우선 WhoIS 페이지로 이동하여 샘플 IP 를 하나 조회해 본다(조회 방법을 모른다면 10교시 내용을 보고오자).

http://whois.kisa.or.kr/kor/whois/whois.jsp

 

  위의 내용 중에 주소를 가져오고 싶은데, 주소 위에서 마우스 오른쪽 버튼을 눌러도 컨텍스트 메뉴가 안 뜬다. 그리고 위쪽 주소창을 보면 URL 뒤에 아무 인자도 없으니 없으니 post 방식으로 조회되는 듯 한다. Ctrl+U 를 눌러 소스보기 창으로 간다. IE11 기준으로 개발자 도구가 열리며 아래와 같이 하단의 탭에 두 개의 파일이 보인다.

 

 

  whois.jsp 와 whois.jsc 이다. 실제 호출은 하나만 일어난 것 같이(whois.jsp) 주소창에 보이지만, 2개의 파일이(whois.jsp, whois.jsc) 호출됬다고 볼 수 있다. '주소' 항목이 있는 소스 위치를 찾기 위해서,  'Ctrl+F' 를 눌러서 '주소'라고 입력후 검색창 옆의 '▶' 화살표를 눌러 다음 항목을 찾다보면 whois.jsc 파일 안에서 아래의 검색된 내용이 있는 것이 보인다. 소스 형태를 보니 우리가 원하던 html 태그가 아닌 일반 텍스트 형태이다. 흠 그럼 문제가 조금 복잡해 진다.

 

 

  그럼 여기서 10교시에서 사용했던 피들러를 다시 사용한다(설치나 세팅에 대해서는 10교시를 참고한다). 피들러를 띄우고 F5 를 눌러 WhoIS 페이지를 재로딩 하여 다시 주소를 조회해 본다. 피들러로 잡힌 소스 중, js, css 파일을 제외하고(del 키를 눌러 필요없는 애들을 삭제했다), 두 개의 주요 파일만 보면 아래와 같다. 분명히 호출은 jsp 파일만 했는데 내부적으로 보니 jsc 파일이 그 후에 연달아 호출된 것이 보인다.

 

 

해당 호출이 어떻게 일어났나 이해하기 위해서 왼쪽 창에서 'whois.jsp' 파일을 선택 하고, 오른쪽 하단의 Response 창에서 'TextView' 탭을 선택 하고, 아래의 검색 창에 'whois.jsc' 를 입력 후 enter 키를 누른다.

 

  그럼 안에 폼이 하나 있고, 해당 폼을 이용해서 whois.jsc 를 호출하는 듯 하다. 폼의 인자는 query(검색할 IP주소), ip(본인 PC IP-아마 감사용인듯), 그리고 호출 결과는 frm 이라는 iframe 에 담기게 되는 듯 하다. 검색 창에서 'ifrm' 을 넣어 찾아보면 javascript에서 submit 을 하는 코드가 있다.  

 

  또 'frm' 을 넣어 찾아보면, 결과를 담는 iframe 코드가 보인다.

 

  그럼 파이썬으로 구현할때는 굳이 jsp 파일을 호출할 필요 없이 jsc 파일을 직접 호출해도 될것 같다. 대신 호출 할때 post 방식으로 query(검색 대상 IP) 와 ip(본인 PC IP) 를 넣어서 보내면 된다. 이후 결과 값을 받아온 후 파싱 하면 되는데, 불행하게도 돌아오는 데이터는 일반 텍스트 형태이기 때문에, beautifulsoup 을 이용할 순 없고, 8교시에 배운 정규 표현식을 이용해 본다. 

 

 

 

 

[파이썬 코드 제작]

그럼 이제 제작에 필요한 모든 밑 작업은 완료되었으므로, 언제나처럼 우리가 현재 모르는게 무언지 체크해 보자.

1)  post 로 호출해 값을 얻어오는 방법을 모른다(예전 API 는 거의 get 방식이였다)

2) "주소(적당한 공백들  ): 원하는 주소" 를 파싱하는 정규 표현식을 모른다(정규식을 대충 배웠으니 이젠 어떻게든 할수 있어 보이긴 한다)

 

 

  먼저 post 로 호출 하는 방법을 찾아보자. 구글에서 'python requests post' 를 이용해 아래 페이지를 찾았다.
http://docs.python-requests.org/en/master/user/quickstart/

 

  해당 페이지 내에서 아래의 more-complicated-post-requests 항목의 소스를 이용하려고 한다.

http://docs.python-requests.org/en/master/user/quickstart/#more-complicated-post-requests

1
2
3
4
payload = {'key1''value1''key2''value2'}
 
= requests.post("http://httpbin.org/post", data=payload)
print(r.text)
cs

 

 

  그 담에 정규식은 8교시에서 이미 다루었으니, 해결 결과만 얘기하면 아래와 같다.

"주소.*: ([^0-9].*)" -> 해석 해보면 주소란 단어로 시작하고(주소), 아무 글자나(.* : 사실 공백을 나타내는 /s 를 써도 좋지만 굳이 현재 상황상 그럴 필요까진 없을 듯해서 임의의 문자로 지정 했다) 0개 이상 나타나고 : 기호가(:) 가 나온 후 공백( ) 이 하나 나오고, 숫자가 나오지 않으면서 다른 아무 문자들로나 채워진([^0-9].*) 부분을 지정한다. 실제 가져오는 값은 이전에 8교시에 배에 배운 괄호 그룹기호를 이용한다(([^0-9].*)) . 결과에서 숫자를 제외한 이유는 피들러에서 '주소'를 검색했을때 '주소' 란 글자로 시작하는 경우에 'IPv4주소'가 있기 때문에 배제하려고 했다. 첨엔 주소 앞에 ^를 넣어 구분하려 했으나 각 행이 문장의 처음으로는 인식되지 않아서 위와 같이 만들었다. 주소 앞에 특정 문자가 있을 때는 무시하도록 정규 표현식을 짜도 될 듯 하지만 사실 넘어오는 값에 대해서 정확히 텍스트 구조가 파악 안되서 쉬운 길로 갔다. 

1
2
3
4
import re
pattern = re.compile("주소.*: ([^0-9].*)")
match = re.findall(pattern, r.text)
print (match[0]) 
cs

 

[두 개의 주소가 나오는 상황]

IPv4주소           : 202.30.50.0 - 202.30.51.255 (/23) <- 이 부분을 배제하기
주소               : 서울특별시 송파구 중대로  <- 그룹으로 묶어 가져옴

 

 

  위 두 개의 코드를 합치면 아래와 같이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#-*- coding: utf-8 -*-
# jsc 페이지 post 로 호출하기
import requests
payload = {'query''202.30.50.51''ip''본인의 IP 주소'}
= requests.post("https://whois.kisa.or.kr/kor/whois.jsc", data=payload)
r.encoding = 'utf-8'
# print(r.text) 
 
 
# 결과에서 주소 얻어오기
import re
pattern = re.compile("주소.*: ([^0-9].*)")
match = re.findall(pattern, r.text)
print (match[0]) 
cs

 

  위의 소스를 복사해서 '본인의 IP 주소' 부분을 본인의 IP 로 바꾼 후(피들러에서 whois.jsc 검색 후 본 IP를 적음 된다), c:\python\code 폴더에 whois_test.py 라고 저장한다. 이후 아래와 같이 실행 시켜본다. 파싱된 주소를 정상적으로 가져온다.

c:\Python\code>python whois_test.py
서울특별시 송파구 중대로

 

 

 

 

[마무리 하면서]

  위 예제는 웹 파싱이 꼭 beautifulsoup 을 사용할 수 있도록 아름답게만 제공되지 않는다는 것을 보여주려고 진행했다. 또한 html 뿐만 아니라, css, javascript 등의 연관된 언어를 잘 알아야 분석이 용이 하다는 얘기도 하고 싶었다. 실제 웹 대상으로 구현 하다보면 여러가지 파싱 장벽에 만나게 된다. 태그가 일부 깨져서 제대로 모듈이 동작 안할 수도 있고(브라우저는 왠만하면 에러를 안내고 깨진 태그를 적당히 해석해서 잘 보여주려 한다), 개발자가 기존 코드를 복사해서 개발하느라 고유해야 할 id 등이 여러 개 존재할 수도 있고, iframe 등으로 페이지가 복잡하게 꼬여 있을 경우 지금 처럼 해당 되는 실제 소스의 url을 찾아야 할때도 있고, 보안 상의 이유로 이리저리 자바스크립트를 이용해 호출을 숨겨놓거나, 난독화 시킨 코드들도 만들수 있다. 또 겨우 만든 파이썬 코드가, 해당 웹 페이지가 변경되면서 구조가 변경되어 다시 수정해야 되는 일이 어느날 갑자기 생길 수도 있다. 그것은 자기가 만든 웹 사이트가 아닌 이상(하물며 자기 회사 페이지라도 변경이 잘 통제되지 않는 이상) 언제든지 일어날 수 있는 일들이라고 생각해야 한다.

 

  위의 코드를 10교시때 만들었던 API 호출 코드와 적절히 머지하여 반복적으로 IP를 호출하게 할 수도 있지만 앞에서도 얘기했듯이 크롤링과 같은 웹페이지의 반복적인 호출은 사이트에게 절대 환영받는 행동은 아니기 때문에 가능한 API 를 정식으로 발급 받아 필요한 만큼만 사용하는 것을 권장한다. 혹시 API 등도 없고 개인용도로 사용하면서 대량의 페이지를 조회 할 필요가 있을 때에는 아래와 같은 식으로 약간의 호출시간 간격을 두어, 조회하거나 적정한 숫자로 제한해 여러번 나눠 호출 하는 것이 장기적으로 사이트에나 본인에게 바람직 할 듯 싶다. 과유불급(過猶不及)이란 말을 꼭 생각하며 살자. 좀 의견이 고루 한것도 같으나 대부분의 크롤링 관련 글에서 이런 얘기는 잘 못본 것 같아서 균형을 맞추기 위해서 부득이하게 강조하게 된다.

1
2
import time
time.sleep(0.1
cs

 

  참고로 크롤링과 관련된 소송 사례를 두개 링크 한다.

https://www.lawtimes.co.kr/legal-news/Legal-News-View?Serial=98844&kind=AA

http://www.ddaily.co.kr/news/article.html?no=130822

 

 

  그럼 다음 시간에는 이왕 API, 파싱까지 간거, 웹 자동화에 대한 이런저런 얘기들을 라이트하게 진행 할까 계획하고 있다.

 

 

[보충 - 09님 문의에 대한 답변]

 

네이버 책에서 "구글로 공부하는 파이썬" 책의 제목과 가격을 가져와 Sqlite 에 저장하는 코드 입니다. 참고로 요소 검사기로 보면 "구글로 공부하는 파이썬" 책은 옆에 부제(구글에서 찾는 파이썬 실전 예제 분석)가 달려있는데, 부제가 없는 책들은 46 라인에서 결과가 없어 에러가 나기때문에, 판단을 해서 예외처리를 해야 합니다~ 

 

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
#-*- coding: utf-8 -*-
import sqlite3
import requests
from bs4 import BeautifulSoup
import re
 
# sqlite 파일을 열음
conn = sqlite3.connect("price.db", isolation_level=None)
cursor = conn.cursor()
 
# BookInfo 테이블을 생성한다.
cursor.execute("""CREATE TABLE IF NOT EXISTS BookInfo(itemno text, title text, price INTEGER)""")
 
 
# url 요청할 세션 만들기
= requests.session()
 
# URL 만들기
searchItemno = '13394873'
searchurl = 'http://book.naver.com/bookdb/book_detail.nhn?bid='
 
# URL 호출
con = s.get(searchurl + searchItemno)
 
# html 파서 사용
soup = BeautifulSoup(con.text, 'html.parser')
 
# 책 가격 들어 있는 태그 가져오기
item_price = soup.find("div", class_="lowest")
item_price2 = item_price.find("strong")
 
# Beautiful Soup 으로 가져오기엔 태그가 묘해서, 정규표현식으로 실제 가격 가져옴
pattern = re.compile("<strong>(.*)<span"
match = re.search(pattern, str(item_price2))
 
# 책 가격 가져옴
book_price = match.group(1)
 
 
# 책 제목 가져옴(서브 제목이 없을 경우는 에러나서 다르게 찾아야함)
title = soup.find("a", class_="N=a:bil.title,i:98000001_000000000000000000CC63B9")
 
# Beautiful Soup 으로 가져오기엔 태그가 묘해서, 정규표현식으로 실제 제목만 가져옴
pattern = re.compile(">(.*)<span"
match = re.search(pattern, str(title))
book_title = match.group(1)
# "뒤의 \xa0" 문자 제거
book_title = book_title.strip()
 
 
# 테이블에 저장하기
sql = "INSERT into BookInfo(itemno, title, price) values (?, ?, ?)"
cursor.execute(sql, (searchItemno, book_title, book_price))
 
 
# 테이블에 저장한 값 불러오기
sql = "select itemno, title, price from BookInfo"
 
cursor.execute(sql)
row = cursor.fetchone()
 
if row: 
   while row:
      print(row)
      row = cursor.fetchone()
cs

 

c:\Python\code>python naver_book.py
('13394873', '구글로 공부하는 파이썬', 37800)

 

 

1
2017.3.22 by 자유로운설탕 
cs
posted by 자유로운설탕
2017. 3. 11. 21:30 프로그래밍

  이번 시간은 txt 파일에 저장되어 있는 IP 목록을 읽어 각각 루프를 돌리면서 API 를 호출을 한 후, 반환된 json 형태의 데이터에서 원하는 항목을 얻어서, 엑셀에 결과를 저장하는 예제를 시연하려고 한다. 

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  우선 예제를 시작하기 전에, 노파심에 한가지 유의할 부분에 대해서 얘기하려고 한다. 일반적으로 파이썬 예제를 찾다보면 크롤링이나 API 호출 등의 예제들이 많이 나오는데, 해당 방식의 호출에 대해서는 생각보다 좀더 조심스럽게 생각해야 한다는 것이다. 용도에 맞지않게 너무 과도한 호출을 하게 되면, 대상 사이트에서 악성적인 시도로 판단하여 호출 IP 를 블록 할 수도 있고(다만 국내 IP이며, 동적 IP이기 때문에 블록 시키는 기간은 일시적일 가능성이 높다), 또 실습하는 PC가 회사 내 PC 같은 경우 NAT 을 통해 하나의 대표 IP로 나가는 경우가 대부분이기 때문에, 최악의 경우 회사 전체 사람들의 PC에서 해당 사이트로의 연결이 거부되어 버릴 수도 있다. 물론 공식적으로 제공되는 API 의 경우는, 어느 정도 익명 사용자의 잦은 호출을 가정해서 설계 후 제공이 되긴 하지만, 그래도 목적에 필요한 만큼만 호출 하는게 학습 목적에도 맞기 때문에 유의해서 호출 하시기 바란다(혹 지금 실습하는 사이트에서 연락이 올까봐 조금 걱정된다 --;)

 

 

 

 

[API 간단 설명]

  시작하기 전에 API에 대해 확실히 모르는 분을 위해서 간단하게 개념을 설명하려 한다. API(Application Programming Interface)의 예 중 우리가 가장 컴퓨터 안에서 흔하게 볼수 있는 것은 Windows API 이다(물론 이런 OS API와 구분해서 개념지을 수도 있긴 하지만 크게 보면 비슷한듯 하다). 윈도우즈 내의 여러 어플리케이션은 이러한 기본 API 들을 사용해서 윈도우 화면 에 글자를 표시하거나, 윈도우 창과 메뉴를 표시하거나, 효과음 사운드를 내거나, 애니메이션 등을 보여준다(물론 전문화된 다른 API가 또 있을 수 있다). 어찌 보면 파이썬 언어 자체도, 이러한 API 를 잘 사용하기 위해서 상위 수준에서 만들어진 언어라고 봐도 된다. 해당 부분은 리눅스 시스템도 윈도우즈와 마찬가지고 말이다. 비슷한 측면에서 인터넷이 발달한 이후에 이러한 API 들이 컴퓨터 내부를 벗어나서 네트워크(특히 WEB)를 통해서 지원하는 방식이 늘어나게 되었다. 또한 요즘은 SOA(Service Oriented Architecture - 서비스 지향 아키텍쳐), DevOps, MVC, Ajax 같은 설계들이 주류를 형성하게 되면서(개인적으로 봤을때, 해당 항목들의 공통점은 UI와 분리되어 데이터 전달만으로 소통하는 작은 기능, 즉 API 들로의 작은 쪼갬 같다), API 형태의 서비스들이 대세가 된것 같다. (물론 마이크로서비스 같이 좀더 넓은 특성으로 확장하여 지칭하는 용어는 다를 수도 있다)

https://ko.wikipedia.org/wiki/%EC%84%9C%EB%B9%84%EC%8A%A4_%EC%A7%80%ED%96%A5_%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98

 

  우리가 보통 파이썬 등의 언어에서 호출하는 구글, 네이버, 다음 등의 외부 API 는 기존의 웹 환경에서 머리 꼬리를 떼어넨 버전이라고 보면 된다. 그럼 머리 꼬리를 떼면 무엇이 남을까? 웹에서 화려한 화면을 다 떼고 나면 데이터만 남는다.(실제 웹은 브라우저의 꾸밈 역활과 그걸 즐기는 사람들이 없다면 단조로운 태그와 해더들의 세상일지도 모른다) 그럼 데이터를 주고 받을 때도, 사실 기존 get, post 방식으로 보내도 되긴 하는데, 해당 방식은 좀 더 구조적인 데이터를 보내거나 받을 때 다루기 무척 까다롭기 때문에, 첨에는 xml 같은 데이터 형식으로 많이 보내다가 요즘은 실용적인 json 형식 으로 많이 바뀌게 되었다(xml 이 정장이라면 json 은 캐주얼에 가까운 세미정장 같다). 그래서 지금 외부로 노출된 대부분의 API 들은 대부분 xml 이나 json 으로 데이터를 주고 받는 다고 봐도 무방한 듯 싶다.  

 

 

 

 

[WHOIS 사이트 문제] 

  그럼 오늘의 예제를 진행하게 된 배경에 대해 설명한다. IP 주소의 실제 정보(100%정확하진 않지만)를 찾기위해서는 보통 WhoIS라는 웹사이트에서 조회하게 된다.  구글에서 'whois' 라고 검색하면 KISA(한국 인터넷 진흥원)에서 제공하는 WhoIS 페이지가 나온다.  

http://whois.kisa.or.kr/kor/main.jsp

 

 

  위의 페이지에서 밑에 표시된 샘플 정보인 1) '202'로 시작되는 IP 주소를 클릭 하면, 2) 검색 창에 해당 주소가 들어가게 되고, 이후 3) 'Search' 버튼을 클릭 하면 해당 IP 주소에 대해서 저장되어 있는 정보를 보여주게 된다. 보통 사이트 로그에 수상한 호출을 하는 IP가 들어 왔을때, 어떤 나라, 장소에서 들어왔고, 어떤 서비스로 들어왔는지를 아래에 표시된 여러 정보를 참조해서 판단하게 된다.

 

  그런데 문제는 해당 조회 방식을 사용한다면, 조회에 필요한 IP 가 수십개, 수백개로 늘어나는 경우 어떻게 해야 될까? 해당 숫자만큼 매번 페이지를 호출해 확인해야 할 테고, 그럼으로서 생기는 스트레스나 소모되는 시간도 무시할 수 없을 만큼 될 것이다. 물론 웹이나, 프로그램 중 멀티 IP 를 조회해 주는 프로그램도 있지만, 대부분의 프로그램은 국외의 WhoIS 서버(나라, 지역마다 해당 역활을 하는 서버들이 있는 듯 하다)를 호출하기 때문에, 영어로 표시된 정보이기도 하고, 국내 정보도 한국내 WhoIS 만큼 정교하게 나타나지 못할 수 있다. 

 

  그래서 첨에는 많은 웹 크롤링 예제에 있는 것처럼, 해당 페이지를 호출해서 결과 내용을 파싱해서 관련 내용을 뽑아 저장하려고 했었었다(해당 예제와 수반되는 미묘한 문제들은 다음 시간에 다룬다^^). 근데 파싱을 위한 코드들을 찾거나, 상단 페이지의 html 을 분석하는 데에 대해 은근 귀찮음이 밀려오는 상황에서, 마침 해당 페이지의 오른쪽 하단에 자리잡은 OpenAPI 기능 제공이 눈에 띄게 되었다. OpenAPI 라는 것은 비교적 모든 일반 사람들에게 사용을 허가하고 있는 범용 API 이다. 앞의 생선 그림 부분에서 얘기했듯이 API 를 쓰게 되면 일반적으로 정형화된 xml 이나 json 으로 데이터를 받을 수 있으므로, 웹페이지 상에서 html 을 파싱하는 것보다 좀 더 향후 페이지의 변경에 대해서 예외성이 없다.

 

  더 나아가 사이트 들은 보통 페이지 수정 작업 시 크롤링 하는 프로그램을 배려하려 하지 않지만(크롤링은 엄격이 따짐, 유명한 검색엔진들 이외에는 해당 사이트에서 좋아하진 않은 행위기 때문에 그럴 의무도 없고 말이다) , API 의 경우 해당 API 를 이용하여 인터넷 저 너머에서 동작하는 다른 고객의 어플리케이션들이 있을수 있기 때문에, 기존에 해당 API 를 제공한 책임을 생각해서라도 하위 호환성을 고려해서 수정하거나, 기능의 변경이 꼭 필요할땐 차라리 기존 API 를 두고 확장된 대체 API 를 만들 가능성이 많다. 그래서 한번 API 를 호출하는 방식으로 기능을 만들게 되면 해당 사이트가 사라지거나 대대적인 개보수 작업이 있기 전에는 유지보수를 위해 변경할 필요성이 거의 없어지는 장점이 있게 된다.

 

 

 

[API 사용자 등록]

  그럼 첫 페이지에서 아래의 API 링크를 클릭한다.

 

  아무리 모두에게 공개된 OpenAPI 라도 최소한의 호출 체크와 무분별한 사용을 제어하기 위해서 인증키를 발급 하는 경우가 많다. 아래를 보면 이메일을 등록하면, 해당되는 인증 키를 보내준다고 그런다. 그럼 자신이 쓰는 이메일과 사용목적을 적당히 넣고(기관이라 절대 그런일은 없겠지만 마음 한구석에 의심이 남는다면 잘 안쓰는 이메일로 넣는다), '발급신청' 버튼을 누른다. 

 

  잠시 후 자신의 이메일로 가서 확인을 하면, 키가 발급되어 오고 아래의 인증 페이지 링크를 클릭해서 해당 키를 인증 받으라고 그런다. 발급된 키는 앞으로 API 를 사용할때 호출하는 인자에 넣어서 권한을 증명할 키이니, 해당 키를 복사해서 어딘가 잘 저장해 두고, 아래의 인증페이지 URL 을 클릭 해 키 등록 페이지로 이동한다.

 

  이후 발급된 키를 넣고 '등록' 버튼을 누르면, 정상적으로 등록이 되었다고 나온다. 그럼 앞으로 특별히 상도에 어긋나는 행위를 하지 않는한, 해당 WhoIS API 를 당당하게 호출해 사용할 수 있는 권한을 획득하게 된 것이다.      

 

 

 

 

[API 예제 호출 해보기]

  그럼 아래의 Open API 사용 설명 페이지에서 API 를 이용해 IP 를 호출해서 주소를 가져오는 예제를 확인한다. 좀 오타가 있어 헷깔리긴 한데(IP를 도메인 주소라고 적었다), "인터넷주소 검색요청 URL" 의 2번째 예시가 우리가 원하는 기능을 수행하는 API 예제이다.

http://whois.kisa.or.kr/kor/whois/openAPI_KeyCre.jsp

http://whois.kisa.or.kr/openapi/whois.jsp?query=도메인이름&key=발급받은KEY값&answer=[xml,json]

 

 

  해당 값을 편집해, 앞서의 샘플 IP 주소를 호출하는 예제를 만들어 본다. IP 주소는 웹에서 샘플로 사용했던 KISA 주소고, 인증키는 여러분이 발급받은 키로 넣고, answer 형식은 json 으로 한다. beautifulsoup 등을 이용할 수 있는 xml 방식이 편한 사람들은 xml 을 하셔도 되지만, 앞으로 보면 알겠지만, json 도 그 만큼 간단하니 따라와서 확인해 보길 바란다.

http://whois.kisa.or.kr/openapi/whois.jsp?query=202.30.50.51&key=발급받은키&answer=json

 

 

  그럼 해당 쿼리를 복사해서('발급받은키' 부분은 꼭 자기 키로 교체해 넣어야 한다.), IE 주소창에 붙여 넣는다. 그럼 아래와 같이 {, :, " 기호로 구조적으로(실제 나중에 보겠지만 트리 모양이다) 구분된 json 결과가 나온다. 그럼 해당 출력 결과의 문법 기호들을 잘 분석해서 원하는 자료가 있는 위치를 찾거나, 아니면 크롬, 파이어폭스 같은데에 잘 정리해 주는 플러그인이 있을 것 같긴 하다. 하지만 이 시점에서 웹 개발이나, 페이지 구성 분석, 보안 테스팅 등에 두루두루 잘 쓰일 수 있는 하나의 무료 툴을 이용하여 json 결과를 분석하는 방법을 소개하려 한다.(앞으로도 진행 하면서 종종 해당 툴을 쓰게 될 듯 하다)

 

 

 

 

[피들러 설치 - 무료지만 유료 뺨치는 툴] 

  소개하고 싶은 툴은 Fiddler 라는 툴인데(이미 사용해 보신분은 설치는 가볍게 건너뛰어 주시고, 설정 부분만 혹시 몰랐던 부분이 있나 확인하면 된다) 아래와 같이 설치를 한다. 구글에서 'fiddler' 라고 검색하여 아래의 링크로 이동한다.

http://www.telerik.com/fiddler 

 

 

  아래의 무료 다운로드 버튼을 누른다. (회사에서도 무료 사용이 가능하다)

 

 

  이후 이메일 주소를 넣고(아까의 API 키 발급의 경우처럼 메일로 인증 받거나 하는건 아니니 잘 안 쓰는 이메일 주소를 넣음 된다), 아래 그림과 같이 위쪽의 정보 메일 수신 옵션을 uncheck 하고, 아래의 라이센스 동의를 check한다. 이후 'Download' 버튼을 눌러 적절한 폴더에 설치 파일을 저장한다. (뭐 저장안하고 바로 실행해도 된다)

 

 

  설치 파일을 실행해 아래의 라이센스를 'I Agree' 하고, 그 후는 그냥 디폴트 옵션으로 쭉 설치 한다(과정이 간단해서 뒤쪽 스크린샷은 뺌)

 

 

  이후 시작메뉴에서 아래의 Fiddler4를 실행 한다. 이후 아래에 있는 'App Container' 설정이라는 창(아마 10에서만 에지 브라우저 땜에 나오는 듯 하다)이 나오는데, '취소' 를 눌러서 Warning 을 끄자.

 

 

 

  이후 처음 실행하면 브라우저에서 전송하고 가져오는 모든 리소스를 다 표시하기 때문에 몇 가지 사용 편의를 위해 설정할 부분이 있다. 일단 상단의 'Rules' 메뉴로 가서 'Hide Image Request'(보통 웹페이지는 내부에 작은 이미지 들이 많기 때문에 이미지가 모두 리스트에 나타나게 되면, 실제 살펴보길 원하는 웹 프로그램 파일(예:asp, php)을 찾아볼때 방해가 된다. 물론 이미지에 대한 분석이 필요한 경우는 다시 옵션을 바꿔주면 된다)와 'Remove All Encodings'(가져온 페이지 내용에 대해 자동으로 인코딩을 풀어서 피들러에서 바로 평문으로 볼수 있게 해준다) 두 개를 체크 한다. 

 

 

 

  그 다음은 SSL(HTTPS) 복호화 설정이다. 원칙적으로 HTTPS 는 브라우저와 서버 간에 키 교환을 통해 암호화된 데이터가 전송되어서 피들러 같이 중간에서 가로채 보는 툴이 내용을 알아 볼 수 없게 되는데, 컴퓨터 안에 피들러에서 사용할수 있는 테스트용 인증서를 설치하여, 브라우저는 피들러와 암복호화를 해서 통신하고, 다시 피들러가 브라우저 대신 서버와 암복호화를 해서 통신해서 중간에서 내용을 살펴 볼수 있게 해주는 트릭 기법이다. 이 설정이 잘 안 되 있으면, 구글 같이 호출 전체가 HTTPS 로 이루어진 사이트나, 여러 사이트의 로그인 페이지, 회원 정보 페이지 등 정보의 보호를 위해서(법적인 사항이기도 하다) HTTPS 로 호출되는 페이지들은 내용을 볼수 없게 된다(피들러만 쓰는 인증서긴 하지만 혹시나 걱정되는 사람들은 마지막에 설치된 인증서 제거 방법을 명시했으니, 사용 용도가 끝난 후 제거해도 무방하다). 사실 현재 예제에서는 SSL 파싱을 안해도 상관없으니, SSL 설정을 건너뛰셔도 상관없지만, SSL 복호화 기능을 빼고 피들러를 쓰면, http 사이트 들만 볼수 있는 반쪽 짜리 피들러가 되어버려서 설명하는 김에 같이 설명하는 중이다.

 

  그럼 위쪽 메뉴에서 'Tools > Telerik Fiddler Options...' 메뉴를 클릭하여 아래의 옵션 창을 띄운다. 'HTTPS 탭' 을 클릭하고, Capture HTTPS CONNTECTs(https 통신을 캡쳐한다), Decrypt HTTPS traffic(HTTPS 트래픽을 복호화 한다) 를 체크한다.

 

  체크하는 순간에 하단의 Alert 창이 뜬다. HTTPS 트래픽을 가로채기 위해서, 피들러가 루트 인증서를 윈도우에 설치하려 한다는 안내이다. 'YES' 를 클릭 한다.

 

  꼭 설치 하겠냐고, 보안 경고 창이 뜨면, '예'를 누른다. 

 

  다시 또 묻는다 (이게 윈도우즈 10만 이렇게 많이 묻는 듯도 싶다)

 

  최종 확인을 한다.

 

 

  이후 google 페이지에 접속해 본다. 이전에 https 복호화 설정 안했을 때는 안 보이는 구글 페이지의 호출 내용들이 다 보이게 된다.

 

 

 

[부록]

  그럼 아까 얘기한 인증서 제거하는 방법이다. 아까 옵션 창의 'HTTPS' 탭에서 오른 쪽에 있는 'Action' 버튼을 누르고, 'Open Windows Certificate Manager' 를 클릭 한다.

 

  인증서 관리 화면이 뜨면, 왼쪽에서 '신뢰할 수 있는 루트 인증 기관 > 인증서' 를 클릭 한 후, 오른 쪽에 있는 인증서 중, 아까 설치한 'DO_NOT_TRUST_FiddlerRoot' 인증서들을 삭제 한다 (선택하고 마우스 오른쪽 버튼 누르면 '삭제' 메뉴가 나온다). 그럼 설치했던 인증서들은 제거되고, 대신 피들러에서 이제부터는 https 트래픽은 해석 못하게 되버린다. https 복호화 옵션도 같이 끄면 될듯 하다.

 

 

 

 

[피들러의 동작 원리]

  그럼 실제 WhoIS 사이트 보기전에 피들러의 동작 원리에 대해서 얘기하려 한다. 그냥 시키는 데로 사용하는 것 보다는 개념을 이해하며 사용해야 기억에도 잘 남고 스스로의 바라보는 관점도 생기게 되니 말이다.

 

  아래 그림과 같이 웹 브라우저는 웹 서버와 통신을 할때 네트워크 카드를 통해서 직접 통신을 주고 받는다. 근데 Fiddler 같은 웹 프록시들이 설정되게 되면, 네트워크 카드로 패킷을 바로 보내지 않고, 보낼 패킷 들을 피들러에게 전달해 주고, 이후 피들러가 그 패킷을 네트워크 카드로 보내게 된다. 그렇게 함으로서 피들러는 중간에서 내용을 볼수도(사실 우리가 보는 피들러 화면이 바로 그 중간에 잡은 내용을 해석한 것이다) 있고,  (보안 테스트에서 사용하는 기능이지만...) 심지어 중간에서 패킷의 내용을 조작 할 수도 있다. IE 의 개발자 도구와 비슷한 부분 같다고 생각하는 사람도 있겠지만, 개발자 도구는 브라우저 내에서 작동하는 도구라 브라우저에 의존적이고, 피들러 같은 종류의 툴은 브라우저를 벗어나 버린 패킷을 가로채 보여주는 거라서 네트워크에 의존적이다. 할 수 있는 일도 조금 범위가 다르다. 다음 웹 크롤링 시간에 의도한 건 아니지만 그러한 차이 점 중 하나를 보여주는, 개발자 도구로 못보는 화면 데이터를 피들러로 보는 시연이 들어갈 예정이다. 

 

 

  피들러가 브라우저의 전송 내용들을 중개한다는 부분을 간접적으로 확인 해 보자. 아까 보았던 피들러 옵션에서 'Connections' 탭을 클릭 하면, 'Fiiddler linstens on port : 8888' (피들러가 8888 포트에서 신호를 기다리고 있다) 라는 옵션이 있다.

 

 

  피들러를 실행한 상태에서, IE나 크롬 등의 '인터넷 옵션' 의 '연결' 탭을 클릭해 보면, 아래의 '프록시 서버' 라는 옵션이 체크되어 있다(피들러를 끄면 언체크 되니 확인해 보길 바란다). 그 옆의 '고급' 버튼을 누르면, 프록시 설정 창이 뜨는 데 '127.0.0.1'(현재 내 로컬 PC의 IP 주소 - localhost 와 같은 개념이다)주소의 8888포트를 프록시로 사용한다는 옵션이 설정 되 있다. 그 127.0.0.1(내 컴퓨터) 주소에서 8888 포트로 브라우저가 뭔가 보내주기를 기다리는 프로그램이 바로 피들러이다. 반대로 웹서버에서 응답을 받을 때도 피들러가 먼저 응답을 받은 후 브라우저에게 전달해 주게된다. 

 

 

   마지막 팁으로 피들러를 사용 하다보면 옵션에서 이미지는 무시했음에도 불구하고 왼쪽 창에 계속 통신들이 많이 잡혀서 정신 없게 되는데(요즘은 ajax 통신이 많아서 페이지를 열어놓음 계속 뭔가가 잡힐 수도 있다), 아래의 Edit > Remove > All Sessions 메뉴를 이용해 기존 내용을 전체 삭제 하거나, 호출된 리스트를 Ctrl+A 로 모두 선택해서, DEL 키를 누르거나, 그냥 아래에 나온데로 Ctrl +X 를 하거나 하는 3가지 방식으로 모두 클리어 할 수 있다.

 

 

 

 

[피들러로 WhoIS 데이터 호출 보기] 

  그럼 조금 멀리 돌아왔지만(하지만 이제까지 한 얘기도 이전 시간들에 얘기한 프로그램의 외적 요소라는 것은 잊지말자), WhoIS API 의 결과를 피들러로 보면 어떤 부분이 유리한지 살펴 보겠다. 피들러를 켠 상태에서, IE 나 크롬 등에서, 아까 그 샘플 URL(발급 받은 키는 꼭 본인 키로 바꿔 넣어야 한다) 를 주소창에서 실행 한다.(혹시 피들러에서 결과가 잘 안 잡히면 브라우저에서 'F5' 키를 눌러 페이지 재로딩을 해본다. 가끔 이유는 잘 모르겠는데 안 잡힐 때가 있다--;). 아래의 호출된 화면이 나오면, 왼쪽 리스트 창에서 WhoIS API 호출한 항목을 클릭해 선택한 후, 오른쪽에서 Inspectors 탭을 클릭 한다. 

http://whois.kisa.or.kr/openapi/whois.jsp?query=202.30.50.51&key=발급받은키&answer=json

 

 

 

  그리고 오른쪽 아래에 있는 Json 탭을 클릭 한다(피들러의 오른쪽 창은 Request, Response의 두개의 창으로 나눠져 있는데 그 중 아래 쪽 Response 창이다). 그럼 아까 IE 에서는 좀 보기 힘들게 표시되던, Json 결과 데이터가 아래와 같이 트리 형식으로 휠씬 직관적으로 보이게 된다('-' 기호를 클릭함 접히기도 한다~). 저런 모양으로 표시되면 파이썬에서 파싱할 Json 데이터를 분석 할 때 좋고, 그 중 어떤 값이 사용이 필요한지도 쉽게 파악된다. 밑의 json 트리를 기반으로 예제에서 사용할 데이터를 나라이름(CountryCode), 주소(addr), 주소범위(range), 서비스ISP(servName), 입력한 IP 주소(query)  총 5개로 정했다. 

 

 

 

 

[파이썬 코드 만들기]

  그럼 요리를 시작할 환경은 다 준비가 된 것 같고, txt 파일에 IP 들을 한줄에 하나씩 여러개 넣어 두면, 파이썬이 해당 파일을 가져다가 IP 각각을 WhoIS API 를 호출해서, 결과인 json 데이터 안에서 앞에서 결정한 5개 항목을 가져온 후, 엑셀에 정리해 저장하려고 한다. 그림으로 간단히 표시하면 아래와 같다.

 

 

  그럼 다른 시간과 마찬가지로 현재 우리가 모르는게 무엇인지 생각해 본다

1) 텍스트 파일을 읽어와서, 각 IP 들을 루프 돌리는 방법을 모른다.

2) WhoIS API 를 파이썬에서 호출 하는 방법을 모른다.(Request 를 어떻게 해야하나?)

3) Json 응답을 받아와서 원하는 값을 추출하는 방법을 모른다.

(엑셀 저장 부분은 7교시 부록에 함수로 이미 만들어 논 것이 있으니, 가져와 적당히 변형해 쓰면 될듯 하다)

 

 

  먼저 텍스트 파일을 읽어와서 루프를 돌리는 부분을 찾아보자. 텍스트 파일을 읽어와서 각 IP를 리스트에 담아서 예전에 익힌 for 문을 이용해 돌리면 될것 같다, 구글에서 'python file to list' 로 검색한다. 맨 처음 나온 스택오버플로우 페이지를 참조해서, 파일 내용을 리스트로 저장 후, 루프를 돌리는 코드를 만들면 아래와 같다.

http://stackoverflow.com/questions/3925614/how-do-you-read-a-file-into-a-list-in-python

1
2
3
4
5
= open('ip.txt''r')
iplist = f.read().splitlines()
 
for ip in iplist:
  requestWhois(ip)
cs

 

 

 

  그럼 웹 페이지를 인자를 포함해 호출할 수 있는 방법을 찾아야 한다. 일단 한 개의 호출만 정상적으로 하는 코드를 만들어 검증해 보자. 이후 그 코드를 함수로 만들어 호출함 될 것 이다. 구글에서 'python request' 라고 찾아서 아래의 페이지나 한글 페이지 들을 살펴보면, requests 라는 모듈이 쓰이는 것 같다.

http://docs.python-requests.org/en/master/

 

  해당 모듈을 설치하기 위해서 위의 링크를 보다보니 pip 로 인스톨 하는 부분이 있다. 

http://docs.python-requests.org/en/master/user/install/#pip-install-requests

 

  아래와 같이 명령어를 입력하면 특별한 문제 없이 설치가 정상으로 된다~

C:\Python\code>pip install requests
Collecting requests
  Using cached requests-2.13.0-py2.py3-none-any.whl
Installing collected packages: requests
Successfully installed requests-2.13.0

 

 

  호출하는 부분은 대충 해결난거 같으니, json 결과를 받는 방법을 찾아보자.(결과를 받으려면 호출을 해야할테니 코드가 함께 있을 거 같아 두 마리 토끼를 잡을 수도 있을 것 같다) 몇 번 시행 착오를 거쳐서, 'python response json' 이라고 검색을 한후 맨위에서 아래의 페이지를 얻게 되었다.

http://stackoverflow.com/questions/16877422/parsing-json-responses

 

  해당 request 코드와 json 코드를 조합하여, 하나의 IP에 대해서 WhoIS API 를 호출하게 한 코드는 아래와 같다(물론 저렇게 인자를 URL 에 같이 넣지 않고, 따로 분리해 넣는 방법도 있으니, 해당 부분은 위의 Requests 문서 설명 부분을 참고하자. 꼭 POST 형식으로 날려야 할 경우는 반드시 인자의 분리가 필요할 듯 하다). c:\python\code 폴더에 python whois_iptest.py 라고 저장 한다(저장 시 소스 내의 '발급받은키' 부분은 자신의 키로 수정 해야 한다!).

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
# -*- coding: utf-8 -*-
import json
import requests  
 
# 요청 세션을 하나 만든다.
= requests.session()
 
# API 를 호출한다.
con = s.get('http://whois.kisa.or.kr/openapi/whois.jsp?query=202.30.50.51&key=발급받은키&answer=json')
 
# 호출 받은 API 결과를 json 형태로 받는다.
json_data = json.loads(con.text)
 
# 결과를 저장할 빈 리스트 생성
WhoIsData = []
 
# 피들러에서 파악했던 값들을 하나씩 추출해 리스트에 담는다.
WhoIsData.append(json_data['whois']['query'])
WhoIsData.append(json_data['whois']['countryCode'])
WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
 
# 리스트 출력
print(WhoIsData)
cs

 

  아래와 같이 실행 해보면 정상적으로 값을 가져온다(역시 저장 및 실행 방법을 잘 모르면 2교시에 자세히 설명했으니 보고 오자).

c:\Python\code>python whois_iptest.py
['202.30.50.51', 'KR', '서울특별시 송파구 중대로', '202.30.50.0 - 202.30.51.255', 'KRNIC-NET']

 

 

  그럼 필요한 조각 코드들이 다 만들어 졌으니, 적절히 조합하면 아래와 같이 최종 코드가 나온다. 엑셀 저장 부분은 거의 예전 코드 그대로 이고, 전역 변수 하나 사용하는 거만 차이난다. (전역 변수는 큰 프로그램에서 버그를 양산할 수 있다고 많은 경우 사용하지 말라고 하는 얘기도 많은데, 뭐 필요하니 언어에서 지원하겠지 하고 저 같은 경우는 편해서 종종 사용한다. 여기서는 엑셀의 내용을 적을 라인을 지정하느라 썼는데, 만약 엑셀 라이브러리에서 마지막 라인 정보를 지원해 준다면 안 써도 무방할듯 하다. 전역변수 관련 논의는 아래 링크를 참고한다.)

http://www.gpgstudy.com/forum/viewtopic.php?t=2123

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
# -*- coding: utf-8 -*-
import json
import requests
from openpyxl import Workbook
from openpyxl import load_workbook
#from openpyxl.compat import range
 
wb = Workbook()
 
# 엑셀의 액티브 워크시트 선택
ws = wb.active
ws.title = "whois ip info"
column_num = 2
 
 
 
# 엑셀에 컬럼 저장
def saveColName(cName):
    column_char = 'a'
    for name in cName:
        ws[column_char + '1'= name
        column_char = chr(ord(column_char) + 1)
        
 
# 엑셀에 조회결과 저장
def saveContent(cName):  
  column_char = 'a' 
  global column_num 
  for name in cName:
    ws[column_char + str(column_num)] = name
    column_char = chr(ord(column_char) + 1)
 
# 세션 생성
= requests.session()
 
def requestWhois(searchIP):
  # 엑셀 2번째 줄부터 IP정보를 연달아 저장하기 위해서 전역 변수 선언
  global column_num 
  
  # API 호출 하기(발급받은키는 자신의 키로 수정해야 한다)
  con = s.get('http://whois.kisa.or.kr/openapi/whois.jsp?query=' + searchIP + '&key=발급받은키&answer=json')
 
  # 호출된 결과를 json 형태로 저장
  json_data = json.loads(con.text)
  # 결과를 담을 배열 초기화
  WhoIsData = []
 
  # 각 결과 값을 배열에 저장
  WhoIsData.append(json_data['whois']['query'])
  WhoIsData.append(json_data['whois']['countryCode'])
  WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
  WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
  WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
 
  # 결과 화면에 뿌리기
  print(WhoIsData)
 
  # 엑셀에 결과 저장
  saveContent(WhoIsData)
  column_num = column_num + 1
  
 
# 엑셀 1째 줄에 데이터 제목 저장  
excelTitle = ['IP''countryCode''addr''IP_range''servName']
saveColName(excelTitle)
 
# 파일 열어 리스트에 담기
= open('ip.txt''r')
iplist = f.read().splitlines()
 
# 리스트에 담기 IP 정보 얻어 오기
for ip in iplist:
  requestWhois(ip)
 
# 엑셀 저장하기
wb.save("ipinfo.xlsx")
  
cs

 

 

 

 

[에러를 만나다]

  근데 위의 코드를 막상 실제에 사용하다 보니, 에러가 생기는 경우가 존재 한다. 두 가지 겨우인데 외국 IP 나 국내 IP 에서 json 데이터 형태를 다르게 주는 경우가 있어서 였다. 해당 경우를 재현해 보기 위해 외국 아이피를 하나 얻기 위해서 cmd 창에서 중국 사이트인 baidu 사이트에 ping 을 치게 되면 '103.235.46.39' 가 나온다.

c:\Python\code>ping www.baidu.com

Ping www.a.shifen.com [103.235.46.39] 32바이트 데이터 사용:

 

  아까 KISA 샘플 아이피 '202.30.50.51' 와 위의 바이두 IP 두 개를 아래와 같이 두 줄로 ip.txt 에 넣어 c:\python\code 폴더에 저장한다.

202.30.50.51

103.235.46.39

 

  그리고 위의 최종 완성된 소스를 긁어서(소스 내의 '발급받은키' 부분은 꼭 자신의 키로 수정 해야 한다) c:\python\code 폴더에 whoisapi_1st.py 로 저장하고 아래와 같이 실행해 본다.

c:\Python\code>python whoisapi_1st.py
['202.30.50.51', 'KR', '서울특별시 송파구 중대로', '202.30.50.0 - 202.30.51.255', 'KRNIC-NET']
Traceback (most recent call last):
  File "whoisapi_1st.py", line 58, in <module>
  File "whoisapi_1st.py", line 41, in requestWhois
KeyError: 'korean'

 

  이상한 에러가 하나 떨어진다. Korean 이란 키가 없단다(json 에서는 인자를 Key 라고 그러나 보다. 하나 배웠다). 분명히 처음 피들러 결과상에는 있었는데 말이다. 앞의 첫 샘플 IP는 print 결과가 잘 뿌려진거 보니 두번째 해외 IP가 문제인가 보다. 프로그램 상에서 디버깅 해도 좋긴 할테지만, 피들러를 사용해 보면 좀더 간단하게 원인을 발견할 수 있다. 

 

 

  피들러로 아래의 문제 IP 를 넣은 쿼리를 날려서 잡아 본다.(키는 본인 키로 바꾸어야 한다.)

http://whois.kisa.or.kr/openapi/whois.jsp?query=103.235.46.39&key=발급받은키 &answer=json

 

   피들러로 잡아보면 아래와 같이 외국 IP 의 경우 아예 나라(CountryCode) 와 IP(query) 밖에 데이터를 안 보내주고 있다(추가로 나중에 발견한 사실이지만, 국내의 특정 IP 도 ['PI']['netinfo']['addr'] 가 아닌, ['ISP']['netinfo']['addr'] 같이 앞에 있는 json 키가 달라진다)  

 

 

  그럼 급한 나머지 피해갈 코드를 생각하다보니, 현재 상황상 국가와 IP는 무조건 나오니 그대로 두고, 주소 등 뒤의 세 가지 값들이 없는 경우는(마침 없을때는 모두 한꺼번에 없으니) 에러를 내지 않고 그냥 넘어가 버리게 함 어떨까 싶다. 그래서 구글에 'python error pass' 라고 검색한다. 첫번째 스택오버플로우 페이지에서 이런 방식은 다른 모든 종류의 에러도 하나로 퉁 쳐서 패스시키기 때문에 나쁘다곤 하는데, 이해는 되지만 시간이 급하니 에러 코드는 구분 안 하고 일단 사용해 본다.

http://stackoverflow.com/questions/21553327/why-is-except-pass-a-bad-programming-practice

1
2
3
4
5
6
  try:
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
  except:
    pass
cs

 

 

  아래와 같이 try, except, pass 를 사용한 땜빵 코드로 수정한 소스이다.

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
# -*- coding: utf-8 -*-
import json
import requests
from openpyxl import Workbook
from openpyxl import load_workbook
#from openpyxl.compat import range
 
wb = Workbook()
 
# 엑셀의 액티브 워크시트 선택
ws = wb.active
ws.title = "whois ip info"
column_num = 2
 
 
 
# 엑셀에 컬럼 저장
def saveColName(cName):
    column_char = 'a'
    for name in cName:
        ws[column_char + '1'= name
        column_char = chr(ord(column_char) + 1)
        
 
# 엑셀에 조회결과 저장
def saveContent(cName):  
  column_char = 'a' 
  global column_num 
  for name in cName:
    ws[column_char + str(column_num)] = name
    column_char = chr(ord(column_char) + 1)
 
# 세션 생성
= requests.session()
 
def requestWhois(searchIP):
  # 엑셀 2번째 줄부터 IP정보를 연달아 저장하기 위해서 전역 변수 선언
  global column_num 
  
  # API 호출 하기(발급받은키는 자신의 키로 수정해야 한다)
  con = s.get('http://whois.kisa.or.kr/openapi/whois.jsp?query=' + searchIP + '&key=발급받은키&answer=json')
 
  # 호출된 결과를 json 형태로 저장
  json_data = json.loads(con.text)
  # 결과를 담을 배열 초기화
  WhoIsData = []
 
  # 각 결과 값을 배열에 저장
  WhoIsData.append(json_data['whois']['query'])
  WhoIsData.append(json_data['whois']['countryCode'])
 
  try:
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
    WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
  except:
    pass
    
  try:
    WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['addr'])
    WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['range'])
    WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['servName'])
  except:
    pass
 
  # 결과 화면에 뿌리기
  print(WhoIsData)
 
  # 엑셀에 결과 저장
  saveContent(WhoIsData)
  column_num = column_num + 1
  
 
# 엑셀 1째 줄에 데이터 제목 저장  
excelTitle = ['IP''countryCode''addr''IP_range''servName']
saveColName(excelTitle)
 
# 파일 열어 리스트에 담기
= open('ip.txt''r')
iplist = f.read().splitlines()
 
# 리스트에 담기 IP 정보 얻어 오기
for ip in iplist:
  requestWhois(ip)
 
# 엑셀 저장하기
wb.save("ipinfo.xlsx")
cs

 

  에러 처리가 된 소스를(소스 내의 '발급받은키' 부분은 자신의 키로 수정 해야 된다.) c:\python\code 폴더에 whoisapi_2nd.py 로 저장하고 아래와 같이 실행해 본다. 잘 동작이 된다.

c:\Python\code>python whoisapi_2nd.py
['202.30.50.51', 'KR', '서울특별시 송파구 중대로', '202.30.50.0 - 202.30.51.255', 'KRNIC-NET']
['103.235.46.39', 'HK']

 

 

 

 

[최종 - 땜빵 코드 없애기]

  이렇게 마무리하게 되면, 왠지 불완전한 땜빵 코드를 안내하고 마치는거 같은 무거운 마음이 들어서(사실 위의 request 세션 같은 경우도 마지막에 세션을 닫아야 할거 같은 생각은 들지만 그런건 무시하고), 정도를 걷기위해 json 결과의 키 값이 없을 때 예외처리하는 방법을 구글에서 찾아본다. 여러번의 검색어 시행착오를 거쳐서 'python json key exist' 라고 검색해 아래의 스택오버플로우 페이지를 찾는다. 기대한 바와 다르게 하위에 특정 값이 없음을 바로 체크 할 순 없고, 계단식으로 1단계씩 차례로 체크해야 하는거 같다.  (예를 들면 json_data['whois']['korean']['PI']['netinfo']['addr'] 라는 json 데이터가 있다면  'addr' in json_data['whois'] 처럼 트리 단계를 점프해서 찾을 순 없다. 좀 답답해 보이지만, 디렉토리 처럼 'whois' 가 있는지 체크하고, 그 담에 'korean' 을 체크하고, 그담에 ... 하고 해야하는 식이다.
http://stackoverflow.com/questions/24898797/check-if-key-exists-and-iterate-the-json-array-using-python

 

  해당 에러 처리 코드를 구성하면 아래와 같고,

1
2
3
4
5
6
7
8
9
10
  # json 인자가 있는지 체크하는 식으로 에러처리 바꿈
  if 'korean' in json_data['whois']:
    if 'PI' in json_data['whois']['korean']:
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
    elif 'ISP' in json_data['whois']['korean']:
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['addr'])
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['range'])
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['servName'])
cs

 

 

  최종 코드는 아래와 같다. (뭐 혹시 아래도 뭔가 맘에 안드시면 제 능력 밖이라고 밖에는 --;)

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
# -*- coding: utf-8 -*-
import json
import requests
from openpyxl import Workbook
from openpyxl import load_workbook
#from openpyxl.compat import range
 
wb = Workbook()
 
# 엑셀의 액티브 워크시트 선택
ws = wb.active
ws.title = "whois ip info"
column_num = 2
 
 
 
# 엑셀에 컬럼 저장
def saveColName(cName):
    column_char = 'a'
    for name in cName:
        ws[column_char + '1'= name
        column_char = chr(ord(column_char) + 1)
        
 
# 엑셀에 조회결과 저장
def saveContent(cName):  
  column_char = 'a' 
  global column_num 
  for name in cName:
    ws[column_char + str(column_num)] = name
    column_char = chr(ord(column_char) + 1)
 
# 세션 생성
= requests.session()
 
def requestWhois(searchIP):
  # 엑셀 2번째 줄부터 IP정보를 연달아 저장하기 위해서 전역 변수 선언
  global column_num 
  
  # API 호출 하기(발급받은키는 자신의 키로 수정해야 한다) 
  con = s.get('http://whois.kisa.or.kr/openapi/whois.jsp?query=' + searchIP + '&key=발급받은키&answer=json')
 
  # 호출된 결과를 json 형태로 저장
  json_data = json.loads(con.text)
  # 결과를 담을 배열 초기화
  WhoIsData = []
 
  # 각 결과 값을 배열에 저장
  WhoIsData.append(json_data['whois']['query'])
  WhoIsData.append(json_data['whois']['countryCode'])
 
  # json 인자가 있는지 체크하는 식으로 에러처리 바꿈
  if 'korean' in json_data['whois']:
    if 'PI' in json_data['whois']['korean']:
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['addr'])
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['range'])
      WhoIsData.append(json_data['whois']['korean']['PI']['netinfo']['servName'])
    elif 'ISP' in json_data['whois']['korean']:
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['addr'])
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['range'])
      WhoIsData.append(json_data['whois']['korean']['ISP']['netinfo']['servName'])
  
 
  # 결과 화면에 뿌리기
  print(WhoIsData)
 
  # 엑셀에 결과 저장
  saveContent(WhoIsData)
  column_num = column_num + 1
  
 
# 엑셀 1째 줄에 데이터 제목 저장  
excelTitle = ['IP''countryCode''addr''IP_range''servName']
saveColName(excelTitle)
 
# 파일 열어 리스트에 담기
= open('ip.txt''r')
iplist = f.read().splitlines()
 
# 리스트에 담기 IP 정보 얻어 오기
for ip in iplist:
  requestWhois(ip)
 
# 엑셀 저장하기
wb.save("ipinfo.xlsx")
cs

 

 

  최종 소스를(소스 내의 '발급받은키' 부분은 자신의 키로 수정 해야 한다) c:\python\code 폴더에 whoisapi_3rd.py 로 저장하고 아래와 같이 실행해 본다. 잘 동작이 된다. c:\Python\code>python whoisapi_3rd.py
['202.30.50.51', 'KR', '서울특별시 송파구 중대로', '202.30.50.0 - 202.30.51.255', 'KRNIC-NET']
['103.235.46.39', 'HK']

 

  해당 폴더에 생성된 ipinfo.xlsx 파일도 확인해보면 잘 저장되어 있다.

 

 

  이렇게 피들러땜에 좀 설명이 길었던(하지만 꼭 한번 소개 하고 싶었던) 10교시를 마치고, 다음 시간에는 웹 크롤링에 대해서 몇가지 생각해 볼 주제를 Beautiful Soap 과 정규 표현식, 이번 시간 예제와 연결해서 진행해볼 예정이다. 크롤링 부터는 슬슬 자동화의 영역에 발을 들이는 기분이다. 피들러는 구글을 찾아보면 자료가 많이 있으니(다만 사이트마다 올리는 사람의 관심 분야에 따라 설명 포커스는 조금 틀린거 같다) 참고해서, 꼭 이리저리 사용해 보길 바란다. 피들러 홈페이지의 사용 설명을 읽음 더욱 좋다. 개인적으로는 웹세계의 '스위스 아미 나이프' 라고 평가한다.^^ 계속 잘 진행 된다면 언젠가 여기에 피들러 관련한 이런저런 포스팅도 하고 싶긴 하다.

 

 

[보충 #2]

  문의가 와서 보니 openpyxl 최신버전에서는 아래 import 문이 에러가 나네요. 지금 보니 사실상 코드안에서 사용하지 않는 부분이라서 아예 빼버림 에러가 발생안합니다. 위의 예제들에서는 주석처리 해놨어요.

from openpyxl.compat import range

 

 

2017.3.12 by 자유로운설탕 
cs

 

posted by 자유로운설탕
2017. 3. 5. 18:57 프로그래밍

  9교시는 GUI(Graphic User Interface)에 대한 얘기를 풀어보고, 예전 시간에 사용했던 암호화 모듈을 사용해서 윈도우 화면에 입력한 값을 복호화 해서 보여주는 샘플을 만들어 보려한다. 

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

[들어가면서]

  GUI 프로그램을 공부하는 것은 초보자 입장으로는 쉬운일은 아니다. 왜냐하면 사실 조금 간략화 되어 있을 뿐이지 Visual C, JAVA 등으로 만드는 윈도우 GUI 프로그래밍 기법과 거의 동일한 선상의 레벨이기 때문이다. 게다가 그 쪽 언어에서는 보통 Visual Studio 나 Eclipse 같은 사용성이 아주 좋은 IDE 를 통해 개발하기 때문에, GUI 디자인이나, GUI 객체들에 이벤트 연결 하는 부분은 거의 신경 쓰지 않고, 실제 실행되는 이벤트 코드 쪽만 집중해 개발할 수 있지만, 아무래도 아직 파이썬 쪽은 모듈별(pyqt, wxpython) 디자이너 툴이 있다고는 하지만 IDE 와 통합된 형태도 아니며, 그나마 디자인만 잡아주는 수준인듯 하다. 게다가 왠지 수동으로 디자인을 잡는건 미련한 거 같아서 wxpython 을 지원 한다고 하는 wxGlade, wxformBuilder 라는 두 개의 툴을 설치해 검토해 봤는데, 둘 다 다른 언어도 같이 지원하는 일반적인 툴이라서 그런지 윈도우 환경의 python 3.5 에서는 아직 제대로 실행이 안 되는거 같다.(이건 제가 잘 몰라서 그럴 수도 있으니 단정은 못 짓겠다). 또 실제 실행은 안됬지만 생성된 파이썬 코드도 왠지 가독성 측면에서 별로 깔끔해 보이지가 않는다. pyqt 모듈 쪽에서는 디자이너 까진 잘 지원되는 거 같으니 그 쪽 모듈을 사용해 보는 것도 고려해 볼만 하다. pyqt 디자이너에 대해서는 아래 잘 설명되 있는 듯 하다.

http://pythonstudy.xyz/python/article/108-PyQt-QtDesigner-%EB%A9%94%EC%9D%B8%EC%9C%88%EB%8F%84%EC%9A%B0

 

  그리고 이 GUI 모듈들의 사용도, 이전 시간에 다룬 SQL, 정규식 같이 파이썬 언어의 기능 밖의 부분이라고 생각해도 무방 할듯 하다. 만약 다른 언어로 GUI 툴을 개발해 봤거나, 꼭 윈도우 어플리케이션이 아니더라도, 웹 어플리케이션에서 자바스크립트로 DOM 개체와 이벤트로 상호 작용을 하는 부분을 만들어 봤다면, GUI 모듈을 사용할 기초 소양 준비는 다 되있다고 보면 된다. 만약 해당 부분의 경험이 거의 없다면, 이클립스나 비주얼스튜디오 같은 좋은 IDE 로 몇 개의 작은 GUI 어플리케이션을 작성해서, GUI 요소들과 사용자 코드가 상호 작용하는 부분에 대해 감을 익힌 후, 맨 뒤에 소개한 레퍼런스를 참조하여 python GUI 모듈의 디테일한 개발 내용을 보는게 개인적으로 조금 더 나아 보인다. (오해할까봐.. 저도 잘 못하는 부분이긴 하다...). 어떻게 보면 파이썬에서 수동으로 디자인할 때도 비쥬얼스튜디오 같은 IDE로 모양만 잡은 후 관련 좌표등을 참고해 수동으로 옮기는 것도 하나의 방법일 듯 싶다.

 

 

 

[GUI 연결 고리 만들어 보기]

  그럼 2교시때 GUI 샘플을 실행하기 위해서 이미 wxpython 모듈을 아래와 같이 설치 했었다. 혹시 모듈 선택 이유 및 설치 과정이 가물가물하면 2교시 내용을 보면 된다.

 

  스택오퍼플로어에 가이드 된데로 cmd 창에서 아래 명령어를 실행시킨다.

pip install -U --pre -f https://wxpython.org/Phoenix/snapshot-builds/ wxPython_Phoenix

 

 

 

  만들려고 하는 기능은 아래와 같다. 윈도우 창에는 입력 박스 한개와 결과를 출력하는 라벨이 한 개, 버튼이 하나 있다(좀 단촐해서 민망하다 --;). 멀뚱히 비워 놓긴 그래서, 여러가지 디폴트 문장을 표시하게 했다.

 

  텍스트 박스에 암호화 하길 원하는 문장을 넣은 후, 암호화 버튼을 누르면, 아래와 같이 암호화 된 값이 하단 텍스트 라벨에 표시된다.

 

 

  언제나 처럼 위 부분을 만드려면 모르는 게 무언지 생각해 보자. (우선 윈도우즈 창 만드는 법은 안다. 샘플 파일을 이미 실행해 봤으니까...)

1) 버튼을 만들수 있어야 한다.

2) 텍스트 박스를 만들 수 있어야 한다. 안에다 디폴드 값도 넣어 주어야 한다.

4) 결과를 뿌릴 텍스트 라벨 영역을 만들 수 있어야 한다. 이건 샘플파일에 이미 StaticText 란 코드로 출력해 봤다. 이것도 디폴트 값은 넣어주어야 한다.

5) '암호화'버튼이 클릭 됬을때, 특정한 함수 A를 실행 해야 한다. 그 A함수 안에서는 텍스트 박스에 입력한 'my word' 항목을 가져와서, 미리 만들어 놓은 암호화 함수 B를 호출해서 암호화 한 값을 리턴 받은 후, 텍스트 라벨 영역에 출력해 주어야 한다.

 

 

  그럼 첫번째로 버튼을 찾아본다. wxPython 메뉴얼을 쭉 훝어봐도 되겠지만, 구글을 찾는게 편할 듯 하다. 'wxpython show input box'. 맨 위의 스택 오버플로우 결과를 클릭한다.

http://stackoverflow.com/questions/18532827/using-wxpython-to-get-input-from-user

 

  오 여기에 버튼 뿐만아니라 텍스트 박스를 만드는 코드도 같이 있다. 디폴드 값 넣는 부분도 존재한다. 자세히 보면 버튼이 클릭 됬을 때 특정한 함수를 호출 하는 부분도 있다. 1타 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
30
31
32
33
34
35
36
37
#-*- coding: utf-8 -*-
import wx
class Frame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(-1-1))
        self.panel = wx.Panel(self)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
        # 버튼 만드는 코드
        self.btn = wx.Button(self.panel, -1"Name-a-matic")
        # 버튼 클릭 이벤트에 함수(메쏘드) 연결하는 코드
        self.Bind(wx.EVT_BUTTON, self.GetName, self.btn)
        # 텍스트 박스 만들고, 디폴트 값 넣는 코드
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('name goes here')
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.btn)
        sizer.Add(self.txt)
 
        self.panel.SetSizer(sizer)
        self.Show()
        
        # 버튼이 눌렸을 때 실행 되는 함수,  사용자의 이름을 얻어 텍스트 박스에 넣어줌
    def GetName(self, e):
 
        dlg = wx.TextEntryDialog(self.panel, 'Whats yo name?:',"name-o-rama",""
                style=wx.OK)
        dlg.ShowModal()
        self.txt.SetValue(dlg.GetValue())
        dlg.Destroy()
 
    def OnCloseWindow(self, e):
        self.Destroy()
 
app = wx.App()
frame = Frame(None, 'My Nameomatic')
app.MainLoop()
cs

 

 

 

 

  그럼 저 뼈대를 거의 그대로 쓰고, 1) 버튼을 만드는 코드는 아래와 같다.

1
        self.btn = wx.Button(self.panel, -1'암호화')
cs

 

  2) 텍스트 박스를 만들고, 디폴트 값을 넣는 코드는 아래와 같다.

1
2
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('input your value')
cs

 

  3) 텍스트 라벨을 만들고, 디폴트 값을 넣는 코드는 아래와 같다.

1
2
        self.some_text = wx.StaticText(self.panel, size=(140,150), pos=(10,60))
        self.some_text.SetLabel('result is...')
cs

 

  4) 버튼을 누르면 이벤트 함수를 연결하는 코드는 아래와 같다.

1
        self.Bind(wx.EVT_BUTTON, self.GetEncryption, self.btn)
cs

 

  5) 위의 4번에 있는 GetEncryption 함수는 텍스트 박스의 값을 가져다, 암호화 함수에 던저주고, 리턴된 값을 다시 라벨에 넣어주여야 한다. 해당 기능의 코드는 아래와 같다.

  ※ 첨에는 버튼 이벤트에 GetEncryption 함수를 호출할 때 암호화 할 값을 전달해 줘야하나 하고 한참 구글을 헤멨는데, 안에서 텍스트 필드에 접근하면 되는 거라서 그냥 기존 코드를 그대로 이용했다. --;

1
2
3
    def GetEncryption(self, e): 
        self.enc = AESCipher(key).encrypt(self.txt.GetValue())
        self.some_text.SetLabel(self.enc)
cs

 

 

 

 

  그럼 해당 코드를 전체적으로 정리한 코드는 아래와 같다. 예전에 달았던 암호화 관련 주석들은 이번 시간 내용과 관련 없는 부분이니 출처만 빼고 삭제 했다.

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
#-*- coding: utf-8 -*-
# 암호화 코드 출처: http://blog.dokenzy.com/
import wx
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
 
# 암호화 관련 초기화 코드
BS = 16
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS).encode()
unpad = lambda s: s[:-ord(s[len(s)-1:])]
key = 'abcdefghijklmnopqrstuvwxyz123456'
 
def iv():
    return chr(0* 16
 
 
# 윈도우즈 정의 클래스
class Frame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(-1-1))
        self.panel = wx.Panel(self)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
 
        # 버튼 생성
        self.btn = wx.Button(self.panel, -1'암호화')
 
        # 텍스트 박스 생성
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('input your value')
        
        # 텍스트 라벨 생성
        self.some_text = wx.StaticText(self.panel, size=(140,150), pos=(10,60))
        self.some_text.SetLabel('result is...')
 
        # 버튼 클릭 시 이벤트 연결
        self.Bind(wx.EVT_BUTTON, self.GetEncryption, self.btn)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.btn)
        sizer.Add(self.txt)
 
        self.panel.SetSizer(sizer)
        self.Show()
 
    # 버튼 클릭 시 실행 되어, 암호화 하는 함수
    def GetEncryption(self, e): 
        # 텍스트 박스(self.txt)로 부터 값을 얻어와 암호화 함수로 넘겨준다.
        self.enc = AESCipher(key).encrypt(self.txt.GetValue())
        # 받은 값을 텍스트 라벨에 출력한다.
        self.some_text.SetLabel(self.enc)
 
 
    def OnCloseWindow(self, e):
        self.Destroy()
 
 
# 암호화 관련 클래스
class AESCipher(object):
 
    def __init__(self, key):
        self.key = key
 
    def encrypt(self, message):
        message = message.encode()
        raw = pad(message)
        cipher = AES.new(self.key, AES.MODE_CBC, iv())
        enc = cipher.encrypt(raw)
        return base64.b64encode(enc).decode('utf-8')
 
    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        cipher = AES.new(self.key, AES.MODE_CBC, iv())
        dec = cipher.decrypt(enc)
        return unpad(dec).decode('utf-8')
 
 
 
# 메인코드 #
# 윈도우를 띄우고 제목을 넣는다.
app = wx.App()
frame = Frame(None, 'WxEncryption')
app.MainLoop()
cs

 

 

  그럼 최종 코드를 c:\python\code 폴더에 wxEncyrption.py 이름으로 저장한다(역시 모르면 2교시로 가서 복습을...). 아래와 같이 실행하면 원하는 기능을 하는 윈도우 창이 뜨게 된다. 

 

c:\Python\code>python wxEncyrption.py

 

 

 

 

[마무리 하면서]

  자 그럼 앞으로 더 복잡하거나 새로운 GUI 컨트롤 들을 사용해서 만드려면 어떻게 해야할까?(파일 입력 박스, 라디오 버튼, 텍스트 박스 등등) 우리가 다운받아 사용한 wxPython Phoenix 홈페이지의 문서를 보면 wxPython 에서 현재 지원하고 있는 기능이 망라되어 있는 페이지가 있다. 여기 있는 요소들을 참조해 필요한 경우가 있다면 구글을 찾아서 실제 사용 예들을 참조해 보면 될 것 같다. 예를 들어 파일 다이얼로그인 filepickerctrl 을 사용하고 싶다면, 구글에서 'wxpython use filepickerctrl' 이라고 찾아보면 아마도 절절한 스택오버플로우 페이지들이 반겨줄 것이다.

https://wxpython.org/Phoenix/docs/html/wx.1moduleindex.html

 

 

 

 그럼 이것으로 9교시를 마치려고 한다.

 

 

[부록] 콘치즈파파님 문의 내용 답변

 

  위의 예시와 거의 비슷하고, GetQRCode 메소드 내에서 UI 화면에서 입력한 inputValue 값을 기존 만드신 코드와 연결해 가져와 저장하는 코드를 완료하심 될거 같아요.  해당 액션이 잘 끝나면 "저장완료" 라고 화면에 나올거고요.

 

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
import wx
 
 
# 윈도우즈 정의 클래스 입니다.
class Frame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(500200))
        self.panel = wx.Panel(self)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
 
        # 버튼을 생성 합니다.
        self.btn = wx.Button(self.panel, -1'QR 코드 저장')
 
        # 텍스트 박스를 생성합니다.
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('input your url')
        
        # 텍스트 라벨을 생성합니다.
        self.some_text = wx.StaticText(self.panel, size=(140,150), pos=(10,60))
        self.some_text.SetLabel('result is...')
 
        # 버튼 클릭 시 이벤트를 연결 합니다.
        self.Bind(wx.EVT_BUTTON, self.GetQRCode, self.btn)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.btn)
        sizer.Add(self.txt)
 
        self.panel.SetSizer(sizer)
        self.Show()
 
    # 버튼 클릭 시 실행 되어, 암호화 하는 함수 입니다.
    def GetQRCode(self, e): 
        inputValue = self.txt.GetValue()
        
        # 이 inputValue 값을 QR 코드 쪽에 연결하면 됨.
        
        # 받은 값을 텍스트 라벨에 출력합니다.
        self.some_text.SetLabel("저장완료")
 
 
    def OnCloseWindow(self, e):
        self.Destroy()
 
# 메인코드 입니다.
# 윈도우를 띄우고 제목을 넣습니다.
app = wx.App()
frame = Frame(None, 'getQRCode')
app.MainLoop()
 
cs

 

[QR 코드랑 머지한 버전 - 이름을 겹치지 않게 시간을 이용해 만들어 넣음]

  예를 들어 www.google.co.kr 이라고 입력하면 관련된 QR 코드를 만듬.

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
import wx
import qrcode
import datetime
 
# 윈도우즈 정의 클래스 입니다.
class Frame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(500200))
        self.panel = wx.Panel(self)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
 
        # 버튼을 생성 합니다.
        self.btn = wx.Button(self.panel, -1'QR 코드 저장')
 
        # 텍스트 박스를 생성합니다.
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('input your url')
        
        # 텍스트 라벨을 생성합니다.
        self.some_text = wx.StaticText(self.panel, size=(140,150), pos=(10,60))
        self.some_text.SetLabel('result is...')
 
        # 버튼 클릭 시 이벤트를 연결 합니다.
        self.Bind(wx.EVT_BUTTON, self.GetQRCode, self.btn)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.btn)
        sizer.Add(self.txt)
 
        self.panel.SetSizer(sizer)
        self.Show()
 
    # 버튼 클릭 시 실행 되어, 암호화 하는 함수 입니다.
    def GetQRCode(self, e): 
        inputValue = self.txt.GetValue()
        
        # 이 inputValue 값을 QR 코드 쪽에 연결하면 됨.
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L,box_size=10,border=4)
        qrAddress = "http://" + inputValue + "qrCodeId=CAD20181220041519"
        qr.add_data(qrAddress)
        qr.make(fit=True)
 
        img = qr.make_image()
 
        fileName = datetime.datetime.now().strftime("%Y%m%d_%H%M%S"+ ".png" 
        img.save(fileName)
        qr.clear()
        
        
        # 받은 값을 텍스트 라벨에 출력합니다.
        self.some_text.SetLabel("저장완료")
 
 
    def OnCloseWindow(self, e):
        self.Destroy()
 
# 메인코드 입니다.
# 윈도우를 띄우고 제목을 넣습니다.
app = wx.App()
frame = Frame(None, 'getQRCode')
app.MainLoop()
 
cs

 

 

[입력 창을 2개 만들어서, 이름을 받음]

  예를 들어 www.google.co.kr 과 google 이라고 입력 하면 google.png 로 생성해줌.

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
import wx
import qrcode
 
# 윈도우즈 정의 클래스 입니다.
class Frame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(500200))
        self.panel = wx.Panel(self)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
 
        # 버튼을 생성 합니다.
        self.btn = wx.Button(self.panel, -1'QR 코드 저장')
 
        # 텍스트 박스를 생성합니다.
        self.txt = wx.TextCtrl(self.panel, -1, size=(140,-1))
        self.txt.SetValue('input your url')
        
        # 텍스트 박스를 생성합니다.
        self.txt2 = wx.TextCtrl(self.panel, -1, size=(140,-1), pos=(0,50))
        self.txt2.SetValue('input your file name')
        
        # 텍스트 라벨을 생성합니다.
        self.some_text = wx.StaticText(self.panel, size=(140,150), pos=(10,80))
        self.some_text.SetLabel('result is...')
 
        # 버튼 클릭 시 이벤트를 연결 합니다.
        self.Bind(wx.EVT_BUTTON, self.GetQRCode, self.btn)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.btn)
        sizer.Add(self.txt)
 
        self.panel.SetSizer(sizer)
        self.Show()
 
    # 버튼 클릭 시 실행 되어, 암호화 하는 함수 입니다.
    def GetQRCode(self, e): 
        inputValue = self.txt.GetValue()
        
        # 이 inputValue 값을 QR 코드 쪽에 연결하면 됨.
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L,box_size=10,border=4)
        qrAddress = "http://" + inputValue + "qrCodeId=CAD20181220041519"
        qr.add_data(qrAddress)
        qr.make(fit=True)
 
        img = qr.make_image()
 
        fileName =  self.txt2.GetValue() + ".png"
        img.save(fileName)
        qr.clear()
        
        
        # 받은 값을 텍스트 라벨에 출력합니다.
        self.some_text.SetLabel("저장완료")
 
 
    def OnCloseWindow(self, e):
        self.Destroy()
 
# 메인코드 입니다.
# 윈도우를 띄우고 제목을 넣습니다.
app = wx.App()
frame = Frame(None, 'getQRCode')
app.MainLoop()
 
cs

 

 

 

1
2017.3.5 by 자유로운설탕
cs

 

 

 

posted by 자유로운설탕
2017. 3. 4. 12:57 프로그래밍

  8교시는 정규 표현식이 무언지를 알아보고, 파이썬에서 사용하는 샘플을 시연하려 한다.

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

[들어가면서] 

  정규 표현식이란 건 처음 보게 되면 정말 이상한 기호로 이루어진 암호문 같은 언어라는 생각이 든다. '.',  '*',  '[', '-' 등의 보통 프로그램에서는 구조용 기호로만 쓰일 여러가지 문자로 이루어진 문법을 사용하는데, 웬만한 프로그래밍 책에서는 또 한번씩은 슬쩍 다루기도 한다. 이메일이나 전화번호 같은 형태를 검출 하는데 사용한다고 얘기는 들었지만, 당장 프로그래밍 언어도 배우기 힘들어 죽겠는데 이걸 정말 어디다 쓰나 싶다. 또 이메일 검출이 필요하다면 구글에서 관련 샘플을 가져다 쓰면 되지 않나도 싶다. 그래서 아마 한번 쯤 훝어보고 이렇구나 하고 넘어가는 경우가 많을 것 같다. 이 시간을 통해서 어떤 경우 정규 표현식이 유용하게 쓸 수 있을까 하는 부분에 대해 조금이라도 전달이 되고, 나중에 따로 시간을 내서 찬찬히 공부를 해보는 것도 괜찮겠구나 생각이 드는 계기가 된다면 더 바랄 것은 없을 듯하다.

 

  언어나 툴들 사이의 정규 표현식의 사용법 및 기능 차이가 있다. 예를 들면 언어인 perl, python, java 뿐만 아니라, 내용, 파일이름 검색에서 정규 표현식을 지원하는 울트라에디터, Total commander 같은 유틸리티 들도 약간의 사용 차이를 가지고 있다.  아래의 예를 보면 울트라에디터라는 편집기에서 perl, unix, 울트라 에디터 방식의 3가지 정규 표현식 스타일(문법)을 내용 검색을 지원 한다는 것이다.

 

  개인적으로 느끼기에 언어별 이런 정규 표현식 문법 표시 및 사용 스타일의 차이는 어느 언어든간에 해당 언어를 만든 사람이 이렇게 사용하면 편하겠다고 생각해서 디자인한 걸테고, 기본적인 틀은 사실 많이 벗어나지 않는다. 예를 들면 앞에서 얘기했던 관계형 데이터 베이스인, 오라클, Mysql, MSSQL 이 SQL의 기본베이스만 잘 이해하고 있다면 확장된 차이점만 공부하면 되는 것과 마찬가지로(물론 언어별로 깊이 들어가면 디테일한게 있다고 얘기하심 반박하긴 힘들다), 언어들의 사용법들의 세세한 차이보다 중요한건, SQL 의 여러 공통된 요소들에 대한 깊고 정확한 이해라고 본다. 어느 하나라도 기본 베이스가 되면 사실 스타일만 적용하면 된다고 보면 된다.(다만 예외인 것은 SQL 도 마찬가지지만, 사용자의 편의성을 위해서 기본 기능을 확장한 부분들은 각 언어별로 장 단점이 있으리라고 본다)

 

  정규 표현식도 SQL 과 마찬가지로 기본 베이스가 중요하다. 정규식의 기본 뼈대를 구성하는 요소들만 찬찬히 잘 이해하고, 정규 표현식으로 할수 있는 일과 할 수 없는 일을 정확하게 인식만 할수 있다면, 문법을 굳이 암기 하지 못한다고 하더라도(개인적으로는 매번 정규 표현식이 필요할때 마다 다시 가물가물해서 메뉴얼을 찾아 보곤 한다 --;) 그 부분은 구글이나 해당 언어의 정규표현식 메뉴얼을 찾아가면서 필요한 기능을 만들면 된다. 그리고 다시 한번 강조 하고 싶은 부분 중 하나는 정규 표현식 역시 파이썬에서 자주 사용은 하겠지만, 파이썬의 본질적 문법과는 그닥 상관없는 프로그래밍의 외적 요소 중 하나라는 것이다. 다만 익혀 두면 텍스트 원본에서 의미 있는 데이터를 찾아내는 파싱(parsing) 이라는 측면에서 정규식을 모르는 사람과는 조금은 차이가 난다고 생각하여 이렇게 따로 한 챕터를 분리해서 맛보기로 보여주려고 한다.

 

 

  그럼 정규 표현식은 어떨때 많이 유용할까? 텍스트 형태라고 했으니 html 을 파싱 할때, xml 을 파싱 할때, json 을 파싱 할때? 일단 기본적으로 텍스트 형식의 데이터는 모두 적용 가능하지만, 사실 json, xml 같은 많이 쓰이는 정형화된 텍스트 형태의 데이터 들은 이미 정규 표현식 보다 효율적으로 내부에 담겨진 데이터 위치를 쉽게 찾아, 접근 할수 있는 모듈들이 많이 만들어져 있다. 개인적으로 이런데에 정규 표현식을 쓰는건 계륵이라고 생각한다(전동 드라이버가 집에 있는데, 드라이버를 넣을 공간이 안되 힘들다 거나 하는 특별한 이유도 없이 굳이 일반 드라이버를 수동으로 열심히 돌리는 것과 같다고 본다). html, xml 의 구조 파싱의 경우 beautiful soup 이라는 레퍼런스가 풍부한 좋은 모듈이 있는듯 하고, json 은  파이썬에 내장된 모듈이 있다. json 은 뒤의 api 쓰는데서 사용할 예정이고, beautiful soup 도 개인적으로도 궁금해 한 챕터를 추가할까 싶다. (이미 구글에서 소스보기, 크롤링 관련해서 관련해서 잘 설명해 놓은 글들이 많은 것 같긴 하지만...). 그래서 사실 정규 표현식은 적당히 비 공식적인 포맷에 잘 어울린다. telnet 으로 연결한 터미널의 출력 결과 라든지, 로그파일 이라든지, 메신저 프로그램 등의 텍스트 형태의 내보내기(export) 파일 이라든지 말이다. 여튼 적절하게 정형화된 텍스트 형태의 데이터를 대상으로 가장 적합한 사용을 할수 있다고 개인적으로 생각한다.

 

 

[추천하는 공부방법]

  그리고 공부하기 위해서 관련 책을 선택할 때는 조금 헷깔려 할 것 같은 부분이 있다. 정규 표현식 책은 보통 2가지 스타일이 있다고 보는데, 하나는 언어별로 비교해 다루면서 설명하는 책과, 다른 하나는 하나의 언어만 선택해서 설명에 들어가는 책이 있는 듯 한다. 전자는 내가 쓰는 언어를 사용해 샘플을 만들어 시연해 볼수 있는 가능성이 크지만, 너무 여러가지 언어의 차이점을 다루느라 내용 전달에 초점이 안 맞춰질 수 있고, 반대로 후자는 한 가지 언어만을 다루면서 너무 일반적인 내용을 다루거나, 반대로 너무 깊이 들어가 난이도가 어려워 져서 처음 보는 입장에서는 힘들 수 있다. 예전에 봤던 책 중에 추천할 만한 책은(이해 관계는 전혀 없다 --;) "손에 잡히는 정규 표현식" 이라는 책과, 요즘 나온 책 중에서는 괜찮아 보이고 평도 괜찮은 책은 "다양한 언어로 배우는 정규표현식" 이다. 두 번째 책은 정규 표현식의 구조 까지 언급해 들어가는 게 좀 걸리긴 하지만, 한국어 책만 보고 싶다면 위의 두 책을 순서대로 보거나, 앞 책을 우선 보고 다음 책을 계속 볼지 고려해 보는 것도 어떨까 싶다. 앞 책은 개인적으로 평가했을 때, 초보자가 보기에도 확실히 잘 구성되 있다. 

 

  개인적으로 정규 표현식을 공부하는 방법은 일단 너무 어렵지 않은 위에 언급한 것과 비슷한 책을 한권 찬찬히 읽어 전체적인 흐름을 파악한후, 자기가 공부 하고 싶은 언어의 정규 표현식 사용법을 살펴 보는거다. 이후 좀더 전문적으로 속 내용 까지 보고 싶으면 mastering regular expressions 같은 책을 찬찬히 보심 될 듯하다.(얘 쫌 두껍긴 하다)

 

 

  마지막으로 실습을 해볼때는 3가지 방법이 있을 듯 한데, 1) 정규 표현식을 테스트 할수 있는 유틸리티 프로그램으로 실습하는 것, 2) 정규 표현식을 테스트 할 수 있는 웹 사이트를 이용하는 것, 3) 파이썬을 그대로 이용해 실행 해보는 것 이다. 파이썬을 바로 실행하면서 이것저것 공부해 보는 것도 나쁘진 않아 보이는데, 파이썬에 익숙하지 않다고 생각하는 사람은 1), 2) 번이 정규 표현식 자체의 공부에만 집중 할 수 있어 좋다.

 

  유틸리티 프로그램 사용 방식은 아래의 툴을 다운 받으면 소스가 .net 기반인 거 보니 .net 형식의 정규식 일거 같긴 하지만, 처음 공부할 레벨의 기본 정규식은 거의 호환이 될 것이다.

https://sourceforge.net/projects/regulator/

 

  사용 방법은 프로그램을 다운받아 압축을 풀고 Regulator.exe 을 실행 시킨다. 텍스트 편집기에 테스트할 내용을 아래와 같이 적은 후 c:\python\code 폴더에  reg.txt 로 저장한다.

1
2
<hi> test
  me <hello>
cs

 

  이후 아래의 프로그램 메뉴에서,  

   1) 내용 파일을 선택 해서 저장한 reg.txt 를 불러오면 해당 내용이 Input 창 내에 보인다.

   2) 이후 문서창에 우리가 원하는 정규 표현식을 넣는다. 여기서 넣은 내용은 "^<.*>" 

   3) 상단의 'Match' 버튼을 클릭 한다.

   4) 정규 표현식과 매치된 결과들이 나온다.

  입력된 정규 표현식(^<.*>)을 해석 하면 "행의 처음 시작(^)이 '<' 문자이고, 그 뒤에 어떤 글자든(.) 0개 이상 나오고(*) 다시 '>' 로 끝나는 단어들을 찾아줘" 이다. 그래서 1번째 행의 <hi> 는 '<'가 행의 처음 시작이기 때문에 선택되고, 2번째 행의 <hello> 는 행의 내용 중간에 '<' 가 시작되서 선택이 안되었다. 위쪽의 정규 표현식에서 '^' 기호만 빼고 다시 'Match' 버튼을 누르면, 이제는 '행의 시작'이라는 조건이 사라졌기 때문에 <hi>, <hello> 모두 선택 되니 해보길 바란다. 

 

 

 

  두 번째는 제가 한참 공부할땐 생각 못했던 웹 페이지에서 쓰는 방식이다. 구글에서 'regular expression tool' 이라고 찾음 맨 처음에 나오는, https://regex101.com/ 페이지 이다. 사용법은 거의 비슷하다. 첫 번째로 왼쪽에서 FLAVOR 섹션에서 'python' 을 선택한다. 그럼 이제 부터 적용하는 모든 정규 표현식은 파이썬 스타일로 적용된다.

 

  그 후 밑의 그림에 있는데로 TEST STRING 안에 아까 텍스트 파일로 저장했던 내용을 복사해 넣고, REGULAR EXPRESSION 안에 우리가 넣었었던 ^<.*> 식을 넣으면, 실시간으로 왼쪽 밑의 MATCH INFOMATION 쪽에 매칭된 내용이 나온다. 파이썬으로 정규 표현식을 만들 필요가 있을때는 이런 웹페이지에서 원하는 패턴의 텍스트와, 정규 표현식을 충분히 테스트 하고, 테스트가 완료되면 파이썬 코드에 살포시 집어 넣어주면 버그도 없고 좋을 듯 하다.

 

 

 

 

[파이썬으로 정규 표현식 실행해 보기]

  ㅎㅎ 사실 이 시간은 언어속의 부록 언어같은 정규 표현식에 대해서 얘기하는 시간 이였어서, 위의 도입 부분에서 이미 할 모든 얘기를 다 한듯 싶기도 하다 --; 그럼 이제 부턴 맘 편하게 파이썬에서 어떻게 정규 표현식이 사용 되는지 간단한 샘플과 함께 시연을 하고 마치려 한다. 우선 파이썬에서 정규 표현식이 어떻게 쓰이는지 알고 싶다면 구글에서 'python regular expression' 이라고 검색한다.  그럼 맨위에 바로 파이썬 공식 페이지의 문서가 있다.

https://docs.python.org/2/library/re.html

 

 

  본인이 정규 표현식만 다른 언어에서 잘 경험해 보았다면, 해당 페이지를 슬슬 훝어보면 다른 언어와의 차이를 빨리 캐치하고, 바로 사용에 들어갈 수도 있을 것이다.  그럼 파일을 읽어와서 파일안에서 특정 정규 표현식과 일치하는 단어들을 찾는 예제를 만들어 보려 한다. 위의 파이썬 예제 페이지의 샘플은 그다지 바로 쓰기엔 직관적이지 못한듯 해서, 구글에다 'python regular expression file' 라고 검색한다. 가장 상단의 스택오버플로우 글을 본다

http://stackoverflow.com/questions/10477294/how-do-i-search-for-a-pattern-within-a-text-file-using-python-combining-regex

 

  제일 위에 있는 아래 샘플이 맘에 든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록
pattern = re.compile("<(\d{4,5})>")
 
# test.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('test.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 몇 번째 라인에서, 어떤 값을 찾았는지 모두(group()) 보여준다.
        print 'Found on line %s: %s' % (i+1, match.groups())
cs

 

 

  그럼 위의 샘플을 적당히 변경해서, 우리가 아까 만든 reg.txt 에 대한 정규 표현식 예제를 만들어 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록(처음 시작이 < 이고 > 로 닫히는 단어)
pattern = re.compile("^<.*>")
 
# reg.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('reg.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 찾은 값을 뿌림.
        print (match.groups())
cs

 

  근데 해당 파일을 c:\Python\code 폴더에 reg_sample1.py 로 저장해서 실행하면 아래와 같이 빈 괄호 기호만 나오게 된다.

c:\Python\code>python reg_sample1.py
()

 

 

  샘플이 잘못된거 같진 않은데 왜 그럴까? 원인을 찾기위해서 처음 찾았던 파이썬 공식 페이지의 regular expression 페이지에서 groups 에 대한 내용을 본다. (7.2.4 Match Objects) 페이지에 있는 group 과 groups 예제를 보니, groups 는 기본적으로 튜플로 반환하고(현재로서는 리스트랑 튜플이 뭐가 다른진 모른다), group(0) 을 하거나 group() 을 했을 때 전체 결과를 문자열로 반환한다고 한다. 그래서 마지막 print 문 안의 groups 를 group 으로 바꿔 보았다.

https://docs.python.org/2/library/re.html

 

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록(처음 시작이 < 이고 > 로 닫히는 단어)
pattern = re.compile("^<.*>")
 
# reg.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('reg.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 찾은 값을 뿌림.
        print (match.group()) #여기 변경
cs

c:\Python\code>python reg_sample1.py
<hi>

 

  흠 근데 실행해 보니 s 만 하나 뺐을 뿐인데, 값이 제대로 출력되어 나온다. 원래대로라면 groups 가 아무리 튜플이라는 리스트 비슷한 구조로 표시한다 하더라도, 안에 뭔가 들었기 때문에 어떤 결과든 나와야 할것 같은데, 상식적으로 빈 괄호로만 나오는게 이해가 안된다. 그럼 뭔가 원인이 있을테니 구글에 좀더 자세히 검색해 본다. 'python regular expression group vs groups'. 가운데 쯤 나오는 스택 오버플로우 페이지에 원하던 설명이 나오는 것 같다.

http://stackoverflow.com/questions/9347950/whats-the-difference-between-groups-and-group-in-pythons-re-module

 

groups() only returns any explicitly-captured groups in your regex (denoted by ( round brackets ) in your regex), whereas group(0) returns the entire substring that's matched by your regex regardless of whether your expression has any capture groups.

-> 결국 우리가 패턴을 등록할때 명시적으로 그룹이 나올 수 있도록 정규식 내에 넣는 '( )' 부분이 없기 때문이라고 한다.

  ※ group 에 대한 설명은 정규 표현식을 정식으로 공부하다보면 알수 있다. 파이썬으로 예를 들면, 클래스 개념 같은 약간 고급의 응용 문법이다.

 

 

  그럼 해당 부분을 증명하기 위해 첫번째 에러난 코드를 한번 수정해 보자. 패턴 부분을 ( )로 감싸서 그룹 결과가 나오게 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록(처음 시작이 < 이고 > 로 닫히는 단어)
pattern = re.compile("(^<.*>)")  #패턴을 가로로 감싸보자
 
# reg.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('reg.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 찾은 값을 뿌림.
        print (match.groups())
cs
c:\Python\code>python reg_sample1.py
('<hi>',)

 

  자 이제 결과가 나오고 찾은 설명이 맞았다는게 증명됬다.

 

  부록으로 그럼 첫번째 잘 돌아간 reg_sample1.py 에서 group 을 출력하는 부분에서 <> 를 제외한 hi 만 가져오려 한다면 어떻게 될까? 이럴때 우리를 괴롭혔던 그룹표시() 가 반대로 도움을 준다. 패턴 코드에서 re.compile("^<(.*)>") <- 요렇게 안쪽에 괄호를 써보자 그럼 <> 안에 있는 어떤 글자든(.) 0개 이상 나오면(*) 1번째 그룹으로 지정이 된다. 그후 match.group(1) 로 수정해서 첫번째 그룹을 가져온다. 저장하고 실행 하면 아래와 같이 'hi' 만 가져오게 된다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록(처음 시작이 < 이고 > 로 닫히는 단어)
pattern = re.compile("^<(.*)>") #원하는 부분에 괄호 넣기
 
# reg.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('reg.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 찾은 값을 뿌림.
        print (match.group(1)) #여기 변경 #1번 그룹을
cs

c:\Python\code>python reg_sample1.py
hi

 

 

 

  그럼 하나만 하면 정이 없으니 하나 샘플을 더 만들어 보려고 한다. 먼저 c:\Python\code 폴더에 reg2.txt 파일을 만들어 아래 내용을 넣는다.

1
2
3
4
5
6
7
1cake - Right
jelly 12hey - Wrong
maybe12 - Wrong
    3joy - Wrong
4432 - Right
23b - Right
  5555b - Wrong
cs

 

  자 위에서 Right 라고 적힌 3개의 줄 내용만 출력하고 싶다. 3개의 공통 점은 무엇 일까? 숫자로 시작되고 줄 처음에서 시작된다는 것이다. 그럼 위에서 사용한 정규 표현식에다 두 가지 개념만 더 추가함 된다. 1) "숫자로 시작한다는 것" 2) "문장 전체를 가져오려 한다는것". 해당 부분을 목표로 정규 표현식을 작성하면 다음과 같다. ^[0-9]+.*

 

  설명하자면 "숫자가([0-9]) 한개이상 (+) 문장의 맨처음에 나온다(^)", "이후엔 아무 문자나(.) 나오거나 말거나 상관없다(*)" 이다. 

 

  그럼 위의 잘 돌아갔던 소스를 가져다가 파일 가져오는 부분과, 정규 표현식 패턴 부분만 수정해 보자. 해당 파일을 c:\Python\code 폴더에 reg_sample2.py 로 저장해서 실행하면 아래와 같이 Right 줄만 3개 나오게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#-*- coding: utf-8 -*-
# regular expression 모듈 임포트
import re
 
# 정규 표현식 패턴 등록(처음 시작이 < 이고 > 로 닫히는 단어)
pattern = re.compile("^[0-9]+.*"
 
# reg.txt 파일을 한줄 한줄 가져와서(enumerate) 루프를 돌리면서
for i, line in enumerate(open('reg2.txt')):
# 해당 줄에서 원하는 패턴을 모두 찾아 한건 한 건 꺼내어서
    for match in re.finditer(pattern, line):
# 찾은 값을 뿌림.
        print (match.group())
cs

c:\Python\code>python reg_sample2.py
1cake - Right
4432 - Right
23b - Right

 

 

 

[마무리 하면서]

  이 시점에서 우리가 한번 생각해 봐야할 부분이 있다는 생각이 든다. 프로그래밍을 스트레스를 덜 받으면서 하려면, 도메인 지식이나, 정규 표현식 같이 다양하게 연결된 배경 지식들을 잘 알고 있어야 하듯이, 정규 표현식을 잘 쓰려면, 문법 이외에도, 적용하려는 데이터의 패턴을 정규 표현식에서 지원하는 방식으로 인지 할 수 있어야 한다. 만약 적절한 패턴이 없다면 데이터가 만들어질 때부터 가능한 패턴을 가지도록 디자인 하는 부분을 고려해야 할지도 모른다. 어찌 보면 정규 표현식의 문법에 대해서 아는 것보다 이러한 부분이 더 중요하고 어려운 문제일 수도 있다. 이것은 프로그래밍 뿐만 아니라 다른 여러 분야도 비슷한 부분들이 많다. 너무 트랜드에만 휩쓸리진 말고 손가락 보다는 손가락이 가르키는 대상을 보려고 노력하자^^

http://m.blog.naver.com/sukbongcho/10157104127

 

 

  그러면 얼렁 뚱땅 넘어가긴 했지만, 이렇게 정규 표현식(자꾸 정규식이라고 쓴다. 그림 등에 오타가 있는데 그러려니 해주시길... --;) 편을 마치 겠다. 배워야 할께 너무 많은 세상이지만 기초적인 부분은 꼭 찬찬히 짚어가면서 걸어가시길 바라면서..

 

 

 

 

[보충]

  본문에 나왔던 '튜플'에 대해서 궁금해서 찼아봤다. 튜플을 함수형 프로그래밍 등에서 쓰는 불변 값의 리스트로 봐도 좋겠지만, 첫 번째 링크에 나온데로, 인덱스를 가진 작은 읽기전용의 데이터 구조 라고 보는것이 맞을 듯 싶다. 

http://jtauber.com/blog/2006/04/15/python_tuples_are_not_just_constant_lists/

http://news.e-scribe.com/397

 

 

  파이썬 정규식에서 사용하는 search, match, findall, findIter 로 찾는 차이가 궁금해서 정리한다.

> search - 맨 처음 만나는 문자열을 match object 구조로 반환한다.

> match - search 아 다른 점은 항상 패턴 앞에 ^(문장 처음) 가 있는 것처럼 판단하다.

> findall - 해당 되는 문자열을 모두 찾아 문자열 리스트로 반환한다.

> finditer - 해당되는 문자열을 모두 찾아 match object 형태의 iterator 로 반환한다. 그래서 위의 예제 들에서 이 개체를 이용해서 루프를 돌리며 match object 의 group 속성을 얻어냈다.

 

 

 

1
2017.3.5 by 자유로운설탕
cs
posted by 자유로운설탕
2017. 3. 1. 13:46 프로그래밍

  이번 부록 시간에는 7교시때 만들었던 쿼리 결과를 엑셀로 저장했던 코드를 함수를 이용해서 정리해 보는 작업을 해보려고 한다. 과정을 설명하는게 좀 난해하고 속을 보이는 것 같아 창피하게 느껴지니 전개가 어설프더라도 이해하기 바란다.

 

 

[목차]

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. 정리 - 이런저런 이야기

 

 

 

 

[들어가면서]

  함수라는 것은 일반적으로 반복적인 코드를 독립된 기능으로 분리해 냄으로서 유지보수 성을 높여주게 된다. 또 부가적인 효과로 함수로 분리된 기능을 원래 그런 기능이구나 하고 개념적으로 분류해 버림으로서, 함수를 호출하는 메인 코드들에만 집중하게 되어 가독성을 높일 수 있으며, 현재 우리가 사용하는 여러 모듈 처럼, 모듈 내의 구현된 코드를 알지 못하면서도 가져다가 다른 프로그램에서 사용할 수도 있게 된다. (메쏘드 등의 설명과 겹치는 것도 같지만, 어차피 역활상 비슷비슷한 개념이라고 봐서...)

 

  그리고 또 하나 부수 적인 기능 하나가, 우리가 함수안 코드를 몰라도 인자만 집어 넣으면 사용할 수 있게 설계를 하는 과정에서 자연스럽게 함수 내에 영향을 미치는 여러 요소 값들이 가능한 입력을 기준으로 판단하게 되는 '일반화' 가 된다는 것이다. 하드 코딩이 사라진다고 얘기해도 될 듯 하다.(뭐 바람직한건 아니라고들 많이 얘기하지만 글로벌 변수를 참조할 수는 있을 건 같다) 함수에 대한 얘기는 아래 링크들을 참조 해 보길 바란다.

  http://www.hackerschool.org/Sub_Html/HS_University/BOF/essential/PDF_Files/07.pdf

  http://python-guide-kr.readthedocs.io/ko/latest/writing/structure.html#object-oriented-programming

 

  그럼 원래 코드를 함 봐보자.

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
#-*- coding: utf-8 -*-
from openpyxl import Workbook
from openpyxl import load_workbook
#from openpyxl.compat import range
import pymssql
 
wb = Workbook()
 
# grab the active worksheet
ws = wb.active
ws.title = "output"
 
conn = pymssql.connect(server='localhost', user='pyuser', password='test1234', database='mytest')
cursor = conn.cursor()
 
 
column_char = 'a'
# supermarket 의 컬럼들 가져옴
cursor.execute('SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = \'supermarket\';')
 
# 한행 씩 가져오면서
row = cursor.fetchone()
while row:
# 컬럼 문자를 하나씩 증가하면서 해당 행을 넣음.
  ws[column_char + '1'= row[0]
  column_char = chr(ord(column_char) + 1)
  row = cursor.fetchone()
 
 
# 2번째 행 표시
column_num = 2
# supermarket 테이블의 내용을 가져온다.
cursor.execute('SELECT Itemno, Category, FoodName, Company, Price FROM supermarket(nolock);')
 
# 한 행씩 가져오면서
row = cursor.fetchone()
while row:
# 예전 수동 타자기 처럼, 새로운 줄이 오게 되면, 첫째 셀 a 로 돌아가는 초기 값
  column_char = 'a' 
# 1~5 까지 x 가 변하면서 컬럼 문자, row를 하나씩 늘여 결과를 하나씩 담음. 
# ws['a1'] = row[0], ws['b1'] = row[1], ws['c1'] = row[2]...
  for x in range(16):
    ws[column_char + str(column_num)] = row[x-1]
    column_char = chr(ord(column_char) + 1)
 
# 다음 행을 표시하기 위해 뒤의 숫자 증가       
  column_num = column_num + 1
  row = cursor.fetchone()
 
# 파일을 실제 저장
wb.save("test.xlsx")
cs

 

 

 

 

[함수로 기능 분리 해보기]

  함수로 분리할 부분을 생각해 보면 크게 2가지가 있다. 컬럼명 가져와서 엑셀에 저장하기, 테이블 내용 가져와서 엑셀에 저장하기 2가지 이다.

 

 그런데 개인적으로 생각하기에 두 번째 테이블 내용 가져오는 쪽의 select 쿼리에 컬럼 명들이 필요하기(위에서는 'select Itemno, Category, .... , Price' 이 부분) 때문에 첫번째 컬럼명 가져와서 엑셀에 저장하기 기능은 컬러명 가져오기 기능과, 엑셀에 저장하기 기능으로 나누려고 한다. 그리고 컬럼명 가져오기 기능의 결과는 두 번째 컨텐츠 가져올때 인자로 재 사용 하고 싶다(물론 무시하고 select * 카드로 가져와도 되지만, 혹시 나중에 특정 컬럼만 가져오는 기능을 원할 수도 있기 때문에 컬럼명을 명시적으로 정하는 부분을 그대로 쓰려 한다)

 

 

  그럼 대충 분리 시키려는 내용을 그려보면 아래와 같다. 뭐 기능이나 취향에 따라서 두 번째 테이블 내용 엑셀에 저장하는 기능을 첫 번째 엑셀을 저장하는 함수에 머지해도 무방할 듯도 싶지만, 일단은 따로 분리할 필요는 없을 듯해서, 같이 넣었다.

 

    

  그럼 A) 컬럼 이름 얻어오기 부터 보자. 처음에는 얻어온 값을 Array 에 담아서 저장하려 했다. 컬럼 숫자나 순서대로 쓰기 유용할 것 같아서 말이다. 근데, 구글에 'python array' 라고 검색했을 때, 아래와 같이 array 보다는 list 쪽이 저장하는 데이터 형이나, 사용 면에서 편리 할 거 같아서 list 로 방향을 틀었다.

  http://hashcode.co.kr/questions/1093/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%97%90-list%EA%B0%80-%EC%9E%88%EB%8A%94%EB%8D%B0-arrayarray%EB%8A%94-%EC%99%9C-%EC%93%B0%EB%8A%94-%EA%B1%B4%EA%B0%80%EC%9A%94

  http://stackoverflow.com/questions/9405322/python-array-v-list

 

  그럼 리스트에 컬럼 조회한 값을 하나씩 넣어야 되는데, 문법을 알기 위해서 'python list add' 라고 검색한다. 아래의 페이지를 보면 append 라는 명령어를 써야 하는 것 같다.

  https://www.tutorialspoint.com/python/list_append.htm

 

  너무 자세하면 지루할 수 있으므로, 해당 부분을 이용해 만든 코드는 아래와 같다. select 쿼리 내의 \' 표시는 문자열 내에 ' 문자가 필요할때 쓰는 escape 문자 이다. escape 문자 설명은 아래를 참고. 위의 그림과 동일하게 입력은 테이블 이름을, 출력은 배열로 출력한다. 

  http://egloos.zum.com/pythondev/v/125926 

1
2
3
4
5
6
7
8
9
10
# 컬럼이름 얻어오기
def getColName(tName):
  cursor.execute('SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = \'' + tName + '\';')
  row = cursor.fetchone()
  
  cName = []
  while row:
      cName.append(row[0])
      row = cursor.fetchone()
  return cName
cs

 

 

 

  두번째, B) 컬럼이름 엑셀에 저장 부분은 기존 코드를 거의 그대로 가져온다. 입력으로 컬럼 이름들이 담긴 배열을 담고, 루프를 돌리기 위해서 구글에서 'python list loop' 로 검색 한다. 첫번째 페이지의 샘플에 잘 나와 있는데로("for 변수 in 리스트"), 코드를 구현한다.

  https://learnpythonthehardway.org/book/ex32.html

1
2
3
4
5
6
# 엑셀에 컬럼 저장
def saveColName(cName):
    column_char = 'a'
    for name in cName:
        ws[column_char + '1'= name
        column_char = chr(ord(column_char) + 1)
cs

 

 

 

  세번째 C) select 쿼리 부분 제작은 컬럼 들이 A, B, C, D.. 이런식으로 되어야 나중에 쿼리에 끼워 넣는데, 문법에 맞추려면 마지막 E 뒤의 ',' 는 제거 되어야 한다. for 문을 돌리면서 리스트 마지막 요소일때 ',' 를 안 붙이는 방법도 있겠지만, 그냥 무조건 ',' 를 붙이고 마지막 글자 하나를 빼서 마지막에 붙은 ',' 를 없애도록 만드려고 한다. 해당 기능을 위해 구글에서 'python substring' 을 검색 한다. 아래 페이지를 참조해서 맨 마지막 문자를 잘라낸다.("문자열[:-1]")

  http://stackoverflow.com/questions/663171/is-there-a-way-to-substring-a-string-in-python

1
2
3
4
5
6
7
# 컬럼 쿼리 만들기 Category, Food, Price
def makeColumnQuery(cName):
    sCol = ''
    for name in cName:
        sCol = sCol + name + ','
    sCol = sCol[:-1]
    return sCol
cs

 

 

 

  네번째 D) 테이블 컨텐츠 엑셀에 저장 하는 부분이다. 아까 얘기했듯이 엑셀을 저장하는 부분을 따로 떼어내거나 B) 기능과 합칠 수도 있겠지만, 그냥 D 기능에서 조회와 엑셀 저장을 하는게 현재 용도로는 깔끔하게 느껴져서, 그렇게 구현했다. 쿼리 부분을 보면 컬럼 리스트 넣는 부분을 위해 C) 기능(makeColumnQuery)을 이용해 컬럼을 얻어 쿼리에 조합 시켰다. 하나 또 달라진 부분은 아까 얘기한 일반화를 위해서, for 루프 안의 range 값을 고정된 숫자 6에서 인자로 넘어온 컬럼 리스트의 숫자+1 을 하도록(len(cName)+1) 바꾸었다. 이렇게 함으로써 테이블이 다른 테이블로 바뀌게 되더라도, 자동으로 컬럼 수를 추출해서 엑셀로 저장할 수  있게 기능이 일반화가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def getTableContent(tName, cName):
    sCol = makeColumnQuery(cName)
    cursor.execute('SELECT ' + sCol + ' FROM ' + tName + '(nolock);')
    row = cursor.fetchone()
 
    column_num = 2
    while row:
        column_char = 'a'  
        for x in range(1len(cName)+1):  #컬럼수 참조하게 변경
            ws[column_char + str(column_num)] = row[x-1]
            column_char = chr(ord(column_char) + 1)
 
        column_num = column_num + 1
        row = cursor.fetchone()
cs

 

 

 

  마지막으로 해당 함수들을 호출하는 실제 메인 코드를 보면 아래와 같이 나름 심플해 진다. "컬럼 이름을 얻어와 -> 컬럼이름을 저장하고 -> 테이블 조회의 인자로 넘겨주어, 내용을 조회해 저장한다".  각각 함수의 용도를 안 다면 함수의 세부 기능 코드를 무시하게 되고, 그럼 메인 코드에만 집중해서 함수를 사용하면(사실 함수 뿐만 아니라 클래스등 모든 고급 문법 요소가 그런지도 모른다) 가독성이 높아진다는 것을 증명하게 되지 않는가 싶다.

1
2
3
4
# 실행
colName= getColName(tableName)
saveColName(colName)
getTableContent(tableName, colName)
cs

 

 

 

[합쳐 보기]

  그럼 완성된 전체 코드를 봐보자.

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
# -*- coding: utf-8 -*-
from openpyxl import Workbook
from openpyxl import load_workbook
#from openpyxl.compat import range
import pymssql
 
wb = Workbook()
 
# grab the active worksheet
ws = wb.active
ws.title = "output"
 
conn = pymssql.connect(server='localhost', user='pyuser', password='test1234', database='mytest')
cursor = conn.cursor()
 
tableName = 'supermarket'
 
# 컬럼이름 얻어오기
def getColName(tName):
  cursor.execute('SELECT column_name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = \'' + tName + '\';')
  row = cursor.fetchone()
  
  cName = []
  while row:
      cName.append(row[0])
      row = cursor.fetchone()
  return cName
 
# 엑셀에 컬럼 저장
def saveColName(cName):
    column_char = 'a'
    for name in cName:
        ws[column_char + '1'= name
        column_char = chr(ord(column_char) + 1)
 
# 컬럼 쿼리 만들기 Category, Food, Price
def makeColumnQuery(cName):
    sCol = ''
    for name in cName:
        sCol = sCol + name + ','
    sCol = sCol[:-1]
    return sCol
 
# 컨텐츠 가져오기
def getTableContent(tName, cName):
    sCol = makeColumnQuery(cName)
    cursor.execute('SELECT ' + sCol + ' FROM ' + tName + '(nolock);')
    row = cursor.fetchone()
 
    column_num = 2
    while row:
        column_char = 'a'  
        for x in range(1len(cName)+1):  #컬럼수 참조하게 변경
            ws[column_char + str(column_num)] = row[x-1]
            column_char = chr(ord(column_char) + 1)
 
        column_num = column_num + 1
        row = cursor.fetchone()
 
# 실행
colName= getColName(tableName)
saveColName(colName)
getTableContent(tableName, colName)
 
 
# test2.xlsx 파일에 저장하기
wb.save("test2.xlsx")
cs

 

 

  해당 내용을 c:\python\code 폴더에 excel_save_function.py로 저장하고(역시 모르면 2교시 참고),  코드를 실행해 본다. 실행 후 폴더 안을 보면 test2.xlsx 파일이 저장됨을 볼 수 있다. 여기서 만든 함수는 나중에, API 호출 하여 엑셀에 저장되는 기능을 만들 때 재 사용 될 예정이다.

c:\Python\code>python excel_save_function.py

 

 

 

[마무리 하면서]
  데이터베이스에서 새로운 테이블을 만들고 코드 중 "tableName = 'supermarket'" 부분의 테이블 이름만 수정하면 해당 테이블의 내용을 가져다 저장할 수 있다(지금 다시 생각해 보니 getTableContent 안에서 saveColName 호출해서 아예 컬럼과 내용을 모두 저장하는 것도 괜찮아 보이긴 한다). 그리고 데이터베이스 내의 테이블 리스트를 모두 얻어오는 쿼리를 이용하면(구글에서 'mssql table name' 으로 검색) 이 함수들을 거의 그대로 이용해서 루프를 만들어 데이터베이스 내의 모든 테이블 내용을 10행식 조회해(select top 10 ...) 엑셀에 저장하게 할 수도 있을 것이다. 물론 그 경우 엑셀에 저장하는 코드 부분은 테이블 끼리 서로 쓰는 영역이 겹치지 않도록 적당히 수정되야 할 것이다. 

 

  여기서 예를 들은 부분이 정말로 함수를 잘 설명했는지는 알수 없지만(사실 상황에 따라 달라져 정답이 정확히 있는거 같지도 않고, 들이는 시간도 고려해 실용성과 재사용성을 고려하는 것이 맞을 듯하다), 어느 정도는 함수를 만드는 도중에 만나는 문제와 만든 후의 모습은 보여 줄 수 있었다고 생각하며 글을 마친다.

 

 

[보충]

  문의가 와서 보니 openpyxl 최신버전에서는 아래 import 문이 에러가 나네요. 지금 보니 사실상 코드안에서 사용하지 않는 부분이라서 아예 빼버림 에러가 발생안합니다

from openpyxl.compat import range

 

 

 

 

2017.3.1 by 자유로운설탕
cs

 

 

posted by 자유로운설탕
prev 1 ··· 3 4 5 6 7 8 9 next