TigerCow.Door


안녕하세요. 문범우입니다.

이번 포스팅에서는 정규표현식에서 사용되는 메타문자에 대한 총정리를 해보려 합니다.

지난 포스팅에서 기본적인 메타문자들을 정리해보았는데, 그 내용들을 포함해서 함께 정리해보도록 하겠습니다.

정규표현식에 대해 조금 더 궁금하신 분들은 아래의 포스트를 참고해주시길 바랍니다 :)


정규표현식이란? (1)


정규표현식이란? (2)



1. 백슬래시(\)


우리가 메타문자에 대해 총 정리를 하기 앞서서, 정규표현식을 사용할 때 혼란이 될 수도 있는 백슬래시(\)에 대해서 잠깐 이야기해보겠습니다.

먼저 백슬래시는 보통 자판의 엔터위에 있는 버튼을 통해 입력할 수 있습니다.


우리가 정규표현식을 이용하여 TEST라는 파일에서 "\section" 이라는, 백슬래시를 포함한 문자열을 찾으려고 한다고 생각해봅시다.

그럼 다음과 같은 정규식을 만들수 있습니다.


p = re.compile("\section")

 

하지만 위의 정규식은 백슬래시를 이용한 \s 문자가 whitespace로 해석되어 우리가 원하는 문자열을 찾지 못하게 됩니다.

그럼 어떻게 해야 할까요? 바로, 이스케이프 처리를 함으로써 백슬래시가 문자열 자체로 인식되게끔 해야 합니다.

바로 백슬래시 하나를 더 이용하는 건데요, 아래와 같이 코드를 구현하면 됩니다.


p = re.compile("\\section")


즉 위와 같이 코드를 구현하면, 파이썬 정규식 엔진에서는 파이썬의 특정 규칙에 의하여 \\ 를 \ 로 해석하게 됩니다.


* 이러한 문제는 정규식을 파이썬에서만 사용할 때 생기는 문제입니다.


그런데 만약 우리가 백슬래시 두개가 연속으로 포함된 문자열을 탐색하려면 어떻게 할까요? 위와 같은 방법으로 생각해본다면 다음과 같이 코드를 구현할 수 있을 것입니다.


p = re.compile("\\\\section")


하지만 너무 복잡하지 않나요?

이러한 문제 때문에 파이썬에서는 Raw String 이라는 것이 생겨나게 되었습니다.

이는 아래와 같이 사용할 수 있습니다.


p = re.compile(r'\\section')


이와 같이 파이썬에서는 정규식 문자열 앞에 r 을 붙여줌으로써 백슬래시 1개를 두개로 인식하게끔 할 수 있습니다.



2. 메타문자 총 정리


- []


먼저 알아볼 것은 문자 클래스 [] 입니다.

문자 클래스로 만들어지는 정규식은, [와 ]로 둘러쌓인 내부의 문자열과 매치되는 것이라는 의미를 가집니다.




또한 자주 사용되는 문자클래스는 아래와 같이 별도의 표기법으로 사용될 수 있습니다.



문자클래스 내부에서 쓰이는 ^ 는 뒤에서 알아볼 메타문자와는 다른의미인, not의 의미를 가지고 있습니다.





- . (Dot)


.(Dot) 은 줄바꿈 문자인 \n을 제외한 모든 하나의 문자와 매치되는 것을 의미합니다.





- *


*는 반복을 나타내는 메타문자로써, 해당 메타문자 앞의 글자가 0번이상 반복되는 모든 문자열과 매치됩니다.




- +


+는 *과 같은 반복을 나타내는 메타문자입니다. *는 다르게 앞의 글자가 0번을 제외한, 1번이상 반복되는 모든 문자열과 매치됩니다.


 


- {m,n}


{m,n}또한 반복을 나타내는 메타문자입니다. 위와는 다르게 반복횟수를 m과 n으로 정할 수 있습니다. 즉, 앞의 문자가 m번 이상 n번이하 반복되는 모든 문자열과 매치 됩니다.




- ?


? 는 앞의 문자가 0~1번 반복되는 모든 문자열과 매치됩니다.




- |


| 는 보통 키보드의 엔터키위에 있는 키입니다. 백슬래시로도 사용되는 키인데 shift와 함께 눌러주면 됩니다.

