urllib2를 사용하여 인터넷 자원을 가져오는 법

저자: 마이클 푸어드(Michael Foord)
한글판 johnsonj 2008.05.09 금

주의

이 HOWTO의 이전 버전으로 프랑스어 번역본이 있다. urllib2 - Le Manuel manquant에서 보시면 된다.

들어가는 말

urllib2는 URLs (Uniform Resource Locators)을 가져오는 Python 모듈이다. 인터페이스는 아주 단순하며, urlopen 함수의 형태로 제공된다. 이 모듈은 다양한 프로토콜을 사용하여 URL을 가져올 수 있다. 또한 일반적인 상황을 처리하기 위하여 약간 복잡한 인터페이스도 제공한다 - 기본적인 인증, 쿠키, 프록시 등등 같은 상황말이다. 이런 것들을 위해 처리자와 개방자라고 불리우는 객체가 제공된다.

urllib2는 연관 네트워크 프로토콜을 사용하여 (예를 들어 FTP, HTTP) 많은“URL 체계”로 URL을 가져온다 ( URL에서 “:”앞의 문자열로 식별된다 - 예를 들어 “ftp://python.org/“의 URL 체계는 “ftp”이다). 이 자습서는 아주 많이 사용되는 사례인 HTTP에 초점을 둔다.

단순한 상황에서는 urlopen이 아주 사용하기 쉽다. 그러나 HTTP URL을 열 때 에러를 맞이하거나 중요성이 있는 사례를 맞이한다면, 하이퍼텍스트 전송 프로토콜(HyperText Transfer Protocol)을 약간 이해할 필요가 있다. HTTP에 대하여 가장 종합적이고 권위있는 참조서는 RFC 2616이다. 이 문서는 기술 문서이지 쉽게 읽을 수 있도록 하기 위한 의도가 아니다. 이 HOWTO는 urllib2를 사용하여, HTTP에 대하여 충분히 자세하게 설명하여 여러분을 이끄는 것이 목적이다. urllib2 문서를 대체하고자 하는 의도가 있는 것은 아니며, 오히려 보충하기 위한 것이다.

URL 가져오기

가장 단순하게 urllib2를 사용하는 법은 다음과 같다:

import urllib2
response = urllib2.urlopen('http://python.org/')
html = response.read()

urllib2가 많이 사용되는 곳은 이렇게 단순하게 사용하는 것이다 (‘http:’ URL 대신에 ‘ftp:’, ‘file:’, 등등으로 시작하는 URL을 사용할 수 있음에 주목하자). 그렇지만, 이 자습서의 목적은 HTTP를 중심으로 좀 더 복잡한 사례를 설명하는 것이다.

HTTP는 요청과 응답에 기반한다 - 클라이언트는 요청하고 서버는 응답한다. urllib2는 여러분이 만든 HTTP 요청을 대표하는 Request 객체를 흉내낸다. 가장 단순한 형태는 가져오고 싶은 URL을 나타내는 요청 객체를 만드는 것이다. 이 요청(Request) 객체로 urlopen을 요청하면 요청된 URL에 대하여 응답 객체를 돌려준다. 이 응답은 파일-류의 객체인데, 그 응답에 대하여 .read()를 호출할 수 있다는 뜻이다:

import urllib2

req = urllib2.Request('http://www.voidspace.org.uk')
response = urllib2.urlopen(req)
the_page = response.read()

urllib2는 같은 Request 인터페이스를 이용하여 모든 URL 체계를 처리한다. 예를 들어, 그런 식으로 FTP 요청도 처리하면 된다:

req = urllib2.Request('ftp://example.com/')

HTTP의 경우, Request 객체로 할 수 있는 일이 두 가지 더 있다: 첫째, 데이터를 서버에 보낼 수 있다. 둘째, 데이터나 요청 그 자체에 관하여 (“metadata”) 서버에 추가로 정보를 보낼 수 있다 - 이 정보는 HTTP “헤더”로 전송된다. 이런 것들을 하나씩 차례로 살펴보자.

데이터

가끔 데이터를 URL에 보내고 싶은 경우가 있다 (종종 URL은 CGI (Common Gateway Interface) 스크립트나 [1] 또는 기타 웹 어플리케이션을 가리킨다). HTTP로, 이는 종종 POST 요청이라는 것을 사용하여 완수된다. 이는 종종 웹에서 채운 HTML 폼을 제출하면 브라우저가 하는 일이다. 모든 POST가 폼으로부터 와야 하는 것은 아니다: POST를 사용하여 임의의 데이터를 여러분의 어플리케이션에 전송할 수 있다. 일반적인 HTML 폼의 경우, 데이터는 표준 방식으로 인코드한 다음에, Request 객체에 data 인자로 건네야 한다. 인코딩은 urllib2아니라 urllib 라이브러리의 함수를 사용하여 완료된다.

import urllib
import urllib2

url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
          'location' : 'Northampton',
          'language' : 'Python' }

