thenullpage.com

회원가입 폼에서 이메일 형식을 확인하는 코드, 본문에서 링크만 골라내는 코드. 필요할 때마다 검색해서 남이 올려둔 정규식을 복사해 쓰신 적 있으실 거예요. 문제는 그게 어디까지 잡고 어디서부터 새는지 모른 채 붙여 넣게 된다는 점이에요. 오늘은 자주 쓰는 패턴을 쓸 수 있는 형태로 모으고 각각이 놓치는 구멍까지 볼게요.


10.1 이메일 검사는 어디까지 해야 하나요?

실무에서 제일 많이 쓰는 형태는 이거예요.


import re
re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", "문의는 [email protected] 로")
결과: ['[email protected]']


골뱅이 앞에는 글자와 점, 더하기, 하이픈을 허용하고 뒤에는 도메인과 점 하나 이상을 요구해요. 점이 꼭 있어야 하니 hong@localhost는 걸러져요.


그런데 알아둘 게 있어요. 완벽한 이메일 정규식은 존재하지 않아요. 규격을 그대로 옮기면 수천 글자가 되는데, 그걸 써도 그 주소가 살아 있는지는 알 수 없어요. 그래서 정규식은 오타를 거르는 1차 관문으로만 쓰고 진짜 검증은 인증 메일로 해요. 패턴을 욕심내 조이면 멀쩡한 사용자만 가입을 못 해요.


10.2 본문에서 링크만 뽑으려면요?

제일 짧은 형태는 https?://[^\s]+예요. s가 있어도 없어도 되게 ?를 붙이고 공백이 아닌 글자를 계속 먹어요.


글 = "안내는 https://thenullpage.com/ko/. 옛 주소는 (http://old.site.com/a) 예요"
re.findall(r"https?://[^\s]+", 글)
결과: ['https://thenullpage.com/ko/.', 'http://old.site.com/a)']


문장 끝 마침표와 닫는 괄호가 딸려 왔어요. 공백만 빼고 다 먹으라 했으니 시킨 대로 한 거예요.


re.findall(r"https?://[^\s)\]]+", 글)
결과: ['https://thenullpage.com/ko/.', 'http://old.site.com/a']


괄호는 제외 목록에 넣어 해결했어요. 마침표는 주소 중간에도 들어가서 무조건 뺄 수가 없으니, 뽑은 뒤에 .rstrip(".,!?")로 꼬리만 털어내는 게 깔끔해요. 정규식으로 다 하려 들지 않는 것도 요령이에요.


10.3 아이피 주소는 어떻게 잡나요?

대충 잡는 패턴은 이렇게 생겼어요.


로그 = "접속 192.168.0.15 실패 999.1.2.3"
re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", 로그)
결과: ['192.168.0.15', '999.1.2.3']


숫자와 점을 세 번 반복한 건데 999.1.2.3도 통과해요. 각 칸은 0에서 255까지인데 자릿수만 셌으니까요. 범위까지 지키려면 칸 하나를 이렇게 정의해요.


칸 = r"(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)"
아이피 = r"\b" + (칸 + r"\.") * 3 + 칸 + r"\b"
re.findall(아이피, 로그)
결과: ['192.168.0.15']


250에서 255, 200에서 249, 100에서 199, 0에서 99를 따로 적어 세로줄로 이었어요. 이렇게 패턴을 변수로 쪼개서 조립하면 긴 정규식도 읽을 만해져요.


칸을 (?:...)로 묶은 이유도 중요해요. 그냥 (...)로 적으면 findall이 주소 전체가 아니라 괄호에 잡힌 조각만 돌려주거든요.


10.4 휴대폰 번호는 하이픈이 있기도 없기도 한데요?

하이픈 자리마다 ?를 붙여 있어도 없어도 되게 만들어요.


re.findall(r"01[016789]-?\d{3,4}-?\d{4}", "연락처 010-1234-5678 또는 01098765432")
결과: ['010-1234-5678', '01098765432']


앞자리는 실제 쓰는 것만 묶었고 가운데는 세 자리와 네 자리를 모두 받게 했어요.


