65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""三层去重模块的数据模型。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class DedupLayer(StrEnum):
|
|
"""命中去重的层。"""
|
|
|
|
URL = "url" # L1: 完全相同 URL
|
|
CONTENT = "content" # L2: 标准化后 content 完全一致
|
|
SIMHASH = "simhash" # L3: SimHash 汉明距离 <= 阈值
|
|
|
|
|
|
class Fingerprint(BaseModel):
|
|
"""单篇文章的指纹记录,持久化到 SQLite。"""
|
|
|
|
url_hash: str = Field(..., description="主键,与 Article.url_hash 一致")
|
|
content_hash: str = Field(..., description="标准化 content 的 SHA1[:16]")
|
|
simhash: int = Field(..., description="64 位 SimHash 整数(无符号)")
|
|
source_id: str
|
|
url: str
|
|
title: str
|
|
publish_date: str | None = Field(default=None, description="YYYY-MM-DD,用于时间窗口")
|
|
ingested_at: datetime = Field(default_factory=datetime.now)
|
|
|
|
|
|
class DedupResult(BaseModel):
|
|
"""对单篇文章的判重结果。"""
|
|
|
|
url_hash: str
|
|
is_duplicate: bool
|
|
matched_layer: DedupLayer | None = None
|
|
matched_url_hash: str | None = None
|
|
matched_url: str | None = None
|
|
matched_title: str | None = None
|
|
hamming_distance: int | None = Field(
|
|
default=None, description="仅 SimHash 层有值"
|
|
)
|
|
|
|
def short_summary(self) -> str:
|
|
if not self.is_duplicate:
|
|
return f"[UNIQUE] {self.url_hash}"
|
|
layer = self.matched_layer.value if self.matched_layer else "?"
|
|
extra = f" hd={self.hamming_distance}" if self.hamming_distance is not None else ""
|
|
return f"[DUP/{layer}] {self.url_hash} ~ {self.matched_url_hash}{extra}"
|
|
|
|
|
|
class DedupStats(BaseModel):
|
|
"""指纹库统计。"""
|
|
|
|
total: int = 0
|
|
by_source: dict[str, int] = Field(default_factory=dict)
|
|
earliest: str | None = None
|
|
latest: str | None = None
|
|
|
|
|
|
# 类型别名,便于在批处理日志中归类
|
|
DedupVerdict = Literal["unique", "duplicate"]
|