data = urllib.urlencode(values)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
the_page = response.read()

다른 인코딩도 가끔 필요함에 주목하자 (예를 들어, HTML 폼에서 파일 업로드할 경우 - 자세한 사항은 HTML 규격인 폼 제출 참조).

data 인자를 건네지 않으면, urllib2는 GET 요청을 사용한다. GET 과 POST 요청의 차이점 한가지는 POST 요청은 종종 “부-작용”이 있다는 것이다: 시스템의 상태를 바꾼다. (예를 들어 웹 사이트에 명령을 내려서 수 천통의 스팸을 여러분의 문앞에 배달시킬 수 있다). HTTP 표준에 의하면 POST의 의도는 언제나 부작용을 야기하는 것이고, GET 요청은 절대로 부작용을 야기하지 않는 것이 분명하지만, 어떤 것도 GET 요청이 부작용을 가지는 것을 막지 못하고 POST 요청이 부작용을 갖지 못하도록 막을 수도 없다. 데이터는 URL 자체에서 인코딩하면 HTTP GET 요청으로 건넬 수도 있다.

다음과 같이 하면 된다:

>>> import urllib2
>>> import urllib
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.urlencode(data)
>>> print url_values
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib2.open(full_url)

완전한 URL은 URL 뒤에 ?를 추가하고, 바로 뒤에다 인코드된 값을 추가하면 된다.

헤더

여기에서 HTTP 요청에 헤더를 추가하는 법을 예시하기 위하여 한가지 특별한 HTTP 헤더를 연구해 보겠다.

어떤 웹사이트는 [2] 프로그램으로 열람하는 것을 꺼리거나 다양한 버전을 다양한 브라우저에 전송하는 것을 싫어한다[3] . 기본으로 urllib2는 자신을 Python-urllib/x.y로 식별한다 (여기에서 xy는 파이썬 배포본의 대번호와 소번호이다. 예를 들어 Python-urllib/2.5와 같이 말이다), 이 때문에 사이트가 혼란을 일으킬 수도 있고, 또는 그냥 작동하지 않을 수도 있다. 브라우저는 User-Agent 헤더를 통해서 자신을 식별한다 [4]. Request 객체를 만들면 거기에 헤더 사전을 건넬 수 있다. 다음 예제는 위와 똑 같이 요청하지만, 자신을 인터넷 익스플로러 [5]로 식별한다.

import urllib
import urllib2

url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
values = {'name' : 'Michael Foord',
          'location' : 'Northampton',
          'language' : 'Python' }
headers = { 'User-Agent' : user_agent }

data = urllib.urlencode(values)
req = urllib2.Request(url, data, headers)
response = urllib2.urlopen(req)
the_page = response.read()

응답에는 또한 두 가지 유용한 메쏘드가 있다. 다음에 나오는 info 그리고 geturl 섹션을 보자. 일이 잘못되면 무슨 일이 일어나는지 살펴보겠다.

예외 처리하기

urlopen은 응답을 처리하지 못하면 URLError를 일으킨다 (그렇지만 예의 파이썬 API처럼, ValueError, TypeError 등등 같은 내장 예외도 일어날 수 있다).

HTTPErrorURLError의 서브클래스로서 특정한 HTTP URL의 경우에 일어난다.

URLError

