5개 라이브러리 크롤러에 "이번 배치에 뭘 얼마나 수집했나"를 남기는 로그를 붙인 작업의 온보딩 상세
crawler-lib-golang / php / ruby / swift / dotnet (Java·Rust 제외). 디렉터리 이름은 생태계 이름이고 5개 모두 Python, baseCralwer.py(swift만 baseCrawler.py) 구조를 공유한다.INSERT ... ON DUPLICATE KEY UPDATE(upsert)라 DB가 행별로 insert인지 update인지 알려주지 않는다.기존 로그는 "이번 배치에서 패키지 N개 처리"라는 누적 카운트뿐이라, 그 배치가 실제로 새로 들어온 데이터를 모은 건지 기존 데이터를 다시 덮어쓴 건지 구분되지 않았다. 백필·재수집·증분 수집을 함께 돌리는 운영 환경에서 "오늘 신규 유입이 얼마였나"를 로그만 보고 알 수 없었던 것이 출발점이다. 그래서 배치 단위로 insert/update를 분리해 보이게 만드는 게 목표였다.
배치는 1,000행(golang V2는 이벤트 단위)을 한 방의 멀티로우 INSERT로 보낸다. MySQL이 ON DUPLICATE KEY UPDATE에서 돌려주는 affected_rows는 행마다 신규=1, 변경 update=2, 변화 없음=0인데, 배치 전체가 한 statement라 돌아오는 건 합쳐진 숫자 하나뿐이다. 거기서 insert가 몇·update가 몇인지는 역산되지 않는다(무변화 update가 0으로 잡혀 산수마저 깨진다). 게다가 현재 실행 계층은 rowcount를 읽지도 않는다.
그래서 선택지를 셋으로 정리하고 가장 가벼운 정확 방식을 골랐다:
| 방식 | 정확도 | 비용 | 채택 |
|---|---|---|---|
| A. 배치 전 PK 조회 — upsert 직전 그 배치 PK를 SELECT, DB에 있으면 update·없으면 insert | 정확 | 배치당 SELECT 1회(가벼움) | 채택 |
| B. 한 행씩 실행 + rowcount | 가장 정확(무변화까지 구분) | 배치당 수백~천 번 왕복 → 배치 INSERT 장점 상실 | 기각 |
| C. affected_rows 산수 | 부정확(무변화 update에서 깨짐) | 공짜 | 기각 |
baseCralwer.py)에 집계 헬퍼 6개 추가: _resetCollectStats / _entityForTable / _selectExistingKeys / _recordCollectStats / _collectBatchSummary / _collectTotalSummary.setInsertIntoDB 진입부(upsert 직전)에서 _recordCollectStats(table_name, datas) 호출. 대상이 PRODUCT_TABLE / VERSION_TABLE일 때만 집계(status·list·license 테이블은 무시).PRODUCT_KEY, version은 (PRODUCT_KEY, VERSION) 기준으로 500개씩 묶어 WHERE PRODUCT_KEY IN (...)(파라미터 바인딩) 조회 → 있으면 update, 없으면 insert._resetCollectStats()로 배치 카운터·중복제거 set 리셋. 배치 종료 시 [batch summary], 런 종료 STOP 로그 직전 [total summary]._collectSeen set으로 이미 센 키를 건너뛰어, 이 재삽입이 update로 이중 집계되는 것을 막았다.크롤러별 주입 위치 — php/ruby/dotnet은 dataCrawling() 상단 리셋 + 말미 배치요약, run() STOP 직전 총합. swift(cocoapods.py)는 반복 배치 루프가 없는 단일 패스라 crawl 메서드 전체를 1배치로 보고 finally:에서 둘 다 출력(중간 예외에도 보장). golang은 쓰기 경로가 setInsertIntoDB(V1 정렬)와 _setInsertIntoDBWithConnection(V1-core·V2) 둘로 갈려 양쪽 다 훅했고(인스턴스 단위 dedup이라 겹쳐도 1회), V1·V2 각 루프와 STOP 3곳을 모두 처리한다.
메시지 문자열은 5개 크롤러에서 완전히 동일하다(공통 헬퍼의 f-string을 그대로 복사). 로그 라인의 파일명:라인 prefix만 호출 위치에 따라 다르다.
[batch summary] product insert=12 update=3, version insert=45 update=8 [total summary] product insert=1340 update=210, version insert=5021 update=880
전체 라인 포맷도 5개 공통: %(asctime)s %(levelname)s %(process)s %(filename)s:%(lineno)d %(message)s. 파싱 정규식: \[(batch|total) summary\] product insert=(\d+) update=(\d+), version insert=(\d+) update=(\d+) 하나로 5개 모두 처리.
_recordCollectStats를 try/except로 격리 — 실패 시 [collect stats] skipped warning만 남기고 진행.python3 -m py_compile 통과(base + 크롤러 파일)._batchSkipExistingEvents 존재체크 결과 재사용이 후보.affected_rows로 분리하려는 시도는 멀티로우·무변화 update 때문에 막힌다.ai/wiki/concepts/crawler-collection-count-log.md.이 문서는 내부 식별자(repo id crawler-lib-*, 내부 테이블 TB_*·gatheringdb, 메서드명)를 그대로 쓴 본인 참고/온보딩용이다. 포트폴리오 공개가 필요하면 식별자를 일반화하고 "upsert에서 insert/update 가시화"라는 문제·접근·결과 중심으로 다시 쓴다. 일반 기술 명칭(MySQL, ON DUPLICATE KEY UPDATE 등)은 공개에도 유지 가능.
🕷 라이브러리 크롤러 시리즈 ④ — ① 멀티 언어 크롤러 구축 · ② 인기도 차등 수집 · ③ 크롤러 현대화 · ④ 배치 수집량 로그