다만 구멍이 있어요. 010-12345678처럼 하이픈이 한쪽에만 있는 것도 통과해요. 두 자리를 똑같이 맞추라고 시킨 적이 없으니까요.


이럴 땐 패턴을 더 꼬지 말고 순서를 바꾸세요. 하이픈을 먼저 지우고 숫자만 남겨 검사하는 거예요.


번호 = re.sub(r"[-\s]", "", "010-1234 5678")
re.fullmatch(r"01[016789]\d{7,8}", 번호)
결과: <re.Match object; span=(0, 11), match='01012345678'>


fullmatch문자열 전체가 패턴과 딱 맞을 때만 결과를 줘요. 뒤에 글자가 더 붙으면 실패하니 입력값 검증에는 이쪽이 안전해요.


10.5 날짜는 형식만 맞으면 되나요?

형식과 실제로 있는 날짜는 다른 문제예요.


re.fullmatch(r"\d{4}-\d{2}-\d{2}", "2026-13-45")
결과: <re.Match object; span=(0, 10), match='2026-13-45'>


13월 45일이 통과했어요. 숫자를 두 개씩 세라고 했지 월이 12까지라고는 안 알려줬거든요. 범위를 좁혀볼게요.


날짜 = r"\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])"
re.fullmatch(날짜, "2026-13-45")
결과: None
re.fullmatch(날짜, "2026-02-30")
결과: <re.Match object; span=(0, 10), match='2026-02-30'>


13월은 막았는데 2월 30일이 통과했어요. 달마다 마지막 날이 다르고 윤년까지 있으니 정규식이 감당할 일이 아니에요. 진짜 있는 날짜인지는 날짜 도구에 물어봐요.


from datetime import datetime
datetime.strptime("2026-02-30", "%Y-%m-%d")
결과: ValueError: day 30 must be in range 1..28 for month 2 in year 2026


정규식으로 모양만 거르고 값이 말이 되는지는 전용 도구에 맡기는 게 실무에서 제일 흔한 조합이에요.


10.6 HTML 태그를 지우는 패턴도 있나요?

글 미리보기에서 본문만 뽑을 때 자주 써요.


re.sub(r"<[^>]+>", "", "<p>오늘은 <b>정규식</b> 이야기</p>")
결과: '오늘은 정규식 이야기'


여는 꺾쇠부터 닫는 꺾쇠까지를 지웠어요. .+ 대신 [^>]+를 쓴 게 핵심인데, 점을 쓰면 첫 태그부터 마지막 태그까지 한 번에 삼켜 글자가 다 날아가요.


그런데 본문에 부등호가 섞이면 사고를 쳐요.


re.sub(r"<[^>]+>", "", "if (a<b) print(a>b)")
결과: 'if (ab)'


부등호 사이 코드가 통째로 사라졌어요. 그러니 이건 믿을 수 있는 문서에만 쓰세요. 사용자 글에서 위험한 태그를 거르는 보안 필터로 쓰는 건 특히 위험한데, 꺾쇠를 겹쳐 쓰면 얼마든지 피해 갈 수 있거든요.


10.7 이 패턴들을 어떻게 모아두나요?

쓸 때마다 찾아 헤매지 않으려면 한군데 모아 이름을 붙여두세요.


패턴 = {
"이메일": re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"),
"휴대폰": re.compile(r"01[016789]-?\d{3,4}-?\d{4}"),
}
패턴["휴대폰"].findall("문의 [email protected] 이나 010-1234-5678")
결과: ['010-1234-5678']


미리 만들어두면 쓸 때마다 다시 해석하지 않아 반복 작업에 유리하고, 이름으로 부를 수 있어서 읽는 사람이 뭘 찾는지 바로 알아요.


오늘 나온 패턴은 전부 완벽하지 않고 완벽할 필요도 없어요. 마무리는 인증 메일이나 날짜 도구, 전용 라이브러리가 하고 정규식은 그 앞에서 후보를 추리면 충분해요. 복사해 온 패턴을 그냥 믿지 말고 일부러 틀린 값을 몇 개 넣어보세요. 어디까지 잡히고 어디서 새는지 직접 보고 나면 그때부터 그 패턴은 내 것이 돼요.