CODICT

8. Python으로 웹 살펴보기 본문

Programming/Python

8. Python으로 웹 살펴보기

Foxism 2020. 3. 13. 22:51

1989년 영국의 과학자 팀 버너스-리(Tim Berners-Lee)는 유럽 입자물리연구소(CERN)와 연구소 커뮤니티 내에서 정보의 공유를 돕는 제안을 먼저 했습니다. 그는 이것을 월드 와이드 웹(World Wide Web)이라 불렀고, 설계를 간추려 3가지 간략한 아이디어로 나타내었습니다.

  • HTTP(Hypertext Transfer Protocol)

    • 요청과 응답을 교환하기 위한 웹 서버와 클라이언트의 명세

  • HTML(Hypertext Markup Language)

    • 결과에 대한 표현 형식

  • URL(Uniform Resource Locator)

    • 고유의 해당 서버와 자원을 나타내는 방법

이러한 간단한 사용 방법에서 웹 클라이언트는 HTTP로 URL을 요청하고, 서버로부터 HTML을 받습니다.

그는 먼저 웹 브라우저와 서버를 NeXT 컴퓨터(스티브 잡스가 애플 컴퓨터에서 독립해서 창립한 회사에서 만든 컴퓨터)에 작성했습니다. 웹은 1993년부터 널리 알려졌습니다. 그 당시 일리노이 대학의 학생 그룹이 만든 모자이크(Mosaic) 웹 브라우저(윈도우, 맥, 유닉스를 위한 웹 브라우저)와 NCSA(National Center for Supercomputing Applications) httpd 서버가 출시되었습니다. 그때 인터넷은 여전히 공식적으로 비상업적이었고, 전 세계적으로 약 500대의 웹 서버가 있었습니다. 1994년 말까지 웹 서버의 수는 10,000대로 확장되었습니다. 그리고 상업적으로 인터넷 사용을 개방하고, 모자이크 그룹은 상용 웹 소프트웨어를 개발하기 위해 넷스케이프(Netscape)를 설립했습니다. 그 당시 넷스케이프는 인터넷 열풍의 일환이었고, 웹의 폭발적인 성장이 지속되었습니다.

거의 대부분의 컴퓨터 언어는 웹 클라이언트와 웹 서버를 작성하는 데 사용되었습니다. 동적 언어인 Perl, PHP, Ruby는 특히 인기가 많습니다. 이번 게시글에서는 모든 수준에서 웹 작업을 하는 데 파이썬이 어떤 부분에서 제 역할을 할 수 있는지 보여줄 것입니다.

  • 원격 사이트에 접근할 수 있는 클라이언트
  • 웹사이트와 웹 API에서 데이터를 제공하는 서버
  • 웹 페이지 이외의 데이터를 교환하기 위한 웹 API와 서비스

 

1. 웹 클라이언트

 

낮은 수준(Low-Level)의 인터넷 네트워크 배관을 전송 제어 프로토콜/인터넷 프로토콜(Transmission Control Protocol / Internet Protocol)이라고 부릅니다. 혹은 보통 간단하게 TCP/IP라고 합니다(자세한 사항은 추후에 작성될 게시글을 참조하세요). TCP/IP는 컴퓨터 간에 바이트를 전송하지만, 그것이 무엇을 의미하는지에 대해서는 상관하지 않습니다. 이 바이트를 해석하는 일은 더 높은 수준의 프로토콜에서 처리합니다. HTTP는 웹 데이터를 교환하는 표준 프로토콜입니다.

웹은 클라이언트-서버 시스템입니다. 클라이언트는 서버에 대한 요청(Request)을 만듭니다. 이 요청은 TCP/IP 커넥션을 열고, HTTP를 통해 URL과 다른 정보들을 보냅니다. 그리고 요청에 대한 응답(Response)을 받습니다.

응답 포맷 또한 HTTP에 의해 정의되었습니다. 응답 포맷은 요청에 대한 상태와응답 데이터(요청이 성공했을 경우)를 포함합니다.

가장 잘 알려진 웹 클라이언트는 웹 브라우저입니다. 웹 브라우저는 다양한 방법으로 HTTP 요청을 만듭니다. 웹 브라우저의 주소창에 URL을 입력하거나 웹 페이지에서 링크를 클릭하여 요청을 할 수 있습니다. 대부분의 반환된 데이터는 웹사이트를 나타내는 데 쓰입니다(HTML 문서, 자바스크립트 파일, CSS 파일, 이미지 등). 그러나 웹사이트를 나타내는 데 의도적으로 사용되지 않는 어떤 데이터도 반환될 수 있습니다.

HTTP의 중요한 점은 무상태(Stateless)라는 것입니다. 웹 브라우저에서 생성된 각 HTTP 커넥션은 모두 독립적입니다. 이것은 기본적인 웹 작동을 단순하게 만들지만, 그 이외의 사항은 복잡하게 만듭니다. 여기에 몇 가지 사항이 있습니다.

  • 캐싱(Caching)

    • 변하지 않는 원격 콘텐츠는 웹 클라이언트에 저장하고, 다시 서버로부터 이 콘텐츠의 다운로드를 피하기 위해 저장된 콘텐츠를 사용합니다.

  • 세션(Session)

    • 쇼핑 웹사이트는 쇼핑 카트(장바구니)의 콘텐츠를 기억해야 합니다.

  • 인증(Authentication)

    • 아이디와 비밀번호를 요구하는 사이트는 사용자가 로그인할 때, 이 둘을 기억하여 사용자를 식별합니다.

무상태에 대한 해결책 중 쿠키(Cookie)가 있습니다. 서버가 클라이언트를 식별할 수 있도록 보내는 특정 정보를 쿠키라고 합니다. 그리고 다시 클라이언트가 서버에 쿠키를 보내, 서버가 클라이언트를 식별할 수 있게 합니다.

 

1.1. 텔넷으로 테스트하기

 

HTTP는 텍스트 기반 프로토콜입니다. 웹 테스트를 위해 실제 텍스트를 입력할 수 있습니다. 고대의 텔넷(Telnet) 프로그램은 서버의 포트에 연결하여 명령을 입력할 수 있습니다.

$ telnet www.google.com 80

google.com에 80포트의 웹 서버가 있다면, telnet은 연결이 가능한지 정보를 출력합니다. 그리고 마지막으로 텔넷 사용자가 입력해야 할 빈 줄을 표시합니다.

  시도 74.125.128.99 ...
  www.google.com에 연결됨 (74.125.128.99).
  이스케이프 문자는 '^]'입니다.

