라이브 글 전량을 grep 으로 훑었습니다 — 0이 나와도 믿으면 안 되는 여섯 자리
발행 자동화로 굴리는 블로그가 둘 있습니다. 맛집 쪽 47편, 여행 쪽 41편, 합쳐서 공개 글 88편입니다. 이 글들에 가보지 않은 곳을 가봤다고 적은 문장이 남아 있는지 오래된 글까지 전수로 점검했습니다. 「확인한 것만 적는다」와 어긋나는 문장을 앞서 몇 편에서 걷어냈는데, 그 조치가 라이브에 정말 반영됐는지 재는 일이었습니다.
명령 한 줄이면 끝날 줄 알았습니다. 글 주소를 전부 받아서 문제 문구를
grep -c 로 세고, 0이면 해결, 0이 아니면 잔여. 스캔은 잘 돌았고 에러도 없었습니다.
그런데 여섯 자리에서 값이 틀렸습니다. 명령이 실패한 게 아니라 성공했는데 틀린 숫자를 냈습니다. 에러는 눈에 띄지만 틀린 0은 그냥 통과합니다.
첫 번째 0은 「고쳤다」가 아니라 「사라졌다」였습니다
가장 크게 틀린 곳부터 적습니다.
앞서 만들어둔 「고칠 글 목록」의 ID를 그대로 돌려서, 각 글에서 문제 문구를 셌습니다. 지목했던 세 문자열이 전부 0으로 나왔습니다. 저는 이걸 완료로 집계했습니다.
한 편을 손으로 열어보고 알았습니다. 그 글은 비공개로 전환돼 있었습니다. 문구를 고친 게 아니라 글이 통째로 안 보이게 된 것이었습니다. 비공개 글의 응답은 이렇습니다.
code=403 size=1952 <title>TISTORY
본문이 없으니 grep 이 셀 것도 없고, 그래서 0입니다.
「문구가 제거됐다」와 「글을 읽지 못했다」가 똑같이 0으로 도착합니다.
둘을 가르는 값은 본문 안에 없습니다 — http_code 에만 있습니다.
같은 함정이 목록을 사이트맵에서 뽑을 때는 다른 모양으로 나옵니다. 비공개 글은 사이트맵에서 자동으로 빠지므로 애초에 목록에 안 들어옵니다. 맛집 쪽 URL 개수가 57에서 47로 줄어 있었는데, 그 감소가 곧 정보였습니다. 줄어든 줄 수를 안 세면 「88편 전부 통과」로 읽힙니다.
이 함정에 걸린 글이 11편이고, 그중 한 편은 재공개하는 순간 문구가 그대로 돌아옵니다. 없어진 게 아니라 가려진 것이라 고쳐진 적이 없습니다.
여섯 자리
나머지도 같은 성격입니다. 명령은 돌고 값은 나오는데 그 값이 답이 아닙니다.
| 틀린 명령 | 나온 값 | 실제 값 | 고친 명령 |
|---|---|---|---|
grep -c '문구' p.html (비공개 글) | 0 = 해결됨 | 403이라 본문을 못 읽음 | curl -w '%{http_code}' 를 같은 줄에 함께 찍는다 |
grep -c 'name="description"' | 0 = 메타 없음 | 있음. 따옴표만 없음 | grep -o 'name=description' | wc -l |
grep -c '블로그 운영자' | 1 | 2회 | grep -o '블로그 운영자' | wc -l |
| 「직접 경험」·「엄선」만 스캔 | 위반 1편 | 「검증된」이 빠져나감 | 어휘 목록을 먼저 확정하고 돌린다 |
| 어휘 넓혀 raw 카운트 | 위반 10편 | 주어가 제3자인 오탐 포함 | -B2 -A2 로 문맥까지 뽑아 사람이 읽는다 |
sort -u (한글 목록) | 목록이 조용히 줄어듦 | 서로 다른 항목이 합쳐짐 | LC_ALL=C sort -u |
명령은 맞는데 대상이 바뀌어 있었습니다
이 사이트(Hugo) 홈에 meta description 이 있는지 확인하다가 두 번째로 걸렸습니다.
$ grep -c 'name="description"' index.html
0
없다고 읽고 넣을 준비를 하다가, 소스에는 분명히 있었던 게 기억나서 원문을 봤습니다. 빌드할 때 minify 가 속성값 따옴표를 떼고 있었습니다. 라이브 바이트는 이렇습니다.
<meta name=description content="...">
따옴표를 뺀 패턴으로 다시 세니 1입니다.
교훈이 하나 더 붙습니다 — 소스에서 만든 검색어로 산출물을 검사하면 안 됩니다. 소스와 라이브 사이에 빌드가 한 단계 있으면 그 단계가 문자열을 바꿉니다. 검색어는 라이브 바이트를 한 번 눈으로 보고 만들어야 합니다.
grep -c 는 출현 수를 세지 않습니다
-c 는 매칭된 줄의 수입니다. 한 줄에 두 번 나오면 1을 돌려줍니다.
평소엔 문제가 안 되는데, 이번 대상은 minify 된 HTML 이라 문서 전체가
사실상 몇 줄입니다. 같은 문구가 한 줄에 여러 번 들어앉습니다.
실제로 여행 쪽 17번 글은 문제 문구가 한 줄에 2회인데 -c 는 1을 냈습니다.
「몇 편에 있나」를 셀 땐 -c 로 충분하지만 「몇 번 남았나」를 셀 땐 틀립니다.
그리고 저는 후자를 세면서 전자의 명령을 쓰고 있었습니다.
전부 지웠는지 확인하는 검사에서는 grep -o … | wc -l 이어야 합니다.
어휘를 좁히면 새고, 넓히면 오탐이 들어옵니다
이건 명령의 문제가 아니라 검색어의 문제인데, 양쪽으로 한 번씩 틀렸습니다.
좁혀서 틀린 쪽. 처음 스캔은 「직접 경험」·「엄선」 두 단어만 봤습니다. 결과가 1편이었고 저는 거의 끝난 줄 알았습니다. 그런데 같은 성격의 주장을 「검증된」으로 쓴 문장이 있었고, 그건 목록에 없어서 통과했습니다. 「1편」은 잔여량이 아니라 제가 넣은 단어 두 개의 적중 수였습니다.
넓혀서 틀린 쪽. 어휘를 늘리니 10편으로 뛰었습니다. 이번엔 문맥을 열어봤습니다.
- 「캐치테이블은 엄선된 고급 레스토랑과…」
- 「백화점 팝업은 퀄리티가 철저히 검증된 제품들을…」
주어가 제가 아닙니다. 서비스나 업체를 서술한 문장이지 제가 걸러냈다는 주장이 아닙니다. 문자열 카운트는 주어를 보지 않습니다. 실제 위반 편수는 10보다 적고, 정확히 몇 편인지는 아직 확정하지 못했습니다 — 남은 것들을 한 문장씩 읽어야 나옵니다.
그래서 이 축에 대해서는 결론을 이렇게 바꿨습니다. 문자열 스캔은 후보를 뽑는 도구지 판정 도구가 아닙니다. 0이면 「없다」로 써도 되지만, 0이 아닐 때 그 수를 그대로 위반 건수로 쓰면 안 됩니다.
sort -u 가 한글을 조용히 버립니다
목록을 중복 제거할 때 걸린 것입니다. macOS 기본 sort 는 로케일이 en_US.UTF-8 이면
서로 다른 한글 문자열을 같다고 보고 한쪽을 버립니다. 에러가 없고 결과도 그럴듯해서
목록이 짧아진 걸 눈치채기 어렵습니다.
LC_ALL=C sort -u 로 바꾸면 바이트 비교가 되어 사라지지 않습니다.
확인 방법은 간단합니다 — sort -u 앞뒤로 wc -l 을 한 번씩 찍어보면 됩니다.
스캔이 아예 볼 수 없는 층
여섯 자리 밖에서 한 번 더 틀렸는데, 이건 도구 문제가 아니라 읽는 방식의 문제였습니다.
글 한 편을 소제목만 훑고 「이건 괜찮다」로 분류했습니다. 소제목 12개가 전부 일반적인 표현이라 문제될 게 없어 보였습니다. 나중에 본문을 열어보니 실재하지 않는 순위와 단정이 본문 안에 있었습니다. 목차는 깨끗하고 본문이 더러운 글이었습니다.
수치도 하나 틀렸습니다. 앞서 쓴 문서에 「10편」이라 적어둔 묶음이 실측 11편이었습니다. 범위가 겹쳐서 판정은 안 바뀌었지만, 판정이 안 바뀌었다는 건 나중에 안 것이고 적을 당시엔 그냥 틀린 숫자였습니다.
확인하지 못한 것
같은 날 스캔 기록이 두 벌 있는데 측정 시각 표기가 서로 어긋납니다. 한쪽은 13시대 재측정이라 적혀 있고, 제가 직접 잰 시각은 11:46 입니다. 나중 시각의 측정이 먼저 도착할 수는 없으니 둘 중 하나는 틀렸는데, 어느 쪽인지 판정할 근거가 없습니다. 그래서 제 손으로 잰 값만 이 글에 썼고, 다른 한 벌의 시각 표기는 신뢰하지 않기로 했습니다.
정리
- 0은 두 가지 뜻입니다 — 「문구가 없다」와 「본문을 못 읽었다」. 카운트와
http_code를 같은 줄에 찍지 않으면 구분이 안 됩니다 - 전수 검사에서 모수의 개수부터 세십시오. 57이어야 할 줄이 47이면 그 자체가 결과입니다
grep -c는 줄 수입니다. minify 된 HTML에서는 한 줄에 여러 번 들어앉아 과소 집계됩니다- 빌드가 문자열을 바꿉니다. 소스에서 만든 검색어로 산출물을 검사하면 실재하는 것도 0이 됩니다
- 어휘를 좁히면 새고 넓히면 오탐이 섞입니다. 스캔은 후보 추출이고 판정은 사람이 합니다
- macOS에서 한글을 dedup 할 땐
LC_ALL=C를 붙이십시오. 안 붙이면 조용히 줄어듭니다 - 그리고 무엇보다 — 검사 명령이 성공했다는 것은 답이 맞다는 뜻이 아닙니다. 이번에 저를 속인 여섯 개 전부 종료 코드 0으로 끝났습니다