트랙
/
jq
jq
/
연습 문제
/
정규 표현식 챗봇
정규 표현식 챗봇

정규 표현식 챗봇

학습 연습 문제

소개

정규 표현식

정규 표현식(regex)은 텍스트에서 검색 패턴을 지정하는 문자들의 나열이에요.

정규 표현식 문법을 배우는 것은 이 주제의 범위를 벗어나요. 여기서는 jq가 정규 표현식을 활용하기 위해 제공하는 표현식에 집중할게요.

정규 표현식의 종류

도구마다 서로 다른 버전의 정규 표현식을 구현해요. jq는 Oniguruma 정규 표현식 라이브러리를 사용하는데, 이 라이브러리는 Perl v5.8 정규 표현식과 대체로 호환돼요.

jq가 사용하는 구체적인 문법은 Oniguruma GitHub 저장소에서 확인할 수 있어요.

Caution

jq에는 정규 표현식을 위한 특별한 문법이 없어요. 정규 표현식은 그냥 문자열로 표현돼요. 따라서 정규 표현식에 있는 백슬래시는 문자열 안에서 이스케이프해야 해요.

예를 들어 숫자 문자 클래스(\d)는 "\\d"로 써야 해요.

정규 표현식 함수

jq에서 정규 표현식은 몇 가지 필터로 제한돼요.

단순 매칭

문자열이 패턴과 일치하는지 알아야 할 때는 test 필터를 사용해요.

STRING | test(REGEX)
STRING | test(REGEX; FLAGS)
STRING | test([REGEX, FLAGS])

이 필터는 불리언 결과를 출력해요.

"Hello World!" | test("W")    # => true
"Goodbye Mars" | test("W")    # => false
매치에 대한 정보

패턴과 실제로 일치한 부분 문자열을 추출해야 할 때는 match 필터를 사용해요.

STRING | match(REGEX)
STRING | match(REGEX; FLAGS)
STRING | match([REGEX, FLAGS])

이 필터는 다음을 출력해요.

  • 일치하는 것이 없으면 아무것도 출력하지 않아요.
  • 일치하는 것이 있으면 여러 속성을 담은 객체를 출력해요.

이 예제는 역참조 문법 \1을 사용해서 연속으로 나오는 똑같은 모음 두 개를 찾아요.

"Hello World!" | match("([aeiou])\\1")
# => empty

"Goodbye Mars" | match("([aeiou])\\1")
# => {
#      "offset": 1,
#      "length": 2,
#      "string": "oo",
#      "captures": [
#        {
#          "offset": 1,
#          "length": 1,
#          "string": "o",
#          "name": null
#        }
#      ]
#    }

match 필터는 일치할 때마다 객체를 반환해요. 이 예제는 모든 모음을 찾기 위해 "g" 플래그가 어떻게 동작하는지 보여줘요.

"Goodbye Mars" | match("[aeiou]"; "g")
# => { "offset": 1, "length": 1, "string": "o", "captures": [] }
#    { "offset": 2, "length": 1, "string": "o", "captures": [] }
#    { "offset": 6, "length": 1, "string": "e", "captures": [] }
#    { "offset": 9, "length": 1, "string": "a", "captures": [] }
캡처된 부분 문자열

match 필터와 비슷하게, capture 필터는 일치하는 것이 있으면 객체를 반환해요.

STRING | capture(REGEX)
STRING | capture(REGEX; FLAGS)
STRING | capture([REGEX, FLAGS])

반환된 객체는 이름 있는 캡처들의 매핑이에요.

"JIRAISSUE-1234" | capture("(?<project>\\w+)-(?<issue_num>\\d+)")
# => {
#      "project": "JIRAISSUE",
#      "issue_num": "1234"
#    }
부분 문자열만 얻기

scan 필터는 "g" 플래그를 사용한 match와 비슷해요.

STRING | scan(REGEX)
STRING | scan(REGEX; FLAGS)

scan은 부분 문자열의 _스트림_을 출력해요.

