Golang新手尝试制作爬虫搜索引擎的实战记录

Golang新手尝试制作爬虫搜索引擎的实战记录 GitHub

kiri139/scraping-search-engine-golang–

头像

Scraping. * 我对性能和准确性没有信心,我不负责。(私は性能に自信がありませんスクレイピングを行います責任を持ちません) - kiri139/scraping-search-engine-golang–

代码在上面的GitHub上。

这是一段粗糙的代码,但是……

感谢您对改进建议的评论!


更多关于Golang新手尝试制作爬虫搜索引擎的实战记录的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang新手尝试制作爬虫搜索引擎的实战记录的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这是一个很棒的初学者项目!让我来分析一下你的爬虫搜索引擎实现,并提供一些专业的代码改进建议。

1. 并发处理优化

你的爬虫可以更好地利用Go的并发特性。当前版本可能没有充分利用goroutine:

// 改进的并发爬取示例
func crawlConcurrently(urls []string, maxWorkers int) []string {
    results := make(chan string, len(urls))
    sem := make(chan struct{}, maxWorkers) // 控制并发数
    
    var wg sync.WaitGroup
    
    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            sem <- struct{}{}        // 获取信号量
            defer func() { <-sem }() // 释放信号量
            
            if content, err := fetchURL(u); err == nil {
                results <- content
            }
        }(url)
    }
    
    go func() {
        wg.Wait()
        close(results)
    }()
    
    var allResults []string
    for result := range results {
        allResults = append(allResults, result)
    }
    
    return allResults
}

2. 请求限速与礼貌爬取

避免被网站封禁,实现礼貌爬取:

type RateLimiter struct {
    interval time.Duration
    ticker   *time.Ticker
}

func NewRateLimiter(ratePerSecond int) *RateLimiter {
    interval := time.Second / time.Duration(ratePerSecond)
    return &RateLimiter{
        interval: interval,
        ticker:   time.NewTicker(interval),
    }
}

func (rl *RateLimiter) Wait() {
    <-rl.ticker.C
}

// 使用示例
func politeCrawl(url string, limiter *RateLimiter) (string, error) {
    limiter.Wait()
    return fetchURL(url)
}

3. 改进的HTML解析与数据提取

使用更健壮的解析库:

import "github.com/PuerkitoBio/goquery"

func extractContent(html string) (title, body string, links []string) {
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
    if err != nil {
        return "", "", nil
    }
    
    // 提取标题
    title = doc.Find("title").Text()
    
    // 提取正文内容(排除脚本和样式)
    doc.Find("script, style").Remove()
    body = doc.Find("body").Text()
    
    // 提取所有链接
    doc.Find("a[href]").Each(func(i int, s *goquery.Selection) {
        if href, exists := s.Attr("href"); exists {
            links = append(links, href)
        }
    })
    
    return title, body, links
}

4. 简单的倒排索引实现

对于搜索引擎功能,需要建立索引:

type SearchEngine struct {
    index map[string][]IndexEntry
    mu    sync.RWMutex
}

type IndexEntry struct {
    URL   string
    Title string
    Score float64
}

func (se *SearchEngine) IndexPage(url, title, content string) {
    se.mu.Lock()
    defer se.mu.Unlock()
    
    words := tokenize(content)
    for _, word := range words {
        if se.index[word] == nil {
            se.index[word] = make([]IndexEntry, 0)
        }
        se.index[word] = append(se.index[word], IndexEntry{
            URL:   url,
            Title: title,
            Score: calculateTFIDF(word, content),
        })
    }
}

func (se *SearchEngine) Search(query string) []IndexEntry {
    se.mu.RLock()
    defer se.mu.RUnlock()
    
    queryWords := tokenize(query)
    results := make(map[string]IndexEntry)
    
    for _, word := range queryWords {
        if entries, exists := se.index[word]; exists {
            for _, entry := range entries {
                if existing, ok := results[entry.URL]; ok {
                    existing.Score += entry.Score
                    results[entry.URL] = existing
                } else {
                    results[entry.URL] = entry
                }
            }
        }
    }
    
    // 按分数排序
    sortedResults := make([]IndexEntry, 0, len(results))
    for _, entry := range results {
        sortedResults = append(sortedResults, entry)
    }
    
    sort.Slice(sortedResults, func(i, j int) bool {
        return sortedResults[i].Score > sortedResults[j].Score
    })
    
    return sortedResults
}

5. 错误处理与重试机制

func fetchWithRetry(url string, maxRetries int) (string, error) {
    var lastErr error
    
    for i := 0; i < maxRetries; i++ {
        if i > 0 {
            time.Sleep(time.Duration(i*i) * time.Second) // 指数退避
        }
        
        content, err := fetchURL(url)
        if err == nil {
            return content, nil
        }
        lastErr = err
        
        if shouldRetry(err) {
            continue
        }
        break
    }
    
    return "", fmt.Errorf("failed after %d retries: %v", maxRetries, lastErr)
}

func shouldRetry(err error) bool {
    if err == nil {
        return false
    }
    
    // 检查是否为网络错误或5xx服务器错误
    if strings.Contains(err.Error(), "timeout") ||
       strings.Contains(err.Error(), "connection refused") ||
       strings.Contains(err.Error(), "5") {
        return true
    }
    
    return false
}

6. 配置管理与上下文传递

type Config struct {
    MaxConcurrent int           `yaml:"max_concurrent"`
    RequestDelay  time.Duration `yaml:"request_delay"`
    UserAgent     string        `yaml:"user_agent"`
    Timeout       time.Duration `yaml:"timeout"`
}

type Crawler struct {
    config     *Config
    httpClient *http.Client
    limiter    *RateLimiter
    index      *SearchEngine
}

func NewCrawler(cfg *Config) *Crawler {
    return &Crawler{
        config: cfg,
        httpClient: &http.Client{
            Timeout: cfg.Timeout,
        },
        limiter: NewRateLimiter(int(time.Second / cfg.RequestDelay)),
        index:   &SearchEngine{index: make(map[string][]IndexEntry)},
    }
}

func (c *Crawler) Crawl(ctx context.Context, startURL string) error {
    // 使用上下文控制爬取过程
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        c.limiter.Wait()
        return c.crawlPage(ctx, startURL)
    }
}

这些改进可以让你的爬虫搜索引擎更加健壮、高效和可维护。每个组件都可以独立测试和扩展,这是生产级爬虫系统的基础架构。

回到顶部