Py) TM - 정규표현식-06(괄호)

Py) TM - 정규표현식-06(괄호)

Pandas 객체 기반 정규표현식을 활용해서 여러 종류의 괄호와 괄호 내부의 문자열을 처리하는 여러 방법에 대해 알아본다.


개요

텍스트에서 괄호는 여러 용도로 사용된다. 특히 특정 축약어의 원어 설명을 위해 사용되거나 부연설명을 위해 사용되는 경우가 많다. 이 경우에는 괄호 내부의 내용이 중복이 되기 때문에 그대로 분석에 사용되면 특정 단어의 빈도수와 영향력이 과대평가될 수 있다. 그래서 괄호를 포함하여 그 내용까지 일괄 삭제하는 것이 필요하다.

다른 일반 문자와 다르게 괄호는 정규표현식에서 특수한 기능을 하는 메타문자로 사용되기 때문에 해당 사항을 인지하지 못한 채로 정규식을 작성하는 경우 원하는 결과를 얻지 못할 수 있기 때문에 본 게시물을 통하여 여러 예시를 알아보고자 한다.

이론

소괄호(Parentheses)

소괄호는 정규식에서 두 개 이상의 조건을 묶을 때도 사용되고 .str.extract() 메서드에서 그룹을 지정할 때 사용되기 때문에 소괄호 앞에 역슬래시를 추가해주어야 plain text로 온전하게 소괄호를 매칭할 수 있다.

중괄호(Braces)

텍스트에서 자주 사용되는 괄호는 아니지만 간혹 “json” 형식의 데이터를 다루는 경우에 접할 수 있으며 이를 위한 정규표현식은 소괄호와 유사하게 작성할 수 있다.

대괄호(Brackets)

대괄호는 뉴스 기사 또는 유튜브 제목 등 문장 앞에 위치한 경우가 많으며 해당 괄호 내부의 텍스트를 제거하기도 하지만 추출이 필요한 경우도 종종 있다. 그리고 대괄호는 소괄호처럼 범위지정 등 정규표현식에서 특수한 기능이 있기 때문에 plain text를 매칭하고자 한다면 소괄호의 경우처럼 괄호 앞에 역슬래시를 추가해주어야 한다.

꺾쇠 괄호(Angle Brackets)

대부분 HTML태그를 통해 접하는 괄호이며 이 괄호는 정규표현식에서 특수한 기능을 하지 않기 때문에 역슬래시를 사용하지 않고도 매칭이 가능하다.

홑낫표 「 」, 겹낫표 『 』

텍스트에서 드물게 보이는 괄호이며 꺾쇠 괄호처럼 특수한 기능을 하지 않기 때문에 역슬래시를 사용하지 않고도 매칭이 가능하다.

탐욕/비탐욕 매칭

정규표현식에서 * 또는 +를 사용하여 매칭을 하고자 할 때 기본적으로 탐욕적(greedy)으로 매칭을 하게 되어 있어서 최대한 많은 문자열을 매칭하려고 한다. 여기에서 최대한 많은 문자열이 아닌 최대한 적은(짧은) 문자를 매칭하고자 할 때는 비탐욕적(non-greedy, lazy) 매칭이 필요하다. 그래서 탐욕적 매칭을 비탐욕적으로 바꾸기 위해서는 물음표 기호를 뒤에 추가하여 *? 또는 +?를 사용하면 된다.

실습

이론 부분에서 언급한 내용을 실습하기 위해 다음과 같이 라이브러리와 데이터를 준비한다.

1
2
3
4
5
import pandas as pd

ser1 = pd.Series(["abc(1)", "abc(1)def(456)", "123{a}456", "123{a1}456{def}"])
ser2 = pd.Series(["abc[1]", "abc[1]def[456]", "123<a>456", "123<a1>456<def>"])
ser3 = pd.Series(["<body><h1>abcd</h1></body>", "가나다(123)마바사(456)"])

괄호 패턴

먼저 “ser1” 객체를 사용하여 괄호를 제거하는 여러 방법을 알아본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
ser1.str.replace("([0-9])", "", regex = True) # 1
## 0 abc()
## 1 abc()def()
## 2 {a}
## 3 {a}{def}
## dtype: object