"Goodbye Mars" | scan("[aeiou]")
# => "o"
#    "o"
#    "e"
#    "a"

부분 문자열을 모으려면 [...] 배열 생성자를 사용해요.

"Goodbye Mars" | [ scan("[aeiou]") ]
# => ["o", "o", "e", "a"]
문자열 나누기

문자열에서 남기고 싶은 부분을 알고 있다면 match나 scan을 사용해요. 버리고 싶은 부분을 알고 있다면 split을 사용해요.

STRING | split(REGEX; FLAGS)
Caution

인자가 하나인 split 필터는 인자를 고정 문자열로 취급해요.

split에서 정규 표현식을 사용하려면 두 번째 인자를 반드시 제공해야 해요. 빈 문자열을 사용해도 괜찮아요.

임의의 공백을 기준으로 문자열을 나누는 예제예요.

"first   second           third fourth" | split("\\s+"; "")
# => ["first", "second", "third", "fourth"]
치환

sub와 gsub 필터는 입력 문자열에서 일치한 부분을 대체 문자열로 바꿔서 입력 문자열을 변형할 수 있어요.

첫 번째 일치만 바꾸려면 sub 필터를 사용해요. 모든 일치를 바꾸려면 gsub 필터를 사용해요.

STRING | sub(REGEX; REPLACEMENT)
STRING | sub(REGEX; REPLACEMENT; FLAGS)
STRING | gsub(REGEX; REPLACEMENT)
STRING | gsub(REGEX; REPLACEMENT; FLAGS)
"Goodnight kittens. Goodnight mittens." | sub("night"; " morning")
# => "Good morning kittens. Goodnight mittens."

"Goodnight kittens. Goodnight mittens." | gsub("night"; " morning")
# => "Good morning kittens. Good morning mittens."

대체 문자열에서는 일치한 부분 문자열을 참조할 수 있어요. 이름 있는 캡처와 문자열 보간을 사용하면 돼요.

"Some 3-letter acronyms: gnu, csv, png"
| gsub( "\\b(?<tla>[[:alpha:]]{3})\\b";     # find words 3 letters long
        "\(.tla | ascii_upcase)" )          # upper-case the match
# => "Some 3-letter acronyms: GNU, CSV, PNG"

플래그

위의 모든 필터에서 FLAGS는 지원되는 플래그 중 0개 이상으로 이루어진 문자열이에요.

  • g - 전역 검색 (첫 번째만이 아니라 모든 일치를 찾아요)
  • i - 대소문자를 구분하지 않는 검색
  • m - 여러 줄 모드 ('.'가 줄 바꿈도 일치해요)
  • n - 빈 일치 무시
  • p - s 모드와 m 모드를 모두 활성화해요
  • s - 한 줄 모드 ('^' -> '\A', '$' -> '\Z')
  • l - 가능한 가장 긴 일치를 찾아요
  • x - 확장 정규 표현식 형식 (공백과 주석을 무시해요)

예를 들어

"JIRAISSUE-1234" | capture("(?<project>\\w+)-(?<issue_num>\\d+)")

# or with Extended formatting

