Post

(Regex) Python

항상 시작 ^와 끝나는 $를 써주는게 좋다!!! 특히 $ 안붙여서 이어지는 문자가 있어도 match로 뜨는 경우가 있다. 정규표현식은 기본적으로 greedy하다는 것을 항상 명심한다. 긴 문자열을 집어 넣을 때, (page html tag 파싱이라던가.) 반드시 greedy/non-greedy를 신경써주어야 한다.

1
2
3
4
5
JS에서는 $를 사용하지만, python에서는 \를 사용한다. notepad++에서는 둘 다 지원.
$&               : 일치하는 문자열
$`               : 일치하는 부분의 앞부분 문자열
$'               : 일치하는 부분의 뒷부분 문자열
$1, $2, $3... $n : n번 서브그룹

Python regular expression

http://devdocs.io/python~3.6/library/re raw string은 기본으로 사용하는게 좋다. "\n"은 개행문자 1개로 인식되는 반면 r"\n"은 \와 n 두 char로 인식된다.

정규식 컴파일

일반적으로 미리 컴파일 된 코드 객체(stored)를 사용하는 것이 문자열을 쓰는 것(inline)보다 더 빠르다. ( 그래서 eval(), exec()를 사용할 때도 문자열을 넘기는 것 보다 코드 객체를 넘기는게 더 빠르다. ) 패턴 매칭을 수행하려면 정규식 패턴이 정규식 객체로 컴파일 되어야 한다. 따라서 여러번 사용해야 하는 정규식이라면 re.compile()을 사용해 미리 컴파일하여 정규식 객체를 반환받고 이를 사용하는 것이 좋다. re.search() 같이 그냥 사용하면 내부적으로 패턴이 컴파일 된 다음 매치한다. re 모듈은 같은 이름의 함수를 메소드로도 제공한다. 그래서 객체를 생성해 메소드를 사용하든, re 모듈의 함수를 사용하든 같은 이름으로 사용할 수 있다.

search - match 쓰지 말고 search를 쓰자.

전체 문자열에서 처음으로 매치하는 문자열을 찾기 위해서는 search()를 사용한다. match()는 매치여부를 문자열 첫번째 캐릭터부터 검사하기 때문에 search()^붙인거라고 생각하면 된다. 그래서 그냥 search()를 쓰는 편이 좋다.

1
2
3
4
5
6
7
8
9
import re
po = re.compile(regex[, option]) #정규식 객체를 리턴
m = po.search(검사 문자열)
if m:
    print(m.group())
else:
    print("no match")
#이렇게 쓸 수도 있다.
m = re.search(regex, 검사 문자열)

패턴 매칭된 문자열은 m.group(subgroup))으로 얻는다. 메소드이기 때문에 group[index]이 아니라 group(index)라는 것 주의.

replace

replace함수는 sub()다.

1
2
sub(patten, string)
subn()

findall, finditer

전체 문자열에서 매치하는 모든 문자열을 찾기 위해서는 findall()이나 finditer()을 사용한다. findall()은 list하나를 리턴하고, finditer()은 match객체를 리턴한다. match객체의 경우 문자열의 어느 부분에서 매치되었는지를 나타내는 인덱스 등 조금 더 자세한 정보가 담겨있다.

capturing group 검색 등은 finditer를 사용해야 제대로 동작한다.

또한, findall보다는 finditer가 메모리를 좀 더 효율적으로 사용한다. po = re.compile(r"h[^a]\*y")findall(), finditer() 둘 다 제대로 동작한다. 그러나 po = re.compile(r"h([^a])\*y")findall()하면 아무것도 안나온다. finditer()를 사용해야한다.

따라서 리스트를 반환받아야 하는 상황이 아니면 finditer()쪽이 더 낫다.

1
2
3
4
s3 = po.finditer("asd dfew wt wet a  abd qwe cat pow law lamw")
print(next(s3).group())
print(next(s3).group())
...

capturing group

(...) : 둘러싸고 있는 정규식을 매치시켜 서브그룹으로 저장 \N : N번 서브그룹(…)과 매치

“ABCABCABC…반복 DDD”에서 ABCABCABC…반복을 검색하고 싶다면 (ABC)+를 사용할텐데, 위에 적어놓았듯 findall()하면 ABC만 인식된다. 이런 경우 finditer()을 사용해야 한다.

()로 묶은 부분은 group(index)로 접근할 수 있다. index 0은 문자열 전체, 1부터 첫번째 ()그룹, 두번째 ()그룹 순으로 나간다.

이를 사용하면 첫번째 일치한 문자열과 동일한 문자열을 뒤에서 매치할 수 있다. 다음과 같이 사용하면 연속된 두개의 문자열이 매치된다.

1
2
3
po = re.compile(r"\b(\w+)\s+\1")
m = po.search("life is is wonderful")
결과 : is is
단독 그룹을 결과 문자열에 포함 시키고 싶지 않은 경우 Non-capturing group
1
(?:...)

A non-capturing version of regular parentheses. Matches whatever regular expression is inside the parentheses, but the substring matched by the group cannot be retrieved after performing a match or referenced later in the pattern.

lookahead / lookbehind

검색은 하고싶은데 결과 문자열에 포함시키고 싶지는 않은 경우. 전방 탐색(lookahead)은 앞에서 부터 찾아 나가고, 후방 탐색(lookbehind)은 텍스트를 반환하기 전에 뒤쪽을 탐색한다. 즉, 전방 탐색은 일치시킬 문자열이 앞에 위치시켜야 하고 후방 탐색은 일치시킬 문자열을 뒤에 위치시켜야 한다.

1
2
3
4
positive lookahead  : 일치문자열(?=...)
negative lookahead  : 일치문자열(?!...)
positive lookbehind : (?<=...)일치문자열
negative lookbehind : (?<!...)일치문자열
1
2
3
4
polookahead = re.compile("\w+(?=:)")
s2 = polookahead.search("https://naver.com/")
print(s2.group())
결과 : https
특정 문자열 포함하지 않은 라인만 선택하기 (not 연산 / 제외)
1
^((?!특정단어).)*$

negative lookahead 활용

Non-Greedy (*|+|?|{})?

Note ) 정규표현식은 기본적으로 greedy하다. 즉, 패턴을 가능한 가장 긴 부분 문자열과 매치한 결과를 반환한다.

와일드카드를 사용하는 경우 정규식은 왼쪽에서 오른쪽으로 계산되면서 패턴과 매치되는 가능한 한 많은 개수의 문자들을 잡아낸다.

이를 Non-Greedy로 만드려면 뒤에 ?를 붙여준다.

1
2
3
12345pt67890pt12
.*pt로 매칭하는 경우 : 12345pt67890pt
.*?pt로 매칭하는 경우 : 12345pt

e.g.

tasklist를 파싱한다고 가정하자.

1
2
3
System Idle Process              0 Services                   0         12 K
System                           4 Services                   0        292 K
smss.exe                       244 Services                   0        884 K

앞에서부터 문자열 매치를 사용할 수도 있겠지만, 그냥 [^\s]로 사용하는 편이 간단할 것 같다. ^은 무조건 []와 함께 써야 부정이다. ()와 함께 사용하면 문자열 시작을 의미하는 ^로 해석한다. 그러나 이렇게 사용하면 하나의 공백도 포함하지 않기 때문에 System Idle Process같은 문자열이 잘려버린다. 두개 이상의 공백만 매치하지 않도록 하는 것은 다음과 같이 사용한다

#1
1
po = re.compile(r"([^\s]|\s[^\s])+")

이 경우 꼭 finditer를 사용해야 한다. 어떤 문자열에서 abc를 포함하지 않는 문자열을 얻고 싶은 경우 ([^a]|a[^b]|ab[^c])+와 같이 사용할 수 있다. | 연산자 는 하나만 참이어도 매치된다는 점을 이용한 패턴이다. as…의 경우, a[^b]와 매치되므로 매치된다. 공백도 마찬가지다. 공백 하나만 있는 경우 \s[^\s]에서 참이되어 넘어간다.

#2

부정을 빼고 \s\s+으로 split()을 사용해도 된다.

#3

non-greedy ?(?:...)를 사용한다. (?:...)을 사용하면 매치는 시키지만 결과에는 포함하지 않을 수 있다. 이렇게 일반화된 방법을 사용하면 PID와 세션 이름 사이에 공백이 하나라서 붙어서 출력되버린다. 그래서 결국 제대로 읽으려면 앞에서 부터 정규식 매치를 사용해야 한다.

1
po = re.compile(r'(.+?)(?:\s\s+)(\d+)(?:.+?\d+\s+)([\d, ]+ K)')

서브그룹으로 이름, PID, 크기에 접근할 수 있다. match, search와 findall, finditer를 모두 사용할 수 있지만 패턴 자체가 한 line 전체와 매치되도록 되어 있으니 line을 하나씩 받아와서 각각 매치시키는 경우 match, search를 사용하고, tasklist 출력 전체와 매치하는 경우는 finditer 등을 사용하는 것이 좋다. finditer는 문자열 내에 패턴과 매치되는 문자열을 모두 찾는 거니까 이렇게 한 line 전체와 매치되는 패턴에는 별로 적합하지 않은 것 같다. findall은 그냥 리스트로 반환받고 싶으면 쓰면 된다. 어차피 이것도 서브그룹으로 묶어주니까.

1
2
po = re.compile(r'(.+?)(?:\s\s+)(\d+)(?:.+?\d+\s+)([\d, ]+ K)')
po = re.compile(r'([\w.]+(?: [\w.]+)*)\s\s+(\d+) \w+\s\s+\d+\s\s+([\d, ]+ K)')

위는 내 정규표현식, 아래는 책에 나온 예제 정규표현식인데, 아래로 진행하면 “-“같은게 포함되어 있으면 잘린다. 예를들어 notepad++.exe는 .exe만 남는다. 다른 특수문자를 추가해주면 정상작동한다. [\w.]에서 .은 와일드카드 .이 아니라 .을 의미한다. .exe에서 .을 읽기 위해서 넣은 듯. 왜 \를 안써도 .으로 인식되는지는 모르겠다. 아무튼, non-greedy를 안쓰고 처리하려면 아래쪽 정규표현식으로 사용해야한다.

This post is licensed under CC BY 4.0 by the author.