이제 구글 웹 서버로 보낼 텔넷에 대한 실제 HTTP 명령을 입력해봅시다. 가장 일반적인 HTTP 명령(웹 브라우저에서 주소창에 URL을 입력할 때 사용하는 메소드)은 GET입니다. 이는 HTML 파일과 같은 지정된 리소소의 내용을 검색하여 클라이언트에 반환합니다. 이번 게시글의 첫 번째 테스트는 HTTP의 HEAD 메소드를 사용합니다. HEAD 메소드는 리소스에 대한 몇 가지 기본 정보를 검색합니다.

HEAD / HTTP/1.1

HEAD /는 홈페이지(/)에 대한 정보를 얻기 위해 HTTP head 동사(명령)를 보냅니다. 그리고 캐리지 리턴(Carriage Return)을 추가하여 빈 라인을 보냅니다. 이것은 명령이 끝나고, 원격 서버의 응답을 기다리겠다는 것을 의미합니다. 잠시 후 응답을 얻게 될 것인데, 이것은 HTTP 응답 헤더와 헤더 값입니다. Date와 Content-Type 같은 헤더는 꼭 응답이 옵니다. 그리고 Set-Cookie 같은 헤더는 사용자의 여러 방문 활동을 추적하는 데 사용됩니다(이 게시글의 뒷부분에서 상태 관리에 대해 언급합니다). HTTP HEAD 요청을 할 때 헤더만 응답받습니다. HTTP GET 또는 POST 명령을 사용할 때 홈페이지(HTML, CSS, 자바스크립트의 혼합물, 구글 홈페이지에서 나온 결과 등)로부터 데이터도 받게 됩니다.

마지막으로 텔넷을 닫으려면 아래 명령을 입력하세요.

q

 

1.2. 파이썬 표준 웹 라이브러리

 

파이썬 2에서 웹 클라이언트와 서버 모듈은 조금 흩어져있습니다. 파이썬 3에서는 이들 모듈을 두 개의 패키지로 묶었습니다(이전 게시글에서 패키지는 단지 모듈 파일을 포함하는 디렉터리라는 것을 배웠습니다).

  • http는 모든 클라이언트-서버 HTTP 세부사항을 관리합니다.

    • client는 클라이언트 부분을 관리합니다.

    • server는 파이썬 웹 서버를 작성하는 데 도움을 줍니다.

    • cookies와 cookiejar는 사이트 방문자의 데이터를 저장하는 쿠키를 관리합니다.

  • urllib는 http 위에서 실행됩니다.

    • request는 클라이언트의 요청을 처리합니다.

    • response는 서버의 응답을 처리합니다.

    • parse는 URL을 분석합니다.

웹사이트로부터 뭔가를 얻기 위해 표준 라이브러리를 사용해봅시다. 다음 코드의 URL은 포춘 쿠키(Fortune Cookie)와 유사한 임의의 텍스트 문장을 반환합니다.

>>> import urllib.request as ur
>>> url = 'https://random-quote-generator.webflow.io/quote/quote-19'
>>> conn = ur.urlopen(url)
>>> print(conn)
#<http.client.HTTPResponse object at 0x00000195291AD668>

공식 문서에서 conn은 다수의 메소드를 지닌 HTTPResponse 객체라는 것을 볼 수 있습니다. 그리고 read() 메소드는 웹 페이지로부터 데이터를 읽어옵니다.

>>> data = conn.read()
>>> print(data)
#b'~~이전 생략~~
#<p>Accountability is holding people to the standard not because you are against them, but because you are for them. It&#x27;s reminding them of who they are meant to be. It&#x27;s calling them up, not out.</p>
#~~이후 생략~~'

이 코드로 원격 인용구 서버에 TCP/IP 커넥션을 열었고, HTTP 요청을 만들었고, HTTP 응답을 받았습니다. 이 응답은 페이지 인용구 데이터 이외의 데이터가 포함되어 있습니다. 응답에서 가장 중요한 부분 중 하나는 HTTP 상태 코드입니다.

>>> print(conn.status)
#200

200은 성공적으로 요청을 처리했다는 것을 의미합니다. HTTP의 응답 코드는 수십 개가 있는데, 다섯 가지 범위로 나뉩니다(백 자리 첫 번째 숫자에 따라).

  • 1xx(조건부 응답; Information)

    • 서버는 요청을 받았지만, 클라이언트에 대한 몇 가지 추가 정보가 필요합니다.

  • 2xx(성공; Success)

    • 성공적으로 처리되었습니다. 200 이외의 모든 성공 코드는 추가사항을 전달합니다.

  • 3xx(리다이렉션; Redirection)

    • 리소스가 이전되어 클라이언트에 새로운 URL을 응답해줍니다.

  • 4xx(클라이언트 에러; Client Error)

    • 자주 발생하는 404(찾을 수 없음; Not Found)는 클라이언트 측에 문제가 있음을 나타냅니다.

  • 5xx(서버 에러; Server Error)

    • 500은 서버 에러를 나타냅니다. 웹 서버와 백엔드 애플리케이션 서버가 연결되어 있지 않다면 502(불량 게이트웨이; Bad Gateway)를 볼 것입니다.

웹 서버는 원하는 포맷으로 데이터를 전송할 수 있습니다. 일반적으로 HTML(그리고 CSS와 JavaScript)을 전송합니다. 데이터 포맷은 google.com 코드에서 본 것처럼 HTTP 헤더의 Content-Type 값에 있습니다.

>>> print(conn.getheader('Content-Type'))
#text/html

text/html 문자열은 HTML을 위한 MIME(Multipurpose Internet Mail Extensions) 타입입니다. MIME 타입에 대해서는 잠시 후에 살펴보겠습니다.

다른 HTTP 헤더는 잘 받았을까요?

>>> for key, value in conn.getheaders():
	print(key, value)

	
#Connection close
#Content-Length 4429
#Content-Security-Policy frame-ancestors 'self' https://*.webflow.com http://*.webflow.com http://*.webflow.io http://webflow.com https://webflow.com
#Content-Type text/html
#Server openresty
#x-lambda-id e3e2bfb0-636b-4407-80ca-e7dfcc6f1d5f
#Via 1.1 varnish
#Accept-Ranges bytes
#Date Thu, 12 Mar 2020 16:44:37 GMT
#Via 1.1 varnish
#Age 0
#X-Served-By cache-iad2146-IAD, cache-lax8624-LAX
#X-Cache MISS, MISS
#X-Cache-Hits 0, 0
#X-Timer S1584031477.036622,VS0,VE210
#Vary Accept-Encoding

텔넷 예제를 기억하시나요? 파이썬 라이브러리는 모든 HTTP 응답 헤더를 파싱하여 딕셔너리로 제공합니다. Date와 Server는 쉽게 알아볼 수 있지만, 다른 응답 헤더들은 조금 알아보기 힘듭니다. Content-Type과 같은 많은 HTTP 표준 헤더를 알아두면 좋습니다.

 