해당 | 메타문자는 a|b 와 같은 형식으로 사용되며 a 또는 b와 매치되는 문자열을 반환합니다.




- ^


^는 문자열의 맨처음을 의미하는 메타문자입니다. 문자클래서 내부에서 not의 의미로 사용되기도 하므로 혼동되지 않도록 주의하세요. 정규식으로 찾고자 하는 문자열의 앞에 입력합니다.





- $


$는 ^와 반대로 문자열의 맨 마지막을 의미하는 메타문자 입니다. ^와는 다르게 매치할 문자열의 뒤에 입력합니다.





- \A


\A는 기본적으로 ^와 같이 문자열의 맨 처음을 의미하는 메타문자입니다. 하지만 다른점이 있다면 우리가 re.MULTILINE이라는 컴파일 옵션을 사용했을때 ^는 라인별 문자열의 맨처음을 의미하지만 \A는 라인별이 아닌 문자열 전체에서의 맨 처음을 의미합니다.



- \Z


\Z는 기본적으로 $와 같이 문자열의 맨 뒤를 의미하는 메타문자입니다. 하지만 다른점은 \A와 같이, re.MULTILINE이라는 컴파일 옵션을 사용했을때 $는 라인별 문자열의 맨뒤를 의미하지만 \Z는 라인별이 아닌 문자열 전체에서의 맨 뒤를 의미합니다.



- \b


\b는 단어구분자로 사용됩니다. 즉, 해당 메타문자는 단순히 단어로 찾고자할 때 사용되는 메타문자입니다.




- \B


\B는 \b와 반대로 단어가 아닌 문자열을 찾는데 사용되는 메타문자입니다.





이렇게하여 많이 사용되는 메타문자들을 정리해보았습니다.

추후 메타문자에 대해서 추가적인 내용이나, 더 필요한 내용이 있다면 속해서 수정하도록 하겠습니다.

블로그 이미지

Tigercow.Door

Web Programming / Back-end / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요.

이번 포스팅에서는 파이썬에서 정규 표현식을 지원하는 re모듈, 정규식을 이용한 문자열 검색과 정규식 컴파일 수행시 가능한 옵션에 대해서 알아보도록 하겠습니다.

지난 포스팅에서도 말씀 드렸듯, 정규표현식은 단순히 파이썬에서만 사용되는 것이 아닙니다. 기본적인 정규표현식을 익혀두시면 그 쓰임새는 무궁무진하며, 단순히 프로그래밍 언어에 따라 사용법만 약간의 차이가 있을 뿐 입니다.



1. re 모듈


파이썬에서는 정규 표현식을 사용하기 위해 re(regular expression) 모듈을 제공합니다. 파이썬이 설치될 때 함께 설치되는 라이브러리로 단순히 import 하여 사용할 수 있습니다.


import re


그리고 re모듈을 이용하여 우리가 작성하는 정규표현식을 컴파일합니다.

여기서 컴파일한다는 의미를 쉽게 생각해본다면, 우리가 작성한 정규표현식을 이해하라고 명령을 내린다고 생각해 볼 수 있습니다.

예를 들어서, 아래와 같이 코드를 작성했다고 생각합니다.


1
2
import re
= re.compile('[a-z]')
cs


1번 줄에서 re 모듈을 불러 왔고, 2번 줄에서 re모듈의 compile함수를 사용합니다. 그리고 해당 함수의 인자로 정규표현식을 넣어주었습니다.

이렇게 하면 정규식을 컴파일한 '패턴'이라는 객체가 나오게 됩니다.

즉, p는 [a-z]라는 정규식을 이해한 객체, 패턴이 되는 것입니다.

그리고 우리는 해당 객체 (위에서는 p)를 이용하여 문자열 검색을 수행할 수 있습니다.



2. 문자열 검색하기


정규식을 컴파일한 패턴 객체를 이용한 문자열 검색을 하는데 총 4가지의 메서드가 제공됩니다.


 Method

목적 및 기능

 match()

 문자열의 처음부터 정규식과 매치(일지)하는지 확인한다.

 search()

 문자열 전체를 검사하여 정규식과 일치하는지 확인한다.

 findall()

 정규식과 매치되는 모든 문자열(substring)을 리스트로 반환한다.

 finditer()

 정규식과 매치되는 모든 문자열(substring)을 iterator 객체로 반환한다.