ser1.str.replace("\([0-9]\)", "", regex = True) # 2
## 0 abc
## 1 abcdef(456)
## 2 123{a}456
## 3 123{a1}456{def}
## dtype: object

ser1.str.replace("\([0-9]{2,}\)", "", regex = True) # 3
## 0 abc(1)
## 1 abc(1)def
## 2 123{a}456
## 3 123{a1}456{def}
## dtype: object

ser1.str.replace("{[0-9]}", "", regex = True) # 4
## 0 abc(1)
## 1 abc(1)def(456)
## 2 123{a}456
## 3 123{a1}456{def}
## dtype: object

ser1.str.replace("{[0-9a-z]{2,}}", "", regex = True) # 5
## 0 abc(1)
## 1 abc(1)def(456)
## 2 123{a}456
## 3 123456
## dtype: object

1번 코드의 경우 정규식에 사용된 소괄호가 전혀 의미가 없다. 그래서 “[0-9]“ 패턴만 매칭이 되어서 숫자만 제거된다. 반면 2번 코드는 소괄호 앞에 역슬래시를 추가하였기 때문에 plain text로 처리가 되어 문자열에 있는 소괄호와 제대로 매칭이 되고 소괄호 내부에 숫자가 1개 있는 경우가 처리된다. 그리고 3번 코드는 소괄호 내부에 숫자 2개 이상이 있는 경우가 매칭이 되기 때문에 “(1)”은 그대로 남아있는 것을 알 수 있다.

4번 코드는 중괄호를 처리하기 위해 정규식을 사용한 예시이나 중괄호 내부에 숫자 1개가 있는 문자열이 없기 때문에 아무런 변화가 없다. 그리고 5번의 코드는 중괄호 내부에 숫자 또는 영문 소문자가 2개 이상 있는 경우가 매칭되기 때문에 “{a}”를 제외한 나머지 모든 중괄호 패턴이 제거된 것을 알 수 있다.

이번엔 “str2” 객체를 대상으로 처리를 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ser2.str.replace("[[0-9]]", "", regex = True) # 1
## 0 abc[
## 1 abc[def[45
## 2 123<a>456
## 3 123<a1>456<def>
## dtype: object

ser2.str.replace("\[[0-9]\]", "", regex = True) # 2
## 0 abc
## 1 abcdef[456]
## 2 123<a>456
## 3 123<a1>456<def>
## dtype: object

ser2.str.replace("\[[0-9]{2,}\]", "", regex = True) # 3
## 0 abc[1]
## 1 abc[1]def
## 2 123<a>456
## 3 123<a1>456<def>
## dtype: object

ser2.str.replace("<[a-z]>", "", regex = True) # 4
## 0 abc[1]
## 1 abc[1]def[456]
## 2 123456
## 3 123<a1>456<def>
## dtype: object

ser2.str.replace("<[a-z]{1,}>", "", regex = True) # 5
## 0 abc[1]
## 1 abc[1]def[456]
## 2 123456
## 3 123<a1>456
## dtype: object

ser2.str.replace("<[a-z0-9]{1,}>", "", regex = True) # 6
## 0 abc[1]
## 1 abc[1]def[456]
## 2 123456
## 3 123456
## dtype: object

1번 코드는 그 동작을 이해하기 어려울 수 있는데 대괄호 내부에 “[0-9]”가 0~9 숫자로 전체 숫자가 제거되지 않고, “[0-9]]“로 패턴이 매칭되어 숫자 1개 이후 닫히는 대괄호 하나가 이어지는 패턴만 매칭되어 처리된다. 그래서 2번 코드에서 정규식의 바깥 대괄호 각각 앞에 역슬래시를 추가하여 문자열의 plain text인 대괄호와 그 내부의 문자열이 매칭이 제대로 되도록 하였다. 그리고 3번 코드는 대괄호 내부에 숫자 2개 이상이 있는 경우가 매칭되기 때문에 “[1]”은 그대로 남아있는 것을 알 수 있다.

