내가 일하고 있는 업계(Marketing Tech)가 URL을 많이 다루는 곳이라서 그런지, URL에 query string 추가는 어떻게 해야 되는지, 어떻게 path 부분만 예쁘게 떼낼 수 있을지같은 것들을 고민하게 된다. urllib을 보면서, 아래같이 Pythonic하게 URL을 다룰 수 있으면 좋을 것 같다고 생각했다.

url = URL('https://velog.io/tags?sort=name')

print(url.host) # velog.io

print(url.path) # /tags
url.path.extend('JoMingyu')
print(url.path) # /tags/JoMingyu

print(url.args) # [('sort', 'name')]
url.args['size'] = 10
url.args['repeated'] = [1, 2]
print(url.args) # [('sort', 'name'), ('size', '10'), ('repeated', '1'), ('repeated', '2')]
print(str(url.args)) # 'sort=name&size=10&repeated=1&repeated=2'

print(str(url)) # 'https://velog.io/tags/JoMingyu?sort=name&size=10&repeated=1&repeated=2'

직접 만들자니 아무래도 URL이라는 게 꽤 예민하다 보니까 간단한 일이 아니기도 해서, urllib으로 URL을 다루면서 불편하지만 참았던 경험, 관련 라이브러리를 이것저것 찾아서 써본 경험도 공유하고자 한다. 참고로, urllib의 사용법보단 URL을 더 잘 다루기 위해 고민한 것들이 글의 내용을 이룰 것이므로 튜토리얼이 필요하다면 urllib 라이브러리라는 글을 읽어보기 바란다.

urllib.parse

urllib은 파이썬 표준 라이브러리 중 하나다. HTTP 요청, 파싱과 관련된 하위 패키지들이 존재하며, URL 파싱과 관련된 것들은 거의 다 urllib.parse에 들어 있다. Python 2의 urlparse가 옮겨진 것이다.

urlparse, urlunparse

urlparse

urlparse는 URL을 6개의 요소로 이루어진 namedtuple로 만들어 반환한다. 정확히는, URL을 다루기 위해 만들어진 namedtuple상속받아 정의ParseResult의 객체다.

from urllib.parse import urlparse

parts = urlparse('https://velog.io/tags/?sort=name')

print(parts.scheme) # 'https'
print(parts.netloc) # 'velog.io:80'
print(parts.path) # '/tags/'
print(parts.params) # ''
print(parts.query) # 'sort=name'
print(parts.fragment) # ''
print(parts) # ParseResult(scheme='https', netloc='velog.io:80', path='/tags/', params='', query='sort=name', fragment='')

namedtuple 특성 상 프로퍼티들은 기본적으로 immutable이고, 값을 변경하려면 _replace를 사용해서 새로운 프로퍼티가 반영된 새로운 객체를 반환받아야 한다.

from urllib.parse import urlparse

parts = urlparse('https://velog.io/tags/?sort=name')

parts.scheme = 'http' # AttributeError
parts = parts._replace(scheme='http')

print(parts.scheme) # 'http'

urlunparse

ParseResult를 다시 URL로 만드려면, geturl 메소드나 urlunparse를 사용하면 된다. ParseResult.geturl = lambda self: urlunparse(self)라고 보면 된다.

from urllib.parse import ParseResult, urlunparse

parts = ParseResult(scheme='https', netloc='velog.io', path='/tags', params='', query='', fragment='')
print(parts.geturl()) # https://velog.io/tags
print(urlunparse(parts)) # https://velog.io/tags

query string 바꿔치기

_replace를 쓰면 프로퍼티를 변경할 수 있다. 그러나 query string은 sort=name&keyword=planb와 같이 문자열 형태로 이루어져 있기 때문에, query string 중 sortlike로 변경한다거나 하는 작업이 쉽지 않다. urllib.parse에는 query string을 파싱해서 collection으로 반환해주는 parse_qsparse_qsl 함수가 있다.

from urllib.parse import urlparse, parse_qs, parse_qsl

parts = urlparse('https://velog.io/tags?sort=name&keyword=planb')

print(parse_qs(parts.query)) # {'sort': ['name'], 'keyword': ['planb']}
print(parse_qsl(parts.query)) # [('sort', 'name'), ('keyword', 'planb')]

parse_qs는 key에 대해 value들을 list로 묶어서 dictionary로 반환하고, parse_qslkey-value pair 각각을 tuple로 만들어서 list로 반환한다. 그냥 납작한 딕셔너리로 관리하지 못하는 이유는, query string의 key가 표준에 의해서 중복이 허용되기 때문이다.

from urllib.parse import urlparse, parse_qs

parts = urlparse('https://velog.io/tags?sort=name&keyword=planb&keyword=mingyu')

print(parse_qs(parts.query)) # {'sort': ['name'], 'keyword': ['planb', 'mingyu']}

urllib.parse를 통해 URL에서 query string의 수정 작업을 진행하려면, 아래와 같이 코드를 작성해야 한다.

from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse

parts = urlparse('https://velog.io/tags?sort=name&keyword=planb')
# 요소 분리
qs = dict(parse_qsl(parts.query))
# parse_qsl의 결과를 dictionary로 캐스팅
qs['keyword'] = 'new'
# 수정 작업
parts = parts._replace(query=urlencode(qs))
# dictionary로 되어 있는 query string을 urlencode에 넘겨 문자열화하고 replace
new_url = urlunparse(parts)
# urlunparse해서 새로운 URL 얻어내기

print(new_url) # https://velog.io/tags?sort=name&keyword=new

요구되는 코드 양이 파이썬 치고 그렇게 적은 편은 아니었어서, 나랑 똑같은 불편함을 겪는 사람이 그 불편함을 참지 못해 만든 라이브러리가 있을 것 같았다. 파이썬으로 URL 가지고 놀기 - yarl 편으로 이어진다.