Hey, Scripting Guy!모나리자의 눈썹과 정규식

Scripting Guys

이 기사의 코드 다운로드: HeyScriptingGuy2008_05.exe (150KB)

지난 2007년 11월 , Scripting Guys는 Tech•Ed IT Forum 컨퍼런스 참가를 위해 바르셀로나로 가는 길에 파리에서 하루 머물게 되었습니다. 하루뿐인 짧은 시간이었지만 세계적인 예술의 전당, 루브르 박물관을 방문하는 소중한 기회를 얻었습니다.

루브르는 어땠냐구요? 찾는 건 어렵지 않았습니다. 노트르담 대성당까지 걸어가서 왼쪽으로 꺾으면 바로 있으니까요.

아, 루브르 박물관이 마음에 들었는지를 물은 겁니까? 예, 대체로 좋았습니다. 단, 여느 박물관이 그렇듯이 눈으로만 보고 직접 만져 보지는 말라는 식으로 운영하는 건 마음에 들지 않았습니다. 모나리자에게 눈썹이 있었다면 분명 더 아름다웠을 텐데 이상하게도 루브르 박물관에서 일하는 사람들은 그림을 좀 고쳐 보려고 하면 화부터 낼테지요.

참고: 사실 Scripting Guys는 모나리자에 무척 감동을 받았습니다. 소문난 잔치집에 먹을 것 없다고, 이름만 유명하고 그저 그런 그림은 아닐까 생각했지만 눈으로 보는 것만으로도 놀라운 경험이었습니다. 눈썹이 없는 건 아쉬웠지만 말이죠. 그런데 밀로의 비너스는 유명세에 비해 실망스러웠습니다. 우리 중에 그 작품에 크게 감명 받은 사람도 없었을 뿐더러, 필자는 밀로의 비너스를 보는 순간 당황스럽기까지 했습니다. 팔 없는 여자 동상이라니요? 그러면 청소나 설거지는 대체 누가 한단 말입니까?

혹시 이 칼럼을 읽는 여성 독자분이 계시다면(남아 있는지 모르겠지만) 위에 한 말은 단순한 오타일 뿐이라는 점을 밝히고 싶습니다. 실제로는 "팔 없는 여자 동상이라니요? 그래도 어쨌든 남자보다 두 배는 더 빨리 효과적으로 일할 수 있겠지만요"라고 말하려던 것이었습니다.

오해의 소지가 있었던 점 사과 드립니다.

좌우간에 루브르 박물관의 소중한 문화 유산들을 두 눈으로 직접 보는 와중에 두 명의 Scripting Guys의 뇌리에 같은 생각이 스쳤습니다. 화장실은 어디에 있지? 그리고 화장실을 찾아 헤매는 동안 필자는 Scripting Guys가 위선자라는 생각이 들었습니다. 루브르 박물관에서 모나리자에 눈썹을 그려넣지 못하도록 하는 데 발끈하면서, 우리도 똑같은 잘못을 저지르고 있는 것 같아서 말입니다. 2008년 1월호 TechNet Magazine에서 우리는 스크립트에 정규식을 사용하는 방법에 대해 다루었습니다. 그런데 그 칼럼이야말로 '보기만 하고 건드리지는 말라'는 태도의 전형이었습니다. 정규식을 사용하여 텍스트 파일의 문제를 파악하는 방법을 보여 주었지만 문제 해결 방법은 소개하지 않았으니까요. 아! 어떻게 그런 일이!

참고: 2007년 11월에 루브르 박물관에 갔다면서 어떻게 2008년 1월에 TechNet Magazine에 실렸던 기사에 대한 생각이 불현듯 떠오를 수 있었을까요? 그것 참 수수께끼입니다. 아마도 이곳 레드먼드와 파리의 시차 때문이 아닐까요?

다행히 루브르 박물관을 운영하는 사람들과 달리 Scripting Guys는 실수를 인정할 줄 압니다. 정규식을 사용하여 항목을 검색하는 방법만 소개하고 정규식으로 항목을 바꾸는 방법은 빠뜨린 것은 분명 잘못이었습니다. 그림 1과 같은 스크립트를 소개했어야 했습니다.

Figure 1 찾기 및 바꾸기

      Set objRegEx = _
    CreateObject("VBScript.RegExp")

objRegEx.Global = True   
objRegEx.IgnoreCase = True
objRegEx.Pattern = "Mona Lisa"

