diff --git a/search-proxy/main.go b/search-proxy/main.go
index b1a1858..b39946b 100644
--- a/search-proxy/main.go
+++ b/search-proxy/main.go
@@ -23,7 +23,7 @@ import (
"time"
)
-const version = "2.1.0"
+const version = "2.2.0"
// ── CLI ──
var (
@@ -41,6 +41,18 @@ var (
var pidFile = "/tmp/metona-search-proxy.pid"
+// ── 统计 ──
+type statsData struct {
+ ReqTotal atomic.Int64
+ ReqRate atomic.Int64 // 被限流的
+ CacheHit atomic.Int64
+ EngineOK [4]atomic.Int64 // google/bing/baidu/ddg 成功
+ EngineFail [4]atomic.Int64 // google/bing/baidu/ddg 失败
+ StartTime time.Time
+}
+
+var stats = &statsData{StartTime: time.Now()}
+
// ── 限流器 ──
type RateLimiter struct {
tokens atomic.Int64
@@ -63,12 +75,16 @@ func (rl *RateLimiter) Allow() bool {
newTokens := int64(elapsed * float64(rl.rate))
if newTokens > 0 {
rl.lastFill.Store(now)
- current := rl.tokens.Load()
- next := current + newTokens
- if next > rl.capacity {
- next = rl.capacity
+ for {
+ current := rl.tokens.Load()
+ next := current + newTokens
+ if next > rl.capacity {
+ next = rl.capacity
+ }
+ if rl.tokens.CompareAndSwap(current, next) {
+ break
+ }
}
- rl.tokens.Store(next)
}
for {
current := rl.tokens.Load()
@@ -89,6 +105,47 @@ type cacheEntry struct {
ExpiresAt int64
}
+var cacheSize atomic.Int64
+
+func cleanCacheLoop(interval time.Duration) {
+ for {
+ time.Sleep(interval)
+ now := time.Now().Unix()
+ count := 0
+ cache.Range(func(key, value any) bool {
+ ce := value.(cacheEntry)
+ if now >= ce.ExpiresAt {
+ cache.Delete(key)
+ count++
+ }
+ return true
+ })
+ if count > 0 {
+ log.Printf("cache cleaned: %d entries removed", count)
+ }
+ }
+}
+
+// ── 正则预编译(全局,避免每次搜索重新编译)──
+var (
+ reGoogleBlock = regexp.MustCompile(`<(?:div|li)[^>]*class="[^"]*(?:Gx5Zad|g|kvH3mc)[^"]*"[^>]*>([\s\S]*?)(?=<(?:div|li)[^>]*class="[^"]*(?:Gx5Zad|kvH3mc)[^"]*"|<(?:div|g-section)|\z)`)
+ reGoogleTitle = regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>[\s\S]*?]*>([\s\S]*?)
`)
+ reGoogleSnip = regexp.MustCompile(`<(?:div|span)[^>]*(?:data-sncf|class="[^"]*(?:VwiC3b|st|lWMagT)[^"]*")[^>]*>([\s\S]*?)(?:div|span)>`)
+
+ reBingBlock = regexp.MustCompile(`
]*>([\s\S]*?)
`) + + reDDGLink = regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) + + reBaiduBlock = regexp.MustCompile(`]*>([\s\S]*?)
`) - var out []Result - for _, m := range reB.FindAllStringSubmatch(html, -1) { + for _, m := range reBingBlock.FindAllStringSubmatch(html, -1) { if len(out) >= n { break } - tm := reT.FindStringSubmatch(m[1]) + tm := reBingTitle.FindStringSubmatch(m[1]) if tm == nil { continue } @@ -328,7 +386,7 @@ func searchBing(q string, n int) []Result { continue } snip := "" - if sm := reS.FindStringSubmatch(m[1]); sm != nil { + if sm := reBingSnip.FindStringSubmatch(m[1]); sm != nil { snip = clean(sm[1]) } out = append(out, Result{Title: title, URL: tm[1], Snippet: snip, Engine: "bing"}) @@ -339,17 +397,21 @@ func searchBing(q string, n int) []Result { func searchDDG(q string, n int) []Result { u := fmt.Sprintf("https://lite.duckduckgo.com/lite/?q=%s", url.QueryEscape(q)) resp, err := doGet(u) - if err != nil { + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + codeLog("ddg", resp.StatusCode) + } else { + codeLog("ddg", 0) + } return nil } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) html := string(body) - reL := regexp.MustCompile(`]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)`) - var out []Result - for _, m := range reL.FindAllStringSubmatch(html, -1) { + for _, m := range reDDGLink.FindAllStringSubmatch(html, -1) { if len(out) >= n { break } @@ -366,29 +428,31 @@ func searchDDG(q string, n int) []Result { func searchBaidu(q string, n int) []Result { u := fmt.Sprintf("https://www.baidu.com/s?wd=%s&rn=%d", url.QueryEscape(q), n) resp, err := doGet(u) - if err != nil { + if err != nil || resp.StatusCode != 200 { + if resp != nil { + resp.Body.Close() + codeLog("baidu", resp.StatusCode) + } else { + codeLog("baidu", 0) + } return nil } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) html := string(body) - reB := regexp.MustCompile(`