종종, URLError는 네트워크 접속이 되지 않기 때문에 (지정된 서버로 가는 길이 열리지 않음), 또는 지정된 서버가 존재하지 않기 때문에 일어난다. 이런 경우, 일어난 예외는‘reason’ 속성을 가지는데, 이 속성은 에러 코드와 텍스트 에러 메시지를 담은 터플이다.

예를 들어,

>>> req = urllib2.Request('http://www.pretend_server.org')
>>> try: urllib2.urlopen(req)
>>> except URLError, e:
>>>    print e.reason
>>>
(4, 'getaddrinfo failed')

HTTPError

서버로부터 온 HTTP 응답에는 숫자로 “상태 코드”가 담긴다. 보통 상태 코드는 서버가 요청을 완수하지 못했음을 뜻한다. 기본 처리자는 이런 응답을 여러분 대신 처리해 준다. (예를 들어, 응답이“방향전환(redirection)”이라서 클라이언트에게 문서를 다른 URL로부터 가져오도록 요구한다면, urllib2는 그 일을 여러분 대신 처리해 준다). 처리하지 못할 경우, urlopen은 HTTPError를 일으킨다. 전형적인 에러로는 ‘404’ (페이지 없음), ‘403’ (요청 금지), 그리고‘401’(인증 요구)이 있다.

RFC 2616의 섹션 10을 보면 모든 HTTP 에러 코드를 참조할 수 있다.

일어난 HTTPError 실체는 정수 ‘code’속성을 가지는데, 이는 서버가 보낸 에러에 상응한다.

에러 코드

기본 처리자는 (300 범위의 코드) 방향전환을 처리하기 때문에, 그리고 100-299 범위의 코드는 성공을 나타내므로, 보통 400-599 범위에 있는 에러 코드만 볼 것이다.

BaseHTTPServer.BaseHTTPRequestHandler.responses는 응답코드가 담긴 유용한 사전이다. 이 사전은 RFC 2616에 사용되는 모든 응답 코드를 보여준다. 사전은 편의를 위해 여기에서 재생산된다.

# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
responses = {
    100: ('Continue', 'Request received, please continue'),
    101: ('Switching Protocols',
          'Switching to new protocol; obey Upgrade header'),

    200: ('OK', 'Request fulfilled, document follows'),
    201: ('Created', 'Document created, URL follows'),
    202: ('Accepted',
          'Request accepted, processing continues off-line'),
    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
    204: ('No Content', 'Request fulfilled, nothing follows'),
    205: ('Reset Content', 'Clear input form for further input.'),
    206: ('Partial Content', 'Partial content follows.'),

    300: ('Multiple Choices',
          'Object has several resources -- see URI list'),
    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
    302: ('Found', 'Object moved temporarily -- see URI list'),
    303: ('See Other', 'Object moved -- see Method and URL list'),
    304: ('Not Modified',
          'Document has not changed since given time'),
    305: ('Use Proxy',
          'You must use proxy specified in Location to access this '
          'resource.'),
    307: ('Temporary Redirect',
          'Object moved temporarily -- see URI list'),

    400: ('Bad Request',
          'Bad request syntax or unsupported method'),
    401: ('Unauthorized',
          'No permission -- see authorization schemes'),
    402: ('Payment Required',
          'No payment -- see charging schemes'),
    403: ('Forbidden',
          'Request forbidden -- authorization will not help'),
    404: ('Not Found', 'Nothing matches the given URI'),
    405: ('Method Not Allowed',
          'Specified method is invalid for this server.'),
    406: ('Not Acceptable', 'URI not available in preferred format.'),
    407: ('Proxy Authentication Required', 'You must authenticate with '
          'this proxy before proceeding.'),
    408: ('Request Timeout', 'Request timed out; try again later.'),
    409: ('Conflict', 'Request conflict.'),
    410: ('Gone',
          'URI no longer exists and has been permanently removed.'),
    411: ('Length Required', 'Client must specify Content-Length.'),
    412: ('Precondition Failed', 'Precondition in headers is false.'),
    413: ('Request Entity Too Large', 'Entity is too large.'),
    414: ('Request-URI Too Long', 'URI is too long.'),
    415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
    416: ('Requested Range Not Satisfiable',
          'Cannot satisfy request range.'),
    417: ('Expectation Failed',
          'Expect condition could not be satisfied.'),

    500: ('Internal Server Error', 'Server got itself in trouble'),
    501: ('Not Implemented',
          'Server does not support this operation'),
    502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
    503: ('Service Unavailable',
          'The server cannot process the request due to a high load'),
    504: ('Gateway Timeout',
          'The gateway server did not receive a timely response'),
    505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
    }

