정규표현식은 어렵다.
반드시 정규표현식만 고집할 필요는 없다.
모든 걸 전부 정규표현식으로 구현하면 어렵지만,
includes 메서드를 적절히 활용하면 정규표현식 자체는 어렵게 구성하지 않아도 된다.
REGEX_WORD
는 숫자와 특수기호들을 찾는 정규식이다. \d
는 숫자를 찾고 \W
는 특수기호(알파벳, 숫자,언더바(_)
가 아닌 문자들)를 찾는다. REGEX_WORD는 나중에 split의 매개변수로 넣어서 숫자와 특수기호를 제외한 문자들을 찾을 때 사용한다. REGEX_URL
은 외부 링크를 찾을 때 사용될 정규식이다. \S
는 공백이 아닌 하나의 문자와 매칭된다. *
를 붙여줘서 연속적으로 찾을 수 있게 해준다. REGEX_URL은 match 메서드의 매개변수로 넣을 정규식이다. META_URL
은 메타 태그에 존재하는 내 url을 찾을 때 사용된다. pageInfo에는 { 자기 url, 기본점수, 외부 링크, 각각의 인덱스, 매칭 점수 }가 담긴다.
page를 \n
으로 분리한다. \n을 정규식에서 쓰면 얘는 이 자체로 인식된다. 즉, \\n
뭐 이런 식으로 안 써도 된다. (pageArr
)
그리고 META_URL이 있는 위치를 찾아서 자기 url를 찾는다(urlIdx
=> pageURL
)
body 태그가 시작하는 부분과 끝나는 부분을 찾아서, body 내용을 추출한 다음에, .flatMap((str) => str.toLowerCase().split(REGEX_WORD))
를 사용해서 특수 기호를 제외한 나머지 중에서, .filter((e) => e === word).length;
를 사용해서 일치하는 단어의 개수를 측정해준다.
REGEX_URL을 사용해서 외부 링크들도 구해준다.
이렇게 해서 각 pageURL가 가지고 있는 pageInfo를 구해준다.
pageInfo를 반복하면서 점수를 계산해주고 이를 바탕으로 matchPoint를 구해준다.
만약 겹치는 경우 우선하는 인덱스를 답으로 찾아야 하기 때문에 sort를 해준다.
function solution(word, pages) {
word = word.toLowerCase();
const REGEX_WORD = /[\d|\W]/;
const REGEX_URL = /<a href="https:\S*"/gi;
const META_URL = "meta property";
const pageInfo = new Map();
pages.forEach((page, idx) => {
const pageArr = page.split("\n");
const urlIdx = pageArr.findIndex((el) => el.includes(META_URL));
const pageURL = pageArr[urlIdx].match(/"https:\S*"/gi)[0];
const bodyStart = pageArr.findIndex((el) => el.includes("<body>"));
const bodyEnd = pageArr.findIndex((el) => el.includes("</body>"));
const body = pageArr.slice(bodyStart + 1, bodyEnd);
const point = body
.flatMap((str) => str.toLowerCase().split(REGEX_WORD))
.filter((e) => e === word).length;
const outLinks = body
.flatMap((str) => str.match(REGEX_URL))
.filter((e) => e) // null 제거하기 위해
.map((e) => e.substr(8, e.length)); // url만 남기기 위해
pageInfo.set(pageURL, { point, outLinks, idx, matchPoint: 0 });
});
for (const [key, value] of pageInfo) {
const linkPoint = value.point / value.outLinks.length;
for (const link of value.outLinks) {
if (pageInfo.has(link)) {
const origin = pageInfo.get(link);
const calculatedPoint = origin.matchPoint
? origin.matchPoint + linkPoint
: origin.point + linkPoint;
pageInfo.set(link, { ...origin, matchPoint: calculatedPoint });
}
}
}
const answer = [];
for (const [key, value] of pageInfo) {
const { point, idx, matchPoint } = value;
const finalPoint = matchPoint ? matchPoint : point;
answer.push([idx, finalPoint]);
}
answer.sort((a, b) => (a[1] === b[1] ? a[0] - b[0] : b[1] - a[1]));
return answer[0][0];
}
찾아야 할 데이터가 많을 때 map을 활용하면 좋다.
하나의 데이터에 달려 있는 수많은 데이터들을 객체로 묶어 매칭되도록 하기 좋은 구조를 가지고 있기 때문이다.
Array.prototype.flatMap()
각 요소에 대해 map을 진행하고 결과를 새로운 배열로 평탄화해준다.
let arr1 = ["it's Sunny in", "", "California"];
arr1.map(x=>x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]
arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in","","California"]
flatMap은 map을 하는 과정에서 아이템을 추가하거나 제거하여 아이템 개수를 바꿀 수 있다. 즉, 각각의 아이템에 대하여 1:1대응관계 뿐만 아니라 다대다 대응도 가능하다는 것다. 이러한 측면에서 filter가 하는 역할의 반대역할을 한다고 볼 수 있다. 단순히 아무런 변화를 주고 싶지 않을때에는 원소 하나를 가진 배열을 반환할 수도, 아이템을 추가하고 싶을 때는 다-원소 배열을 반환할 수도, 아이템을 제거하고 싶을 때에는 빈 배열을 반환할 수도 있다.
// 다음은 음수는 제거하고 홀수는 1과 짝수로 분리하는 예시입니다.
let a = [5, 4, -3, 20, 17, -33, -4, 18]
// |\ \ x | | \ x x |
// [4,1, 4, 20, 16, 1, 18]
a.flatMap( (n) =>
(n < 0) ? [] :
(n % 2 == 0) ? [n] :
[n-1, 1]
)
// expected output: [4, 1, 4, 20, 16, 1, 18]
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
flatMap은 reduce와 concat을 조합해서 구현할 수 있다.
var arr1 = [1, 2, 3, 4];
arr1.flatMap(x => [x * 2]);
// 다음과 동일합니다
arr1.reduce((acc, x) => acc.concat([x * 2]), []);
// [2, 4, 6, 8]
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
대괄호 안에서의 ^
는 not을 표현한다.
function solution(word, rawPages) {
word = word.toLowerCase();
return rawPages
.map((rawPage, index) => {
const [_, url] = rawPage.match(
/<meta property="og:url" content="([^"]+)"\/>/i
);
const tags = rawPage.match(/<[^>]+>/g);
const basicScore = tags
.reduce((raw, tag) => raw.replace(tag, ""), rawPage)
.split(/[^a-zA-Z]/)
.filter((w) => w.toLowerCase() == word).length;
const outUrls = tags.reduce((links, tag) => {
const result = tag.match(/<a href="([^"]+)">/i);
return result ? links.concat(result[1]) : links;
}, []);
return {
index,
url,
outUrls,
basicScore,
};
})
.map((page, index, pages) => {
page.linkScore = pages
.filter((p) => p.outUrls.some((url) => url == page.url))
.reduce((sum, p) => sum + p.basicScore / p.outUrls.length, 0);
page.totalScore = page.linkScore + page.basicScore;
return page;
})
.sort((a, b) =>
a.totalScore == b.totalScore
? a.index - b.index
: b.totalScore - a.totalScore
)[0].index;
}
^x
: 문자열의 시작을 표현하며 x 문자로 시작됨을 의미한다.
x$
: 문자열의 종료를 표현하며 x 문자로 종료됨을 의미한다.
.x
: 임의의 한 문자의 자리수를 표현하며 문자열이 x 로 끝난다는 것을 의미한다.
x+
: 반복을 표현하며 x 문자가 한번 이상 반복됨을 의미한다.
x?
: 존재여부를 표현하며 x 문자가 존재할 수도, 존재하지 않을 수도 있음을 의미한다.
x*
: 반복여부를 표현하며 x 문자가 0번 또는 그 이상 반복됨을 의미한다.
x|y
: or 를 표현하며 x 또는 y 문자가 존재함을 의미한다.
(x)
: 그룹을 표현하며 x 를 그룹으로 처리함을 의미한다.
(x)(y)
: 그룹들의 집합을 표현하며 앞에서 부터 순서대로 번호를 부여하여 관리하고 x, y 는 각 그룹의 데이터로 관리된다.
(x)(?:y)
: 그룹들의 집합에 대한 예외를 표현하며 그룹 집합으로 관리되지 않음을 의미한다.
x{n}
: 반복을 표현하며 x 문자가 n번 반복됨을 의미한다.
x{n,}
: 반복을 표현하며 x 문자가 n번 이상 반복됨을 의미한다.
x{n,m}
: 반복을 표현하며 x 문자가 최소 n번 이상 최대 m 번 이하로 반복됨을 의미한다.
[xy]
: 문자 선택을 표현하며 x 와 y 중에 하나를 의미한다.
[^xy]
: not 을 표현하며 x 및 y 를 제외한 문자를 의미한다.
[x-z]
: range를 표현하며 x ~ z 사이의 문자를 의미한다.
\^
: escape 를 표현하며 ^ 를 문자로 사용함을 의미한다.
\b
: word boundary를 표현하며 문자와 공백사이의 문자를 의미한다.
\B
: non word boundary를 표현하며 문자와 공백사이가 아닌 문자를 의미한다.
\d
: digit 를 표현하며 숫자를 의미한다.
\D
: non digit 를 표현하며 숫자가 아닌 것을 의미한다.
\s
: space 를 표현하며 공백 문자를 의미한다.
\S
: non space를 표현하며 공백 문자가 아닌 것을 의미한다.
\t
: tab 을 표현하며 탭 문자를 의미한다.
\v
: vertical tab을 표현하며 수직 탭(?) 문자를 의미한다.
\w
: word 를 표현하며 알파벳 + 숫자 + _
중의 한 문자임을 의미한다.
\W
: non word를 표현하며 알파벳 + 숫자 + _
가 아닌 문자를 의미한다.
Flag
g
: Global 의 표현하며 대상 문자열내에 모든 패턴들을 검색하는 것을 의미한다.
i
: Ignore case 를 표현하며 대상 문자열에 대해서 대/소문자를 식별하지 않는 것을 의미한다.
m
: Multi line을 표현하며 대상 문자열이 다중 라인의 문자열인 경우에도 검색하는 것을 의미한다.
|
: or
[]
: 괄호 안 문자 중 하나
[^문자]
: 괄호 안 문자 제외
^문자열
: 특정 문자열로 시작
문자열$
: 특정 문자열로 끝남
?
: 없거나 최대 1개
*
: 없거나 있거나 (여러 개)
+
: 최소 1개 또는 여러 개
*?
: 없거나 최대 1개 ({0}
과 동일)
+?
: 최소 1개 ({1}
과 동일)
{n}
: n개
{min, }
: 최소 min 개 이상
{min, max}
: 최소 min 개 이상, 최대 max 개 이하
()
: 그룹화 및 캡쳐
(?: 패턴)
: 그룹화 (캡쳐 X)
(?=)
: 앞쪽 일치 /ab(?=c)/
(?!)
: 부정 앞쪽 일치 /ab(?!c)/
(?<=)
: 뒤쪽 일치 /(?<=ab)c/
(?<!)
: 부정 뒤쪽 일치 /(?<!ab)c/
// 리터럴 방식
const regex = /abc/;
// 생성자 방식
const regex = new RegExp("abc");
const regex = new RegExp(/abc/);
("문자열").match(/정규표현식/플래그)
: "문자열"에서 "정규표현식"에 매칭되는 항목들을 배열로 반환
("문자열").replace(/정규표현식/, "대체문자열")
: "정규표현식"에 매칭되는 항목을 "대체문자열"로 변환
("문자열").split(정규표현식)
: "문자열"을 "정규표현식"에 매칭되는 항목으로 쪼개어 배열로 반환 (해당 정규표현식을 제외하고 찾고 싶을 때)
(정규표현식).test("문자열")
: "문자열"이 "정규표현식"과 매칭되면 true, 아니면 false반환
(정규표현식).exec("문자열")
: match메서드와 유사(단, 무조건 첫번째 매칭 결과만 반환)
혹은
("문자열").includes(/정규표현식/플래그)
("문자열").includes("문자열")
[프로그래머스] LV.3 매칭점수 (JS)
정규표현식 (Regex) 정리
[JS] 📚 정규표현식(RegExp) - 이해하기 쉽게 정리 + 응용 예제