ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ 번역 ] 손과 눈으로 기억하는 정규표현 입문 4. 최종회: 중급자 테크닉을 마스터하자
    Technique/ETC 2016. 3. 10. 18:06
    반응형

    원문 : http://qiita.com/jnchito/items/b0839f4f4651c29da408

    의 내용을 제 입에 맞춰 번역한 글입니다. 변역, 오역있을수 있습니다. 일본어가 가능하신분은 원문을 보시는 것을 권해드립니다.

    이번화에 나오는 용어인

    • 肯定の先読み、後読み
    • 否定の先読み、後読み

    같은 용어는 저도 정규표현식을 잘 모르기 때문에 우선 있는 그대로  직역하였습니다. 혹시 이런 용어를 한국어로 어떻게 표현하는지 아시는분은 댓글 달아주시면 감사합니다.

    그리고 아래 보면 도쿄도 라고 해놓은 부분이 있는데 그부분도... 양해부탁드립니다 ㅠㅠㅋ



    이기사에서 학습하는 내용은 아래와 같습니다.
    - \b의 의미
    - 긍정의 예측, 후읽기
    - 부정의 예측, 후읽기
    - 후방참조
    - 메타문자의 복잡한 조합
    - 정규표현과 퍼포먼스
    - 메타문자의 에스케이프
    - [] 내의 메타문자의 움직임
    - {n,} 이나 {,n} 의 의미
    - \W, \S, \D, \B의 의미

    제1회~3회까지의 내용에 비교하면 조금 어려울 뿐더러, 기사의 내용도 상당히 많지만, 열심히 따라와 주세요!.

    동작환경
    본 기사의 정규표현은 이하의 환경에서 동작확인을 하고 있습니다.
    - Ruby
    - Javascript
    - Atom

    정규표현이 사용가능한 프로그래밍언어나 텍스트에디터라면 본 기사의 샘플코드는 거의 똑같이 움직일 것 입니다.
    그러나, 일불의 정규표현은 Javascript나 Atom에서는 움직이지 않습니다.
    그럴경우에는 따로 이야기하겠으니 주의해 주세요

    위에 언급한 3개의 환경이외에도ㅓ 정규표현의 사양이나 사용가능한 메타문자가 조금씩 다를경우가 있습니다.
    뭔가 움직임이 이상하다고 생각이든다면, 자신이 사용하는 환경의 정규표현의 사양을 확인해 주세요.
    그럼 제4회 본문으로 들어가겠스브니다.

    영단어에 정확히 매치시키다 ( \b의 사용방법 )
    예를들어 이런 영어의 예문이 있다고 합시다.
    (예문은 인터넷에서 적당히 가져온것이에 특별한 의미는 없습니다.)


    sounds that are pleasing to the ear.
    ear is the organ of the sense of hearing.
    I can't bear it.
    Why on earth would anyone feel sorry for you?



    이문장 안에서 'ear' 라고하는 단어만을 간략히 추출하고 싶다면 당신은 어떻게 하겠습니까?
    우선, Rubular에 위의 예문을 붙여 넣어봅시다.


    Kobito.Aeuz1M.png


    그뒤에 Your regular expression 란에 ear라고 입력해 주세요.


    Kobito.i8ktCs.png



    흠... 단순히 'ear'를 검색한다면 'hearing' 이나 'bear', 'earth'도 함께 검색되어 버렸네요.

    이럴때 편리한 것이 \b 라는 메타문자 입니다.
    이것은[단어의 경계]를 나타냅니ㅏ디. (위치를 나타내기에 앵커의 한 종류 입니다)
    단어의 경계라는 것은 스페이스 라던가, 피리오드(.) 이라거나, 더블쿼터(")거나 문장의 맨처음이나, 가장끝 등  여러가지 정류가 있습니다.

    한번 \b만  Rubular에 입력해보세요.
    Match  Result란이 다음과 같이 표시됩니다.


    Kobito.J0vUF3.png

    하늘색의 부분이 단어의 경계입니다.
    이것들은 스페이스나, 피리오드(.)에 매치되는 것이 아니라, 어디까지나 단어의 직전이나 직후라고하는 위치에 매치됩니다.

    이런이유로 정규표현에 \bear\b 라고 입력해 보세요.
    조금 이상하게 느껴지시겠지만, 위의 정규표현은 \b + ear + \b 로 구성되어, [직전과 직후의 단어의 경계가 오고, ear] 라는 의미 입니다. ("bear"가 아닙니다.)


    Kobito.BI3Uhl.png



    이렇게 한다면 깔끔하게 "ear"만 검출하는것이 가능합니다.

    검색 효과가 낮은 메소드를 깔끔하게 검색(\b의 사용법 2)
    \b의 이용예를 하나더 살펴 볼까요.
    Ruby On Rails의 개발경험이 있는 사람은 알고 계시겠지만, Rails 에는 I18n.t 라는 메소드가 있습니다.
    이것은 인수로 전해진 키에 대해, 영어나 일본어등의 언어의 번역문을 출력하는 메소드 입니다.

    참조 : 3.1 訳文を追加する | Railsガイド

    그러나 뷰(화면의 코드)의 안에는 I18n. 을 생략하고, t만으로도 이 메소드를 호출할 수 있도록 되어 있습니다.
    구체적으로 이런 느낌 입니다.


    <td>
    <%= link_to I18n.t('.show'), user %>
    <%= link_to t('.edit'), edit_user_path(user) %>
    </td>



    2행째의 I18n.t('show')와 3행째의 t('.edit')가 t메소드를 사용하고 있는 부분입니다. (예문 이기에 굳이 일관성 없는 작성법 입니다.)

    [리팩토링을 위해, 이 코드의 내부에 t 메소드를 사용하는 부분만을 찾고싶어]라고 생각한다면 어떻게 하시겠습니까?
    예를 들어 간략히 't'만을 검색한다면 아래와 같습니다.


    Kobito.9LFpyL.png


    당연합니다만, 아주 많은 "t"에 매치되기에  대량의 "t"가 검색됩니다.

    그러나, 메소드의 호출은 I18n.t('.show')나 t('.edit')와 같이 작성됩니다.
    또 Ruby는 괄호를 생략가능하기에 t '.edit' 와 같은 작성법도 가능합니다.
    즉 "t"의 전후에는 피리오드(.)나 괄호, 스페이스등이 들어옵니다.
    이것들은 정규표현상 단어의 경게로 여겨집니다.

    따라서 \bt\b 라고하는 정규표현을 사용한다면, t메소드를 호출하고있는 부분만을 검출하는 것도 가능합니다.
    실제로 해볼까요?


    Kobito.AbPYpT.png



    보시는 것과 같이, t 메소드만을 검출하고 있습니다.
    이런식으로 \b를 사용한다면, 소스코드로부터 특정 메소드나, 변수를 검색하거나 할 때에 도움이 됩니다.

    파일명만을 피리오드로 검출( 긍정의 앞읽기 )
    다음에 등장하는 것은 이른 텍스트 입니다.


    <td>
    <%= link_to I18n.t('.show'), user %>
    <%= link_to t('.edit'), edit_user_path(user) %>
    </td>



    이 텍스트에는 [키 = 값]을 여러개 묶는것이 하나의 레코드로 표현됩니다.

    자 그럼 이안에서 파일명만을 검출해볼까요?
    즉 "users.zip" 과 "posts.xml" 만을 검출하는 것 입니다.

    지금까지 익혀왔던 지식을 사용한다면 다음과 같은 정규표현이 작성가능할 것입니다.


    filename=[^;]+


    위의 정규표현의 의미가 잘 이해되지 않는 분은 제2회의 기사를 다시읽어 보세요.
    [이정도야 뭐 껌이지]라고 여겨지는 분 이시라면 이것을 Rubular에 입력해 주세요


    Kobito.9LFpyL.png



    오... 됫다!!!!
    뭐.. 나쁘진 않습니다만 "filename="의 부분도 같이 같이 매치되어 버리네요.

    그렇다면 캡쳐의 괄호를 넣어 봅시다.
    즉 이런 정규표현입니다.

    filename=([^;]+)


    Kobito.MusL3m.png


    네, Match Groups란에 "users.zip", "posts.zip"이 나왔습니다.
    다음은 이것을 프로그램이 무언가 조작한다면 OK...  이런 방법도 역시 괜찮다고 생각합니다.

    그렇지만, 중급자이상을 목표로 한다면 [먼저 읽기] 와 [이후 읽기]라는 테크닉도 마스터하고 싶으실 것입니다.

    이번의 케스로 치자면 [긍정의 먼저 읽기]라고하는 테크닉이 사용됩니다.
    우선 (?<=filename=) 라는 정규표현을 Rubular에 입력해보세요.
    그렇다면 아래와 같은 결과가 됩니다.


    Kobito.Nuaeie.png



    Match result란을 확대한다면 아래와 같습니다.


    Kobito.X9JpHp.png

    "="와 파일명의 사이에 스페이스는 들어가 있지 않습니다만, 어찌된게 그 부분에 하이라이트가 들어가 있습니다.
    사실 이것은 "filanme=" 이라는 문자열의 [직후의 위치]에 매치되고 있습니다.

    일반적으로 (?<=abc)와 같이 작성한다면 "abc"라는 문자열 그자체가 아니라 그 문자열의 [직후의 위치] (abc라고 치면 c의 바로뒤)에 매치됩니다.

    이것만 듣고도 뭔가 팍 하고 오지 않을수도 있겠지만, 우선은 진행하겠습니다.
    (?<=filename=) 이것만으로는 [위치]에 매치되지 않기때문에 파일명도 매치시켜 봅시다.
    다음과같은 정규표현을 Rubular에 입력해 주세요


    (?<=filename=)[^;]+



    이렇게 한다면 "users.zip"과 "posts.xml"만이 매치됩니다.


    Kobito.zkhetu.png



    (?<=filename=)[^;]+의 의미를 한국어로 적는다면 이렇게 됩니다.
    ["filename="이라는 문자열의 뒤에서부터 시작하여, ";" 이외의 문자가 1문자이상 연속]
    반복하지만 (?<=filename=)은 "filename=" 이라는 문자열 그 자체가 아니라 그 문자열의 직후라는 [위치]를 나타내는 것이기 때문에 주의하셔야 합니다.

    흠... 이상해! 전혀 모르겠어! 라는 분을 위하여
    네, 확실히 이상하죠
    터놓고 이야기해서, 나중에 읽기나, 먼저 읽기( 뒤에 서술 하겠습니다.)는 굳이 사용하지 않더라도 어떻게든 됩니다.
    사람이 직접 확인하는 것이라면, 전에 사용한 filename=[^;]+ 정도의 정규표현식으로도 충분합니다.

    나중에 읽기나 먼저 읽기를 사용해서 편리한 것은, Ruby의 scan 메소드나 Javascript의 match 메소드를 사용할 때 입니다.
    Ruby의 경우 이전의 문자열로부터 "users.zip"과 ,"posts.xml"을 검출하고 싶다면 다음과 같이 한방에 검색하는 것도 가능합니다.


    text = <<-TEXT
    type=zip; filename=users.zip; size=1024;
    type=xml; filename=posts.xml; size=2048;
    TEXT
    text.scan(/(?<=filename=)[^;]+/)
    # => ["users.zip", "posts.xml"]


    나중에 읽기를 사용하지 않는 경우에는 조금 번거로움이 발생합니다.
    ('='로 나누어 뒷부분을 습득, 이라고하는 처리가 필요합니다.)


    text = <<-TEXT
    type=zip; filename=users.zip; size=1024;
    type=xml; filename=posts.xml; size=2048;
    TEXT
    text.scan(/filename=[^;]+/).map { |s| s.split('=').last }
    # => ["users.zip", "posts.xml"]



    이런 연유로, [나중에 읽기나 먼저 읽기를 구사하지 않으면 처리할 수 없어!]라는 케이스는 거의 없습니다.
    물론 잘 사용하는 편이 편리합니다만, 굳이 사용하지 않더라도 어떻게든 됩니다.

    주의 : Javascript나 Atom에서는 [나중에 읽기]는 사용할 수 없습니다.
    여기서 소개한 [ 나중에 읽기 ]입니다만, 안타깝지만 Javascript(JS)나 Atom에서는 사용할 수 없습니다.
    (?<=)가 정귶표현에 포함된다면 구문에러가 발생합니다.


    var text = "type=zip; filename=users.zip; size=1024;\ntype=xml; filename=posts.xml; size=2048;\n";
    
    var matched = text.match(/(?<=filename=)[^;=]+/g);
    // => SyntaxError: Invalid regular expression: /(?<=filename=)[^;=]+/: Invalid group


    Kobito.YsJ5iN.png



    그러나 인터넷을 조사해 보았더니 JS에서도 이후 사용할수 있게된다는 정보가 있었습니다.

    참고 : 정규표현의 나중에 읽기가 도입된다!

    또, 이후의 GitHub issue에서는 [JS에서 사용할 수 없는 정규표현은 Atom에서도 사용할 수 없다]라는 의미의 코멘트가 달려 있엇습니다.(Atom은 내부적으로JS가 돌아가고 있으니까??)

    참고 : positive and negative lookbehind (lookaround) · Issue #571 · atom/find-and-replace

    JS나 Atom은 저의 전문이 아니기에 이 부분의 동향에 밝은 사람이 있으면 코멘트를 주시면 감사하겠습니다.

    특정악기를 담당하고 있는 멤버를 검색
    다음은 나중에 읽기의 반대의 [먼저 읽기]를 사용해 봅시다.
    예를드러 이런 텍스트가 있다고 칩니다 (뭔가 밴드 멤버소개 같군요)


    John:guitar, George:guitar, Paul:bass, Ringo:drum
    Freddie:vocal, Brian:guitar, John:bass, Roger:drum


    이 텍스트 안에서 베이스를 담당하고 있는 멤버의 이름을 검색해 봅시다.
    즉 "Paul"과 "John"을 검색하면 됩니다.

    이 텍스트는 [이름 : 파트] 의 순으로 정보가 정렬되어 있습니다.
    \w+:bass 라고 하면 우선 "Paul:bass" 와 "John:bass"가 검색됩니다만, ":bass" 부분은 좀 거치적거리네요


    Kobito.LSmgZJ.png



    여기서 [나중에 읽기]의 테크닉이 사용됩니다.
    우선, (?=:bass) 라는 정규표현을 입력해 주세요


    Kobito.trgyRB.png

    Match result 부분만 보면 이런식으로 되어 있습니다.


    Kobito.fwpBOw.png


    여기서는 ":bass" 라는 문자열의 [직전의 위치]에 매치되어 있습니다.

    일반적으로(?=abc)처럼 적는다면 "abc"라는 문자열 그 자체가 아니라, 그 문자열의 [직전의 위치] (abc라고 치면 a의 직전)에 매치 됩니다.
    이것을 (긍정의) 먼저 읽기 라고 합니다.

    자 그럼 "Paul"이나 "John" 과 같은 이름은 ":bass"의 우측에 있으므로, 이름을 검색하는 정규표현도 "(?=:bass)" 의 우측에 적어봅시다.
    즉 이렇게 됩니다.


    \w+(?=:bass)


    이것은 [":bass" 라고하는 문자열의 직전에  있는, 1글자 이상 연속되는 영단어의 구성문자 ]라는 의미 입니다.
    위의 정규표현을 Rubular에 입력해 봅시다


    Kobito.V7GR90.png



    네, 제대로 "Paul" 과 "John"이 검색 되었습니다.

    Javascript에서 사용
    먼저 읽기는 JS에서도 사용가능 합니다.
    이하는 먼저 읽기를 사용한 JS의 샘플코드 입니다.


    var text = "John:guitar, George:guitar, Paul:bass, Ringo:drum\nFreddie:vocal, Brian:guitar, John:bass, Roger:drum";
    
    console.log(text.match(/\w+(?=:bass)/g));
    // => [ 'Paul', 'John' ]


    Atom에서 사용
    JS와 같이 Atom에서도 먼저 읽기를 사용할 수 있습니다.


    Kobito.Gb00Lk.png



    잘못된 도도부현을 발견 ( 부정의 나중에 읽기 )
    먼저읽기와 나중에 읽기는 부정조건을 지정하는 것이 가능합니다.
    예를들면 아래와 같은 도도후현의 텍스트가 있다고 합니다.


    東京都
    千葉県
    神奈川県
    埼玉都


    여러분이 아시는 것 처럼 도도부현명에 "도"가 사용가능한 곳은 도쿄도 뿐입니다.
    그이외는 도도부현의 기술이 잘못되었습니다.

    여기서 (?<!도쿄)도 라고하는 정규표현을 입력해보세요.
    그렇게 한다면 잘못된 [도]의 사용방법을 발견할 수 있을것 입니다.


    Kobito.8F4rPa.png

    네, 제일 마지막에 나온 사이타마도의 도가 매치되었습니다.

    일반적으로 (?<!abc)같이 작성하면 "abc"라는 문자열 이외의 [직후의 위치]에 매치합니다.
    이것을 부정의 나중에 읽기 라고 합니다.

    (?<!도쿄)도 는 ["도쿄" 이외의 문자열의 직후에 나오는 "도"]의 의미가 되므로, "사이타마도"의 "도"가 매치됩니다.

    ["도쿄"이외의 문자열의 직후]라고하는 말 뿐만으로는 확실히 이해가 안될지도 모르지만 Rubular에 (?<!도쿄)만 입력해본다면 알게 될 것입니다.


    Kobito.YZMHZs.png

    위의 이미지를 잘 보면 "도쿄도"의 "경"과 "도"의 사이만 하이라이트가 없군요.
    따라서 "도쿄도"는 매치되지 않고 "사이타마도"가 매치되는 것 입니다.

    거기에, [긍정의 나중에 읽기]와 같이 [부정의 나중에 읽기]도 JS나 Atom에서는 사용할 수 없습니다.

    [식품의 사자에]를 검색 ( 부정의 먼저 읽기 )
    만일, 부정의 먼저읽기를 사용할 수도 있습니다.
    조금 어처구니 없는 예입니다만, 다음의 텍스트로부터 [식품의 사자에]를 검출해 봅시다.


    つぼ焼きにしたサザエはおいしい
    日曜日にやってるサザエさんは面白い


    일본의 오래된 사람이라면 무엇이 식품이고, 무엇이 애니메이션이름인가 알고 계시겟죠?
    이런 경우에는 사자에(?!)씨 라는 정규표현을 사용한다면 [식품의 사자에]가 검출됩니다.


    Kobito.x2snrb.png



    일반적으로 (?!abc)와 같이 작성하면 "abc" 라는 문자열 이외의 [직전의 위치]에 매치됩니다.
    이것을 부정의 먼저 읽기 라고 합니다.

    사자에(?!)씨 는 ["씨" 이외의 문자열의 위치에 나오는 "사자에"]의 의미가 되므로 첫번째행의 "사자에"가 검출됩니다.

    만일을 위하여 (?!씨)가 어디에 매치되는가( 오히려 매치하지 않는가)를 확인해 두세요.


    Kobito.RcZEuF.png

    보고계시는것 처럼, "사자에씨"의 "에"와 "자" 의 사이만 하이라이트가 없습니다.
    따라서 사자에(?!)씨는 "사자에씨"에 매치되지 않습니다.

    또한 긍정의 먼저 읽기와 같이 [부정의 먼저 읽기는 JS 나 Atom 에서 사용할 수 없습니다.]

    URL이 그대로 화면상에 표시되고있는 링크를 발견 ( 후방 참조 )
    제2회의 기사에서는 () ㅇ ㅡㄹ 사용하여 문자열을 캡쳐하고, 변환할때에 \1이나 $1과 같은 번호로 참조한다, 라고하는 테크닉을 소개했습니다.
    이것은 변환할 때가 아니라, 정규표현내부에서도 똑같이 참조하는 것이 가능합니다.
    이것을 후방참조 라고 합니다.

    예를들어 다음과 같은 HTML 텍스트가 있다고 칩시다


    <a href="http://google.com">http://google.com</a>
    <a href="http://yahoo.co.jp">ヤフー</a>
    <a href="http://facebook.com">http://facebook.com</a>



    여기서부터 [URL이 그대로 화면에 보여지는 링크(첫번째줄과 세번째 줄)]을 검색하여 봅시다.
    이 경우 다음과 같은 정귶표현을 사용합니다.


    <a href="(.+?)">\1<\/a>


    정규표현내에서 사용하고 있는 \1 에 주의해 주세요
    이것은 [()에서  캡쳐된 ㅣ제일 첫번째 문자열]을 나타냅니다.
    즉, (.+?)와 \1은 같은 문자열을 가르키고 있는 것이 됩니다.

    이런 연유로 다음은 Rubular의 실행화면 입니다.


    Kobito.9uScid.png



    의도한대로 URL가 바로 화면상에 표시되어있는 링크를 검출하였습니다.

    트위터, 아카운트, 트위터 시간을 검색 ( 메타문자의 북잡한 조합 )
    자 그럼 여기까지 배워온 지식을 총동원하여 조금 복잡한 정규표현을 만들어 봅시다.
    이번에 준비한 것은 이런 텍스트 입니다.


    You say yes. - @jnchito 8s
    I say no. - @BarackObama 12m
    You say stop. - @dhh 7h
    I say go go go. - @ladygaga Feb 20
    Hello, goodbye. - @BillGates 11 Apr 2015



    이것은(가공의) 트위터 리스트 입니다.
    [(트위터) - (아카운트)(트위터 시간)]이라고하는 형식의 트위터가 표시되고 있습니다.

    여기서부터 트위터와 아카운트, 트위터 시간을 각각 검출해 봅시다.
    즉 이런 Match groups를 만드는게 목표입니다.


    Kobito.nDwlSA.png

    여러분 이라면어떤 정규표현을 만들겠습니까?
    조금 어려울지도 모르겠지만, 여기까지 공부해 왔던 지식을 사용한다면 어떻게된 될 것입니다.

    트위터를 검출
    트위터 부분은 "You Say yes" 와같이 되어 있기때문에, [문장의 시작에 하이픈 까자의 임의의 문자열] 이라고하는 패턴이 되었습니다.
    이렇기에 다음과 같은 정규표현으로 트위터를 검색합니다.


    ^(.*) -


    Rubular에 입력한다면 제대로 트위터가 검출됩니다.


    Kobito.rlSAzW.png



    아카운트를  검색
    아카운트를 검출하는 것도 어렵지 않을 것입니다.
    이 경우에는 ["@"로 시작하고, 임의의  알파벳이 연속하는 문자열]이라는 패턴으로 될 것입니다.
    실제는 알파벳에 한정하지 않고, [ 영단어를 구성하는 문자] 정도마녀 OK이기 때문에, \w를 사용합시다.


    (@\w+)



    위의 정규표현을 Rubular에 입력한다면 보시는것 처럼 아카운트가 검출됩니다.


    Kobito.2Y0pST.png



    트위터 시간( 초, 분, 시간 )을 검색
    애매한 것은 트위터 시간 부분 입니다.
    날짜는 수초전 이라면 "1s" 몇분전 이라면 "12m" 몇시간전이라면 "1h" 1일 이상이전 이라면 "Feb20", 1년전이라면 "11 Apr 2015"와 같은 형식으로 표시됩니다.
    여기서는 한번에 전부처리하려 하지 않고, 일단 각각의 케이스에  대응하는 정규표현을 생각해 봅시다.

    우선 수초, 수분, 수시간의 케이스 입니다.
    이 경우에는 [숫자 + s/m/h] 라는 패턴이 되어 있기에, 정규표현을 작성하기 쉬울 것 입니다.
    우선 (\d+[smh])처럼 적어두면 괜찮겟죠


    Kobito.YyvBEh.png



    네 젤처음의 3건에 대하여 트위터 시간을 검색하였습니다.

    트위터시간 ( 1일이상 이전 )을 검색
    1일이상 이전의 경우에는 [알파벳 3문자 + 숫자] 라는 패턴이 됩니다.
    알파벳을 \w로 간략적으로 표현하고, (\w{3} \d+)로 해 볼까요?


    Kobito.edByPR.png



    흠.. 조금 불필요한 캡쳐를 너무 많이 하고 있기에 조금더 엄밀한 [대문자 1문자 + 소문자2문자 ]라고하는 패턴으로 바꿔볼까요?
    이런 연유로 다음의 정규표현을 입력해 주세요


    ([A-Z][a-z]{2} \d+)


    Kobito.tqjdOx.png



    이걸로도 아직 의도하지 않은 "11 Apr 2015"의 "Apr 2015"이 매치되어 버립니다만 뭐 일단 넘어가죠

    트위터 시간(1년이상 이전)을 검색
    자 그럼,  최후의 1년잇ㅇ 이전의 날짜를 검색하는 케이스에 대해 생각해봅시다.
    이것은 [숫자 + 알파벳 3문자 + 숫자] 라는 패턴입니다.
    이것을 다시 날짜를 검출하는 세번째의 정규표현으로서 작성해도 좋습니다만, 패턴만 본다면 방금전의 [알파벳 3문자 + 숫자]에 좀 닮아 있습니다.
    거기서, 1일이상 이전과 1년이상 이전을 동시에 검색하는 정규표현을 만들어 봅시다.
    차이는 알파벳 3문자의 앞에 숫자가 오느냐 마느냐 인데 [(알파벳의 앞) 숫자 있음, 또는 없음]으로 한다면 OK 입니다.
    즉 이런 느낌이로 작성할 수 있겟죠?


    ((?:\d+ )?[A-Z][a-z]{2} \d+)


    Rubular에서도 제대로 검색 가능 합니다.


    Kobito.I77aQz.png



    이 규표현을 간단히 설명하겠습니다

    (?:\d+ )?부분이 [숫자 있음 또는 없음] 의 정규표현입니다.

    [직전의 문자가 1개, 또는 없음]을 나타냄 ? 는 () 의 뒤에 둔다면, [괄호로 둘러쌓인 문자열이 1개, 또는 없음]을 의미합니다.
    ()의 뒤에 올 수 있는 량지정자는 ? 만은 아닙니다.
    +나 *도 ()의 뒤에 올 수 있습니다.
    이것은 이번에 처음으로 새개하는 내용이기에 꼭 기억해 두도록 하세요

    그렇지만, 단순히 ()로 감싸으면 캡쳐의 대상이 되기 때문에 (?:) 을 사용하여 캡쳐대상외 로 합니다.
    또 엄밀히 말하자면 날짜의 뒤에 스페이스가 들어가기 때문에 (?:\d+ )도 \d+ 의 뒤에 스페이스가 들어갑니다.

    날짜정보 전체를 한번에 검출
    자 그럼, 지금까지 날짜를 검색하는 정규표현을 나누어서 생각해 보았습니다.
    - 초/분/시간의 경우 (\d+[smh])
    - 1일이상 이전, 1년이상이전의 경우 ((?:\d+ )?[A-Z][a-z]{2} \d+)

    하나의 정규표현으로 양쪽의 케이스를 검색하기 위해서는 어떻게 하면 될까요?
    이것은 단순히 양쪽을 OR 조건으로 조합핳면 됩니다.
    OR조건을 작성할 경우에는 | 이라고하는 메타문자를 사용햇엇죠?
    즉 이렇게 하면 됩니다.


    (\d+[smh]|(?:\d+ )?[A-Z][a-z]{2} \d+)


    OR조건을 넣으면 날짜표현을 전부 검색하는것이 가능합니다.


    Kobito.k5quei.png



    이렇게 OR조건의 | 을 사용할 때에는 각각의 조건안에서도 메타문자를 사용하여 복잡한 조건(패턴)을 지정하는 것이 가능합니다.

    마무리 : 트위터, 아카운트, 날짜를 한번에 검색
    자, 여기까지 왔다면 끝난것이나 다름 없습니다.
    각각의 정규표현을 연결한다면, 트위터와 아카운터, 트위터 날짜를 한번에 검출하는 정규표현이 만들어 집니다.
    만일으 위해 지금까지만든 정규표현을 정리해 볼까요?

    - 트위터 : ^(.*) -
    - 아카운트 : (@\w+)
    - 날짜 : (\d+[smh]|(?:\d+ )?[A-Z][a-z]{2} \d+)

    자 그럼 정규표현을 연결 해봅시다.


    ^(.*) - (@\w+) (\d+[smh]|(?:\d+ )?[A-Z][a-z]{2} \d+)



    이것만 보면 길어서 복잡한 것 처럼 보이지만 순서대로 설명해 왔기때문에 어느 부분이 무엇을 나타내고 있는지 여러분은 모두 이해 하셨을것 입니다.

    Rubular에 넣어서 동작을 확인해 봅시다.


    Kobito.e0hk60.png



    네 제대로 각 파츠가 검색되었습니다.

    이렇듯 정규표현의 룰을 완벽히 이해해둔다면, 길고 복잡한 정규표현도 읽고 쓸 수 있습니다.
    그리고 프로그램내에서 정규표현을 사용한다면, 깔끔한 코드로 복잡한 문자열 처리를 제어하는 것도 가능합니다.

    중요 : 정규표현과 퍼포먼스
    자 그럼, 여기까지 계속해서 [ 정규표현은 굉장해! 편리해!!]라고 말하는 자세로 정규표현의 편리기능을 소개해 왔습니다.
    그러나, 안타깝지만 정규표현도 무적의 툴은 아닙니다.
    사실은 작성하는 방법을 잘못하면 말도 안되게 구린 정규표현을 만들어 낼 수도 있습니다.

    지금부터 소개하는 정규표현은 절대 로컬 환경에서 실행해 주세요.

    Rubular와 같은 온라인 툴에서 실행한다면 서버의 부하가 걸리기 일쑤입니다.
    알겠습니까? 꼭 약속을 지켜주세요.

    네, 자그럼 소개해 드르겠습니다.
    이런 텍스트와 정규표현이 말도 안되게 느려지는 원입니다.
    - 텍스트 : _a____
    - 정규표현 : (_+|\w+)*a

    예를 들면 Atom에서 실행한다면 어떻게 될까요?


    Kobito.Osj9cJ.png



    음? [간단히 움직이는데?] 라고??
    괜찮군요.

    자 그럼 다음은 _a_________와 같은 문장끝의 언더스코어를 점점더 길게 한 다음 Find 버튼을 클릭해 주세요.
    (Rubular에서 실행하면 안됩니다!!)


    Kobito.aLo8i8.png

    아직 괜찮습니까?? 자 그럼 조금더 길게가볼까요?
    예를 들면 이정도
    _a_____________________


    Kobito.8q4nXu.png

    슬슬 여러분의 비명이 들려올 때가 되었다고 생각됩니다 ^^
    이 정규표현은 Atom뿐만 아니라 Ruby나 Javascript에서 실행해도 굉장이 느릴 것 입니다.

    왜냐하면, 이것은 백트랙 이라고 불리는 정규표현의 동작원리에 의해서.... 라고 설명하고 싶습니다만, 이야기가 나오기 시작하면 굉장히 길어지기 때문에 이쯤에서 생략하겠습니다.
    (알기 쉽게 설명할 정도로 필자가 이해하지 못한 것도 있습니다. 죄송합니다.)

    정규표현이 느려지는 기술적인 설명이나, 문제를 일으키는 정규표현의 구체적인 예를 조금도 알고 싶은 분은
    - 정규표현 퍼포먼스
    - 정규표현 느림
    - 정규표현 백트랙

    이란 키워드로 인터넷에 검색해 보시기 바랍니다.

    또한 필자는 정규표현의 퍼프먼스 문자를 학습하는데 있어서, 아래의 자료를 참고했습니다.


    툴을 이용하여 퍼포먼스를 체크
    안타깝지만 [이렇게 작성하면 느려짐], [이러허게 작성하면 겁나 안전!] 과같은 룰에 대하여 간단히 설명할 순 없지만, 적어도 방금 언급한
    (_+|\w+)*a와 같은 + 나 *이 () 의 안과 밖에서 둘다 등장하는 정규표현은 위험합니다.
    이런 정규표현은 내부적인 조합이 폭발적으로 늘어나, 말도안되게 느려지는 경우가 다반사입니다.

    혹시 [ 이 정규표현 조금 수상한데..?? ]라고 생각된다면, 실제로 움직이기 전에 온라인 툴을 이용하여 퍼포먼스를 확인하는 것이 좋다고 생각합니다.
    아래의 툴을 사용한다면 퍼포먼스의 좋고 그름정도는 판단할 수 있을 것 입니다.


    Online regex tester and debugger

    구체적인 사용방법은 Rubular와 같습니다.
    정규표현과 텍스트를 입력하면, 그 정규표현이 텍스트에 매치하는지 어떤지 판정을 내 줍니다.
    이 툴은 그뿐만이 아니라, 폼의 우측상단에 [ 매치하는데 까지 (또는 매치실패하는 경우 까지) 의 스탭 수 ]가 표시됩니다.
    (스탭수는 정규표현엔진의 내부적인 처리스탭의 숫자 입니다.)

    아래는 느린 정규표현으로 제일먼저 소개해드린 _a____ 와 (_+|\w+)*a의 실행 결과 입니다.
    이 경우, 매치하는데까지 491스탭이 걸렸습니다.

    Screen Shot 2016-02-22 at 08.44.00.png

    스탭수는 입력한 텍스트에 따라 바뀝니다.
    예를들어 _a_____________________와 같은 텍스트를 입력해 봅시다.
    (이 사이트라면 도중에 중단시키므로 실행해도 괜찮습니다.)

    Kobito.rOJd0I.png

    보시는것과 같이 붉은 박스에 ERROR라고 표시됩니다.
    ERROR가 된 것은 스탭수가 너무 많기 떄문입니다.
    즉, 이것은 퍼포먼스가 매우 나쁜 정규표현이라는 것이 알게 됩니다.

    이런 느낌으로 정규표현이나 텍스트를 여러가지 바꿔가면서 퍼포먼스의 좋고 나쁨이 판단 됩니다.

    덧붙이자면 스탭수가 표시되는것은 화면의 우측에있는 FLAVOR로부터 "pcre(php)" 가 "python"을 불러 들일 때 뿐입니다.


    Kobito.NwrljS.png

    그렇기 때문에 표시되는 스탭수와 Ruby나 JS에서 실행했을 경우의 스탭수는 전혀 일치 하지 않을 수도 있습니다.
    그렇다고 하지만, 퍼포먼스의 체크로서 활용하는 것 이라면, 언어나 환경에 관계없이 충분히 참고정도는 할 수 있겟죠.
    왜냐하면 대체의 정규표현엔진은 NFA(Non determistic Finite Automaton) 라고 불리는 방식을 사용하고 있고, 동작원리는 어느것이든 대체로 같습니다.

    그외, 알아두면 도움이되는 지식
    자, [손과 눈으로 기억하는 정규표현입문]도 거의 마무리 입니다.
    여기서부터는 지금까지 설명해왔던 [알아두면 도움이되는 지식] 을 소개합니다.

    매우 어려운 지식을 설명하는 것이 아니기떄문에, 각 세션은 간단한 설명으로 그치겠습니다.

    메타문자의 에스케이프
    제1회의 기사로부터 여기까지 뒤돌아 보면, 꽤 많은 메타문자를 소개해 왔습니다.
    정규표현을 쓸때 특별히 언급되는 문자를 모아보면 아래와 같습니다.

    - \ (\t, \w 등에서 사용함 )
    - ^
    - $
    - *
    - +
    - ?
    - .
    - |
    - {, }
    - (, )
    - [, ]
    - / (/abc/ 와 같은 기술이 정규표현 리터럴이 되는 언어의 경우)

    그러나 상황에 따라서는 순수히 "(abc)"라는 문자열이나, "+" 라고하는 기호를 정규표현중에 검색하고 싶을 경우도 있습니다.
    그럴경우에는 백슬러쉬를 붙여서 특수한 문자를 에스케이프 합니다.

    예를들어 "users[100]" 이나 "users[123]"이라고 하는 문자열이 있고 "[100]" 이나 "[123]"의 부분만 매치시키고 싶을경우에는 "[\d+\]"라고 적습니다.


    Kobito.dxLRfS.png


    []는 그대로 사용한다면 메타문자가 됩니다만, \[ \]와 같이 백슬러쉬로 에스케이프한다면 순수히 "[]"라고하는 문자에 매치하게 됩니다.
    이것은 그외의 메타문자 ( 특별 취급되는 문자 )에 대해서도 같습니다.

    자주 있을법한 잘못에는 "user.rb"나 "base.css"와 같은 확장자가 달린 파일명을 검색할때 \w+.\w{1,3}와 같이 작송하고 맙니다.
    피리오드(.)는 [임의의 1개문자]의 의미이므로 위의 정규표현식이라면 "user#rb" 나 "base%css"의 경우에도 매치되고 맙니다.


    Kobito.M7kL1d.png



    확장자가 달린 파일명ㅇ르 검색하려고할 때에는 \w+\.\w{1,3} 와 같이 피리오드를 잊지말고 에스케이프 해주세요.


    Kobito.qlhBDi.png


    []안에서 움직임이 바뀌는 메타문자와 바뀌지 않는 메타문자
    언젠가 1문자를 나타내는 [] 의 안에 메타문자가 들어가면, 메타문자가아니라 [ 일반 문자 ]로 취급되는 것이 있습니다.
    예를들어 [()$.*+?|{}] 라고 작성하면 ["(" 나 ")"나 "$" 이거나 .... 중 어느1문자 ]의 의미가 됩니다.

    예를들어 이런 텍스트를 사용하여 확인해 봅시다.


    begin
      5.times { |n| puts (-10 * n + 1 / 0).zero? ^ true }
    rescue
      puts $!
    end


    이 텍스트에 [()$.*+?|{}] 라는 정규표현으 입력해 봅시다.
    그러면, 메타문자의 움직임이 없어지고, [일반 문자]처럼 각종 기호에 매치되는 것이 보일 것 입니다.


    Kobito.YgJg1m.png


    반면 [\w\d\s\n] 라고 적는경우에는 [ 영단어를 구성하는 문자, 또는 반각 숫자, 또는 공백문자, 또는 개행문자의 어느것 1개문자 ]의 의미가 되어메타문자로서 움직임이 남아 있습니다.
    아래는 그 결과 입니다.


    Kobito.B71A0Q.png


    자 그럼, 남은것은 -외 ^ 입니다.
    이전의 기사에서도 설명했지만, 이것은 [] 안의 위치에 따라 의미가 바뀝니다.
    [a-z]로 작성하면 ["a" 또는 "b" ... 또는 "z" 중 어느것 한문자]의 의미가 됩니다.
    그러나 [-az]나 [az-]로 작성하는 경우에는 ["a" 또는 "z" 또는 "-" 중 어느것 1문자]의 의미가 됩니다.

    [^abc]는 ["a" 도아니고 "b"도 아니고 "c" 도아닌 임의의 문자 ]를 의미합니다. ( 부정조건을 나타냅니다 )
    그러나, [abc^]와 같은 선두이외의 장소에 ^를 적는 경우에는 ["a" 또는 "b" 또는 "c" 또는 "^" 중 어느것 하나 ]의 의미가 됩니다.

    덤: [\b]는 백스페이스 문자를 나타냄
    본기사의 전반은 [단어의 환경]을 나타내는 \b 라고하는 메타문자를 소개했습니다.
    이것을 [] 의 내부에 포함시켜 [\b] 와 같이 작성하면 단어의 경계가 아닌 [ 백스페이스의 문자 (0x08)] 로 사용됩니다.
    ....그치만 백스페이스 문자를 검색하는 기회는 거의 없을것이라 생각되기 때문에 딱히기억할 필요는 없습니다.
    (적어도 저는 필요를 느낀적이 없습니다.)

    [n 개이상]이나 [n개 이하]를 지정
    제1회의 기사에서 {n}이나 {n,m}으로 [ 직전의 문자가 n개 이상 ]또는 [직전의 문자가 n개이상 m개이하] 로 어떤 패턴을 설명했습니다.
    그 바리에이션으로 {n}이나 {n,m} 이라는 작성방법이 있습니다.
    이것은 각각 [ 직전의 문자가 n개 이상 ] 과 [ 직전의 문자가 n개이하 ]의 의미가 됩니다.
    별로 실용적인 예는 아니지만 다음과 같은 텍스트에서 확인해 봅시다.


    google
    gooogle
    goooogle
    gooooogle
    goooooogle



    go{4,}gle 이라는 정규표현을 입력하면 ["o"가 4개이상]의 경우 매치됩니다.


    Kobito.KSaNSt.png

    go{,3}gle 라고 입력하면, ["o"가 3개이하 ]의 경우 매치합니다


    Kobito.SjY3IJ.png



    소문자와는 반대의 의미가되는 \W, \S, \D, \B
    지금까지 \w, \s, \d, \b 라는 메타문자를 소개 했습니다.
    정규표현에는 이것들의 반대의 이미가 되는 \W, \S, \D, \B 가 존재합니다.
    각각의 메타문자의 의미는 다음과 같습니다.
    - \W : 영단어의 구성문자 이외( 기호나 공백문자 등)
    - \D : 반각숫자 이외
    - \S : 공백문자 이외
    - \B : 단어의 경계 이외의 위치

    마지막의 \B는 조금 의미가 이해하기 힘들 지도 모르겠습니다만, 전반부에 등장한 예문을 사용하여 확인해 본다면 의미가 이해될 지도 모르겠습니다.


    sounds that are pleasing to the ear.
    ear is the organ of the sense of hearing.
    I can't bear it.
    Why on earth would anyone feel sorry for you?


    \Bear\B 를 Rubular에 입력하면 이렇게 됩니다.


    Kobito.F575rV.png



    \B는 [단어의 경계 이외의 위치] 이므로, 스페이스나 피리오드가 좌우에 없는 'hearing'의 'ear'가 매치됩니다.

    정리
    본 기사에서는 아래와 같은 것을 공부했습니다.
    - \b는 단어의 경계를 나타냄
    - (?=abc)는 [abc라는 문자열의 바로 직전의 위치]를 나타냄 ( 먼저 읽기 )
    - (?<=abc)는 [abc라는 문자열의 바로 이후의 위치]를 나타냄 ( 나중에 읽기 )
    - (?!abc)는 [abc라는 문자열이외의 직전에 위치] 를 나타냄 ( 부정의 먼저 읽기 )
    - (?<!abc)는 [abc라는 문자열 이외의 직후에 위치]를 나타냄 ( 부정의 나중에 읽기 )
    - 캡쳐한 문자열은 정규표현내에서도 \1 이나 \2 로서 번호로 참조 가능하다 ( 후방참조 )
    - ? 나 *, + 와같은 수지정자는 () 의 뒤에 붙이는 것도 가능
    - | 을 사용한 OR 조건에서는 조건안에서도 메타문자가 사용가능
    - 작성하는 방법에 따라 말도안되게 느린 정규표현이 작성될 수도 있음
    - 메타문자는 백슬래쉬로 에스케이프한다
    - [] 안에서 메타문자의 종류나 사용되는 위치에 따라 각 문자의 움직임이 달라짐
    - {n,} 나 {,n}은 각각 [직전의 문자가 n개이상][n개이하]의 의미를 가집
    - \W, \S, \D, \B는 각각 \w, \s, \d, \b의 반대의 의미를 가집

    이러한 내용은 [때때로 정규표현을 사용해]라고 생각하는 사람이라도 전부는 이해못하고 있을 수 도있습니다.
    그렇기에 여기까지 마스터 한다면 [정규표현 사용자]라고 해도 충분할정도의 스킬을 익히신 것 입니다.

    또한 다른 언어나 환경에 따라서도 조금더 다양한 메타문자나 테크닉을 사용하는 것도 가능합니다.
    조금도 지식을 늘리고싶으신 분은 자신이 사용하는 언어나 실행환경의 정규표현 도큐먼트를 읽어 주세요.

    자 그럼 [ 손과 눈으로 기억하는 정규표현 입문 ]은 이번회로 종료입니다.
    제1회 부터 읽어오신 분은 정규표현을 이해 할 수 있게되셧나요?

    이 기획에서는 [정규표현 진짜 좋아! 재밋어!] 라고 생각하실 수 있도록 예제를 만드는것으 중점을 두었습니다.
    단순히 정규표현의 룰을 이해하는것 뿐 아니라, [그렇구나, 이런 사용법이 가능하구나] [ 확실히 이럴때 사용하면 편리하네 ] 라고 느끼실 만한 예제를 찾아 보았습니다.

    이기사 들을 한번 읽어보시고 [ 나도 한번 정규표현을 써보자 ]라고 생각하신다면 정말 기쁩니다.
    혹시 뭔가 잘 이해가 안되는  것이 있으면 코멘트를 달아주세요.

    자 그럼 여기까지 읽어주셔서 정말 감사합니다.


    반응형

    댓글

Designed by Tistory.