strSearchString = _
    "The Mona Lisa is in the Louvre."
strNewString = _
    objRegEx.Replace(strSearchString, _
                     "La Gioconda")

Wscript.Echo strNewString 

사실 여기서 설명할 내용은 지극히 간단한 정규식 사용 예라고 할 수 있습니다. Mona Lisa라는 문자열 값을 모두 La Gioconda("눈썹을 어디에 두었더라?"라는 뜻의 이탈리아어)로 바꾸기만 할 겁니다. 물론 VBScript Replace 함수를 사용하면 훨씬 쉽게 바꾸기 작업을 수행할 수 있지만, 이 간단한 스크립트를 통해 정규식을 사용하여 찾기 및 바꾸기 작업을 수행하는 방법을 살펴볼 수 있습니다. 그리고 스크립트에 대한 설명이 끝나면 정규식을 사용하여 보다 멋진 기능을 구현하는 방법도 몇 가지 보여 드리도록 하겠습니다.

보시다시피 이 스크립트는 매우 간단합니다. 먼저 VBScript.RegExp 개체의 인스턴스를 만듭니다. 잘 알겠지만 이 개체는 VBScript 스크립트에서 정규식을 사용할 수 있도록 해 줍니다. 이 개체를 만든 후에는 다음과 같은 개체의 세 가지 속성에 값을 할당합니다.

Global 이 속성을 True로 설정하면 스크립트가 대상 텍스트에서 Mona Lisa라는 문자열을 모두 찾아 바꾸고, Global 속성을 False(기본값)로 설정하면 스크립트가 Mona Lisa의 첫 번째 인스턴스만 찾아 바꿉니다.

IgnoreCase IgnoreCase를 True로 설정하면 스크립트가 검색 시에 대/소문자를 구분하지 않습니다. 즉, mona lisa와 Mona Lisa가 동일하게 취급됩니다. VBScript에서는 기본적으로 대/소문자를 구분하여 검색을 수행합니다. 즉, 대문자와 소문자의 차이로 인해 mona lisa와 Mona Lisa는 완전히 다른 값으로 인식됩니다.

Pattern Pattern 속성에는 우리가 찾고자 하는 값이 들어갑니다. 이 예에서는 Mona Lisa라는 간단한 문자열 값을 찾으려고 합니다.

다음으로, strSearchString이라는 변수에 검색 대상 텍스트를 할당합니다.

strSearchString = "The Mona Lisa is in the Louvre."

그리고 정규식 메서드 Replace를 호출하여 이 메서드에 검색 대상 텍스트(strSearchString 변수)와 대체 텍스트(La Gioconda)를 매개 변수로 전달합니다. 해당 코드는 다음과 같습니다.

strNewString = objRegEx.Replace(strSearchString, "La Gioconda")

이것으로 끝입니다. 수정된 텍스트가 strNewString 변수에 저장되고 StrNewString 값을 출력하면 다음과 같은 결과를 얻게 됩니다.

The La Gioconda is in the Louvre.

문법에는 맞지 않지만 코드가 어떻게 작동하는지 이해하는 데에는 무리가 없습니다.

앞서 설명한 것처럼 이 방법도 문제는 없지만 너무 복잡합니다. 다음의 훨씬 간단한 코드로도 똑같은 결과를 얻을 수 있으니까요. 사실 마음만 먹으면 코드 한 줄로도 충분히 가능합니다.

strSearchString = "The Mona Lisa is in the Louvre."
strNewString = Replace(strSearchString, "Mona Lisa", "La Gioconda")
Wscript.Echo strNewString

이제부터는 VBScript의 Replace 함수로는 불가능하고 정규식으로만 가능한 작업을 구현해 보겠습니다.

좋은 예가 없을까요? 이건 어떻습니까? Scripting Guys는 서로 다른 형식의 문서 간에 텍스트를 복사하는 경우가 많습니다. 복사가 잘될 때도 있지만 그렇지 않을 때도 있습니다. 특히 단어 간격과 관련해서 예상치 못한 문제가 발생하여 아래와 같은 결과를 얻게 되는 때가 있습니다.

Myer Ken, Vice President, Sales and Services

