방문자 카운터나 조회수를 직접 구현하면 금방 부딪히는 문제가 있습니다. 사람은 몇 명 안 왔는데 숫자가 쑥쑥 올라가요. 로그를 열어 보면 대부분 검색엔진 크롤러입니다. 네이버 Yeti, Googlebot, Bingbot 같은 봇이 페이지를 한 번씩 긁어 갈 때마다 조회수가 같이 올라가는 거죠. 그래서 조회수 증가, 방문 통계 저장, 팝업 노출 같은 처리 앞에 "이 요청이 봇인가"를 한 번 거르는 함수를 두는 경우가 많습니다.
아래 함수는 그 용도로 퍼져 있는 코드입니다. 알려진 크롤러 이름을 배열로 모아 두고, 이걸 | 로 이어 붙여 정규식 하나로 만든 다음 User-Agent에 하나라도 걸리면 true 를 돌려줍니다.
function checkRobot($ua){
if(empty($ua)){ $ua = $_SERVER['HTTP_USER_AGENT']; }
$list = array(
"Yeti",
"YandexBot",
"Googlebot",
"Cowbot",
"NaverBot",
"Daum",
"Daumoa",
"TechnoratiSnoop",
"Allblog\.net",
"CazoodleBot",
"nhn\/1noon",
"Feedfetcher\-Google",
"Yahoo\! Slurp",
"msnbot",
"bingbot",
"MSNBot",
"Technoratibot",
"sproose",
"CazoodleBot",
"ONNET\-OPENAPI",
"UCLA CS Dept",
"Snapbot",
"DAUM RSS Robot",
"RMOM",
"S20 Wing",
"FeedBurner",
"xMind",
"openmaru feed aggregator",
"ColFeed",
"MJ12bot",
"Twiceler",
"ia_archiver",
"BingPreview", // 2022-02-06 추가
"NetcraftSurveyAgent", // 2022-02-06 추가
"Bot", // 2022-03-28 추가
"bot", // 2022-03-28 추가
);
$check = "/(".implode("|",$list).")/";
if(preg_match($check, $_SERVER['HTTP_USER_AGENT'])){
return true;;
}else{
return false;
}
}
$botchk = checkRobot(); // true or false
목록을 보면 Allblog\.net, nhn\/1noon, Feedfetcher\-Google 처럼 정규식 특수문자 앞에 역슬래시가 붙어 있습니다. 배열 항목이 그대로 패턴에 들어가기 때문에 점(.)이나 슬래시(/)를 이스케이프하지 않으면 엉뚱한 문자열에 매칭되거나, 구분자 / 와 충돌해서 패턴 자체가 깨집니다. 새 봇 이름을 추가할 때도 이 규칙을 지켜야 합니다.
끝에 추가된 Bot, bot 두 줄이 사실상 대부분을 잡아 줍니다. 요즘 크롤러는 이름에 bot을 넣는 경우가 많아서 앞쪽 목록의 상당수(Googlebot, bingbot, YandexBot 등)는 이 두 줄만으로도 걸러집니다. 반대로 Yeti나 ia_archiver처럼 이름에 bot이 없는 크롤러는 개별 항목이 있어야 잡히고요.
그대로 쓰면 에러가 나는 이유
이 코드는 그대로 복사해 쓰면 문제가 두 가지 있습니다.
하나는 호출 방식입니다. 함수는 checkRobot($ua) 로 매개변수를 받게 되어 있는데 기본값이 없습니다. 그런데 사용 예는 checkRobot() 처럼 인자 없이 부르죠. PHP 7.1부터는 이렇게 필수 인자를 빼고 호출하면 경고가 아니라 ArgumentCountError 예외가 발생해서 페이지가 멈춥니다. 예전 PHP에서는 경고만 뜨고 넘어가서 몰랐던 문제가, 서버 PHP 버전을 올리고 나서 갑자기 터지는 전형적인 경우입니다.
다른 하나는 검사 대상입니다. 맨 위에서 $ua 가 비어 있으면 서버 값을 채워 넣지만, 정작 preg_match 에는 $ua 가 아니라 $_SERVER['HTTP_USER_AGENT'] 를 직접 넘깁니다. 그래서 로그에 저장된 User-Agent 문자열을 인자로 넘겨 검사하려고 해도 그 값은 무시되고 현재 요청의 값만 봅니다. CLI에서 돌리거나 User-Agent 헤더 없이 들어온 요청이면 이 키가 아예 없어서 Undefined index 경고도 납니다.
그 밖에 return true;; 의 세미콜론 두 개는 빈 문장일 뿐이라 동작에는 영향이 없습니다. CazoodleBot 이 두 번 들어 있는 것도 마찬가지로 해는 없습니다.
고친 버전
위 두 가지를 손본 코드입니다. 매개변수에 기본값을 주고, 실제 검사도 $ua 로 하게 바꿨습니다. 대소문자를 무시하는 i 플래그를 붙이면 Bot/bot 처럼 두 번 적을 필요도 없지만, 원래 목록을 살리기 위해 플래그만 추가하고 항목은 중복된 CazoodleBot 하나만 뺐습니다.
function checkRobot($ua = ''){
if(empty($ua)){
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
}
if($ua === ''){
return false;
}
$list = array(
"Yeti", "YandexBot", "Googlebot", "Cowbot", "NaverBot", "Daum", "Daumoa",
"TechnoratiSnoop", "Allblog\.net", "CazoodleBot", "nhn\/1noon",
"Feedfetcher\-Google", "Yahoo\! Slurp", "msnbot", "bingbot", "MSNBot",
"Technoratibot", "sproose", "ONNET\-OPENAPI", "UCLA CS Dept", "Snapbot",
"DAUM RSS Robot", "RMOM", "S20 Wing", "FeedBurner", "xMind",
"openmaru feed aggregator", "ColFeed", "MJ12bot", "Twiceler", "ia_archiver",
"BingPreview", "NetcraftSurveyAgent", "Bot", "bot",
);
$check = "/(".implode("|",$list).")/i";
return preg_match($check, $ua) === 1;
}
$botchk = checkRobot(); // 현재 요청 검사
// $botchk = checkRobot($row['user_agent']); // 저장된 값 검사
User-Agent가 비어 있는 요청을 봇으로 볼지 사람으로 볼지는 용도에 따라 다릅니다. 위 코드는 빈 값이면 false(사람)로 처리했는데, 조회수 집계라면 오히려 true 로 돌려서 세지 않는 쪽이 더 정확할 수 있어요. User-Agent는 클라이언트가 마음대로 바꿀 수 있는 값이라 이 함수는 통계를 깨끗하게 하는 정도로만 쓰고, 보안 차단 용도로는 믿지 않는 게 맞습니다.