이때, match()와 search()는 정규식과 매치될 때, match 객체를 반환하고 그렇지 않을 때는 None을 반환합니다.


하나씩 코드를 보며 확인해보도록 하겠습니다.



- match()


위에서 언급했던 것 처럼, match() 함수는 문자열에 대해 처음부터 검사하여 정규식과 매치될 때 match 객체를 반환하고 그렇지 않으면 None을 반환합니다.

이때, match 객체로 반환되었다면 group() 함수를 통해 매치된 문자열을 확인할 수 있습니다.

match는 search와 다른점을 구별하기 위해서, match 는 문자열의 처음부터 검사한다는 것을 기억하시길 바랍니다.



위의 코드에서와 같이, 먼저 p 에 정규식 '[a-z]+' 을 컴파일 하였습니다.

그리고 총 3개의 테스트를 진행해보았습니다.


첫번째 테스트에서는 itsfine 이라는 문자열 모두가 정규식에 매치되어 그대로 반환되었습니다.

두번째 테스트에서는 3이라는 숫자로 시작하는 문자열이기 때문에 애초 3부터 정규식과 매치되지 않아 None을 반환하였고, None에 대해 group() 함수는 오류를 발생시킵니다.

세번째 테스트에서는 중앙에 있는 숫자 4의 위치전까지의 what 이라는 문자열이 정규식에 매치되어 그 값을 반환할 수 있었습니다.



- search()


search() 함수는 문자열 전체를 검사하여 정규식과 매치되는지 확인합니다.

match() 함수와 거의 동일하지만, match 는 문자열 처음부터 검사하고, search는 문자열 전체를 검사하는 차이가 있습니다.

이 차이가 실질적으로 어떤 차이가 있을지 코드를 통해 확인해보도록 하겠습니다.



위에서 확인해본 match() 함수에서 사용한 코드에서 단순히 검색 메소드를 search() 로 바꾸어보았습니다.

아까는 오류가 났었던 두번째 테스트에서 fail을 잘 반환해주고 있습니다.

즉, match() 함수는 문자열 처음부터 검색을 시작하다가 정규식과 매치되지 않는 문자열이 발견되면, 검색을 멈추고 지금까지 매치된 것을 반환하게 됩니다.

이와 다르게 search() 함수는 정규식과 매치되지 않는 문자열이 발견되더라도 지금까지 매치된 문자열이 없으며 탐색을 계속합니다. 세번째 테스트에서 확인할 수 있듯이, 매치되지 않는 문자열이 발견되었는데, 그 전에 매치된 문자열이 있었다면 match() 와 동일한 행동을 취하게 됩니다.



- findall()


이번에 알아볼 함수는 문자열 전체에서 정규식과 매치되는 모든 문자열을 리스트로써 반환하는 findall() 함수 입니다.

위에서 확인했던 테스트들과 동일한 값으로 테스트를 진행해보았습니다.



차이점이 보이시나요?

일단 findall() 함수는 특정 객체가 반환되는 것이 아니고, 정규식과 일치되는 모든 문자열을 리스트로 묶어서 반환해주기 때문에 group() 과 같은 함수를 사용할 필요가 없습니다.

또한 세번째 테스트를 확인해보면 match() 와 search()에서는 반환되지 않았던 'o'가 반환되는 것을 볼 수 있습니다.



- finditer()


finditer() 함수는 findall() 함수와 기능적으로 동일하지만 반환하는 형태가 다릅니다.

코드를 통해 어떻게 다른지 확인해보겠습니다.


위의 코드를 보시면, finditer() 함수는 findall() 함수와 같이 정규식과 매치되는 모든 문자열을 반환하지만 리스트형이 아닌 iterator객체로 반환합니다.

이를 확인하기 위해서 for 문을 이용하였고, 세번째 테스트를 보시면 그 결과를 확인할 수 있습니다.

해당 iterator 객체에서 문자열을 꺼내기 위해서는 match() 와 search() 에서 이용했던 group() 함수를 이용하면 됩니다.



+ 모듈로 한번에 실행하기


지금까지 우리는 아래 두줄의 코드와 같이 p 라는 것을 re.compile() 에 대한 객체로 두고, 해당 객체를 통해 검사 메소드를 실행시켜 m 객체를 얻었습니다.