이상하게 불필요한 공백이 많이 들어가 있는 것을 볼 수 있습니다. 이 경우 Replace 함수의 사용이 제한됩니다. 왜냐고요? 불필요한 공백이 무작위로 들어가기 때문입니다. 즉, 단어 사이에 공백이 7개 들어갈 수도 있고 2개나 6개가 들어갈 수도 있습니다. 때문에 Replace를 사용해서는 문제를 수정하기가 어렵습니다. 예를 들어 2개의 연속된 공백을 검색하여 공백 하나로 바꾸는 경우 다음과 같은 결과를 얻게 됩니다.

Myer Ken, Vice President, Sales and  Services

좀 나아 보이긴 하지만 완벽하다고는 할 수 없습니다. 완벽한 결과를 얻으려면 임의의 개수만큼 공백(예: 39개)을 검색하여 바꾼 후 처음 개수에서 하나가 적은 38개의 공백을 검색하여 다시 바꾸는 식의 과정을 반복해야 합니다. 그런데 정규식 스크립트를 사용하면 다음과 같이 훨씬 간단하고 안전하게 같은 작업을 수행할 수 있습니다.

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = " {2,}"

strSearchString = _
"Myer Ken, Vice President, Sales and Services"
strNewString = objRegEx.Replace(strSearchString," ")

Wscript.Echo strNewString

이 스크립트의 핵심(또한 대부분의 정규식 스크립트의 핵심)은 바로 Pattern입니다.

objRegEx.Pattern = " {2,}"

이 코드에서는 2개 이상의 연속된 공백을 찾습니다. 이 Pattern이 2개 이상의 공백을 찾는지 어떻게 알 수 있을까요? 큰따옴표 안에 공백 하나와 {2,} 형식의 문자열이 있는 것을 보면 알 수 있습니다. 정규식 구문에서 이 문자열은 앞에 있는 문자가 2개 연속으로 있는 경우를 찾으라는 의미입니다. 그렇다면 공백이 3개, 4개 또는 937개 연속으로 있으면 어떻게 될까요? 그러한 공백도 모두 문제 없이 검색됩니다. (만약 2개 이상, 8개 이하의 공백을 찾으려 한다면 {2,8}이라는 구문을 사용하면 됩니다. 여기서 8은 일치 항목의 최대 수를 나타냅니다.)

즉, 서로 인접해 있는 공백을 2개 이상 발견할 때마다 연속된 공백이 모두 검색되어 단일 공백으로 바뀝니다. 앞서 살펴본 불필요한 공백이 많은 문자열 값에 이 코드를 적용하면 어떻게 될까요? 바로 다음과 같은 결과가 얻어집니다.

Myer Ken, Vice President, Sales and Services

완벽하지 않습니까? 역시 Scripting Guys는 뭐든지 척척 고쳐내는군요. 루브르 박물관에서도 우리를 좀 믿고 Mona Lisa를 수정하도록 맡겨 주면 좋을 텐데 말이죠.

이제 흥미롭지만 매우 일반적인 시나리오를 하나 살펴보겠습니다. 회사에서 사용하는 전화번호부에 전화 번호가 모두 다음과 같은 형식으로 수록되어 있다고 가정해 보겠습니다.

555-123-4567

그런데 상사가 모든 전화 번호를 다음과 같은 형식으로 바꾸라고 합니다.

(555) 123-4567

이 경우 대체 전화 번호 형식을 어떻게 바꿔야 할까요? 우리가 그렇게 대담한 사람들이라면 다음과 같은 스크립트를 사용하라고 조언해 줄 겁니다.

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString, "($1) $2-$3")

Wscript.Echo strNewString

이 코드는 3자리 숫자(\d{3})와 대시, 또 다른 3자리 숫자와 대시, 그리고 4자리 숫자를 검색합니다. 즉, 다음과 같은 형식을 검색하는 것입니다. 여기서 X는 0 ~ 9 사이의 숫자를 나타냅니다.

XXX-XXX-XXXX

참고: \d{3}이 이어진 3개의 숫자를 찾도록 한다는 것은 어디서 알게 됐냐고요? 흠, 잘 기억은 안 나지만 어디서 읽은 것 같군요. 사실 다빈치 코드의 인상 깊은 마지막 장이나 MSDN® 온라인의 VBScript 언어 참조(go.microsoft.com/fwlink/?LinkID=111387 참조)에서 본 것이 분명합니다.

