无法正常退出,但也能用

This commit is contained in:
2026-04-08 21:14:13 +08:00
parent 6c6a94c043
commit 7844495c98
2 changed files with 50 additions and 27 deletions
+9 -13
View File
@@ -7,7 +7,6 @@ package analyzer
import ( import (
"encoding/json" // JSON 反序列化(加载屏蔽词列表) "encoding/json" // JSON 反序列化(加载屏蔽词列表)
"log" // 日志(gojieba panic 恢复时输出)
"math" // 数学运算(最小值、开方) "math" // 数学运算(最小值、开方)
"os" // 文件系统操作(读取屏蔽词文件) "os" // 文件系统操作(读取屏蔽词文件)
"strings" // 字符串操作 "strings" // 字符串操作
@@ -87,35 +86,32 @@ func loadStopWords(path string) map[string]bool {
// searchMode=true:搜索模式分词(更细粒度,适合搜索查询); // searchMode=true:搜索模式分词(更细粒度,适合搜索查询);
// searchMode=false:精确模式分词(适合页面内容分析)。 // searchMode=false:精确模式分词(适合页面内容分析)。
// 策略:纯 ASCII 字母数字按空格切分;含中文/其他字符的片段交给结巴处理。 // 策略:纯 ASCII 字母数字按空格切分;含中文/其他字符的片段交给结巴处理。
func (a *Analyzer) Tokenize(s string, searchMode bool) (result []string) { func (a *Analyzer) Tokenize(s string, searchMode bool) []string {
// 超长文本截断(jieba 对极长文本处理效率下降) // 超长文本截断(jieba 对极长文本处理效率下降)
if len(s) > 10000 { if len(s) > 10000 {
s = s[:10000] s = s[:10000]
} }
// 清洗非 UTF-8 字节,防止 gojieba 的 C++ 层报 "decode failed" 错误 // 清洗非 UTF-8 字节,防止 gojieba 的 C++ 层报 "decode failed" 错误
s = strings.ToValidUTF8(s, "") s = strings.ToValidUTF8(s, "")
var result []string
for _, part := range strings.Fields(s) { for _, part := range strings.Fields(s) {
if isASCIIAlnum(part) { if isASCIIAlnum(part) {
// 纯 ASCII 片段直接保留,不走 jieba // 纯 ASCII 片段直接保留,不走 jieba
result = append(result, part) result = append(result, part)
} else { } else {
// 非 ASCII(含中文/日文等):加锁后调用 jieba 分词 // 非 ASCII(含中文/日文等):加锁后调用 jieba 分词
// defer recover 防止 gojieba C++ 崩溃(如 Ctrl+C 时 SIGSEGV)导致锁泄漏
a.mu.Lock() a.mu.Lock()
defer func() { var tokens []string
if r := recover(); r != nil { if searchMode {
log.Printf("[analyzer] gojieba panic recovered: %v", r) tokens = a.jieba.CutForSearch(part, true)
} else {
tokens = a.jieba.Cut(part, true)
} }
a.mu.Unlock() a.mu.Unlock()
}() result = append(result, tokens...)
if searchMode {
result = append(result, a.jieba.CutForSearch(part, true)...)
} else {
result = append(result, a.jieba.Cut(part, true)...)
} }
} }
} return result
return
} }
// Normalize 标准化字符串:去除非字母数字非 CJK 字符,并转为小写。 // Normalize 标准化字符串:去除非字母数字非 CJK 字符,并转为小写。
+33 -6
View File
@@ -162,14 +162,42 @@ func (c *Crawler) Run(entryURL string, maxEpoch int) {
} }
// visitURL 抓取一个 URL,提取关键词、缓存摘要、更新网站元信息,返回页面中发现的子链接。 // visitURL 抓取一个 URL,提取关键词、缓存摘要、更新网站元信息,返回页面中发现的子链接。
func (c *Crawler) visitURL(rawURL string) []string { func (c *Crawler) visitURL(rawURL string) (hrefs []string) {
// recover 保护:防止任何模块(analyzer/storage/parser)的 panic 杀死 goroutine
defer func() {
if r := recover(); r != nil {
log.Printf("[crawler] visitURL panic recovered: url=%s error=%v", rawURL, r)
}
}()
atomic.AddInt64(&c.stats.VisitedURLs, 1) // 计数器 +1 atomic.AddInt64(&c.stats.VisitedURLs, 1) // 计数器 +1
// 礼貌模式抓取(遵守 robots.txt + 限流),超时 10 秒,不限制大小 // 使用 sync.WaitGroup + select 实现硬超时包装器,
res, err := c.fetcher.fetchWithHistory(rawURL, true, 10*time.Second, 0) // 确保即使 http.Client.Timout 被某些底层操作忽略,goroutine 也不会永久阻塞。
if err != nil || res == nil { fetchTimeout := 30 * time.Second
var res *FetchResult
var fetchErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 礼貌模式抓取(遵守 robots.txt + 限流),不限制大小
res, fetchErr = c.fetcher.fetchWithHistory(rawURL, true, fetchTimeout, 0)
}()
waitCh := make(chan struct{})
go func() {
wg.Wait()
close(waitCh)
}()
select {
case <-waitCh:
// fetch 正常返回(成功或错误)
case <-time.After(fetchTimeout + 5*time.Second):
log.Printf("[crawler] fetch timeout: %s", rawURL)
}
if fetchErr != nil || res == nil {
c.updateSiteFailure(rawURL) // 记录失败,更新该网站成功率 c.updateSiteFailure(rawURL) // 记录失败,更新该网站成功率
return nil return
} }
atomic.AddInt64(&c.stats.SuccessURLs, 1) // 成功计数器 +1 atomic.AddInt64(&c.stats.SuccessURLs, 1) // 成功计数器 +1
@@ -334,7 +362,6 @@ func (c *Crawler) sendToHarvester(finalURL string, kws []analyzer.Keyword) {
// 冷却结束,切换到半开,放行一个试探请求 // 冷却结束,切换到半开,放行一个试探请求
atomic.StoreInt32(&c.circuitState, circuitHalfOpen) atomic.StoreInt32(&c.circuitState, circuitHalfOpen)
atomic.StoreInt64(&c.circuitExpiry, now+int64(circuitCooldownSeconds)) atomic.StoreInt64(&c.circuitExpiry, now+int64(circuitCooldownSeconds))
log.Println("[crawler] circuit: half-open, probing harvester")
case circuitHalfOpen: case circuitHalfOpen:
if now < expiry { if now < expiry {
return // 半开冷却中,只放行第一个,其余跳过 return // 半开冷却中,只放行第一个,其余跳过