파이썬 정규표현식 연습(비밀번호 패턴, html 태그 제거)
오늘은 정규표현식에 대해 공부한 내용을 조금 정리해보고자 합니다.
사실 정규표현식 자체가 워낙 익숙치 않기도 하고 자주 사용되는 느낌은 아니라서...
'필요할 때마다 검색해서 쓰면 되겠지~' 생각했는데 막상 쓰고 싶을 때 활용할 능력도 없었습니다..😱
하지만 정규표현식은 크롤링과 자연어 전처리에서 필수적이라고 합니다.
인공지능 모델이 학습하기 위한 좋은 품질의 데이터를 마련하기 위해서 필요한 것이죠!
그래서 이번에 정규표현식을 활용했던 내용을 토대로 정규표현식에 관한 아주 기초적인 것들을 정리해보겠습니다.
메타 문자
. ^ $ * + ? { } [ ] \ | ( )
메타 문자란 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용하는 문자를 뜻합니다.
이 문자들은 정규표현식에서 다른 문자들과 달리 조금 특별한 기능을 수행합니다.
- Dot(.) : 줄바꿈 문자인 '\n'을 제외한 모든 문자와 매치됩니다.
즉 '.'을 사용하면 어떤 문자든지 올 수 있다는 뜻이 됩니다. - * : * 앞에 위치하는 문자가 0번~무한대로 반복될 수 있음을 뜻합니다.
예를 들어 a* 라면 공백, a, aa, ab 등을 전부 만족시킬 수 있는 조건이 됩니다. - + : 앞에 위치하는 문자가 최소 한 번 이상 반복됨을 뜻합니다.
*와의 약간 다르다는 점에 유의합니다. - {m, n} : 반복의 횟수를 설정해줍니다. m~n회 반복을 뜻합니다.
{m}은 m번, {m,}은 m번 이상, {,n}은 n번 이하를 의미하게 됩니다. - ? : 있거나 없거나 둘 다 가능하다는 뜻입니다. 위의 표현을 빌리면 {0, 1}로 이해할 수 있습니다.
- ^ : 문자열 또는 줄의 시작을 알립니다. 하지만 대괄호 [ ] 안의 맨 앞에 쓰이게 되면 부정(not)의 의미를 갖게 됩니다.
- $ : 반대로 문자열 또는 줄의 끝임을 알립니다.
- | : 또는(or)의 뜻을 가집니다.
- [ ] : 대괄호 안의 문자를 의미합니다. 문자를 독립적으로 사용할 수도 있지만 a-z 또는 A-Z 처럼 구간을 설정할 수도 있습니다.
- ( ) : 문자 그룹을 정의하여 괄호 내 쌍이 그룹을 형성합니다.
- \d : 0-9의 숫자를 의미합니다. 의미 그대로 0-9로 대체할 수 있습니다.
- \w : 모든 글자, 숫자, 밑줄을 의미합니다.
- \s : 공백을 의미합니다.
의미가 잘 와닿지 않는 것들이 있을 겁니다..!
저는 특히 ^, [ ], ( ) 이 기호들이 가장 이해되지 않았는데요, 실제 예시를 보면서 공부해보겠습니다.
complie / match / search / sub
파이썬에서 정규표현식을 사용하기 위해서는 re 라는 모듈을 불러와야 합니다.
그리고 위 세 함수를 이용하여 문자열이 특정 조건을 만족하는지 확인하거나 문자열의 일부를 교체할 수 있습니다.
import re
p = re.compile('[a-z]')
m = p.match("python")
m
<re.Match object; span=(0, 1), match='p'>
p라는 객체에 complie 함수를 사용하여 정규표현식을 저장합니다.
이후 match 함수를 이용하면 "python" 이라는 문자열에 대해 p에 저장된 정규식과 일치하는 문자를 찾아줍니다.
반환되는 값은 '인덱스로 표현된 구간'과 '해당하는 문자'입니다.
당연히 p,y,t,h,o,n 모두 a-z에 해당하는 문자이지만 match를 사용하면 앞에서부터 탐색하여 '딱 한 개만 반환'하는 것을 알 수 있습니다.
import re
word = "python"
m = re.match('[a-z]+',word)
m
<re.Match object; span=(0, 6), match='python'>
이번에는 complie을 사용하지 않고 match만 사용해봤습니다.
이 경우 re.match로 함수를 사용했으며 (정규식, 문자열) 의 형태로 인자를 준 것을 알 수 있습니다.
기존의 방식과 큰 차이는 없지만 이 방식은 일회성이라는 것을 알아둬야 합니다. 🥲
즉, complie을 특정 객체에 저장하면 여러 문자열에 대해 반복적으로 사용할 수 있지만, match만 사용하면 특정 문자열에 대해 한 번만 사용할 수 있는 것입니다.
추가로 정규표현식을 보면, [ ] 뒤에 + 가 붙어있습니다.
이 +는 '한 번 이상 반복됨'을 의미하므로 [a-z]에 해당하는 문자가 여러 번 반복된 것을 전부 match한 것을 알 수 있습니다.
만약 문자열이 "python"이 아니라 "python is good" 이었다면, 공백 문자는 [a-z]에 해당하지 않으므로 동일한 결과를 반환할 것입니다.
import re
# complie x
sentence = "Python is Good."
m = re.sub('[a-z]','@',sentence)
m
'P@@@@@ @@ G@@@.'
# compile o
p = re.complie('[a-z]')
m = p.sub('!',sentence)
m
'P!!!!! !! G!!!.'
이번에는 re.sub 함수를 사용해봤습니다.
보시는 것처럼 위에서는 compile을 사용하지 않았고 아래에서는 complie을 사용했습니다.
여기서도 마찬가지로 compile을 사용하면 이후에 같은 과정을 여러 문자열에 대해 적용할 수 있습니다.
sub 함수는 '정규식 패턴'을 '특정 문자열'로 '모두' 변경할 수 있도록 해줍니다.
첫 번째 예시에서는 [a-z]에 해당하는 문자열, 즉 소문자를 모두 @로 바꿔주었습니다.
그래서 대문자인 P,G와 알파벳에 해당하지 않는 .이 그대로 남아있고 나머지는 모두 @가 된 것을 알 수 있습니다.
[a-z]에 해당하는 문자열을 !로 바꾼 아래의 예시도 이해하실 수 있겠죠? 😄
📖 파이썬에서 정규표현식을 사용하려면 import re 를 입력하자!
📖 compile을 사용하면 객체에 특정 정규표현식을 저장하여 여러 번 이용할 수 있다.
📖 match는 문자열에서 정규식에 해당하는 것을 딱 하나 찾아준다.
📖 sub는 문자열에서 정규식에 해당하는 내용들을 모두 바꿔준다.
자, 이제 기초적인 내용들을 다루어 살펴봤으니 실제 어떤 식으로 구현되는지, 그리고 각 정규표현식이 무엇을 의미하는지 살펴보겠습니다.
비밀번호 패턴
요즘 비밀번호들은 대부분 '10자 이상, 특수문자 1개 이상, 대문자 1개 이상'의 조건을 만족시키는 것으로 설정하게 되어있습니다.
이런 패턴을 만족시키는 정규표현식은 어떻게 될까요?
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,30}$"
상당히 길고 복잡합니다 😱
하나씩 살펴보도록 하겠습니다.
- ^ : 문자열의 시작을 알리는 기호
- 지금은 ^ 기호가 " " 안쪽 가장 맨 앞에 위치하고 있죠. 따라서 문자열이 시작되었음을 알립니다. 뒤의 패턴을 보겠습니다.
- ?= : 전방 탐색
- 전방 탐색은 말 그대로 '앞 쪽을 탐색'하는 것을 의미합니다.
그렇다면 무엇을 기준으로 앞과 뒤를 말하는 걸까요? - '전방 탐색 기호 뒤에 오는 패턴을 기준'으로 앞에 오는 문자열을 반환해줍니다.
- 전방 탐색은 말 그대로 '앞 쪽을 탐색'하는 것을 의미합니다.
- .* : 모든 문자를 0번 이상 반복
- 어떤 문자든지 0번 이상 반복되면 해당 문자열을 찾아주게 됩니다. 즉, 없어도 된다는 뜻입니다 😅
- [a-z] : 영어 소문자 a부터 영어 소문자 z까지의 모든 문자
- 여기서는 +/* 와 같은 기호가 없기 때문에 글자 한 개를 의미합니다.
여기까지 괄호 ( ) 안의 내용을 정리해보겠습니다.
[a-z]에 해당하는 글자 한 개가 존재하고, 그 앞에 어떤 문자든지 0개 이상(.*) 존재해야 한다는 것,
즉, 문자가 있어도 되고 없어도 된다는 것을 의미합니다.
따라서 영어 소문자 한 개, 혹은 다른 문자들과 결합된 영어 소문자 한 개를 기준으로 그 앞의 문자열을 반환하게 됩니다.
❗️ 만약 영어 소문자가 한 개라도 존재하지 않으면 이 패턴을 만족시킬 수 없겠죠?
바로 뒤의 괄호 ( ) 안에는 [A-Z] 말고 다른 것이 없기 때문에 스킵하고 넘어갑니다.
- \d : 숫자 한 개
- 0-9의 숫자 중 하나가 존재하는지 확인하게 됩니다.
위에 적어둔 것처럼 [0-9] 를 대신 사용할 수도 있습니다.
- 0-9의 숫자 중 하나가 존재하는지 확인하게 됩니다.
- [!@#$%^&*] : 특수문자 중 한 개
- 대괄호 [ ] 안에 적힌 문자 중 한개를 뜻합니다.
이런 식으로 여러 개의 문자를 이어서 나열하면 이 '글자들 중 하나'를 의미합니다.
- 대괄호 [ ] 안에 적힌 문자 중 한개를 뜻합니다.
따라서 지금까지의 소괄호 네 개를 살펴본 결과,
위 정규표현식은 특정 문자열에 '최소 한 개 이상의 영어 소문자, 영어 대문자, 숫자, 특수문자'가 존재하는지 확인하는 의미를 지닌다는 것을 알 수 있습니다.
지금까지 최소 조건을 만족하는지 확인하는 정규표현식을 살펴보았습니다.
남은 것은 연속된 괄호들을 제외한 [A-Za-z\d!@#$%^&*]{10,30}$ 입니다.
- $ : 문자열의 끝을 알리는 기호
- ^와 정반대의 의미를 갖습니다. 우리는 이 정규표현식이 ^(시작)과 $(끝) 사이에 어떤 패턴을 지니는지 확인하고 있는 것입니다.
- [A-Za-z\d!@#$%^&*]
- 대괄호 안에서 구간을 설정할 수 있고, 여러 개의 문자들이 나열된 경우 그 중 하나라는 것을 위에서 배웠습니다.
따라서 [영어 대/소문자, 숫자, 특수문자] 중 1개를 의미하게 됩니다. - 여기서의 ^, $ 는 문자열의 시작과 끝이 아니라는 점, 아시죠? 😋
- 맨 마지막 *에 유의하세요! 이전에는 * 기호가 '0번 이상의 반복'을 의미했지만 여기서는 기호 자체를 나타냅니다.
- 대괄호 안에서 구간을 설정할 수 있고, 여러 개의 문자들이 나열된 경우 그 중 하나라는 것을 위에서 배웠습니다.
- {10, 30} : 10개 이상 30개 미만
- 우리의 비밀번호는 10자 이상으로 구성되어야 합니다. 최대 길이는 29까지라고 가정하면 이처럼 표현할 수 있습니다.
- 따라서 문자열 전체 길이가 10 이상 30 미만으로 구성된다는 조건임을 알 수 있습니다.
📝 지금까지의 내용을 종합하면 다음과 같습니다.
1. 문자열의 길이는 10 이상 30 미만이다.
2. 적어도 한 개의 소문자가 사용된다.
3. 적어도 한 개의 대문자가 사용된다.
4. 적어도 한 개의 숫자가 사용된다.
5. 적어도 한 개의 특수문자가 사용된다.
이 중 2-5번은 소괄호 ( ) 안에서 확인하는 조건으로 구현되고, 1번은 중괄호 { } 로 구현되는 것을 알 수 있었습니다.
HTML 태그 제거
크롤링한 데이터를 살펴보면 html 태그가 그대로 존재하기 때문에 이를 바로 활용하기 어렵습니다.
따라서 < > 이렇게 생긴 html 태그와, 반드시 필요한 정보가 아닌 소괄호 ( ) 를 삭제하는 정규표현식을 알아봅니다.
여기서 page라는 변수는 list라는 점이 전제되어 있습니다.
# 1줄 코딩
sents = [re.sub(r'\(.*?\)', "", (re.sub(r'\<.*?\>', "", sent))).strip() for sent in page]
# 쌩구현
sent = re.sub(r"<[^>]+>","",sent).strip() # 태그 제거
sent = re.sub(r"\([^\)]+\)","",sent).strip() # 괄호 제거
1줄 짜리는 조금 복잡하니 아래 쌩구현부터 이해해보겠습니다.
여기서는 re.sub 함수를 사용하고 있습니다.
이 함수는 ( 찾을 패턴, 바꿀 패턴, 탐색 대상 ) 을 인자로 받습니다.
어떤 문자열이 sent라는 변수에 저장되어 있을 때 어떻게 변화할지 생각해보죠.
❗️ 찾을 패턴을 나타내는 정규표현식 앞에 r 이라는 글자가 붙어 있습니다.
이는 정규표현식임을 나타내주는 표지로 이해하시면 됩니다.
- <[^>]+> : < 글자 여러 개 > 의 패턴을 의미합니다.
- 순서대로 살펴보면 <, [^>]+, > 세 가지로 구성되어 있는 것을 알 수 있습니다.
<, > 는 문자 그대로 사용됩니다. 즉 꺾쇠로 열고 닫히는 패턴을 찾으라는 것이죠. - 중간에 들어가는 대괄호는 '> 기호가 아닌 어떤 문자'를 뜻하고 + 기호를 통해 이것이 한 개 이상 반복된다는 것을 의미합니다.
그러니까 닫는 꺾쇠가 나오기 전에 어떤 글자들이 존재해야 한다는 뜻이죠.
html 태그는 보통 <b>, </b>, <head> 이런 식으로 사용되기 때문에 꺾쇠의 시작과 끝을 포함한 태그 덩어리 자체를 패턴으로 인식하게 됩니다.
- 순서대로 살펴보면 <, [^>]+, > 세 가지로 구성되어 있는 것을 알 수 있습니다.
- "" : 삭제를 의미합니다.
- 이 자체로는 문자열의 '존재'만 뜻하게 됩니다.
따라서 위의 패턴을 이 존재로 바꾸게 되면 사실상 위 패턴을 만족시키는 문자열을 없애버릴 수 있게 됩니다.
- 이 자체로는 문자열의 '존재'만 뜻하게 됩니다.
- sent : 어떤 문자열이 저장된 sent라는 변수입니다.
- 여기서는 예시로 든 변수이므로 "<p><b>자연어 처리</b>" 라는 문자열이 저장되어 있다고 가정해봅니다.
그러면 <p>, <b>, </b> 는 패턴을 만족하는 문자열들이므로 모두 사라지게 됩니다.
따라서 "자연어 처리"만 남습니다.
- 여기서는 예시로 든 변수이므로 "<p><b>자연어 처리</b>" 라는 문자열이 저장되어 있다고 가정해봅니다.
- \([^\)]+\) : ( 글자 여러 개 ) 의 패턴을 의미합니다.
- 이번에는 소괄호 입니다.
소괄호는 '비밀번호 패턴'에 관한 정규표현식을 다룰 때 하나의 묶음으로 사용되는 것을 본 적이 있습니다.
여기서는 소괄호 (, )를 자체 기호로 사용할 수 있도록 역슬래시 \ 를 붙여줍니다.
따라서 (, ) 두 개의 괄호로 둘러 쌓인 구조임을 알 수 있습니다. - 중간에 들어가는 내용은 대괄호 때와 동일합니다.
닫는 괄호가 아닌 다른 문자들이 한 개 이상 존재한다는 뜻이죠.
예를 들어 (chanmuzi 기자) 라는 문자열이 존재한다고 상상해봅시다.
그러면 괄호 시작 '(', 한 개 이상의 문자 'chanmuzi 기자', 괄호 끝 ')' 의 괄호 패턴이 만족되는 것입니다.
- 이번에는 소괄호 입니다.
re.sub 함수를 두 차례 적용함으로써 sent라는 변수에 적용된 문자열에 변화를 줄 수 있습니다.
1. <문자열> 를 제거한다.
2. (문자열) 를 제거한다.
여기서는 이 두 개의 과정을 통해 문자열을 변화시켰습니다.
이번엔 1줄 짜리 코드를 볼까요?
sents = [re.sub(r'\(.*?\)', "", (re.sub(r'\<.*?\>', "", sent))).strip() for sent in page]
보기만 해도 어지러운 이 코드는 파이썬의 list comprehension 기법을 이용하고 있습니다.
의미를 풀어서 설명해보겠습니다.
📖 page라는 list에 존재하는 원소들을 sent라는 변수로 불러옵니다.
📖 각각의 sent에 대해 re.sub(r'\<.*?\>', "", sent) 함수를 적용합니다. # 태그 제거
📖 이 결과물에 re.sub(r'\(.*?\)', "", 결과물) 함수를 한 번 더 적용합니다. # 괄호 제거
이해가 되지 않는 분들을 위해 알파벳으로 대체하여 다시 표현해보겠습니다.
이때 re.sub는 ( 찾을 패턴, 바꿀 패턴, 탐색 대상) 을 인자로 받는다는 것을 잊으시면 안됩니다 😗
re.sub( 찾을 패턴1, "", 탐색 대상1)
탐색 대상 1 = re.sub(찾을 패턴2, "", 탐색 대상2)
즉, 패턴2, 대상2를 통해 먼저 처리한 결과를 탐색 대상 1로 취급하여 패턴 1을 통해 한 번 더 sub함수를 적용해주는 것입니다.
그러면 각각의 패턴이 무엇을 의미하는지 살펴보도록 하겠습니다.
- \<.*?\>
- 괄호 패턴은 동일합니다. \<, \> 를 보면 꺽쇠 < > 로 닫히는 구조임을 알 수 있습니다.
- .*? : 이건 조금 생소하네요. 이는 최소 크기 매칭을 의미합니다.
이전에 .* 는 어떤 문자든지 0번 이상 반복됨을 의미한다고 했었죠.
* 와 ? 가 동시에 쓰이면 이 패턴을 만족시키는 최소한의 문자열을 찾아주게 됩니다.
따라서 빈 문자열 혹은 한 글자 이상으로 구성된 문자열 전부를 의미하게 됩니다. - 두 내용을 조합하면 '( 문자열 )' 패턴을 ""으로 대체하겠다는 의미가 됩니다.
- \(.*?\)
- 괄호 패턴이 소괄호 ( ) 로 바뀐 것 말고는 동일합니다.
- 따라서 위의 태그 제거를 먼저 수행한 뒤 괄호 제거를 진행한다는 것을 알 수 있습니다.
📝 HTML 태그를 제거하는 방식에 대해 공부한 내용을 정리하겠습니다.
1. 기존 문자열에 존재하는 어떤 요소를 제거하고 싶을 때는 re.sub 함수를 사용할 수 있습니다.
2. 이 함수는 ( 찾을 패턴, 바꿀 패턴, 탐색 대상 ) 을 인자로 받습니다.
3. *? 는 최소 매칭을 의미합니다. 따라서 이 *를 통해 구현되는 패턴을 최소한으로 매칭해줍니다.
4. list comprehension 기법을 통해 여러 개의 함수를 적용한 결과를 한 줄로 표현할 수 있습니다.
정규표현식은 쓰기 나름에 따라 활용도가 천차만별인 것 같습니다..! (저는 아직 잘 못쓰겠어요 😂)
위에서 다룬 것들 말고도 엄청나게 많은 패턴들을 볼 수 있는데요, 사실 정규식이 무엇을 의미하는지 모르는 상태에서 무지성으로 복붙을 하는 것은 제 성장에 도움이 되지 않는 것 같아 천천히 정리해보았습니다.
혹시 제가 잘못 해석을 했거나 잘못 알고 있는 내용들이 있으면 댓글로 남겨주세요 🙇♂️
바로바로 수정하도록 하겠습니다.