p = re.compile('[a-z]+')

m = p.match('hello')


하지만 단순히 모듈에서 실행할 수 있는 방법이 있습니다. 즉, 위의 두줄 코드를 한줄로 줄일 수 있는 방법입니다.


m = re.match('[a-z]+', 'hello')


이와 같이 작성해도 위에서와 같은 결과를 반환합니다.

물론, match 이외의 나머지 함수또한 동일한 방법으로 사용가능합니다.



3. 컴파일 옵션


우리는 위에서 정규식을 컴파일하고, 특정 함수들의 특징에 따라 문자열에 대해 검색하는 방법에 대해서 알아보았습니다.

추가적으로 알아볼 내용은, 정규식을 컴파일할 때 추가할 수 있는 옵션들에 대해서 입니다.

추가할 수 있는 옵션으로는 총 4개가 있습니다.

옵션을 사용하는 방법으로는, 컴파일 함수에 두번째 인자로 옵션값을 입력해주면 됩니다.

예를 들어,


p = re.compile('[a-z]+', DOTALL)


과 같은 식으로 DOTALL 옵션을 사용할 수 있습니다. 이후 검색메소드를 사용하는 등의 행위는 그전과 동일합니다.


하나씩 천천히 알아보도록 하겠습니다.



- DOTALL or S


우리가 지난 포스팅에서 메타문자에 대해서 알아볼 때, dot( . )이라는 메타문자는 줄바꿈(\n) 을 제외한 모든 문자에 대한 의미라고 설명드렸습니다.

이때, 줄바꿈 조차 포함을 하기 위해서는 DOTALL 또는 S 컴파일 옵션을 사용하면 됩니다.


ex)

p = re.compile('[a.b]', re.DOTALL)

또는

p = re.compile('[a.b]', re.S)



- IGNORECASE or I


해당 옵션은 문자의 대소문자를 무시하는 옵션입니다.

즉, 원래대로라면 알파벳에서 대소문자 모두를 포함한 문자열을 의미하는 정규식은, '[a-zA-Z]' 이었겠지만, 해당 옵션을 사용하면 단순히 '[a-z]' 를 사용해도 됩니다.


ex)

p = re.compile('[a-z]', re.IGNORECASE)

또는

p = re.compile('[a-z]', re.I)



- MULTILINE or M


해당 옵션은 우리가 추후에 더 알아볼 메타문자 ^ 와 관련이 되어 있습니다.

옵션에 대한 설명을 위해 간단히 말씀드리면, 메타문자 ^ 는 문자열의 처음(시작)을 의미합니다. 즉 정규식 '^hello' 라고 한다면, hello 로시작하는 문자열을 의미하는 것입니다.

그런데 일반적으로 '^hello\s\w+' 라는 정규식을 이용하면

'''

hello python

Im beomwoo

hello everyone

'''

과 같은 여러개의 줄로 이루어진 문장에 대해서 첫번째 문장인 hello python 만을 반환하게 됩니다. findall() 함수를 사용해도 첫번째 문장만 반환하게 되는데 그 이유는 위의 문단이 결국 'hello python\nIm beomwoo\nhello everyone' 과 같은 문장이기 때문입니다.

이때 MULTILINE 옵션을 사용하면 각 문장에 대해서 정규식에 대해 검색을 진행합니다.

즉, MULTILINE을 사용한 컴파일에서 findall 메소드를 사용한다면 위의 문단이 반환하는 리스트는, ['hello python', 'hello everyone'] 이 될 것입니다.


ex)

p = re.compile('^hello\s\w+', re.MULTILINE)

또는

p = re.compile('^hello\s\w+', re.M)



- VERBOSE or X


해당 옵션은 특정 기능에 대한 것이라기 보다는 정규식을 보다 시각적으로 좋게 보기 위한 옵션입니다.

예를 들어서,

charref = re.compile(r'&[#](0[0-7]+|[0-9]+|x[0-9a-fA-F]+);')

이러한 정규식이 있다고 생각해봅시다. 아직 우리가 정규식에 대해 100% 공부를 하고 이해가 된것이 아니라는 것을 감안해도, 해당 정규식을 제대로 이해하기 어려운 부분이 있습니다. 하지만 VERBOSE 또는 X 정규식을 사용하면, 아래와 같이표현이 가능합니다.


