Skip to content

接口定义

HTML 库定义了以下核心接口:

Extractor

HTML 内容提取的主要接口,Processor 实现了此接口。

go
type Extractor interface {
    // 核心提取
    Extract(htmlBytes []byte) (*Result, error)
    ExtractWithContext(ctx context.Context, htmlBytes []byte) (*Result, error)
    ExtractFromFile(filePath string) (*Result, error)
    ExtractFromFileWithContext(ctx context.Context, filePath string) (*Result, error)

    // 文本提取
    ExtractText(htmlBytes []byte) (string, error)
    ExtractTextFromFile(filePath string) (string, error)
    ExtractTextWithContext(ctx context.Context, htmlBytes []byte) (string, error)
    ExtractTextFromFileWithContext(ctx context.Context, filePath string) (string, error)

    // 格式化输出
    ExtractToMarkdown(htmlBytes []byte) (string, error)
    ExtractToMarkdownFromFile(filePath string) (string, error)
    ExtractToJSON(htmlBytes []byte) ([]byte, error)
    ExtractToJSONFromFile(filePath string) ([]byte, error)
    ExtractToMarkdownWithContext(ctx context.Context, htmlBytes []byte) (string, error)
    ExtractToMarkdownFromFileWithContext(ctx context.Context, filePath string) (string, error)
    ExtractToJSONWithContext(ctx context.Context, htmlBytes []byte) ([]byte, error)
    ExtractToJSONFromFileWithContext(ctx context.Context, filePath string) ([]byte, error)

    // 批量处理
    ExtractBatch(htmlContents [][]byte) *BatchResult
    ExtractBatchWithContext(ctx context.Context, htmlContents [][]byte) *BatchResult
    ExtractBatchFiles(filePaths []string) *BatchResult
    ExtractBatchFilesWithContext(ctx context.Context, filePaths []string) *BatchResult

    // 链接提取
    ExtractAllLinks(htmlBytes []byte) ([]LinkResource, error)
    ExtractAllLinksFromFile(filePath string) ([]LinkResource, error)
    ExtractAllLinksWithContext(ctx context.Context, htmlBytes []byte) ([]LinkResource, error)
    ExtractAllLinksFromFileWithContext(ctx context.Context, filePath string) ([]LinkResource, error)

    // 生命周期
    Close() error
}

StatsProvider

统计信息和缓存管理接口。

go
type StatsProvider interface {
    GetStatistics() Statistics
    ClearCache()
    ResetStatistics()
}

Processor 同时实现了 ExtractorStatsProvider。通过接口类型引用 Processor,可以将「提取能力」与「监控能力」分别注入不同的消费者:

go
type ExtractionService struct {
    extractor     html.Extractor     // 只需要提取能力
    statsProvider html.StatsProvider // 只需要监控能力
}

func NewService(p *html.Processor) *ExtractionService {
    return &ExtractionService{
        extractor:     p, // *Processor 满足 Extractor
        statsProvider: p, // *Processor 满足 StatsProvider
    }
}

依赖注入与 Mock 测试

Extractor 接口使提取逻辑可解耦,便于单元测试中注入 mock 实现:

go
// mockExtractor 实现 html.Extractor 接口,用于测试
type mockExtractor struct {
    result *html.Result
    err    error
}

func (m *mockExtractor) Extract([]byte) (*html.Result, error) { return m.result, m.err }
func (m *mockExtractor) ExtractWithContext(ctx context.Context, b []byte) (*html.Result, error) {
    return m.result, m.err
}
func (m *mockExtractor) ExtractFromFile(string) (*html.Result, error)         { return m.result, m.err }
func (m *mockExtractor) ExtractFromFileWithContext(context.Context, string) (*html.Result, error) {
    return m.result, m.err
}
func (m *mockExtractor) ExtractText([]byte) (string, error)                   { return m.result.Text, m.err }
func (m *mockExtractor) ExtractTextFromFile(string) (string, error)           { return m.result.Text, m.err }
func (m *mockExtractor) ExtractTextWithContext(context.Context, []byte) (string, error) {
    return m.result.Text, m.err
}
func (m *mockExtractor) ExtractTextFromFileWithContext(context.Context, string) (string, error) {
    return m.result.Text, m.err
}
// ... 其余方法返回零值

// 业务代码依赖接口而非具体类型
type ArticleService struct {
    extractor html.Extractor
}

func (s *ArticleService) GetTitle(htmlBytes []byte) (string, error) {
    result, err := s.extractor.Extract(htmlBytes)
    if err != nil {
        return "", err
    }
    return result.Title, nil
}

// 测试中注入 mock
func TestGetTitle(t *testing.T) {
    svc := &ArticleService{
        extractor: &mockExtractor{
            result: &html.Result{Title: "测试标题"},
        },
    }
    title, err := svc.GetTitle([]byte("<html></html>"))
    assert.NoError(t, err)
    assert.Equal(t, "测试标题", title)
}

ContentNode

HTML 节点的抽象接口,用于内容评分算法。

go
type ContentNode interface {
    Type() string                    // 节点类型 ("element", "text", "comment" 等)
    Data() string                    // 标签名或文本内容
    AttrValue(key string) string     // 属性值
    Attrs() []NodeAttr               // 所有属性
    FirstChild() ContentNode         // 第一个子节点
    NextSibling() ContentNode        // 下一个兄弟节点
    Parent() ContentNode             // 父节点
}

Scorer

内容评分算法接口,用于自定义文章识别策略。

go
type Scorer interface {
    Score(node ContentNode) int          // 计算节点相关性分数
    ShouldRemove(node ContentNode) bool  // 判断节点是否应移除
}

注意:实现必须并发安全

当单个 Processor 被多个并发 Extract 调用共享时,会从多个 goroutine 同时触发 Score/ShouldRemove。因此任何 Scorer 实现都必须自身保证并发安全

库内置的默认评分器只读、天然满足并发安全;自定义 Scorer 若持有可变状态(如缓存、计数器),必须自行加锁同步

通过 Config.Scorer 字段注入自定义评分器:

go
type MyScorer struct{}

func (s *MyScorer) Score(node html.ContentNode) int {
    // 自定义评分逻辑
    return 0
}

func (s *MyScorer) ShouldRemove(node html.ContentNode) bool {
    // 自定义移除逻辑
    return false
}

cfg := html.DefaultConfig()
cfg.Scorer = &MyScorer{}

ContentNode 与内部类型的关系

ContentNode 是一个抽象接口,屏蔽了底层 golang.org/x/net/html.Node 类型。库内部通过 contentNodeAdapter*html.Node 包装为 ContentNode

text
调用方 Scorer.Score(ContentNode)

                    contentNodeAdapter(适配器)

                  内部 *html.Node(golang.org/x/net/html)

这种双层接口设计实现了:

  • 公共 API 干净——用户实现自定义 Scorer 无需导入 golang.org/x/net/html
  • 内部高性能——库内部的 DefaultScorer 直接操作 *html.Node(通过内部 Scorer 接口),避免适配层开销
  • 适配由库自动完成——scorerAdapter 在公共 Scorer 和内部 Scorer 之间双向转换,用户无感知

ContentNode 返回 nil 的约定

FirstChild()/NextSibling()/Parent() 在没有对应节点时返回 nil。自定义 Scorer 遍历子树时务必做 nil 检查,否则会 panic。

AuditSink

审计日志输出接口。

go
type AuditSink interface {
    Write(entry AuditEntry)
    Close() error
}

内置 Sink 实现详见 审计系统