홈페이지에 검색 기능을 구현하다 보니 한 가지 문제가 생겼는데, 영문 검색의 경우 입력한 키워드의 대소문자가 일치하지 않으면 검색이 되지 않는 것이었다. 예를 들어 글 제목을 검색할때 Hello 라고 검색하면 hello 라는 문자열이 들어 있는 경우에는 같은 단어인데도 검색이 되지 않는 식이다. 이런 동작은 바람직하지 않으므로 개선을 해 주고 싶었다. 물론 유저가 Hello, hello 두번 검색을 해 볼수도 있겠지만 꼭 첫글자만 대문자라는 법도 없고, 모든 경우의 수 (이 경우 2^5 = 32가지)를 사용자가 일일이 검색한다는것은 생각하기 어렵다.
따라서 입력한 키워드를 이용해 가능한 모든 영문 대소문자 조합을 만들고, 이를 모두 검색해서 일치하는 결과값이 있으면 표시해주기로 했다.
처음엔 for 반복문을 이용해서 구현해보려고 했지만 쉽지 않았다. 검색을 해 보니 다행히 파이썬은 itertools
라는 훌륭한 도구를 제공하고 있어 그것을 이용한 방법을 기술해 보겠다. 내용은 stack overflow의 아래 문답을 기초로 했다.
일단 결론부터 써 보면
import itertools
def case_combination(word):
sequence = ((c.lower(), c.upper()) for c in word)
return [''.join(x) for x in itertools.product(*sequence)]
이렇게 하면
>>> print(case_combination('fox'))
['fox', 'foX', 'fOx', 'fOX', 'Fox', 'FoX', 'FOx', 'FOX']
이렇게 'fox' 라는 단어의 각 글자를 대소문자로 조합한 결과를 모두 리턴받게 된다.
아래는 각 단계별로 하나하나 공부해 본 내용.
파이썬의 itertools
모듈에는 여러가지 조합을 만들어낼 수 있는 다양한 메서드를 제공하고 있는데, 내용이 많으므로 다른 것들은 생략하고.. product()
메서드에 대해서만 알아보면, 이 함수는 전달받은 임의의 개수의 인자에 대해서 각 인자의 원소들로 데카르트 곱(Cartesian product)을 구해 이를 튜플 형식의 원소를 가진 반복자로 반환해준다.
라고 써놓고 보니 오히려 더 복잡한데, 실제 동작을 보면 그렇게 어렵지는 않다. 데카르트 곱이라는것은 어떤 집합이 있을때 각 집합에서 원소를 하나씩 골라서 각각의 원소로 이루어진 쌍을 모두 구한다는 것이다.
즉 집합 A = {a,b}, 집합 B = {1,2,3} 이라고 할때 데카르트 곱을 A x B 라고 하면 결과는
A x B = {(a,1), (a,2), (a,3), (b,1), (b,2), (b,3)}
가 되는 것이다.
만약 집합 A, B, C 가 각각 A = {a, b}, B = {1,2}, C = {가,나} 라면 A x B x C 는
A x B x C = {(a,1,가), (a, 1, 나), (a, 2, 가), (a, 2, 나), ......... , (b, 2, 가), (b, 2, 나)}
가 될 것이다.
itertools.product()
메서드는 바로 이 동작을 하는 것이다.
즉 어떤 단어를 이루는 글자들의 모든 대소문자 조합을 구하려면, 글자 하나하나의 대소문자 쌍을 원소로 하는 집합들을 만들어 각 집합들에 대해 데카르트 곱을 구하면 되겠다.
원하는 단어가 'fox' 라면, (f, F), (o, O), (x, X) 세 집합의 데카르트 곱을 구하면 되는것이다.
위의 코드를 다시 한줄한줄 살펴 보면,
>>> word = 'fox'
>>> sequence = ((c.lower(), c.upper()) for c in word)
>>> print(list(sequence))
[('f', 'F'), ('o', 'O'), ('x', 'X')]
제너레이터 표현식을 이용해 fox라는 단어를 이루는 문자들의 대소문자를 원소로 갖는 튜플들을 생성해준다. 제너레이터를 리스트로 만들어 프린트해보면 원하는대로 각각의 글자의 대소문자 튜플들이 생성되어 있다.
>>> letters = itertools.product(*sequence)
>>> for i in letters:
... print(i)
...
('f', 'o', 'x')
('f', 'o', 'X')
('f', 'O', 'x')
('f', 'O', 'X')
('F', 'o', 'x')
('F', 'o', 'X')
('F', 'O', 'x')
('F', 'O', 'X')
튜플 언패킹을 이용해 제너레이터 안의 원소들을 itertools.product()에 전달해주면 각각의 원소들의 데카르트 곱을 구해준다. 프린트해보면 f, o, x 각각의 대소문자에 대한 8가지 조합을 모두 만들어준 것을 알 수 있다.
즉 위처럼 튜플을 언패킹해주는 것은 다음과 같은 동작이다.
>>> letters = itertools.product(('f','F'),('o','O'),('x','X'))
>>> for i in letters:
... print(i)
...
('f', 'o', 'x')
('f', 'o', 'X')
('f', 'O', 'x')
('f', 'O', 'X')
('F', 'o', 'x')
('F', 'o', 'X')
('F', 'O', 'x')
('F', 'O', 'X')
이렇게 만들어진 letters 객체의 각 원소 튜플에서, 글자들을 join() 함수를 이용해 연결해주고, 즉
>>> ''.join(('F','o','x'))
'Fox'
이렇게 조합된 단어들을 리스트 컴프리헨션을 이용해 리스트로 만들어 준다.
import itertools
def case_combination(word):
sequence = ((c.lower(), c.upper()) for c in word)
return [''.join(x) for x in itertools.product(*sequence)]
>>> print(case_combination('fox'))
['fox', 'foX', 'fOx', 'fOX', 'Fox', 'FoX', 'FOx', 'FOX']
이렇게 해서 함수 리턴값으로 원래 단어에 대해 가능한 모든 대소문자 조합을 받아서, 사이트 검색시에 사용자가 무슨 단어를 입력했든지 대소문자 상관없이 검색해주는 기능에 이용하였다.
파이썬 반복자는 결과물이 바로 눈에 보이는것이 아니라 필요할때마다 하나씩 꺼내 쓰는 식이라 처음에는 직관적으로 이해하기가 어려운것같다. itertools 내의 함수들의 동작도 이와 관련되어 좀 머리가 아프지만 잘 이해하고 사용하면 복잡한 코딩을 피하고 요긴하게 쓸 일이 있을 듯하니 조금 더 공부를 해 봐야겠다.