charref = re.compile(r""" &[#] # Start of a numeric entity reference ( 0[0-7]+ # Octal form | [0-9]+ # Decimal form | x[0-9a-fA-F]+ # Hexadecimal form ) ; # Trailing semicolon """, re.VERBOSE)

아직 우리는 이렇게 되어있어도 이해가 어려울 수 있지만 나중에 정규식을 더 공부하고나면 위의 코드가 더 이해하기 편할 것입니다. 또한 # 을 통해 주석을 쓸 수 있기 때문에, 보다 쉽게 코드를 이해할 수 있습니다.



이렇게 해서 정규식에 대한 두번째 포스팅을 진행하였습니다.

이번 포스팅에서는 정규식에서 사용되는 4가지 메소드와 4가지 컴파일 옵션에 대해서 알아보았습니다.

추가적으로 궁금한 사항은 댓글 및 이메일을 이용해주세요 :)

블로그 이미지

Tigercow.Door

Web Programming / Back-end / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

지난 포스팅에서 파이썬의 내장 및 외장함수까지 알아보며 파이썬에 대한 전체적인 설명은 마무리 된 것 같습니다.

추가적으로 이번 포스팅에서는 정규 표현식이라는 개념에 대해서 알아보려고 합니다.

정규 표현식은 파이썬 이외에도 많은 곳에서 사용될 수 있습니다.


1. 정규 표현식(Regular Expressions)이란?


정규 표현식(Regular Expressions)은 복잡한 문자열을 처리할 때 사용하는 기법입니다. 위에서도 말씀드렸듯, 파이썬 만의 고유 무법이 아니라 문자열을 처리하는 모든 곳에서 사용되는 것입니다. 즉, 정규표현식을 배우는 것과 파이썬과의 직접적인 연관은 크게 없습니다.

정규 표현식은 고급 주제이기 때문에 아직 프로그래밍이 익숙하지 않으신 분들에게는 많이 어려울 수 있습니다.


정규식이라고도 불리는 정규표현식은 왜 필요할까요?

아래의 간단한 예제를 보면 쉽게 이해할 수 있을 것입니다.

다음과 같은 문제가 주어졌다고 가정합니다.


- 주민등록번호를 포함하고 있는 텍스트가 있다. 이 텍스트에 포함된 모든 주민등록번호의 뒷자리를 * 문자로 변경하시오.


먼저, 정규식을 몰랐다고 했을 때 여러개의 조건을 활용하여 위의 문제를 해결하기 위한 코드를 작성한다면 다음과 같을 것입니다.



이와 다르게, 정규식을 사용한다면 아래와 같은 코드를 통해 문제를 해결할 수 있습니다.



아직 정규식에 대해 제대로 배우지 않았으니 간편히 코드만 어떤느낌인지 보시면 됩니다.

이렇게 간단한 문제 하나를 비교해보아도 한눈에 보이는 것은 코드의 복잡성입니다.

정규식으로 해결한 코드는 코드가 매우 간단하고 직관적으로 보입니다.

문자열에 대해 우리가 보다 복잡한 문제가 주어졌을때 정규식은 보다 큰 효용성을 보여줄 것입니다.

그리고 이러한 것이 우리가 정규표현식을 배우고, 알아야 할 이유와 같습니다.



2. 정규 표현식의 기초, 메타 문자


먼저 정규 표현식에서는 메타 문자라는 것을 사용 합니다.

메타문자란, 원래 그 문자가 가진 뜻이 아닌 특별한 용도로 사용되는 것을 이야기 합니다.

정규 표현식에서 사용되는 메타 문자(meta characters)는 다음과 같은 것들이 있습니다.


. ^ $ * + ? { } [ ] \ | ( )


그럼 하나씩 알아보도록 하겠습니다.


먼저, 문자 클래스 [ ]

우리가 가장 먼저 살펴볼 메타 문자는 문자 클래스(character class)인 [ ] 입니다.

문자 클래스로 만들어진 정규식은 [ 과 ] 사이의 문자들과 매치하는 것 이라는 의미를 가집니다.