1.3. 표준 라이브러리를 넘어서: Requests

 

앞부분에서 urllib.request와 json 표준 라이브러리를 사용하여 웹사이트에 접근하는 프로그램을 작성했습니다. 여기서는 써드파티의 requests 모듈을 사용하여 같은 프로그램을 만듭니다. requests 모듈을 사용한 예제는 조금 더 짧고, 이해하기 더 쉽습니다.

대부분의 경우 requests 모듈을 사용하면 웹 클라이언트 개발이 더 쉬워집니다. 문서를 참고하면 requests 모듈에 대한 자세한 내용을 볼 수 있습니다. 이번 절에서는 requests의 기본 사용법을 익히고, 앞으로 할 웹 클라이언트 작업에서 이 모듈을 사용합니다.

먼저 pip(파이썬 패키지 인스톨러)로 requests 패키지의 최신 버전을 내려받아서 설치합니다. 터미널 창(윈도우 사용자는 실행 창에서 cmd를 입력합니다)에서 아래 명령을 입력하면 됩니다).

$ pip install requests

설치하는 데 문제가 있다면 나중에 따로 게시글을 작성해보겠습니다. 인용문 호출에 대한 예제를 requests로 다시 작성해봅시다.

>>> import requests
>>> url = 'https://random-quote-generator.webflow.io/quote/quote-19'
>>> resp = requests.get(url)
>>> resp
<Response [200]>
>>> print(resp.text)
#~~이전 생략~~
#<p>Accountability is holding people to the standard not because you are against them, but because you are for them. It&#x27;s reminding them of who they are meant to be. It&#x27;s calling them up, not out.</p>
#~~이후 생략

urllib.requests.urlopen 모듈을 사용한 것과 차이가 없지만, 코드가 좀 더 간편합니다.

 

2. 웹 서버

 

파이썬은 웹 서버와 서버 사이트 프로그램을 작성하는 데 뛰어난 언어로 웹 개발자들에게 알려져 있습니다. 파이썬 기반의 다양한 웹 프레임워크 중에서 무엇을 사용해야 하는지 선택이 힘들 수 있습니다. 이번 절에서 다양한 웹 프레임워크를 살펴봅니다.

웹 프레임워크는 웹사이트를 구축할 수 있는 기능을 제공합니다. 그래서 단순한 웹(HTTP) 서버보다 더 많은 작업을 수행합니다. 라우팅(Routing; 서버 함수의 URL), 템플릿(Templete; HTML을 동적으로 생성), 디버깅(Debugging) 등에 대한 기능을 살펴볼 것입니다.

여기서 모든 프레임워크를 다루진 않습니다. 상대적으로 사용하기 쉽고 간단한 웹사이트에 적합한 수준만 살펴봅니다. 그리고 파이썬 웹사이트의 동적인 처리 부분과 기존의 웹 서버와 무엇이 다른지에 대해서도 살펴봅니다.

 

2.1. 간단한 파이썬 웹 서버

 

단 한 라인의 코드로 간단한 웹 서버를 실행할 수 있습니다.

$ python -m http.server

이것은 순수한 파이썬 HTP 서버를 구현합니다. 문제가 없다면, 다음과 같은 초기 상태 메시지를 출력합니다.

