내가 일하고 있는 업계(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은 파이썬 표준 라이브러리 중 하나다. HTTP 요청, 파싱과 관련된 하위 패키지들이 존재하며, URL 파싱과 관련된 것들은 거의 다 urllib.parse에 들어 있다. Python 2의 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'
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
_replace
를 쓰면 프로퍼티를 변경할 수 있다. 그러나 query string은 sort=name&keyword=planb
와 같이 문자열 형태로 이루어져 있기 때문에, query string 중 sort
를 like
로 변경한다거나 하는 작업이 쉽지 않다. urllib.parse
에는 query string을 파싱해서 collection으로 반환해주는 parse_qs
와 parse_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_qsl
은 key-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 편으로 이어진다.
좋은 포스트 감사합니다!
저도 저번주에 urllib 의 parse_qs 를 사용할 일이 있었는데
keyword=planb&keyword=mingyu
이런식의 쿼리스트링을 리스트로 받는게 조금 신기했어요.저는 기존에
keyword=planb,mingyu
이런식으로 사용해왔었는데.. city7310 님은 어떤걸 선호하시나요? ㅎㅎ