[REFAC#100] 파싱 규칙 DB 일원화 — 단일 page parser engine (모든 웹페이지 도메인 중립) - #145
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a database-driven parsing system for news articles and lists, replacing hardcoded site-specific parsers. It includes a rule-based Parser using goquery, a Resolver with TTL caching, and a PostgreSQL implementation for rule storage. Review feedback recommends improving robustness by using timeout contexts for database lookups, refining custom error matching logic, enhancing diagnostic details in error messages, and ensuring consistent error handling for empty parsing results.
기존에는 naver/daum/yonhap/cnn 의 selector 가 각 parser.go 에 hardcode 되어 새 사이트 지원 시 코드 추가 + 재배포가 필요했다. 본 테이블은 단일 source 로 사이트별 파싱 규칙을 관리하여 단일 rule-based parser engine 이 런타임에 조회 가능. 스키마: - 자연키: (source_name, host_pattern, target_type, version) - target_type: "article" | "list" (CHECK 제약) - version: 정수, > 0 (CHECK 제약) - selectors: JSONB — 필드별 CSS selector / attribute / multi 보관 (top-level 컬럼화는 새 필드 추가 시 migration 필요 → 진화 친화 위해 JSONB) - enabled: 활성 플래그 (1건 보장은 application 책임) - updated_at: trigger 로 자동 touch 인덱스: - (host_pattern, target_type) WHERE enabled — URL 기반 lookup 핫패스 - (source_name, enabled, target_type) — 운영 대시보드용 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DB 일원화된 파싱 규칙에 대한 데이터 접근 layer.
추가:
- internal/storage/parsing_rule.go
- TargetType ("article" | "list")
- FieldSelector { CSS, Attribute, Multi } — 단일 필드 추출 규칙
- SelectorMap — article (Title/Body/Author/Date/Category/Tags/ImageURLs/Summary) +
list (ItemContainer/ItemLink/ItemTitle/ItemSummary) 통합. nil 필드는 미설정.
- ParsingRuleRecord — DB row 표현
- ParsingRuleFilter — List 필터 (SourceName/HostPattern/TargetType/OnlyEnabled)
- ParsingRuleRepository 인터페이스 (Insert/Update/GetByID/FindActive/List/Delete)
- internal/storage/postgres/parsing_rule.go
- pgxpool 기반 구현
- JSONB selectors 직렬화/역직렬화
- FindActive: (host_pattern, target_type, enabled=true) → version DESC LIMIT 1
(RuleResolver 핫패스, idx_parsing_rules_lookup 인덱스 사용)
- 자연키 충돌 → ErrDuplicate (pgerrcode.UniqueViolation 매핑)
- Delete: idempotent (미존재여도 nil)
- go.mod: github.com/jackc/pgerrcode 추가 (UniqueViolation 코드 매핑용)
라이브 DB 에 INSERT/SELECT sanity check 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(이슈 #100) DB 기반 파싱 규칙의 핫패스 lookup. 매 fetch 마다 DB roundtrip 회피하기 위해 in-memory TTL cache (양성 5min / negative 30s) 적용. goroutine-safe (sync.RWMutex). API: - NewResolver(repo, opts...) — repo 가 nil 이면 panic (wire 누락 즉시 가시화) - ResolveByURL(ctx, url, targetType) — URL parse → host 추출 → Resolve - Resolve(ctx, host, targetType) — host 직접 입력 - Invalidate(host, type) / InvalidateAll() — 운영자 hook (rule 변경 직후 호출) Options: - WithCacheTTL / WithNegativeCacheTTL / WithLogger 에러 정규화 (rule.Error): - ErrInvalidURL — URL parse / host 미존재 - ErrNoRule — host+type 매칭 활성 rule 없음 (LLM 자동 생성 fallback 진입점) - ErrEmptySelector / ErrParseFailure — Parser 단계용 (다음 commit) 매칭 없을 때 storage.ErrNotFound 가 아닌 *rule.Error{Code: ErrNoRule} 반환 → 호출자가 errors.Is(err, &rule.Error{Code: rule.ErrNoRule}) 로 분기 가능. negative cache 로 미매칭 host 도 짧게 캐싱 — 동일 host 폭주 시 DB 부담 회피. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#100) DB 기반 파싱 규칙으로 동작하는 단일 parser. 사이트별 hardcode parser (NaverParser/DaumParser/YonhapParser/CNNParser) 대체 — 새 사이트 지원이 parsing_rules row 추가만으로 가능. 설계: - 두 인터페이스 모두 구현 (compile-time interface assertion) - news.NewsArticleParser.ParseArticle(raw) → NewsArticle - news.NewsListParser.ParseList(raw) → []NewsItem - stateless — Resolver + dateLayouts 만 보유, 모든 worker 단일 인스턴스 공유 흐름 (ParseArticle): 1. raw.URL host 로 article rule lookup (Resolver, cache hit 핫패스) 2. Title selector 누락 → ErrEmptySelector (필수 필드) 3. selector 적용 — Title/Body/Author/Summary/Category/Date + Tags/ImageURLs (multi) 4. Body 빈 결과 → ErrParseFailure (selector 는 있지만 매칭 0건 = stale rule 진단) 흐름 (ParseList): 1. raw.URL host 로 list rule lookup 2. ItemContainer/ItemLink selector 누락 → ErrEmptySelector 3. 각 item element 안에서 Link/Title/Summary 추출 4. 상대 URL 은 raw.URL base 로 절대 URL 화 선택자 추출 헬퍼: - extractField — 단일 필드 (Multi=false: 첫 매치, Multi=true: 줄바꿈 합침) - extractFieldMulti — Tags/ImageURLs 용 (각 element 별 항목 슬라이스) - extractValue — Attribute=="" 면 .Text(), 그 외엔 attribute 값 Date 추출: - 9가지 layout 순회 (RFC3339, Korean "2006.01.02 15:04" 등) - 모두 실패 시 zero time (호출자가 validator 에서 zero 검사) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 테스트 케이스. fakeRepo (in-memory ParsingRuleRepository) 로 외부 DB 의존 없이 cache + selector 추출 동작 모두 검증. Resolver (10 케이스): - ResolveByURL 성공 / host 대소문자 정규화 / invalid URL → ErrInvalidURL - 매칭 없음 → ErrNoRule (errors.Is 호환) - 양성 cache hit → repo 호출 안 함 - negative cache → 미매칭 host 폭주에도 repo 호출 1회 - Invalidate / InvalidateAll → 다음 호출 repo 도달 - TTL 만료 → repo 다시 호출 (50ms TTL 로 빠른 검증) - nil repo → panic Parser (12 케이스): - ParseArticle 성공 — Title/Body(multi join)/Author/Category/Date(RFC3339 datetime attr)/ Tags(slice)/ImageURLs(src attribute slice) 모두 정확히 추출 - ParseArticle: NoRule / Title selector 누락 → ErrEmptySelector / Body selector 매칭 0건 → ErrParseFailure / 빈 raw → ErrParseFailure - ParseList 성공 — 상대 URL 자동 절대화 (raw.URL base) + 이미 절대 URL 보존 - ParseList: ItemContainer/ItemLink 누락 → ErrEmptySelector / NoRule - nil resolver → panic 전체 race 테스트 23 패키지 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@gemini-code-assist review again |
There was a problem hiding this comment.
Code Review
This pull request introduces a database-driven parsing system designed to replace site-specific hardcoded parsers with a flexible, rule-based engine. It includes new interfaces for content and link extraction, a rule resolver with in-memory caching, and a Postgres repository implementation. The reviewer feedback identifies several critical improvements, including the necessity of passing context.Context through the parser interfaces for better execution control and tracing, addressing potential OOM risks in the unbounded cache, and removing unnecessary files and unused logger fields.
사용자 요구 반영: 본 시스템은 뉴스 한정이 아닌 모든 웹페이지의 핵심 내용을 추출.
news 도메인에 묶여있던 backbone 을 generic web parser 로 재구성.
위치/명명 (다른 모듈 — fetcher 등 — 명명과 통일):
- internal/crawler/domain/news/rule/ → internal/crawler/parser/rule/
- 신규 패키지 internal/crawler/parser — 도메인 중립 인터페이스 + 모델
- test/internal/domain/news/rule/ → test/internal/parser/rule/
새 도메인 모델:
- parser.Page { URL, Title, MainContent, Summary, Author, PublishedAt,
Language, Category, Tags, Images, Metadata }
뉴스 article / 블로그 / 제품 페이지 / 일반 문서 모두 표현
- parser.LinkItem { URL, Title, Snippet }
새 도메인 인터페이스:
- parser.ContentParser : ParsePage(raw) → *Page
- parser.LinkListParser : ParseLinks(raw) → []LinkItem
- rule.Parser 가 두 인터페이스 모두 구현 (compile-time assertion)
SelectorMap 필드 일반화:
- Body → MainContent (모든 페이지의 핵심 본문)
- ImageURLs → Images
- Date → PublishedAt
- ItemSummary → ItemSnippet (link list item 의 짧은 설명)
TargetType:
- TargetTypeArticle → TargetTypePage (호환 alias 유지: TargetTypeArticle = TargetTypePage)
- migration 006 의 CHECK 제약도 ('article'/'list') → ('page'/'list') 변경
뉴스 도메인 (news.NewsArticleParser/NewsListParser) 의존 완전 제거 — 호출자가
필요 시 Page → news.NewsArticle 어댑터 작성 (후속 PR).
전체 race 테스트 23 패키지 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis pull request introduces a database-driven parsing rule system for the crawler. It adds domain models (Page, LinkItem), storage contracts for parsing rules, PostgreSQL repository implementation, a rule resolver with TTL caching, and a rule-based parser engine that normalizes content extraction across sites. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Parser Client
participant Parser as Rule Parser<br/>(parser.go)
participant Resolver as Rule Resolver<br/>(resolver.go)
participant Cache as TTL Cache
participant Repo as PostgreSQL Repo<br/>(parsing_rule.go)
participant DB as Database
Client->>Parser: ParsePage(ctx, RawContent)
Parser->>Resolver: ResolveByURL(ctx, url, TargetTypePage)
Resolver->>Cache: Check (host, type)
alt Cache Hit (positive)
Cache-->>Resolver: ParsingRuleRecord
else Cache Miss or Expired
Resolver->>Repo: FindActive(ctx, host, type)
Repo->>DB: SELECT * FROM parsing_rules<br/>WHERE host_pattern, target_type, enabled<br/>ORDER BY version DESC LIMIT 1
DB-->>Repo: ParsingRuleRecord
Repo-->>Resolver: ParsingRuleRecord
Resolver->>Cache: Store with TTL
Cache-->>Resolver: Done
else Not Found
Resolver->>Cache: Store ErrNoRule (negative cache)
Cache-->>Resolver: Done
Resolver-->>Parser: ErrNoRule
end
Resolver-->>Parser: ParsingRuleRecord
Parser->>Parser: Extract via CSS selectors<br/>(title, content, metadata)
Parser->>Parser: Parse datetime layouts<br/>Absolutize URLs
Parser-->>Client: Page{Title, MainContent, Metadata}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 53 minutes and 40 seconds.Comment |
Gemini code review 8건 통합 처리. parser.go: - ParsePage / ParseLinks 의 Resolver 호출에 5초 timeout context 적용 (인터페이스가 ctx 인자 미수용 — background ctx 무한 block 회피) - validateRaw 헬퍼 추출 — raw nil/HTML 비어있을 때 raw.URL 진단 정보 포함 (ParsePage / ParseLinks 공통 코드 중복 제거) - ParsePage: Title 도 추출 결과 빈 문자열이면 ErrParseFailure (이전엔 selector 누락만 검사 — selector 있어도 매칭 0건 stale 케이스 누락) - ParseLinks: 결과 0건이면 ErrParseFailure (사이트 구조 변경 진단) errors.go: - Is(target) AND 비교로 변경 — Code/Host/URL/TargetType 모든 비어있지 않은 필드 매칭 (이전엔 Code 만 검사 → false positive 가능) resolver.go: - extractHost: u.Host → u.Hostname() — 포트 ":8080" 제거 (DB host_pattern 이 순수 호스트네임이라 포트 포함 매칭 실패 회피) 전체 race 테스트 23 패키지 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.vscode/settings.json:
- Around line 2-3: The workspace setting "chat.tools.terminal.autoApprove"
currently enables auto-approval for the "gh" terminal tool; change this to
disable committed auto-approval by removing the "gh": true entry or setting it
to false in .vscode/settings.json, and, if needed, recommend moving any personal
preference to user-level settings instead of committing it to the repo so
contributors don't inherit auto-approve behavior for the GitHub CLI.
In `@internal/crawler/parser/rule/parser.go`:
- Around line 86-93: The selector validation currently only checks for nil
pointers (e.g., rule.Selectors.Title) but must also treat zero-value selectors
(empty CSS string) as missing; update the validation in parser.go so each
required selector (Title, MainContent and the selectors checked by the list/page
guards) is rejected when selector == nil OR selector.CSS == "" and return the
existing &Error{Code: ErrEmptySelector, Message: "...", URL: raw.URL,
TargetType: string(storage.TargetTypePage)} (or the appropriate TargetType for
list rules) instead of letting empty-CSS selectors fall through to
ErrParseFailure/stale-rule handling; apply the same CSS-empty check to the other
validation sites mentioned (the checks around lines 112-120, 145-152, and
184-191) so all required selectors fail with ErrEmptySelector when CSS is empty.
In `@internal/crawler/parser/rule/resolver.go`:
- Around line 118-123: ResolveByURL currently reduces the input to u.Hostname(),
preventing any host_pattern or path-scoped/wildcard rules from matching; update
ResolveByURL to pass a normalized full host/path (or both host and path) instead
of only the hostname and adjust extractHost (or add extractHostAndPath) to
return the components needed; then modify Resolver.Resolve to accept and forward
the full host pattern or URL-path pair to storage lookup logic (so it can
evaluate wildcard subdomains and path rules against host_pattern and any
path_pattern fields) and ensure storage lookup functions (used by Resolve)
perform pattern matching instead of exact equality.
In `@internal/storage/parsing_rule.go`:
- Around line 82-84: The struct field comment for TargetType is stale—update the
inline comment for the TargetType field (seen next to the TargetType symbol) to
reflect current valid values "page" | "list" instead of `"article" | "list"`,
keeping the rest of the field comments (HostPattern, Version) unchanged so
maintainers see the accurate schema in the parsing rule declaration.
In `@internal/storage/postgres/parsing_rule.go`:
- Around line 109-130: FindActive can return a nondeterministic row when
multiple enabled rules share host_pattern, target_type and version because the
SQL only filters by host and type; change the lookup to include source_name as
part of the key. Update the sqlFindActiveParsingRule SQL constant to add
source_name = $1 in the WHERE clause (shifting other params to $2/$3) and change
the pgParsingRuleRepository.FindActive signature to accept sourceName (e.g.,
func (r *pgParsingRuleRepository) FindActive(ctx context.Context, sourceName,
host string, targetType storage.TargetType) ) and pass sourceName into
r.pool.QueryRow so the query deterministically returns the intended rule;
alternatively, if you cannot change the signature now, add a deterministic
tie-breaker like ORDER BY version DESC, source_name ASC to the SQL constant.
In `@migrations/up/006_create_parsing_rules.sql`:
- Around line 41-43: The UNIQUE constraint parsing_rules_natural_key_unique
currently includes source_name but your runtime lookup only uses (host_pattern,
target_type) and selects the highest version, so duplicates across source_name
can cause wrong matches; update the constraint to enforce uniqueness on the
columns used by the resolver by replacing UNIQUE (source_name, host_pattern,
target_type, version) with UNIQUE (host_pattern, target_type, version) so that
only one rule exists per host_pattern/target_type/version (and adjust any
related index or migration statements to match).
In `@test/internal/parser/rule/resolver_test.go`:
- Around line 145-152: The test currently discards errors returned by r.Resolve
calls which can mask regressions; change each call like the two initial Resolve
calls and the post-Invalidate Resolve to capture the returned error and assert
it with assert.NoError(t, err) (or require.NoError), e.g., call r.Resolve(...),
check NoError before asserting repo.calls(), and do the same for the other
similar blocks (the ones around lines 162-170 and 178-184); keep the
Invalidate("news.example.com", storage.TargetTypePage) and repo.calls()
assertions unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8a314cbe-d06f-42fc-a1b5-37af3b758548
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (13)
.gitignore.vscode/settings.jsongo.modinternal/crawler/parser/parser.gointernal/crawler/parser/rule/errors.gointernal/crawler/parser/rule/parser.gointernal/crawler/parser/rule/resolver.gointernal/storage/parsing_rule.gointernal/storage/postgres/parsing_rule.gomigrations/down/006_create_parsing_rules.sqlmigrations/up/006_create_parsing_rules.sqltest/internal/parser/rule/parser_test.gotest/internal/parser/rule/resolver_test.go
Gemini code review #4, #5 — 로컬 전용 파일이 tracked 상태로 commit 에 포함됨. 제거: - debug.log - debug_ext.log - .claude/scheduled_tasks.lock .gitignore 추가: - debug.log / debug_ext.log / debug_*.log - .claude/scheduled_tasks.lock 향후 같은 파일 재발생 시 자동 ignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gemini code review #1, #2, #3 — I/O 가 수반되는 파싱 작업에 ctx 매개변수 누락. 호출자의 cancellation / timeout / trace metadata 전파를 위해 인터페이스 변경. 변경: - parser.ContentParser.ParsePage(ctx, raw) — ctx 매개변수 추가 - parser.LinkListParser.ParseLinks(ctx, raw) — ctx 매개변수 추가 rule.Parser: - 호출자 ctx 의 cancel/trace metadata 보존 — context.Background() 대신 ctx 사용 - resolveTimeout (5s) 안전망은 유지하되 호출자 ctx 위에 합성: context.WithTimeout(ctx, resolveTimeout) — ctx 의 더 짧은 deadline 이 우선 - 옛 background ctx 사용은 trace ID / logger 필드 유실 → metadata 보존 테스트: - 모든 ParsePage / ParseLinks 호출에 context.Background() 추가 - context import 추가 전체 race 테스트 23 패키지 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gemini code review #6, #7, #8 통합 처리. Resolver (#6, #7): - DefaultMaxCacheEntries (10,000) — cache 폭증 시 OOM 방어 - WithMaxCacheEntries(n) Option — 운영자 override - evictExpiringSoon: 가득 차면 가장 만료 임박 entry 제거 (LRU 가 아니라 expiry-order eviction — 단순하지만 본 패키지의 부하 패턴 (소수 host 반복) 에 충분, 외부 의존성 회피) - log 필드 제거 + WithLogger Option 제거 — 실제 미사용. 향후 cache hit/miss 로깅이 필요해지면 재추가. pgParsingRuleRepository (#8): - log 필드 제거 — 모든 메소드에서 미사용 - NewParsingRuleRepository 시그니처는 보존 (다른 Repository 와 일관성, 향후 logging 추가 대비) - 인자는 받되 _ = log 로 명시적으로 무시 전체 race 테스트 23 패키지 통과. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
migrations/up/006_create_parsing_rules.sql (2)
29-30: Constrainselectorsto JSON objects.
selectorscurrently accepts any JSONB value. A manual SQL/admin insert like[]or"foo"would satisfy the schema but break once the repository reads it back intostorage.SelectorMap. Adding a top-level shape check now will harden the DB-backed contract before admin tooling lands.💡 Suggested schema hardening
CONSTRAINT parsing_rules_version_positive CHECK (version > 0), + CONSTRAINT parsing_rules_selectors_object + CHECK (jsonb_typeof(selectors) = 'object'), -- Lookup 키 (host_pattern, target_type, version) 와 동일하게 UNIQUE.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@migrations/up/006_create_parsing_rules.sql` around lines 29 - 30, Add a CHECK constraint to ensure the selectors JSONB column is always a JSON object: modify the migration that defines the selectors column (column name selectors in the CREATE TABLE / ALTER TABLE block) to include a constraint such as CHECK (jsonb_typeof(selectors) = 'object') and give the constraint a clear name (e.g., selectors_is_object) so future migrations/rollbacks can reference it; keep the DEFAULT '{}::jsonb' and ensure any existing rows are validated/converted before applying the constraint in a migration that can run safely in production.
50-53: Align the lookup index withFindActive'sORDER BY version DESC.The resolver hot path filters by
(host_pattern, target_type)and then picks the highest enabled version. This partial index still storesenabledas a key and omitsversion, so PostgreSQL may still scan/sort multiple enabled rows for the same host/type. Indexingversion DESChere would better match the query shape.⚙️ Suggested index shape
CREATE INDEX IF NOT EXISTS idx_parsing_rules_lookup - ON parsing_rules (host_pattern, target_type, enabled) + ON parsing_rules (host_pattern, target_type, version DESC) WHERE enabled = TRUE;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@migrations/up/006_create_parsing_rules.sql` around lines 50 - 53, The partial index idx_parsing_rules_lookup should include version DESC to match the resolver hot path (FindActive) which filters by host_pattern and target_type and orders by version DESC; update the migration to create the index on parsing_rules using columns (host_pattern, target_type, version DESC) with the same WHERE enabled = TRUE predicate so PostgreSQL can satisfy the ORDER BY without additional sorting when FindActive queries for the highest enabled version.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/crawler/parser/rule/parser.go`:
- Around line 205-216: validateRaw currently treats whitespace-only HTML as
non-empty; update the check in validateRaw (function name validateRaw, return
&Error{...} logic) to use strings.TrimSpace(raw.HTML) and treat trimmed == "" as
empty so it returns the ErrParseFailure error with raw.URL; add the strings
import if missing and keep the same error construction path.
- Around line 168-195: The current loop appends to items only when
extractFieldFromSelection(container, rule.Selectors.ItemLink) is non-empty, so
when ItemContainer matches but every ItemLink is empty we still hit the
"ItemContainer selector matched 0 elements" error; change the parsing logic in
the doc.Find(... rule.Selectors.ItemContainer ...) block to track both
containerCount and validLinkCount (or separately count containers and appended
items): if containerCount == 0 keep the existing ErrParseFailure message about
ItemContainer, but if containerCount > 0 and validLinkCount == 0 return a
distinct ErrParseFailure (or clearer Error) whose Message explicitly states that
ItemContainer matched elements but ItemLink resolved to empty values (include
raw.URL and TargetType as the existing error does), using the same Error struct
and preserve other fields.
---
Nitpick comments:
In `@migrations/up/006_create_parsing_rules.sql`:
- Around line 29-30: Add a CHECK constraint to ensure the selectors JSONB column
is always a JSON object: modify the migration that defines the selectors column
(column name selectors in the CREATE TABLE / ALTER TABLE block) to include a
constraint such as CHECK (jsonb_typeof(selectors) = 'object') and give the
constraint a clear name (e.g., selectors_is_object) so future
migrations/rollbacks can reference it; keep the DEFAULT '{}::jsonb' and ensure
any existing rows are validated/converted before applying the constraint in a
migration that can run safely in production.
- Around line 50-53: The partial index idx_parsing_rules_lookup should include
version DESC to match the resolver hot path (FindActive) which filters by
host_pattern and target_type and orders by version DESC; update the migration to
create the index on parsing_rules using columns (host_pattern, target_type,
version DESC) with the same WHERE enabled = TRUE predicate so PostgreSQL can
satisfy the ORDER BY without additional sorting when FindActive queries for the
highest enabled version.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bedd3622-4b8d-4a44-816d-0ab57006b4e6
📒 Files selected for processing (5)
.gitignoreinternal/crawler/parser/rule/parser.gointernal/storage/parsing_rule.gomigrations/up/006_create_parsing_rules.sqltest/internal/parser/rule/resolver_test.go
✅ Files skipped from review due to trivial changes (2)
- .gitignore
- internal/storage/parsing_rule.go
Coderabbit code review 7건 통합 처리. #1 .vscode/settings.json 추적 제외: - gh CLI 자동 승인이 commit 에 포함됨 (다른 contributor 영향) - .vscode/settings.json 제거 + .gitignore 의 ".vscode/" 활성화 #2 hasRequiredSelector 헬퍼 (parser.go): - nil 만 검사하면 zero-value selector (CSS 빈 문자열) 가 ErrParseFailure 로 잘못 분류 - nil + CSS trim 후 빈 문자열 모두 ErrEmptySelector 로 명확 분류 - ParsePage: Title + MainContent 둘 다 필수 - ParseLinks: ItemContainer + ItemLink 둘 다 필수 #4 TargetType 주석 수정: - "article" | "list" → "page" | "list" (도메인 일반화 commit 후 미반영 부분) #5, #6 자연키 ↔ lookup 키 정렬 (migration 006): - 이전 UNIQUE: (source_name, host_pattern, target_type, version) → FindActive 가 (host_pattern, target_type) 만 lookup 하면 동일 host/type/version 의 두 source row 가 활성화될 때 nondeterministic - 신규 UNIQUE: (host_pattern, target_type, version) → resolver lookup 키와 정렬, 의도치 않은 두 row 활성화 schema 단계 차단 - source_name 은 metadata 로 보존 (어느 source 가 등록했는지 추적) #7 test require.NoError: - 이전 _, _ := r.Resolve(...) 에러 무시 → call-count assertion 만 검증 - 모든 Resolve 호출에 require.NoError 추가 — 회귀 가시성 강화 #3 (wildcard host_pattern 매칭) 은 별도 후속 — 본 PR 범위 밖, 후속 issue 권장. 전체 race 테스트 23 패키지 통과. 라이브 DB migration 재적용 + UNIQUE 제약 검증 완료. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Coderabbit code review 2건.
ParseLinks 에러 메시지 분기:
- 이전엔 "ItemContainer 0건" 메시지가 두 가지 경우를 모두 표현 →
운영자가 어떤 selector 를 고쳐야 하는지 모호
- 신규 분기:
- ItemContainer 자체가 0건 → "ItemContainer selector matched 0 elements"
- ItemContainer 매칭됐지만 모든 ItemLink 가 빈 결과 → "ItemContainer matched but
no valid ItemLink found (ItemLink selector may be stale)"
validateRaw whitespace 처리:
- raw.HTML 이 " \n" 같은 whitespace-only 일 때 통과 → stale-rule 오인 회피
- strings.TrimSpace(raw.HTML) != "" 검사로 변경
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
연관 이슈
구현 내용
#100 의 "사이트별 파싱 규칙을 DB로 일원화하고 미지원 페이지는 LLM API로 자동 생성" 작업의 backbone. 사용자 요구사항을 반영하여:
6개 논리적 commit:
[FEAT]migration 006 — `parsing_rules` 테이블[FEAT]ParsingRuleRepository — `internal/storage/{,postgres/}parsing_rule.go`[FEAT]rule.Resolver — `internal/crawler/parser/rule/resolver.go`[FEAT]rule.Parser — `internal/crawler/parser/rule/parser.go`[FEAT]단위 테스트 (22 케이스) — `test/internal/parser/rule/`[REFAC]도메인 일반화 — news → 모든 웹페이지 (사용자 요구 반영)도메인 모델
사용 예시
CI / 머지 게이트 점검
변경 영향 범위
Required Status Checks
로컬 검증:
롤백 계획
TODO (후속 PR)
본 PR 은 backbone 만. 후속 작업:
논의 사항
🤖 Generated with Claude Code
Summary by CodeRabbit
Chores
Tests
Infrastructure