fix(crawlers): 크롤러 사이의 기능 편차 12건을 메운다 - #17
Merged
Conversation
added 12 commits
August 10, 2026 17:00
PR #14가 API 4종(threads/x/linkedin/reddit)만 고치고 feed 쪽은 그대로 뒀다. hackernews/geeknews/producthunt는 try가 HTTP 호출까지만이라, 상류 응답 구조가 바뀌면 파싱 예외가 crawl 루프로 올라가 그 회차 게시글이 통째로 저장 0건이 된다. - hackernews: walk()와 반환 dict 구성을 try 안으로 - geeknews: BeautifulSoup 파싱과 셀렉터를 try 안으로 - producthunt: DOM 순회를 _parse_comment_section으로 분리해 try 안으로. 조용히 None을 반환하던 것도 경고 한 줄을 남기게 했다 회귀 테스트 3클래스를 추가했다. 수정 전 hackernews 케이스가 실제로 AttributeError로 실패하는 것을 확인했다.
PR #14에서 make_retrying_session을 만들어 놓고 정작 데일리에서 조용히 멈춰 있던 두 소스에는 붙이지 않았다. 단발 요청이라 503 한 번이면 그날 논문이 유실되는데, arxiv는 카테고리당 최신 50건만 받으므로 다음 날 창이 겹쳐도 그 사이 제출분까지 거슬러 가지 못한다. - arxiv: _fetch_category로 감싸 실패를 None으로 돌린다. feedparser에 URL을 직접 주면 실패해도 빈 피드를 조용히 주기 때문에, 4개 카테고리 중 하나가 죽어도 나머지가 정상 카운트를 만들어 0건 회귀 감지에 안 걸렸다 - huggingface: raise_for_status가 crawl 안에 있어 예외가 CLI까지 올라갔다 - enrichment: PDF 요청도 _HTTP_SESSION으로. 한 회차에 arxiv.org/pdf를 최대 50건 연속으로 때리는 경로다 실측: arxiv 50건, huggingface 49건 정상 수집.
HF 행은 본문을 arXiv HTML/PDF에서 뽑아오므로 같은 논문의 arxiv 행과 사실상 같은 글이다. HF만 갖는 것(저자가 직접 단 코드·데모 링크, 평문 요약, 독자 반응)이 통째로 빠져 HF Daily Papers를 따로 읽을 이유가 남지 않았다. 논문 페이지의 SSR 페이로드(data-target=PaperContent의 data-props)가 댓글을 통째로 담아 준다. 로그인도 토큰도 필요 없고 논문당 요청 1건이다. latest.raw가 이미 마크다운 원문이라 HTML 파싱도 필요 없다. - numComments가 목록 응답에 있어 0건 논문은 조회하지 않는다 (판정 비용 0) - HTTP와 파싱을 함께 try로 감싸 댓글 실패가 논문 저장을 막지 않는다 - 숨김 댓글과 comment가 아닌 노드는 제외한다 별건: _item_to_post가 num_comments= 로 넣고 있어 Post의 extra=allow에 흡수됐고, db.py가 읽는 posts.comments는 항상 None이었다. comments= 로 고쳤다. 실측: 논문 3건 전부 댓글 섹션이 붙고 comments 값이 들어간다.
456행(ailabs/OpenAI News 424, blogs/Phil Schmid 17, blogs/Addy Osmani 15)이 작성자 영구 공백이었다. export/bundle의 front matter, research --fields author, 데스크톱 리더가 전부 빈 값을 찍는다. 두 갈래가 다 필요했다. - upsert에 author 복구 분기를 넣고 WHERE 가드에도 author 조건을 추가했다. SET만 넣으면 WHERE에서 문장이 통째로 걸러져 실행되지 않는다 (title/summary 복구가 동작하고 author만 안 되던 이유가 이것이다) - backfill_blank_authors로 옛 행을 채운다. 424행이 크롤 창보다 오래돼 재크롤될 일이 없어 upsert만으로는 영원히 빈 채로 남는다 값은 fetch_feed가 이미 쓰는 폴백과 같다 (source의 '/' 뒷조각). 프로덕션 실측: 456건 채움, 빈 author 0행. 백업 후 적용.
threads와 linkedin은 본문이 비면 파싱 단계에서 None을 돌려 DB에 행 자체가 안 생겼다. 그래서 그날 그 계정이 무엇을 올렸는지가 digest와 research에서 통째로 빠졌다. x는 이미 사다리(alt text -> media link -> drop)를 갖고 있었다. - threads: contents가 비면 수집해둔 image_urls를 본문으로 쓴다 - linkedin: 임계를 len < 10에서 빈 값만 drop으로 낮춘다. 10자 임계는 "꾸준함은 능력이다." 같은 유효한 짧은 글도 잘랐다 - 폴백 본문은 content_status extras로 표시해 소비 시점에 구분되게 한다 - x의 스레드 병합 경로가 세 번째 반환값을 버리던 것도 고쳤다. 단 스레드 전체가 폴백일 때만 표시한다 곁들여 linkedin _extract_text의 str() 폴백을 막았다. dict에서 텍스트를 못 찾으면 dict repr이 본문이 됐는데, 길이 임계가 우연히 이걸 막고 있어서 임계를 낮추는 순간 표면화된다 (프로덕션에는 아직 0행).
reddit 링크 게시물은 제목 한 줄만 남고 원문 URL조차 소비자에게 닿지 않았다. hackernews/lobsters/everyto는 defuddle 단발만 타서 3단 사다리를 못 받아 파이프라인에서 가장 약한 추출 경로였다. reddit: - JSON listing 경로는 url_overridden_by_dest를 이미지 판별에만 쓰고 버렸다. _external_link로 셀프 포스트/이미지/reddit 호스트를 걸러 원문만 남긴다 - Atom 폴백 경로(JSON 403 시)가 더 심했다. strip_html이 [link] 앵커를 텍스트로 만들며 href를 버려, 본문이 "submitted by /u/x [link] [comments]" 껍데기가 됐다. href를 살리고 껍데기는 제목으로 바꾼다 - attach_articles가 원문을 추출해 ## Original Article로 잇는다. 추출 실패는 게시글 저장을 막지 않는다 enrichment: - 링크 애그리게이터 4종을 _enrich_article_item으로 보낸다 - min_words를 인자로 뚫었다. 60단어 게이트는 짧은 릴리스 노트를 failed로 떨어뜨려서, 애그리게이터는 20으로 낮춘다 - 추출 실패 + .pdf 링크면 extract_pdf_text로 폴백한다 - 플랫폼 분기를 _extract_for_platform으로 분리했다 (복잡도 게이트 통과용) 실측(r/programming 5건): 링크 게시물 4건의 본문이 38~44자에서 1,783~31,924자로 채워졌다. 그중 1건은 playwright 렌더까지 폴백했다.
producthunt: 피드가 런치별 고유 id(tag:...,2005:Post/1219088)를 주는데 _item_to_post가 버려서 592행 전부 해시 id였다. 해시 id는 db가 URL로 병합하므로 같은 제품 페이지의 두 번째 이후 런치가 아예 저장되지 않았다. ailabs: RSS guid가 있으면 그걸 쓰고 없으면 URL 기반이라 두 체계가 공존했다. 소스가 RSS/HTML 경로를 오가면 같은 글이 두 행으로 갈라진다. URL 기반으로 고정했다. dedupe_by_url과 `skim dedupe <platform>`을 추가했다. 본문이 가장 긴 행을 남긴다 - 짧은 쪽이 추출 실패본일 수 있어서다. 행을 지우므로 기본은 미리보기고 --apply를 붙여야 실제로 지운다. 프로덕션 적용(백업 후): ailabs 660 -> 478행, 중복 182건 삭제, 남은 중복 0. 지운 쪽과 남긴 쪽의 본문 길이가 완전히 같은 것을 표본으로 확인했다. integrity ok.
geeknews/youtube/producthunt/everyto/blogs/ailabs는 조회 창 안의 글 전체를 원문 추출(defuddle/yt-dlp)과 댓글·지표 조회까지 끝낸 뒤에야 CLI가 앞 N건만 남기고 버렸다. N건을 얻자고 수십 건분의 시간과 외부 요청을 치르고, 그 결과물은 DB에 저장조차 되지 않는다. hackernews/lobsters/arxiv/huggingface가 이미 쓰는 패턴(정렬 직후 items[:count])을 나머지에 옮겼다. ailabs만 처리가 다르다. 소스별 limit이 이미 count라 소스 9개면 최대 9*count개가 모인다. 소스별 limit은 과수집 방지용으로 두고, dedupe와 정렬 뒤에 전역 상한을 한 번 더 건다.
hackernews: hnrss count 기본값이 20이라 24시간 창에 30점 넘긴 글이 65건 있어도 20건만 들어왔다(2026-08-10 실측). newest를 문서상 최대인 100으로 올린다. show/ask는 30으로 둔다. 점수 문턱이 없어 셋 다 100으로 두면 한 회차 239건이 되고(종전 60건의 4배) 저품질까지 전부 원문 추출과 댓글 조회를 돈다. 조정 후 실측 97건. 상한에 닿아 창을 다 못 덮으면 경고를 남긴다. 조용히 넘어가면 "그날 HN에 이만큼밖에 없었다"로 읽힌다. arxiv: arxiv_api_url에 start 오프셋을 뚫고 _entries로 페이지를 흘린다. 한 장(50건)이 실측 4시간25분치라 count를 그보다 크게 잡으면 페이징 없이는 못 채운다. 종료 조건 셋 - count 충족, 빈 페이지, 마지막 항목이 창 밖. 페이지 상한 10, 연속 요청 사이 3초(arXiv 권고). count가 한 장 안에 들어오는 기본값(50)에서는 요청이 지금과 똑같이 카테고리당 1회다. 볼륨을 늘리는 변경이 아니라 count를 키울 수 있게 하는 변경이다. 조사 리포트의 "hnrss count=40/50/100이 502"는 재측정에서 재현되지 않았다 (30/40/50/100 모두 200). 그래서 Algolia 전면 전환은 하지 않았다.
Every.to 글은 무료 미리보기까지만 저장돼서 본문이 문장 중간에서 끊기고
나머지는 프로모 문구다. 표시가 없으면 research/digest/데스크톱이 반쪽짜리를
완결된 글로 읽고 결론부가 빠진 채 요약한다.
로그인 연결은 하지 않는다(사용자 결정). 유료 구독과 세션 관리가 따라붙는다.
대신 잘린 행에 content_status="paywalled"를 붙여 소비 시점에 구분되게 한다.
판정 문구는 프로덕션 실측이다 ("...to unlock this piece and learn about:").
저장된 68건에 돌려보니 12건이 잘린 글로 잡힌다.
이유는 docs/TODO의 "알려진 한계"에 남겼다.
2026-08-10 전수 조사(크롤러 14종 x 축 20개)에서 나온 것들이다. 같은 결함이 새 크롤러에서 반복되지 않게 계약으로 남긴다. - 본문이 비면 버리기 전에 폴백을 본다 - 링크 게시물은 원문 URL을 보존하고 추출한다 - 상류가 주는 고유 id를 버리지 않는다. 체계를 바꾸면 백필이 함께 간다 - count는 enrichment 앞에서 자른다 - 창이 한 페이지보다 넓으면 페이징하고, 상한에 닿으면 경고한다 - 불완전한 본문은 content_status로 표시한다 댓글 파싱 try 계약에 feed형 3종이 빠져 있던 이력도 함께 적었다. huggingface 댓글 행을 표에 추가했다.
HN 피드 세 장이 전부 hnrss.org라 502 한 번에 셋이 같이 0건이 된다. 데일리는 고정 창으로 돌아 다음 날 창에 그 글이 다시 안 들어오므로 그 회차 HN이 그대로 영구 유실이다 (2026-08-10 프로덕션 run #264 degraded). 세 피드가 전부 0건일 때만 Algolia search_by_date로 폴백한다. 한 장이라도 살아 있으면 돌리지 않는다. 중복 요청만 늘어난다. 폴백 항목은 fetch_feed가 만드는 item과 같은 모양으로 맞춘다. external_id는 hnrss guid와 같은 `item?id=<id>` 형태라 두 경로로 들어와도 행이 갈리지 않고, Algolia가 points/num_comments를 같이 줘서 지표 폴백 요청도 안 나간다. tags의 콤마는 AND다. 괄호로 싸면 OR이 돼 필터가 통째로 풀린다 (실측). 실측: hnrss를 죽었다고 가정하고 돌려 155건, external_id/timestamp 결손 0.
added 4 commits
August 10, 2026 18:08
앞 커밋은 "세 피드가 전부 0건일 때만" 폴백했다. 실제 hnrss 502는 피드 단위로 온다. 직후 프로덕션 재수집에서 newest만 502가 나고 show/ask는 살아 있어서 폴백이 안 돌았고, 30점 이상 글 전량이 그대로 빠졌다 (60건만 저장. 정상 회차는 97건). HN은 하루 창에 어느 피드도 0건이 되는 날이 없다. 0건은 글이 없어서가 아니라 그 피드가 죽어서다. 정말 없었다면 Algolia도 0건을 주므로 무해하다. 실측: newest만 죽었다고 가정하고 돌려 156건 수집, 그중 30점 이상 96건. external_id/timestamp 결손 0.
fetch_feed는 item["platform"]에 피드 이름(hackernews/show)을 넣고, _item_to_post가 그걸 Post.platform에 그대로 실었다. db.py는 Post의 platform을 인자보다 우선하므로 DB에 `hackernews/show`, `hackernews/ask` 라는 별도 플랫폼 행이 생긴다. Show/Ask 피드를 추가한 #14부터 있던 결함인데, hnrss show/ask가 실제로 저장된 회차가 오늘이 처음이라 그동안 안 드러났다. 프로덕션에 60행이 그렇게 갈려 있었고 별도 스크립트로 수리했다 (충돌 2건은 본문이 긴 쪽을 남김, 백업 후 실행, integrity_check ok). 서브피드는 source에 남긴다. blogs가 이미 쓰는 방식이다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
크롤러 14종을 20개 축으로 전수 대조해서, 다른 크롤러는 하는데 이 크롤러만 안 하던 것 10건을 고쳤습니다. 검증 중 프로덕션에서 2건이 더 터져 12건이 됐습니다. 새 크롤러·새 소스는 없습니다.
submitted by /u/...껍데기) -> 원문 추출로 1,783~31,924자comments가 항상NULL이던 필드 오배치이 중 2건은 #14에서 제가 절반만 하고 끝낸 것입니다. 아래 표에 그대로 적었습니다.
고친 것
make_retrying_session을 만들어 두고 연결 안 함num_comments=오타로comments항상NULLSET절 누락)backfill_blank_authors()None반환 -> 행 자체가 안 생김content_status="media_link"--count절단이 enrichment 뒤content_status="paywalled"(로그인 연결 안 함 — 사용자 결정)search_by_date로 다시 채움Post.platform으로 새어 DB에hackernews/show별도 플랫폼 행 생성platform고정 + 서브피드는source로. 프로덕션 60행 수리11번을 넣은 이유 (프로덕션에서 두 번 터졌습니다)
이 브랜치 검증으로 프로덕션 데일리를 돌리던 중 hnrss.org가 502를 냈고, HN 세 피드가 동시에 0건이 됐습니다 (run #264
degraded). 재시도 3회로도 못 뚫었습니다.데일리는 고정 창으로 돌기 때문에 다음 날 창에 그 글이 다시 안 들어옵니다. 그대로 영구 유실입니다.
직후 재측정에서는
count유무와 무관하게 전부 200이었습니다. 즉 이번 PR의count=100변경 탓이 아니라 간헐적 서버 장애이고, 원래 있던 단일 호스트 의존 취약점입니다.처음엔 폴백 조건을 "세 피드가 전부 0건일 때만"으로 잡았는데, 그게 틀렸습니다. 커밋 직후 프로덕션 재수집에서 newest만 502가 나고 show/ask는 살아 있었고, 살아남은 피드가 있으니 폴백이 안 돌아 30점 이상 글 전량이 그대로 빠졌습니다 (60건만 저장, 정상 회차는 97건). 판정을 피드 단위로 바꿨습니다.
HN은 하루 창에 어느 피드도 0건이 되는 날이 없으므로 0건은 곧 장애 신호이고, 정말 없었다면 Algolia도 0건을 주므로 무해합니다.
실측:
external_id/timestamp결손 0기각한 4건과 이유
전수 조사에서 후보로 올라왔지만 넣지 않은 것들입니다.
comments_zero_guard를 hackernews/lobsters에도 — 그쪽은 같은 요청이 본문까지 같이 주므로, 댓글 0건이라고 건너뛰면 본문을 잃습니다.db.py의 upsert가 이미 접습니다.검증
프로덕션 데일리 1회차를 실제로 돌려 확인했습니다 (266건 저장). 11번과 12번은 그 과정에서 잡힌 것입니다 — 로컬 테스트만으로는 둘 다 안 드러났습니다.
12번 수리는 백업 후 실행했고
pragma integrity_checkok,skim doctor --strictexit 0입니다.ailabs0건도 같은 회차에 떴는데, 크롤러 문제가 아니라 그 창에 실제로 새 글이 없었습니다 (7일 창으로 찌르면 30건 정상 수집). 다만doctor의 "14일간 수집되던 플랫폼이 0건" 경고는 ailabs처럼 발행 간격이 넓은 소스에 오탐이 됩니다 — 이 PR 범위 밖이라 남겨 둡니다.