Serving HTTP on :: port 8000 (http://[::]:8000/) ...

0.0.0.0은 모든 TCP 주소를 의미합니다. 그래서 웹 클라이언트는 서버가 어떤 주소를 가졌든 상관없이 그곳에 접근할 수 있습니다. 네트워크와 TCP의 낮은 수준(Low-Level)에 대한 내용은 추후에 작성될 게시글에서 살펴봅니다.

이제 현재 디렉터리에 대한 상대 경로로 파일을 요청할 수 있습니다. 그리고 요청한 파일이 반환될 것입니다. 웹 브라우저에서 http://localhost:8000을 입력했다면, 현재 디렉터리의 리스트가 나타나고, 서버에 아래와 같은 액세스 로그 라인이 출력됩니다.

::1 - - [13/Mar/2020 02:58:50] "GET / HTTP/1.1" 200 -

localhost와 127..0.0.1은 로컬 컴퓨터에 대한 TCP 동의어입니다. 그래서 인터넷 연결과 상관없이 명령이 실행됩니다. 위 라인은 다음과 같이 해석할 수 있습니다.

  • ::1은 클라이언트의 IP 주소입니다.
  • 첫 번째 "-"는 원격 사용자 이름입니다(발견된 경우).
  • 두 번째 "-"는 로그인 사용자 이름입니다(요구한 경우).
  • [13/Mar/2020 02:58:50]는 접근한 날짜와 시간입니다.
  • "GET / HTTP/1.1"은 웹 서버로 보내는 명령입니다.
    • HTTP 메소드(GET)
    • 리소스 요청(/)
    • HTTP 버전(HTTP/1.1)
  • 200은 웹 서버로부터 반환된 HTTP 상태 코드입니다.

아무 파일이나 클릭해봅시다. 브라우저는 포맷(HTML, PNG, GIF, JPEG 등)을 인식하여 그것을 보여줍니다. 그리고 서버는 요청에 대한 로그를 기록합니다 현재 디렉터리에 tistory.png 파일이 있다고 가정해봅시다. http://localhost:8000/tistory.png에 대한 요청은 해당 이미지를 반환하고, 다음과 같은 로그가 기록됩니다.

127.0.0.1 - - [13/Mar/2020 03:02:13] "GET /tistory.png HTTP/1.1" 200 -

같은 디렉터리에 다른 파일이 있는 경우, 이들을 목록 형태로 표시합니다. 그리고 파일을 클릭하여 내려받을 수 있습니다. 만약 브라우저에서 클릭한 파일의 형식을 표시하도록 설정한 경우에는 그 결과가 화면에 나타납니다. 그렇지 않으면 브라우저는 파일을 내려받아서 저장할 것인지 묻습니다.

기본 포트 번호는 8000이지만, 다른 포트 번호를 지정할 수도 있습니다.

$ python -m http.server 9999

다음과 같은 결과를 볼 수 있습니다.

Serving HTTP on :: port 9999 (http://[::]:9999/) ...

순수한 파이썬 서버는 빠른 테스트에 적합합니다. 그리고 프로세스를 죽여서(대부분의 경우 Ctrl + C) 서버를 중지시킬 수 있습니다.

이 기본 서버를 실제 웹사이트에서 사용하면 안 됩니다. 아파치(Apache)나 엔진엑스(Nginx) 같은 전통적인 웹 서버는 정적 파일을 더 빠르게 제공합니다. 간단한 기본 서버는 대규모의 서버들이 매개변수를 받아서 동적인 콘텐츠를 처리하는 일을 수행할 수 없습니다.

 

2.2. 웹 서버 게이트웨이 인터페이스

 

우리는 간단한 파일 목록을 제공하는 웹 서버가 아닌, 동적으로 프로그램을 실행하는 웹 서버를 원합니다. 웹 초기 시절, 공용 게이트웨이 인터페이스(Common Gateway Interface; CGI)는 클라이언트를 위해 웹 서버가 아닌 외부 프로그램을 통해 실행하고, 그 결과를 반환하도록 설계되었습니다. CGI는 클라이언트에서 받은 입력 인자를 서버를 통해 처리하여 외부 프로그램으로 전달합니다. 프로그램은 각 클라이언트의 접근을 위해 처음부터 다시 시작됩니다. 이러한 접근 방식은 확장성을 떨어뜨립니다. 작은 프로그램도 시작하는 데 상당한 시간이 걸릴 수 있기 때문입니다.

이러한 시동 지연을 피하기 위해 개발자들은 웹 서버에 인터프리터를 두기 시작했습니다. 아파치(Apache)는 mod_php 모듈 내에서는 PHP, mod_perl 모듈 내에서는 펄, mod_python  모듈 내에서는 파이썬을 실행합니다. 그리고 이러한 동적 언어의 코드는 외부 프로그램이 아닌 장기적으로 작동하는 아파치 프로세스 내에서 실행됩니다.

또 다른 방법은 별도의 장기적으로 작동하는 프로그램 내에서 동적 언어를 실행하고, 웹 서버와 통신하는 것입니다. 그 예로 FastCGI와 SCGI가 있습니다.

파이썬 웹 개발은 파이썬 웹 애플리케이션과 웹 서버 간의 범용적인 API인 웹 서버 게이트웨이 인터페이스(Web Server Gateway Interface; WSGI)의 정의에서부터 시작되었습니다. 이 장 나머지 부분에서 모든 파이썬 웹 프레임워크와 우베 서버는 WSGI를 사용합니다. 보통 어떻게 WSGI가 작동하는지(아주 자세히) 알 필요는 없지만, 핵심 내용을 숙지한다면 웹 개발에 도움이 됩니다.

 

2.3. 프레임워크

 

웹 서버는 HTTP와 WSGI의 세부사항을 처리합니다. 반면 웹 프레임워크를 사용한다면 파이썬 코드를 작성하여 강력한 웹사이트를 만들 수 있습니다. 잠시 프레임워크에 대해 이야기한 뒤, 프레임워크를 사용하여 실제 사이트를 제공하는 또 다른 대안에 대해 본격적으로 살펴봅니다.

웹사이트를 구축하기 위한 (아주) 많은 파이썬 웹 프레임워크가 존재합니다. 웹 프레임워크는 최소한 클라이언트의 요청과 서버의 응답을 처리합니다. 이는 다음 내용의 일부 혹은 모든 기능을 제공합니다.

  • 라우트(Route)

    • URL을 해석하여 해당 서버의 파일이나 파이썬 코드를 찾아줍니다.

  • 템플릿(Templete)

    • 서버 사이드의 데이터를 HTML 페이지에 병합합니다.

  • 인증(Authentication) 및 권한(Authorization)

    • 사용자 이름과 비밀번호, 퍼미션(Permission)을 처리합니다.

  • 세션(Session)

    • 웹사이트에 방문하는 동안 사용자의 임시 데이터를 유지합니다.

이어지는 절에서는 두 프레임워크(bottle과 flast)에 대한 코드를 작성합니다. 그러고 나서 또 다른 대안(특히 데이터베이스 기반 웹사이트)에 대해 이야기합니다. 당신이 생각하는 웹사이트를 구현하기 위해 파이썬 프레임워크를 찾게 될 것입니다.

 

2.4. Bottle

 

Bottle은 하나의 파이썬 파일로 구성되어 있어서 시도해보기 쉬우며, 나중에 쉽게 배포할 수 있습니다. Bottle은 파이썬의 표준 라이브러리가 아닙니다 아래 명령으로 Bottle을 설치합니다.

$ pip install bottle

다음 코드는 테스트 웹 서버를 실행하는 코드입니다. 브라우저의 주소 창에 http://localhost:9999/를 입력하면 한 줄의 텍스트를 반환합니다. 이 코드를 bottle1.py로 저장합니다.

from bottle import route, run

@route('/')
def home():
    return "It isn't fancy, but it's my home page"

run(host = 'localhost', port = 9090)

Bottle은 route 데커레이터를 사용하여 함수와 URL을 연결합니다. 이 경우 / (홈페이지)는 home() 함수가 처리합니다. 아래 명령을 입력하여 서버 스크립트를 실행해봅시다.

$ python bottle1.py

브라우저의 주소 창에 http://localhost:9090을 입력하면 아래 내용을 볼 수 있습니다.

It isn't fancy, but it's my home page

run() 함수는 bottle의 내장된 파이선 테스트 웹 서버를 실행합니다. 이 함수는 bottle 프로그램에서 사용할 필요는 없지만 초기 개발 및 테스트에 유용합니다.

이번에는 홈페이지의 텍스트를 코드에 작성하는 대신 index.html이라는 HTML 파일을 따로 만들어서 아래 텍스트를 저장합니다.

My <b>new</b> and <i>improved</i> home page!!

홈페이지를 요청할 때 bottle에서 위 HTML 파일의 내용을 반환하도록 만들어줍니다. 다음 코드를 bottle.py로 저장합니다.

from bottle import route, run, static_file

@route('/')
def main():
    return static_file('index.html', root = '.')

run(host = 'localhost', port = 9090)

static_file() 호출에서 root가 가리키는 디렉터리('.'는 현재 디렉터리를 의미합니다)에서 index.html 파일의 내용을 반환합니다. 이전 코드가 계속 실행되고 있었다면 종료하고, 다시 새로운 서버를 실행합니다.

$ python bottle2.py

브라우저의 주소 창에 http://localhost:9090을 입력하면 다음과 같은 결과를 볼 수 있습니다.

My new and improved home page!!

마지막 bottle 코드는 URL에 인자를 전달하여 사용하는 방법을 보여줍니다. 다음 코드를 bottle3.py로 저장합니다.

from bottle import route, run, static_file

@route('.')
def home():
    return static_file('index.html', root='.')

@route('/echo/<thing>')
def echo(thing):
    return "Say hello to my little friend: %s!" % thing

run(host='localhost', port=9090)

새로운 echo() 함수는 URL에서 문자열 인자를 전달받습니다. @route('/echo/<thing>') 라인을 살펴봅시다. 라우트의 <thing>은 /echo/ 다음에 오는 URL의 문자열을 문자열 인자 thing에 할당하고, 이것을 echo 함수에 전달합니다. 이전 서버가 계속 실행되고 있다면 종료하고, 다시 새로운 서버를 실행하여 결과를 살펴봅시다.

$ python bottle3.py

그리고 웹 브라우저의 주소 창에 http://localhost:9090/echo/SeungSu를 입력하면 아래 텍스트를 볼 수 있습니다.

Say hello to my little friend: SeungSu!

bottle3.py를 잠시 떠나서, 다른 뭔가를 시도해봅시다. 전에는 브라우저에서 URL을 입력하여 표시되는 페이지를 보고, 예제들이 잘 작동하는지 확인했습니다. requests와 같은 클라이언트 라이브러리를 사용하여 잘 작동하는지 확인할 수도 있습니다. 다음 코드를 bottle_test.py로 저장합니다.

import requests

resp = requests.get('http://localhost:9090/echo/SeungSu')

if resp.status_code == 200 and \
    resp.text == 'Say hello to my little friend: SeungSu!':
    print('It worked! That almost never happens!')
else:
    print('Argh, got this:', resp.text)

이제 실행해봅시다.

$ python bottle_test.py

터미널에서 다음과 같은 결과를 볼 수 있습니다.

It worked! That almost never happens!

이것은 유닛 테스트(Unit Test)의 작은 예시입니다. 

다음은 bottle에 대한 팁입니다. run() 함수를 호출할 때 아래 인자를 추가하여 실행할 수 있습니다.

  • debug=True는 HTTP 에러가 발생하면 디버깅 페이지를 생성합니다.
  • reloader=True는 파이썬 코드가 변경되면 변경된 코드를 다시 불러옵니다.

위 내용은 개발자 사이트에 문서화되어 있습니다.

 

2.5. Flask

 

Bottle은 좋은 초기의 웹 프레임워크입니다. 만일 더 많은 기능이 필요하다면 Flask를 사용합니다. Flask는 2010년 만우절 농담으로 등장했지만, 반응이 좋아서 실제로 만들어졌습니다. 개발자는 Bottle(병)에 대한 농담으로 이 프레임워크의 이름을 Flask(실험용 병)이라고 지었습니다.

Flask는 Bottle과 같이 간단하게 사용할 수 있지만, 페이스북 인증과 데이터베이스 연결과 같이 전문적인 웹 개발에 유용한 많은 확장 기능을 지원합니다. Flask는 필자가 개인적으로 좋아하는 파이썬 웹 프레임워크입니다. 사용에 대한 용이성과 풍부한 기능의 균형이 잘 갖춰져 있기 때문입니다.

Flask 패키지는 werkzeug WSGI 라이브러리와 jinja2 템플릿 라이브러리를 포함합니다. 터미널에서 아래 명령으로 Flask를 설치합니다.

$ pip install flask

Bottle의 마지막 예시 코드를 Flask로 바꿔봅시다. 먼저 두 가지 참조사항이 있습니다.

  • Flask의 정적 파일에 대한 기본 홈 디렉터리는 static입니다. 그리고 파일에 대한 URL 또한 /static으로 시작합니다. 폴더를 '.'(현재 디렉터리)로, URL 접두사를 ''(빈 문자열)로 바꿔서 URL /를 index.html 파일에 매핑할 수 있습니다.

  • run() 함수에서 debug=True는 서버의 코드를 다시 불러옵니다. Bottle에서는 디버깅과 코드를 다시 불러오는 인자가 분리되어 있습니다.

다음 코드를 flask1.py로 저장합니다.

from flask import Flask

app = Flask(__name__, static_folder='.', static_url_path='')

@app.route('/')
def home():
    return app.send_static_file('index.html')

@app.route('/echo/<thing>')
def echo(thing):
    return "Say hello to my little friend: %s" % thing

app.run(port=9090, debug=True)

터미널 혹은 커맨드 창에서 서버를 실행합니다.

$ python flask1.py

웹 브라우저에서 URL을 입력해서 홈페이지를 테스트합니다.

http://localhost:9090

bottle 예제에서 봤던 것처럼, 다음과 같은 결과를 볼 수 있습니다.

My new and improved home page!!!

그리고 /echo의 엔드포인트(Endpoint)에 접근해봅시다.

http://localhost:9090/echo/SeungSu

다음과 같은 결과를 볼 수 있습니다.

Say hello to my little friend: SeungSu

run()을 호출할 때 debug=True로 설정하면 또 다른 이점이 있습니다. 서버 코드에서 예외가 발생했다면, Flask는 유용한 정보와 함께 어디에서 잘못되었는지에 관한 내용을 특별한 형식의 페이지로 표시해줍니다. 더 좋은 점은 서버 프로그램의 변숫값을 보기 위해 몇 가지 명령을 입력할 수 있습니다.

주의할 점은, 웹 서버를 배포할 때는 debug=True로 설정하지 마십시오. 서버에 대한 너무 많은 정보가 잠재적인 침입자에게 노출됩니다.

지금까지 Flask 코드는 Bottle 코드를 단지 복제했을 뿐입니다. Bottle에서는 할 수 없지만, Flask에서는 할 수 있는 것은 무엇일까요? Flask는 더 광범위한 템플릿 시스템임 jinja2를 포함하고 있습니다. flask와 jinja2를 함께 사용한 코드를 살펴봅시다.

templates 디렉터리를 생성하고, 다음 코드를 flask2.html 파일로 저장합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask2 Example</title>
</head>
<body>
Say hello to my little friend: {{thing}}
</body>
</html>

이 템플릿에 맞춰 thing 값을 전달하고, HTML에 렌더링하기 위해 서버 코드를 작성합니다(공간 절약을 위해 home() 함수는 생략했습니다). 이 코드를 flask2.py에 저장합니다.

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/echo/<thing>')
def echo(thing):
    return render_template('flask2.html', thing=thing)
    
app.run(port=9090, debug=True)

thing=ting 인자는 thing  변수의 문자열 값을 템플릿의 thing에 전달하는 것을 의미합니다. flask1.py가 실행되어 있지 않은 것을 확인한 후 flask2.py를 실행합니다.

$ python flask2.py

이제 URL을 입력합니다.

http://localhost:9090/eco/SeungSu

다음과 같은 결과를 볼 수 있습니다.

Say hello to my little friend: SeungSu

템플릿을 수정하여 templates 디렉터리에 flask3.html 파일로 저장합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask3 Example</title>
</head>
<body>
Say hello to my little friend: {{ thing }}.
Alas, it just destroyed {{ place }}!
</body>
</html>

여러 가지 방법으로 두 번째 인자를 echo URL에 전달할 수 있습니다.

 

URL 경로에 인자 전달하기

다음 코드는 단순히 URL 자체를 확장합니다(다음 코드를 flask3a.py로 저장합니다).

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/echo/<thing</<place>')
def echo(thing, place):
    return render_template('flask3.html', thing=thing, place=place)

app.run(port=9090, debug=True)

평소와 같이 이전 테스트 서버 스크립트가 실행되고 있다면, 멈추고 새로운 스크립트를 실행하세요.

$ python flask3a.py

다음 URL을 입력합니다.

http://localhost:9090/echo/SeungSu/Army

그러면 다음과 같은 결과를 볼 수 있습니다.

Say hello to my little friend: SeungSu. Alas, it just destroyed Army!

혹은 GET 매개변수로 인자를 제공할 수 있습니다(다음 코드를 flask3b.py로 저장합니다).

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/echo/')
def echo():
    thing = request.args.get('thing')
    place = request.args.get('place')
    return render_template('flask3.html', thing=thing, place=place)

app.run(port=9090, debug=True)

새 서버 스크립트를 실행합니다.

$ python flask3b.py

이번에는 다음 URL을 입력합니다.

http://localhost:9090/echo?thing=SeungSu&place=Army

다음과 같은 결과를 볼 수 있습니다.

Say hello to my little friend: Seungsu. Alas, it just destroyed Army!

GET 명령이 URL에 사용되는 경우 인자가 &key1=val1&key2=val2&... 형태로 전달됩니다.

또한 딕셔너리 ** 연산자를 사용하여 한 딕셔너리로부터 여러 인자를 템플릿에 전달할 수 있습니다(다음 코드를 flask3c.py로 저장합니다).

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/echo/')
def echo():
    kwargs = {}
    kwargs['thing'] = request.args.get('thing')
    kwargs['place'] = request.args.get('place')
    return render_template('flask3.html', **kwargs)

app.run(port=9090, debug=True)

**kwargs는 thing=thing, place=place처럼 작동합니다. 입력 인자가 많을 경우 타이핑을 줄일 수 있습니다.

jinja2 템플릿 엔진은 이보다 더 많은 일을 수행합니다. PH로 개발한 경험이 있다면 유사점이 많다는 것을 알 수 있습니다.

 

2.6. 비파이썬 웹 서버

 

지금까지 우리가 사용한 웹 서버(표준 라이브러리의 http.server 혹은 Bottle과 Flask의 디버깅 서버)는 간단했습니다. 제품으로 배포할 때는 빠른 웹 서버로 파이썬을 실행하는 것이 좋습니다. 일반적으로 다음을 선택합니다.

  • 아파치(Apache)와 mod_wsgi 모듈
  • 엔진엑스(Nginx)와 uWSGI 앱 서버

둘 다 잘 작동합니다. 아파치는 가장 인기가 많고, 엔진엑스는 안정성과 메모리를 적게 사용하는 것으로 유명합니다.

 

아차피

아파치 웹 서버에 최적화된 WSGI 모듈은 mod_wsgi입니다. 이 모듈은 아파치 프로세스 안에서 혹은 아파치와 통신하기 위해 분리된 프로세스 안에서 파이썬 코드를 실행합니다.

리눅스나 맥 OS X 사용자인 경우에는 기본적으로 아차피가 설치되어 있습니다. 윈도운 사용자인 경우에는 아파치를 설치해야 합니다.

OS X 기준으로 WSGI 기반의 파이썬 웹 프레임워크를 사용합니다. 여기에서는 bottle을 사용하며, 아파치 설정에 대한 부분은 운영체제에 따라 조금 다릅니다(시작하기 전에 mod_wsgi를 설치하세요)

다음 코드를 /Library/WebServer/documents/home.wsgi로 저장합니다.

import sys
sys.path.append("/Library/WebServer/Documents/") # home.wsgi가 있는 디렉터리
sys.path.append("/Library/Frameworks/Python.framework/Versions/3.5/bin") # Python이 있는 디렉터리

import bottle
application = bottle.default_app()

@bottle.route('/')
def home():
    return "apache and wsgi, sitting in a tree"

이번에는 run()을 호출하지 않습니다. 내장된 파이썬 웹 서버를 작동하기 때문입니다. 웹 서버와 파이썬 코드를 결합하기 위해, application 변수를 할당하여 이를 mod_wsgi가 찾을 수 있게 해줍니다.

아차피와 mod_wsgi 모듈을 위의 파이썬 스크립트에 연결하기 위해 아차피 설정 작업이 필요합니다. 먼저 /etc/apache2/httpd.conf 파일에서 mod_wsgi와 가상 호스트(Virtual Host)에 관한 주석을 해제합니다. 저는 아래의 두 줄을 해제했습니다.

LoadModule wsgi_module /usr/local/Cellar/mod_wsgi/4.4.11/libexec/mod_wsgi.so
Include /private/etc/apache2/extra/httpd-vhosts.conf

기본 웹사이트를 설정하기 위해 /etc/apache2/extra/httpd-vhosts.conf 파일에서 다음을 추가합니다.

<VirtualHost *:80>
    DocumentRoot "/library/WebServer"
    
    WSGIScriptAlias "/" "/Library/WebServer/Documents/home.wsgi"
    
    <Directory "/Library/WebServer/Documents">
        Order deny,allow
        Allow from all
    </Directory>
</VirtualHost>

아파치를 시작합니다. 현재 실행 중인 경우에는 새로운 구성을 사용하기 위해 다시 시작해야 합니다.

$ sudo apachectl restart

브라우저에서 http://localhost/를 입력하여 다음 결과를 확인합니다.

apache and wsgi, sitting in a tree

이것은 아파치의 한 부분으로 mod_wsgi를 임베디드(Embedded) 모드로 실행한 것입니다.

아파치로부터 하나 이상의 프로세스를 분리한 데몬(Daemon) 모드로 실행할 수 있습니다. 이를 위해 아파치 설정 파일에 두 라인을 추가합니다.

$ WSGIDaemonProcess domain-name user=user-name group=group-name threads=25

WSGIProcessGroup domain-name

$ WSGIDaemonProcess domain-name user=user-name group=group-name threads=25
WSGIProcessGroup domain-name

위 코드에서 user-name과 group-name은 운영체제의 사용자와 그룹 이름입니다. 그리고 domain-name은 인터넷 도메인 이름입니다. 아파치의 최소 설정은 다음과 같습니다.

<VirtualHost *:80>
    DocumentRoot "/library/WebServer"
    
    WSGIScriptAlias "/" "/Library/WebServer/Documents/home.wsgi"
    WSGIDaemonProcess mydomain.com user=_www group=_www threads=25
    
    <Directory "/Library/WebServer/Documents">
        WSGIProcessGroup mydomain.com
        WSGIApplicationGroup %{GLOBAL}
        Order deny,allow
        Allow from all
    </Directory>
</VirtualHost>

참고로, 운영체제와 버전에 따라서 아파치 설정이 다르지만 OS X에서는 /Library/WebServer/Documents에서 home.wsgi를 만들었습니다. 어디에서 만들든 상관없지만 웹 브라우저에서 테스트했을 때 접근 문제가 생긴다면 /etc/apache2/httpd.conf 파일의 <Directory> 섹션에서 home.wsgi가 있는 디렉터리를 추가하여 권한 문제를 해결하면 됩니다.

 

엔진엑스 웹 서버

엔진엑스(Nginx) 웹 서버는 파이썬 모듈이 없습니다. 대신 uWSGI와 같은 별도 WSGI 서버를 사용하여 통신합니다. 이들은 파이썬 웹 개발을 위해 매우 신속하고, 설정 가능한 플랫폼을 만들어줍니다.

엔진엑스 웹사이트에 설치 방법이 나와 있습니다. 또한 uWSGI를 설치해야 합니다. uWSGI는 아주 다양한 설정을 할 수 있는 큰 시스템입니다. 짧은 문서 페이지는 Flask와 엔진엑스, uWSGI를 결합하는 방법을 제공합니다.

 

2.7. 기타 프레임워크

 

웹사이트와 데이터베이스는 땅콩버터와 젤리 같습니다. 이들은 대부분 같이 사용됩니다. Bottle과 Flask 같은 작은 프레임워크는 데이터베이스를 직접 지원하지 않습니다. 일부 프레임워크에서는 부가기능을 지원하기도 합니다.

데이터베이스 기반의 웹사이트를 만들고 싶고, 그 데이터베이스의 설계가 자주 변경되지 않는다면, 더 큰 파이썬 웹 프레임워크를 시도해보는 것도 좋은 방법입니다. 현재 주요 경쟁자들은 다음과 같습니다.

 

장고(django)

큰 규모의 사이트에서 가장 인기 있는 프레임워크입니다. 장고를 배워야 하는 여러 가지 이유 중 하나는 파이썬 구인 광고에서 장고 개발자를 찾는 것을 많이 볼 수 있기 때문입니다. 장고는 전형적인 CRUD(Create, Read, Update, Delete) 웹 페이지를 자동으로 생성하기 위한 ORM(Object-Relational Mapping) 코드를 포함합니다. 장고에서 제공하는 ORM을 사용하지 않아도 됩니다. 개발자의 취향대로 SQLAlchemy 혹은 바로 SQL 쿼리를 사용해도 무방합니다.

 

web2py

장고와 비슷한 또 다른 스타일의 웹 프레임워크입니다.

 

pyramid

이 프레임워크는 pylons 프로젝트에서 성장했습니다. 장고와 범위가 비슷합니다.

 

turbogears

이 프레임워크는 ORM, 여러 데이터베이스, 다중 템플릿 언어를 지원합니다.

 

wheezy.web

성능에 최적화된 새로운 웹 프레임워크입니다. 2012년 9월에 있었던 테스트 결과에서 다른 파이썬 웹 프레임워크보다 더 빨랐습니다.

 

온라인 테이블에서 파이썬 웹 프레임워크를 비교할 수 있습니다.

관계형 데이터베이스 기반의 웹사이트를 구축하려는 경우 반드시 큰 프레임워크를 선택할 필요는 없습니다. Bottle, Flask, 혹은 다른 웹 프레임워크에서 바로 관계형 데이터베이스 모듈로 연결하거나 SQLAlchemy를 사용하여 사용하려는 프레임워크의 장점을 살릴 수 있습니다. 그리고 특정 ORM 코드 대신 일반적인 SQL문을 작성할 수 있습니다. 개발자들은 특정 ORM 구문보다 SQL문을 더 잘 알고 있습니다.

또한 데이터베이스가 반드시 관계형일 필요는 없습니다. 만약 데이터 스키마가 크게 바뀌는 경우(행에 걸쳐 있는 열이 많이 다른 경우), 이전 게시글에서 다룬 NoSQL 데이터 스토어와 같은 스키마 없는(Schemaless) 데이터베이스를 고려해 볼 수 있습니다. 필자는 웹사이트에서 데이터를 먼저 NoSQL 데이터베이스에 저장했다가, 관계형 데이터베이스로 전환하고, 다시 또 다른 관계형 데이터베이스로, 다시 또 다른 NoSQL 데이터베이스로, 마지막으로 다시 관계형 데이터베이스로 전환한 경험이 있습니다.

 

기타 파이썬 웹 서버

동시 요청을 처리하기 위해 멀티 프로세스와 스레드를 사용하는 아파치와 엔진엑스처럼 독립적인 파이썬 기반의 WSGI 서버는 다음과 같습니다.

다음은 이벤트 기반의 서버들입니다. 싱글 프로세스지만, 하나의 요청에 대한 블로킹(Blocking)을 피합니다.

추후 작성될 게시글에서 병행성(Concurrency)을 설명할 때 이벤트(Event)에 대해 좀 더 자세히 살펴볼 것입니다.

 

3. 웹 서비스와 자동화

 

HTMl 페이지를 소비하고 생성하는 전통적인 웹 클라이언트와 서버 애플리케이션을 살펴봤습니다. 그러나 웹은 HTML보다 더 많은 형식으로 애플리케이션과 데이터를 연결하는 강력한 방법이 존재합니다.

 

3.1. webbrowser 모듈

 

그냥 무작정 시작해봅시다. 터미널 창을 띄워 파이썬을 시작하고, 다음을 입력합니다.

>>> import antigravity

이것은 비밀리에 표준 라이브러리의 webbrowser 모듈을 호출하여 파이썬 찬양 링크를 브라우저에 던져줍니다(링크를 볼 수 없다면 xkcd에 접속하세요).

이 모듈은 바로 사용할 수 있습니다. 다음 코드는 브라우저에 파이썬 사이트를 불러옵니다.

>>> import webbrowser
>>> url = 'http://www.python.org/'
>>> webbrowser.open(url)
#True

다음은 브라우저를 새 창에 엽니다.

>>> webbrowser.open_new(url)
#True

설치된 브라우저가 탭을 지원한다면 새 탭에 엽니다.

>>> webbrowser.open_new_tab(url)
#True

webbrowser 모듈에서 브라우저의 기능을 수행할 수 있습니다.

 

3.2. 웹 API와 REST

 

웹 페이지 내에서만 데이터가 사용되는 경우가 있습니다. 데이터에 접근하려면 웹 브라우저를 사용하여 페이지에 접속하고 데이터를 읽습니다. 그리고 웹사이트 방문자가 마지막으로 방문하고 난 이후, 개발자는 웹사이트에서의 데이터 위치와 스타일을 변경할 수 있습니다.

웹 페이지 대신 웹 애플리케이션 프로그래밍 인터페이스(Application Programming Interface; API)를 통해서 데이터를 제공할 수 있습니다. 클라이언트는 URL 요청(Request)으로 서비스에 접근하여, 요청에 대한 상태와 데이터가 들어 있는 응답(Response)을 받을 수 있습니다. HTML 페이지 대신 (이전 게시글에서 배운) JSON과 XML 같은 포맷을 사용하여 데이터를 쉽게 소비하는 프로그램을 작성할 수 있습니다.

REST(REpresiontational State Tranfer)는 로이 필딩(Roy Fielding)의 박사학위 논문에서 처음으로 정의되었습니다. 많은 곳에서 REST 인터페이스 혹은 RESTful 인터페이스를 사용합니다. 실제로 웹 서비스에 접근할 수 있는 URL을 정의하여 인터페이스만 제공할 수 있습니다.

RESTful 서비스는 다음의 특정 HTTP 동사(Verb)를 사용합니다.

  • HEAD

    • 실제 데이터가 아닌, 리소스에 대한 정보를 얻어옵니다.

  • GET

    • 이름에서 알 수 있듯이 GET은 서버에서 리소스의 데이터를 검색합니다. GET은 브라우저에서 사용되는 표준 메소드입니다. 물음표(?)와 함께 인자들이 따라오는 URL이 바로  GET 요청입니다. GET 요청은 데이터를 생성, 변경, 삭제하는 데 사용해서는 안됩니다.

  • POST

    • 이 동사는 서버의 데이터를 갱신합니다. 주로 HTML 폼과 웹 API에서 사용합니다.

  • PUT

    • 이 동사는 새로운 리소스를 생성합니다.

  • DELETE

    • 이 동사는 서버의 데이터를 삭제합니다.

또한 RESTful 클라이언트는 HTTP 요청 헤더를 사용하여 서버로부터 하나 이상의 콘텐트 타입을 요청할 수 있습니다. 예를 들어 REST 인터페이스의 복합적인 서비스는 입력과 출력 형식으로 JSON 문자열을 선호합니다.

 

3.3. JSON

 

이전 게시글에서는 동영상 정보를 얻기 위해 두 개의 파이썬 코드를 작성했습니다. 그리고 JSON에 대해서도 소개했습니다. 특히 JSON은 웹 클라이언트와 서버 간에 데이터를 교환하는 데 유용하게 쓰입니다. 오픈스택(OpenStack)과 같은 웹 기반의 API에서 인기가 높습니다.

 

3.4. 크롤링과 스크래핑

 

때때로 영화 평점, 주가, 상품 등에 관한 정보가 필요할 때가 있습니다. 대부분 이러한 정보는 광고와 쓸데없는 내용으로 둘러싸인 HTML 페이지로 제공됩니다.

다음을 수행하여 수동으로 원하는 내용을 추출할 수 있습니다.

  1. 브라우저에 URL을 입력한다.

  2. 원격 페이지가 불려올 때까지 기다린다.

  3. 원하는 정보를 페이지를 통해서 본다.

  4. 어딘가에 이 정보를 기록한다.

  5. 다른 정보들도 URL을 입력하여 이 과정을 반복한다.

그러나 이러한 과정의 일부 혹은 전체를 자동화하는 것이 더 좋습니다. 자동화된 웹 패처(Fetcher)는 크롤러(Crawler) 혹은 스파이더(Spider)라고 부릅니다. 원격 웹 서버에서 콘텐츠를 찾은 후, 스크래퍼(Scraper)에서 원하는 정보를 찾기 위해 파싱합니다.

크롤러와 스크래퍼의 환상적인 조합인 Scrapy를 내려받습니다.

$ pip install scrapy

Scrapy는 BeautifulSoup과 같은 모듈이 아닌 프레임워크입니다. Scrapy는 기능은 더 많지만 설정이 복잡합니다. Scrapy에 대해 좀 더 배우고 싶다면 사이트의 문서 또는 예제를 참고하세요.

 

3.5. HTML 스크랩하기: BeautifulSoup

 

웹 페이지의 HTML로부터 데이터를 추출하고 싶다면 BeautifulSoup은 좋은 선택입니다. HTML 파싱은 생각보다 어렵습니다. 공개된 웹 페이지의 HTML 대부분이 문법적으로 유효하지 않기 때문입니다(닫지 않은 태그, 잘못된 중첩 등). 정규표현식을 사용하여 HTML 파서를 만들 때 이러한 문제에 부딪힐 것입니다.

BeautifulSoup을 설치하기 위해 다음 명령을 입력합니다(최신 버전은 4입니다. pip에서 이전 버전을 서치하려 하면 실패할 것입니다).

$ pip install beautifulsoup4

웹 페이지의 모든 링크를 가져옵시다. HTML의 a 엘리먼트(Element; 요소)는 링크를 나타내고, href는 링크 목적지를 나타내는 속성입니다. 다음 코드에서 링크를 얻기 위해 get_links() 함수를 정의합니다. 그리고 메인 프로그램은 커맨드 라인의 인자와 같이 하나 이상의 URL을 얻습니다.

def get_links(url):
    import requests
    from bs4 import BeautifulSoup as soup

    result = requests.get(url)
    page = result.text
    doc = soup(page)
    links = [element.get('href') for element in doc.find_all('a')]
    return links

if __name__ == '__main__':
    import sys
    for url in sys.argv[1:]:
        print('Links in', url)
        for num, link in enumerate(get_links(url), start=1)
            print(num, link)
        print()

이 프로그램을 links.py로 저장하고, 다음 명령을 실행합니다.

$ python links.py http://boingboing.net 

다음은 출력된 결과의 처음 몇 줄입니다.

#Links in http://boingboing.net
#1 https://boingboing.net
#2 https://boingboing.net/sub
#3 https://boingboing.net/search
#4 https://store.boingboing.net
#5 javascript:void(0)
#6 https://boingboing.net/blog
#7 https://bbs.boingboing.net
#8 https://bbs.boingboing.net/faq
#9 https://store.boingboing.net
#10 https://bit.ly/boingboingdealssupport