에러가 일어나면 서버는 HTTP 에러 코드 그리고 에러 페이지를 돌려줌으로써 응답한다. HTTPError 실체를 반환된 페이지에 대한 응답으로 사용할 수 있다. 이는 코드 속성뿐만 아니라 read 메쏘드와 geturl 메쏘드 그리고 info 메쏘드도 가진다는 뜻이다.

>>> req = urllib2.Request('http://www.python.org/fish.html')
>>> try:
>>>     urllib2.urlopen(req)
>>> except URLError, e:
>>>     print e.code
>>>     print e.read()
>>>
404
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<?xml-stylesheet href="./css/ht2html.css"
    type="text/css"?>
<html><head><title>Error 404: File Not Found</title>
...... etc...

요약하면

그래서 HTTPError 또는 URLError를 대비하고 싶다면 두 가지 기본적인 접근법이 있다. 나는 두 번째 방법을 선호한다.

첫 번째 방법

from urllib2 import Request, urlopen, URLError, HTTPError
req = Request(someurl)
try:
    response = urlopen(req)
except HTTPError, e:
    print 'The server couldn\'t fulfill the request.'
    print 'Error code: ', e.code
except URLError, e:
    print 'We failed to reach a server.'
    print 'Reason: ', e.reason
else:
    # everything is fine

주의

except HTTPError가장 먼저 와야 한다, 그렇지 않으면 except URLErrorHTTPError도 역시 잡을 것이다.

두번째 방법

from urllib2 import Request, urlopen, URLError
req = Request(someurl)
try:
    response = urlopen(req)
except URLError, e:
    if hasattr(e, 'reason'):
        print 'We failed to reach a server.'
        print 'Reason: ', e.reason
    elif hasattr(e, 'code'):
        print 'The server couldn\'t fulfill the request.'
        print 'Error code: ', e.code
else:
    # everything is fine

info 그리고 geturl

urlopen (또는 HTTPError 실체)이 돌려주는 응답은 두 가지 유용한 메쏘드가 있다. infogeturl이 바로 그것이다.

geturl - 이 메쏘드는 가져온 페이지의 실제 URL을 돌려준다. 이는 유용하다 urlopen (또는 사용된 opener 객체)가 방향전환을 따라갔을 수 있기 때문이다. 가져온 페이지의 URL은 요청한 페이지의 URL과 같지 않을 수 있다.

info - 이 메쏘드는 가져온 페이지를, 특히 서버가 전송한 헤더를 기술한 사전-류의 객체를 돌려준다. 현재로 httplib.HTTPMessage 실체이다.

전형적인 헤더에는‘Content-length’, ‘Content-type’, 등등이 포함된다. HTTP 헤더에 대한 간편 참조서를 보면 유용한 HTTP 헤더 목록과 함께 그 의미와 사용법에 관하여 간략한 설명을 볼 수 있다.

오프너와 처리자

URL을 가져올 때 opener를 사용할 수 있다 (약간 이름이 혼란스러운 urllib2.OpenerDirector의 실체). 보통 기본 오프너를 - urlopen를 통하여 - 사용해 왔지만 맞춤 오프너를 만들 수 있다. 오프너는 처리자를 사용한다. 모든 “고된 일”은 처리자가 완수한다. 각 처리자는 특별한 URL 체계(http, ftp, 등등.)에 대하여 URL을 여는 법을 안다. 또는 예를 들어 HTTP 방향전환이나 HTTP 쿠키등 URL 여는 법 등등의 URL을 여는 법의 여러 측면들을 이해한다.

설치된 특정한 처리자로 URL을 가져오고 싶다면 오프너를 만들고 싶을 것이다. 예를 들어 쿠키를 처리하는 오프너를 얻는다든지 방향전환을 처리하지 않는 오프너를 얻고 싶다든지 하면 말이다.