이제 편리하게도 정규식을 사용하여 임의의 전화 번호를 검색할 수 있습니다. 하지만 아직 큰 문제가 하나 남아 있습니다. 이렇게 검색한 임의의 전화 번호를 똑같은 형식의 다른 임의의 전화 번호로 바꾸는 것이 아니라 똑같은 번호를 형식만 약간 다르게 바꾸어야 하기 때문입니다. 이 결과를 얻으려면 대체 어떻게 해야 할까요?

바로 다음과 같은 대체 텍스트를 사용하면 됩니다.

"($1) $2-$3"

여기서 $1, $2, $3은 정규식 "역참조"의 좋은 예입니다. 역참조란 저장한 후에 재사용할 수 있는 검색된 텍스트의 일부분을 말합니다. 이 스크립트 예제에서는 다음 세 가지 "하위 검색 항목"을 검색합니다.

  • 3자리 숫자
  • 다음 3자리 숫자
  • 4자리 숫자

이러한 각각의 하위 검색 항목에는 자동으로 역참조가 할당됩니다. 즉, 첫 번째 하위 검색 항목은 $1 두 번째는 $2와 같은 식으로 $9까지 할당됩니다. 따라서 이 스크립트에서는 전화 번호의 세 부분에 그림 2와 같이 역참조가 자동으로 할당됩니다.

Figure 2 전화 번호 역참조

전화 번호 부분 역참조
555 $1
123 $2
4567 $3

올바른 전화 번호가 재사용되도록 하기 위해 예제 스크립트의 대체 문자열에 이러한 역참조를 사용합니다. 예제의 대체 텍스트는 첫 번째 역참조($1)를 가져와 괄호 안에 넣고 공백을 하나 추가한 다음 두 번째 역참조($2)와 대시를 추가합니다. 그리고 마지막으로 세 번째 역참조($3)를 넣습니다.

그러면 결과는 어떻게 될까요? 바로 다음과 같은 전화 번호를 얻게 됩니다.

(555) 123-4567

다시 한번 깔끔하게 문제를 해결했습니다.

그럼 다음으로 이 전화 번호 스크립트를 변형한 예제를 하나 살펴보겠습니다. 조직에서 새 전화 시스템을 설치하고 시스템 교체 작업의 일환으로 원래 666, 777, 888 등 여러 가지 국번으로 시작하던 전화 번호를 333이라는 국번으로 통일하려고 합니다. 전화 번호 형식과 국번을 동시에 변경할 방법은 없을까요? 물론 있습니다.

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\d{3})-(\d{3})-(\d{4})"

strSearchString = "555-123-4567"
strNewString = objRegEx.Replace _
(strSearchString,"($1) 333-$3")

Wscript.Echo strNewString

자, 이 코드에서 어떤 작업이 이루어졌는지 알겠습니까? 교체 텍스트에서 이전 국번(역참조 $2)을 제거하고, 그 대신 하드 코딩되고 표준화된 333이라는 국번 값을 넣습니다. 이 수정된 스크립트를 실행한 후 555-123-4567라는 전화 번호는 어떻게 바뀔까요? 바로 다음과 같이 됩니다.

(555) 333-4567

역참조의 일반적인 사용 예를 하나 더 소개하겠습니다. 다음과 같은 문자열 값이 있습니다.

Myer, Ken

이 문자열 값을 뒤집어 이름을 다음과 같이 표시할 방법은 있을까요?

Ken Myer

물론 방법이 없다면 필자가 실없는 사람이 되겠죠? 다음 스크립트를 사용하면 됩니다.

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "(\S+), (\S+)"

strSearchString = "Myer, Ken"
strNewString = objRegEx.Replace _

strSearchString,"$2 $1")

Wscript.Echo strNewString

이 스크립트는 특정 단어(\S+)와 쉼표, 공백, 그리고 다른 단어가 연속되어 있는 경우를 찾습니다. (이 예제에서는 \S+라는 구문으로 "단어"를 나타냅니다.) \S+라는 구문은 연속된 비공백 문자를 나타냅니다. 즉, 공백, 탭 문자 또는 캐리지 리턴 줄바꿈을 제외한 글자, 숫자, 기호 등의 어떠한 문자도 여기에 포함될 수 있습니다. 예제에서는 성($1)을 나타내는 하위 검색 항목과 이름($2)을 나타내는 하위 검색 항목이 검색됩니다. 따라서 다음 구문을 사용하면 사용자 이름을 '이름 성'의 형식으로 표시할 수 있습니다.

"$2 $1"

쉼표는 어떻게 된 것일까요? 수정된 표시 형식에서는 필요 없으므로 버린 것입니다.