즉, [abc] 라는 정규 표현식이 있다면 해당 표현식의 의미는 'a, b, c 중 한개의 문자와 매치되는 것'이라는 의미를 가집니다.

예를 들어 본다면 [abc]는 문자열 "a", "before"와 매치되며 "dude"와는 매치되지 않습니다.


그리고 문자클래스 [ ] 안에서 하이픈(-)을 사용하게 되면 범위를 말하게 됩니다.

즉, [0-4] 는 [01234]와 같은 의미의 정규표현식이며 [a-e] 는 [abcde]와 같은 의미의 정규표현식 입니다.


또한 자주 사용되는 문자 클래스는 아래와 같이 별도의 표기법으로 표현할 수도 있습니다.


별도 표기법

정규 표현식

설명

\d

[0-9] 

모든 숫자와 매치되는 의미 

\D

[^0-9]

숫자가 아닌 것과 매치됨을 의미

\s

[ \t\n\r\f\v]

whitespace 문자와 매치되는 것을 의미

\S

[^ \t\n\r\f\v]

whitespace 문자가 아닌 것과 매치됨을 의미

\w

[a-zA-Z0-9]

문자+숫자와 매치 되는 것을 의미 

\W

[^a-zA-Z0-9]

문자+숫자가 아닌 것과 매치됨을 의미



두번째로, Dot(.)

정규 표현식의 Dot(.) 메타 문자는 줄바꿈 문자인 \n을 제외한 모든 문자와 매치됨을 의미합니다.

즉, a.b 는 a와 b사이에 줄바꿈 문자를 제외한 어떤 문자가 들어가도 모두 매치되는 것입니다.

하지만 주의할 점은 a와 b 사이에 어떤 문자라도 하나 이상은 꼭 들어가야 매치 됩니다.

예를 들어보면, a.b 정규식과 "aab"와 "a0b"는 매치되지만 "abc"는 매치되지 않습니다. 문자 a와 b 사이에 어떤 문자가 최소 하나 존재하지 않기 때문입니다.


추가적으로, a[.]b 라는 정규식은 "a.b"와 매치되는 정규식입니다.



세번째, 반복(*)

go*gle 이라는 정규식을 보겠습니다.

여기서 반복의 의미를 가진 메타문자 * 는 해당 메타문자 앞의 문자가 0번이상 무한대로 반복될 수 있음을 의미합니다.

즉, go*gle 이라는 정규식은, "ggle", "gogle", "google", "goooooooogle" 과 모두 매치됩니다.



네번째, 반복(+)

반복을 나타내는 메타문자는 하나 더 있습니다. 바로 + 인데 *와 다른 점은, + 메타문자는 반복 회수가 최소 1번이상이어야 합니다.

즉, go+gle 이라는 정규식에 대해 "ggle"은 매치되지 않습니다.



다섯번째, 반복({m,n})

그런데, 반복 회수를 정하고 싶다면 어떻게 해야될까요?

무한번 반복까지 매치되는 것이 아니라, 예를 들어, 3번 또는 2번만 반복되는 문자열만 매치되게 하고 싶다면?

이럴떈 { } 메타문자를 이용하면 됩니다.

{m, n} 정규식을 사용하면 반복 횟수가 m부터 n까지 인 것을 매칭할 수 있습니다. 또한 m 이나 n 둘중 하나를 생략할 수 있습니다. 즉, {,3} 이라면 반복회수가 3이하인 것을 의미합니다.

이를 통한 몇가지 예시를 보면 다음과 같습니다.

go{2}gle : 해당 정규식은 o가 반드시 2번 반복되는 문자열과 매치됩니다.

go{2,4}gle : 해당 정규식은 o가 반드시 2번이상, 4번이하 반복되는 문자열과 매치됩니다.



여섯번째, 반복(?)

마지막으로 알아볼 메타문자는 ? 입니다.

? 가 의미하는 메타문자는 {0,1} 입니다.

즉 0번~1번 반복되는 것을 의미합니다.


이렇게 해서 정규식에 대한 기본적인 개념과 몇가지 메타문자에 대해 알아보았습니다.

다음 포스팅에서는 파이썬에서 정규 표현식을 지원하는 re 모듈에 대해 알아보도록 하겠습니다.

블로그 이미지

Tigercow.Door

Web Programming / Back-end / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요