오프너를 만들려면, OpenerDirector를 실체화하고, 다음 각각 .add_handler(some_handler_instance)를 호출하면 된다.

대안적으로, build_opener를 사용할 수 있다. 이는 단 한 번의 호출로 오프너 객체를 만드는 편의 함수이다. build_opener에는 여러 처리자가 기본으로 추가되어 있지만, 기본 처리자를 오버라이드 하거나/하고 더 추가하는 신속한 방법을 제공한다.

여러분이 원할만한 다른 종류의 처리자는 프록시 처리와 인증 처리 그리고 기타 일반적이지만 약간 특수한 상황을 처리할 수 있다.

install_openeropener 객체를 (전역적) 기본 오프너로 만드는데 사용할 수 있다. 그 의미는 urlopen을 호출하면 여러분이 설치한 오프너를 사용할 것이라는 뜻이다.

오프너 객체는 open 메쏘드가 있다. 이 메쏘드는 urlopen 함수와 똑같은 방식으로 url을 가져오기 위하여 직접 호출할 수 있다: install_opener를 호출할 필요가 없다. 단 편의를 위한 경우를 제외하고 말이다.

기본 인증

처리자를 보여주고 설치하기 위하여 HTTPBasicAuthHandler를 사용하겠댜. 이 주제에 관한 좀 더 자세한 연구는 – 기본 인증이 작동하는 방식에 관한 설명을 포함하여 - 기본 인증 자습서를 보자.

인증이 요구되면, 서버는 (401 에러 코드와 함께) 헤더를 보내서 인증을 요구한다. 이는 인증 체계와 ‘영역(realm)’을 지정한다. 헤더의 모습은 다음과 같다 : Www-authenticate: SCHEME realm="REALM".

예를 들어,

Www-authenticate: Basic realm="cPanel Users"

클라이언트는 그러면 요청에 헤더로 포함된 영역에 대하여 적절한 이름과 패스워드를 가지고 요청을 다시 시도해야 한다. 이것이‘기본 인증’이다. 이 절차를 간소화 하기 위하여 HTTPBasicAuthHandler 실체를 만들고 오프너를 만들어 이 처리자를 사용하면 된다.

HTTPBasicAuthHandler은 패스워드 관리자라고 부르는 객체를 사용하여 URL과 영역(realms)을 패스워드와 사용자이름에 짝짓는다. (서버가 보낸 인증 헤더로부터) 영역이 무엇인지 알고 있다면, HTTPPasswordMgr를 사용해도 된다. 보통은 영역이 무엇인지 신경쓰지 않는다. 그런 경우 HTTPPasswordMgrWithDefaultRealm를 사용하면 편리하다. 이렇게 하면 기본 사용자이름과 패스워드를 URL에 지정할 수 있다. 특정한 영역에 대하여 대안적 조합을 제공하지 않을 경우 이것이 공급된다. None을 영역 인자로 하여 add_password 메쏘드에 제공함으로써 이를 나타낸다.

최상위-수준의 URL은 인증을 요구하는 첫 URL이다. 여러분이 .add_password()에 건넨 URL보다“더 깊은”URL들도 역시 부합한다.

# create a password manager
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()

# 사용자이름과 패스워드를 추가한다.
# 영역을 알고 있다면, ``None`` 대신에 그것을 사용해도 좋다.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)

handler = urllib2.HTTPBasicAuthHandler(password_mgr)

# "오프너" (OpenerDirector 실체)를 만든다
opener = urllib2.build_opener(handler)

# 만든 오프너를 사용하여 URL을 가져온다
opener.open(a_url)

# 오프너를 설치한다.
# 이제 urllib2.urlopen을 호출하면 모두 우리가 만든 오프너를 사용한다.
urllib2.install_opener(opener)

주의

위의 예제에서는 build_openerHHTPBasicAuthHandler를 공급했다. 기본적으로 오프너는 정상적 상황에 대한 처리자가 구비되어 있다 – ProxyHandler, UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, HTTPErrorProcessor.