"JIRAISSUE-1234" | capture("
                     (?<project>   \\w+ )  # the Jira project
                     -                     # followed by a hyphen
                     (?<issue_num> \\d+ )  # followed by digits
                   "; "x")

지침

여러분은 챗봇을 개발하는 회사에서 정규 표현식 전문가로 일하게 되었어요.

아직 개발이 아주 초기 단계예요. 여러분의 임무는 정규 표현식을 사용해서 챗봇이 자연어를 이해하고 생성하는 능력을 향상시키는 것이에요.

1. 유효한 명령 확인하기

챗봇은 똑똑할 뿐만 아니라 충실한 비서이기도 해요. 챗봇에게 무언가를 물어보려면 명령의 맨 앞에 "Chatbot"이라는 단어를 말해야 해요. 키워드가 대문자든 소문자든 상관없어요. 중요한 것은 단어의 위치예요.

사용자가 명령을 내리는지 챗봇이 알아볼 수 있도록 is_valid_command 함수를 구현해요.

"Chatbot, play a song from the 80's." | is_valid_command
# => true
"Hey Chatbot, where is the closest pharmacy?" | is_valid_command
# => false
"CHATBOT, do you have a solution for this challenge?" | is_valid_command
# => true

2. 암호화된 이모지 제거하기

챗봇은 사람들이 감정을 표현하려고 이모지를 어떻게 사용하는지 이해하는 데 어려움을 겪어요. 챗봇이 사용자 메시지를 받으면 각 이모지는 "emoji" 문자열 뒤에 _id 번호_가 붙은 형태로 표현돼요.

문자열을 받아서 메시지 전체에 있는 모든 이모지를 제거하는 remove_emoji 메서드를 구현해요.

이모지가 없는 줄은 그대로 반환해야 해요. 이모지 문자열만 제거해요. 공백은 그대로 둬요.

"I love playing videogames emoji3465 it's one of my hobbies" | remove_emoji
# => "I love playing videogames  it's one of my hobbies"

3. 유효한 전화번호 확인하기

챗봇과 대화하다 보면 사용자가 전화번호를 알려줄 때가 있어요. 챗봇은 특정 형식의 번호로만 전화를 걸 수 있어요.

check_phone_number 함수를 구현해요.

번호가 유효하면 챗봇은 사용자에게 감사하고 번호를 확인해 주는 메시지로 답해요. 번호가 유효하지 않으면 챗봇은 전화번호가 유효하지 않다고 사용자에게 알려줘요.

기대하는 형식은 (+NN) NNN-NNN-NNN이고, 여기서 N은 숫자예요.

"chatbot my phone number is (+34) 659-771-594" | check_phone_number
# => "Thanks! Your phone number is OK."
"chatbot, call me at 659-771-594" | check_phone_number
# => "Oops, it seems like I can't reach out to 659-771-594."

4. 웹사이트 링크 가져오기

챗봇은 정말 호기심 많은 소프트웨어예요. 특정 주제에 대해 인터넷을 검색할 수 있으면서도, 관련 정보를 찾기 위해 방문할 만한 멋진 웹사이트를 사용자에게 묻는 것을 좋아해요.

대화 예시:

챗봇: 안녕하세요, 저는 JavaScript로 코딩하는 법을 배우고 싶어요. 제가 배울 수 있는 멋진 웹사이트를 아시나요?

사용자: 저는 exercism.org에서 많이 배웠어요. 거기에 좋은 내용이 정말 많아요.

웹사이트 도메인 배열을 반환하는 get_domains 함수를 구현해요.

"I learned a lot from exercism.org and google.com" | get_domains
# => ["exercism.org", "google.com"]

5. 사용자에게 인사하기

예의 바른 챗봇은 사용자를 이름으로 부르며 말해요. 사용자가 자신을 소개하면 우리 챗봇은 그 이름을 알아내고 다정하게 인사해요.

nice_to_meet_you 함수를 작성해요.

입력 문자열에 "My name is Someone."이라는 문장이 포함되어 있으면 이름을 캡처해서 "Nice to meet you, Someone."이라는 문자열을 반환해요.

"My name is Jean-Luc" | nice_to_meet_you
# => "Nice to meet you, Jean-Luc"

6. 아주 간단한 CSV 파싱하기

"기능 증식증"에 굴복한 김에, 챗봇에 CSV 파싱 함수를 추가해 볼게요.

문자열을 받아서 파싱된 필드들의 배열을 반환하는 parse_csv 함수를 구현해요. 필드 구분자는 "쉼표와 그 뒤에 올 수 있는 공백"이에요.

CSV 형식의 여러 예외적인 경우(예를 들어 필드에 쉼표가 포함된 경우)는 걱정하지 않아요.

"first, second,third,   fourth" | parse_csv
# => ["first", "second", "third", "fourth"]
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
jq Exercism

정규 표현식 챗봇 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 jq 트랙을 개념 12개연습 문제 75개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.