참고: 흠, 어찌된 일인지 이 시점에서 Scripting Editor를 염두에 두게 되는군요. 갑자기 왜 그럴까요.

마지막으로 예를 하나만 더 소개하고 오늘은 이만 마치도록 하겠습니다. (아, 예. 맞습니다. "이번 달"은 이만 마치도록 하겠습니다.) 이 코드는 간단하지만은 않습니다. 그러나 필자도 이런 입문 단계의 기사에서 너무 복잡한 내용을 다룰 생각은 없습니다. (경우에 따라 정규식은 매우 복잡해질 수 있습니다.) 각설하고, 다음 스크립트는 대부분의 경우에 0000.34500044와 같은 값에서 앞에 있는 0을 제거합니다.

Set objRegEx = CreateObject("VBScript.RegExp")

objRegEx.Global = True
objRegEx.Pattern = "\b0{1,}\."

strSearchString = _
"The final value was 0000.34500044."
strNewString = objRegEx.Replace _
strSearchString,".")

script.Echo strNewString

여기서도 "\b0{1,}\"라는 패턴이 스크립트의 핵심입니다. 먼저 단어 경계(\b)를 찾습니다. 이는 100.546 같은 값에서 0을 제거하는 일이 없도록 하기 위함입니다. 그리고 하나 이상의 0(0{1,})과 소수점(\.)을 찾습니다. 이러한 패턴이 발견되면 해당하는 0과 소수점을 단일 소수점 "."으로 바꿉니다. 스크립트가 문제 없이 실행된다면 문자열이 다음과 같이 변환됩니다.

The final value was .34500044.

이번 달에 준비한 내용은 여기까지입니다. 마치기 전에 여담을 하나 하자면 모나리자는 물감이 채 마르기도 전에 벌써 큰 논란을 불러일으켰습니다. 대체 이 신비한 여인이 누구인가? 왜 이렇듯 오묘한 미소를 짓고 있는가? 눈썹은 왜 없는가? 미술사학자 중에는 모나리자가 여자가 아니라 레오나르도 다빈치의 자화상이라고 주장하는 사람까지 있었습니다. (만약 이게 사실이라면 재봉사를 바꾸라고 심각하게 조언해 주고 싶군요.) 그런 와중에 Unarius Educational Foundation에서는 한술 더 떠서 사실 모나리자는 "상위 세계"에 있는 레오나르도의 "다른 인격"이며, 이 또 다른 인격이 레오나르도의 손을 빌어 그림을 그렸다고 주장했습니다. 놀랍게도 이번 달 Hey, Scripting Guy!도 바로 이런 방법으로 쓰여졌습니다.

다시 말하면 불만이 있다면 제 2의 microsoft.com에서 칼럼을 쓰는 Scripting Guys의 다른 인격에게 보내시라는 말입니다. 감사합니다.

Dr. Scripto의 Scripting Perplexer

매달 퍼즐 풀이 실력뿐만 아니라 스크립팅 기술까지 시험해 볼 수 있는 문제입니다.

2008년 5월: 스크립트-도쿠

이번 달에는 스도쿠를 약간 변형한 퀴즈를 준비했습니다. 일반적인 스도쿠의 모눈 안에 있는 1 ~ 9 사이의 숫자를 Windows PowerShell™ cmdlet을 구성하는 문자와 기호로 바꾸었습니다. 퀴즈를 다 풀면 행 중의 하나가 cmdlet 이름이 됩니다.

참고: 스도쿠를 푸는 방법을 모르는 독자는 인터넷을 검색하면 스도쿠에 대해 설명한 수천 개가 넘는 웹 사이트를 찾을 수 있으므로 참조하시기 바랍니다. 따라서 여기서는 따로 설명하지 않겠습니다.

ANSWER:

Dr. Scripto의 Scripting Perplexer

답: 스크립트-도쿠, 2008년 5월

Scripting Guys는 Microsoft에 고용된 직원들로, 좋아하는 야구 경기와 기타 여러 활동을 하는 시간을 제외하고는 항상 TechNet 스크립트 센터를 운영합니다. 자세한 내용은 www.scriptingguys.com에서 확인하십시오.

© 2008 Microsoft Corporation 및 CMP Media, LLC. All rights reserved. 이 문서의 전부 또는 일부를 무단으로 복제하는 행위는 금지됩니다..