top_level_url는 사실 “완전한 URL” 이거나 또는 “권위(authority)”이다. ( 완전한 URL은 http://example.com/와 같이‘http:’ 체계 컴포넌트와 호스트이름 그리고 선택적으로 포트 번호가 포함된다. 그리고 권위는 “example.com”또는“example.com:8080”과 같이 호스트이름, 선택적으로 포트 번호가 포함된다 (뒤의 예는 포트 번호가 포함되어 있다)). 혹 권위(authority)가 존재하면“userinfo”컴포넌트가 포함되면 안된다 - 예를 들어 “joe@password:example.com”는 올바르지 않다.

프록시

urllib2는 자동으로 프록시 설정을 탐지하여 사용한다. 이는 ProxyHandler를 통하는데 이는 보통의 처리자 사슬의 일부이다. 보통 그것은 좋은 일이다. 그러나 때로는 도움이 되지 않을 경우도 있다 [6]. 이렇게 하는 한가지 방법은 프록시를 정의하지 말고 독자적으로 ProxyHandler를 설정하는 것이다. 이는 기본 인증 처리자를 설정할 때와 비슷한 단계를 밟아 완수한다:

>>> proxy_support = urllib2.ProxyHandler({})
>>> opener = urllib2.build_opener(proxy_support)
>>> urllib2.install_opener(opener)

주의

현재 urllib2는 프록시를 통하여 https 위치를 가져오지 못한다. 그렇지만, 이것은 urllib2를 요리법에 보여주는 바와 같이 확장하면 활성화시킬 수 있다 [7].

소켓과 레이어

파이썬이 웹에서 자원을 가져오는 방식은 계층으로 지원한다. urllib2은 httplib 라이브러리를 사용한다. 이 라이브러리는 차례로 socket 라이브러리를 사용한다.

파이썬 2.3부터 소켓이 타임 아웃하기 전에 응답을 얼마나 오래 기다려야 하는지 지정할 수 있다. 이는 웹 페이지를 가져와야 하는 어플리케이션에 유용하다. 기본으로 socket 모듈은 타임아웃이 없으며 무한회돌이에 빠질 수 있다. 현재, 소켓의 타임아웃은 httplib 또는 urllib2 수준에 노출되어 있지 않다. 그렇지만, 기본 타임아웃을 전역적으로 모든 소켓에 대하여 설정할 수 있다. 다음과 같이 하면 된다.

import socket
import urllib2

# 몇초 후에 타임아웃
timeout = 10
socket.setdefaulttimeout(timeout)

# 이렇게 urllib2.urlopen을 호출하면 이제 기본 타임아웃을 사용한다
# 소켓 모듈에 설정한다
req = urllib2.Request('http://www.voidspace.org.uk')
response = urllib2.urlopen(req)

각주

이 문서는 존 리(John Lee)가 검토하고 수정하였다.

[1] CGI 프로토콜에 대한 소개는 파이썬으로 웹 어플리케이션 작성하기를 참조하자.
[2] 예를 들어 구글(Google) 같은 경우다. 프로그램에서 제대로 구글을 사용하는 방법은 물론 PyGoogle을 사용하는 것이다. Voidspace Google을 보시면 Google API의 사용법에 관한 예제를 보실 수 있다.
[3] 브라우저 탐지는 웹사이트 디자인에서 아주 나쁜 관습이다 - 웹 표준으로 사이트를 구축하는 편이 훨씬 더 현명하다. 불행하게도 많은 사이트에서 여전히 브라우저에 따라 다른 버전을 전송한다.
[4] MSIE 6에 대한 사용자 에이전트는 ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)’이다
[5] HTTP 요청 헤더에 관한 더 자세한 정보는 HTTP 헤더에 대한 간편 참조서 참고하자.
[6] 본인의 경우 프록시를 사용하여 인터넷에 접근해 작업해야 했다. 이 프록시를 사용하여 localhost URL을 가져오려고 시도하면 접속이 차단된다. IE는 프록시를 사용하도록 설정되어 있는데, urllib2가 그 위에 올라탄다. 로컬 호스트 서버로 스크립트를 테스트하기 위해, 나는 urllib2가 프록시를 사용하지 못하도록 막아야 했다.
[7] SSL 프록시를 위한 urllib2 오프너 (CONNECT 메쏘드): ASPN Cookbook Recipe.