4~6번 코드는 대괄호와 꺾쇠 괄호를 처리하기 위한 정규식을 사용한 예시이다. 4번 코드는 꺾쇠 괄호 내부에 영문 소문자 1개가 매칭되어 처리된다. 그리고 5번 코드는 꺾쇠 괄호 내부에 영문 소문자 1개 이상인 경우에 매칭되어 처리된다. 마지막으로 6번 코드는 꺾쇠 괄호 내부에 영문 소문자 또는 숫자가 1개 이상 있는 경우가 매칭되어 처리된다.

그리고 “ser2” 객체에 대하여 추출을 위한 코드를 작성해보자. str.extract() 메서드를 사용하면 정규식 패턴을 그룹으로 지정하여 추출할 수 있는데 이 때 그룹은 소괄호로 지정한다.

1
ser2.str.extract("(\[[0-9]\])")
0
0 [1]
1 [1]
2 NaN
3 NaN

결과는 데이터프레임으로 반환되는데 그룹지정 위해 소괄호 쌍을 1개만 사용했기 때문에 열이 1개인 데이터프레임이 반환된다. 그리고 그룹이 매칭되지 않은 경우에는 결측(NaN)으로 처리된다. 그리고 다음과 같이 두 개 이상의 그룹을 지정할 수도 있다.

1
ser2.str.extract("(\[[0-9]\]).*?(\[[0-9]{2,}\])")
0 1
0 NaN NaN
1 [1] [456]
2 NaN NaN
3 NaN NaN

첫 번째 그룹은 이전 코드와 같이 대괄호 내부에 숫자가 1개인 것, 그리고 두 번째 그룹은 대괄호 내부에 숫자가 2개 이상인 패턴을 매칭하도록 지정하였다. 그리고 두 개 이상의 그룹을 지정할 때는 데이터프레임의 열이 2개로 반환된 것을 볼 수 있다.

탐욕/비탐욕 매칭

이번에는 “ser3” 객체를 대상으로 처리를 해보자. 정규표현식에 물음표 포함 여부에 따라서 탐욕과 비탐욕으로 나뉘어지니 이 부분을 잘 확인해보자.

1
2
3
4
ser3.str.replace("<.*>", "", regex = True)
## 0
## 1 가나다(123)마바사(456)
## dtype: object

위 코드는 꺾쇠 괄호 사이에 있는 모든 문자열을 제거하도록 정규식을 작성한 것이다. 그러나 탐욕적 매칭으로 인해 첫 번째 문자열은 모두 제거가 된 것을 볼 수 있다. 이번에는 비탐욕적 매칭을 시도해보자.

1
2
3
4
ser3.str.replace("<.*?>", "", regex = True)
## 0 abcd
## 1 가나다(123)마바사(456)
## dtype: object

여기서 사용된 정규식은 특히 HTML 또는 XML 문서를 처리할 때 많이 사용되는 패턴이다. 꺾쇠 괄호 사이의 모든 문자열을 제거하도록 정규식을 작성한 것으로 비탐욕적 매칭을 사용하였기 꺾쇠와 꺾쇠 사이의 문자열(ex. 태그, 속성 등)이 모두 제거되고 기존 텍스트 “abcd”만 남은 것을 볼 수 있다.

이번에는 소괄호에 대해서 탐욕적 매칭과 비탐욕적 매칭을 비교해보자.

1
2
3
4
5
6
7
8
9
ser3.str.replace("\(.*\)", "", regex = True) # 1
## 0 <body><h1>abcd</h1></body>
## 1 가나다
## dtype: object

ser3.str.replace("\(.*?\)", "", regex = True) # 2
## 0 <body><h1>abcd</h1></body>
## 1 가나다마바사
## dtype: object

기존 텍스트 “가나다(123)마바사(456)”에서 탐욕적 매칭인 1번 코드의 경우 “(123)마바사(456)”이 매칭되어 제거되고 비탐욕적 매칭인 2번 코드의 경우 “(123)”과 “(456)”이 각각 매칭되어 제거되는 것을 볼 수 있다.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×