feat: 香港服务器搜索代理 search-proxy (Go) — Google/Bing/DDG直连API

This commit is contained in:
thzxx
2026-06-10 17:06:36 +08:00
parent 7f3f4d3914
commit 74a5a5609b
3 changed files with 329 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# search-proxy
香港服务器上的搜索引擎代理。本应用通过 HTTP API 调用,直连 Google/Bing/DDG。
## 部署
```bash
# 1. 拷贝到服务器
scp search-proxy user@your-server:/opt/search-proxy/
# 2. 编译(如果有 Go 环境)
cd /opt/search-proxy && go build -o search-proxy .
# 或者本地交叉编译后上传:
# GOOS=linux GOARCH=amd64 go build -o search-proxy .
# 3. 测试
./search-proxy &
curl "http://localhost:8080/health"
curl "http://localhost:8080/search?q=hello&engine=google&n=5"
# 4. 安装 systemd 服务(可选,开机自启)
sudo cp search-proxy.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now search-proxy
```
## API
```
GET /search?q=关键词&engine=google&n=10
engine: google | bing | ddg | all
n: 1-20,默认10
缓存: 5分钟
GET /health
→ {"status":"ok"}
```
## 本应用配置
在设置面板的代理输入框中填入:`http://你的服务器IP:8080`
或者通过代理设置:在 search-proxy 前挂 nginx 反代加 HTTPS。
+271
View File
@@ -0,0 +1,271 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
)
var cache = sync.Map{}
type cacheEntry struct {
Results []SearchResult `json:"-"`
ExpiresAt int64 `json:"-"`
}
type SearchResult struct {
Title string `json:"title"`
URL string `json:"url"`
Snippet string `json:"snippet"`
Engine string `json:"engine"`
}
type SearchResponse struct {
Success bool `json:"success"`
Query string `json:"query"`
Total int `json:"total"`
Results []SearchResult `json:"results"`
Error string `json:"error,omitempty"`
}
var httpClient = &http.Client{Timeout: 15 * time.Second}
var ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
// ── Google ──
func searchGoogle(q string, n int) []SearchResult {
u := fmt.Sprintf("https://www.google.com/search?q=%s&num=%d&hl=zh-CN", url.QueryEscape(q), n)
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", ua)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
resp, err := httpClient.Do(req)
if err != nil {
log.Printf("Google: %v", err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
log.Printf("Google HTTP %d", resp.StatusCode)
return nil
}
body, _ := io.ReadAll(resp.Body)
html := string(body)
reBlock := regexp.MustCompile(`<div class="g"[^>]*>([\s\S]*?)(?=<div class="g"|</div>\s*</div>\s*</div>)`)
reTitle := regexp.MustCompile(`<a[^>]*href="(https?://[^"]*)"[^>]*>[\s\S]*?<h3[^>]*>([\s\S]*?)</h3>`)
reSn := regexp.MustCompile(`<(?:div|span)[^>]*(?:data-sncf|class="[^"]*st[^"]*"|class="[^"]*VwiC3b[^"]*")[^>]*>([\s\S]*?)</(?:div|span)>`)
var results []SearchResult
for _, m := range reBlock.FindAllStringSubmatch(html, -1) {
if len(results) >= n {
break
}
tm := reTitle.FindStringSubmatch(m[1])
if tm == nil {
continue
}
title := clean(tm[2])
link := tm[1]
if title == "" || strings.Contains(link, "google.com") {
continue
}
sn := ""
if sm := reSn.FindStringSubmatch(m[1]); sm != nil {
sn = clean(sm[1])
}
results = append(results, SearchResult{Title: title, URL: link, Snippet: sn, Engine: "google"})
}
return results
}
// ── Bing ──
func searchBing(q string, n int) []SearchResult {
u := fmt.Sprintf("https://www.bing.com/search?q=%s&count=%d", url.QueryEscape(q), n)
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", ua)
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
resp, err := httpClient.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
html := string(body)
reBlock := regexp.MustCompile(`<li class="b_algo"[^>]*>([\s\S]*?)</li>`)
reTitle := regexp.MustCompile(`<a[^>]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)</a>`)
reSn := regexp.MustCompile(`<p[^>]*>([\s\S]*?)</p>`)
var results []SearchResult
for _, m := range reBlock.FindAllStringSubmatch(html, -1) {
if len(results) >= n {
break
}
tm := reTitle.FindStringSubmatch(m[1])
if tm == nil {
continue
}
title := clean(tm[2])
if title == "" {
continue
}
sn := ""
if sm := reSn.FindStringSubmatch(m[1]); sm != nil {
sn = clean(sm[1])
}
results = append(results, SearchResult{Title: title, URL: tm[1], Snippet: sn, Engine: "bing"})
}
return results
}
// ── DuckDuckGo Lite ──
func searchDDG(q string, n int) []SearchResult {
u := fmt.Sprintf("https://lite.duckduckgo.com/lite/?q=%s", url.QueryEscape(q))
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", ua)
resp, err := httpClient.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
html := string(body)
reLink := regexp.MustCompile(`<a[^>]*href="(https?://[^"]*)"[^>]*>([\s\S]*?)</a>`)
var results []SearchResult
for _, m := range reLink.FindAllStringSubmatch(html, -1) {
if len(results) >= n {
break
}
link := decodeEntities(m[1])
title := clean(m[2])
if title == "" || strings.Contains(link, "duckduckgo.com") {
continue
}
results = append(results, SearchResult{Title: title, URL: link, Snippet: "", Engine: "ddg"})
}
return results
}
func clean(s string) string {
s = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(s, "")
s = decodeEntities(s)
return strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
}
func decodeEntities(s string) string {
repl := strings.NewReplacer(
"&nbsp;", " ", "&lt;", "<", "&gt;", ">", "&amp;", "&",
"&quot;", `"`, "&#39;", "'", "&apos;", "'",
"&hellip;", "…", "&mdash;", "—", "&ndash;", "",
)
return repl.Replace(s)
}
// ── Handler ──
func handleSearch(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
if q == "" {
writeJSON(w, http.StatusBadRequest, SearchResponse{Success: false, Error: "missing q"})
return
}
engine := r.URL.Query().Get("engine")
if engine == "" {
engine = "google"
}
n := 10
fmt.Sscanf(r.URL.Query().Get("n"), "%d", &n)
if n > 20 {
n = 20
}
ck := fmt.Sprintf("%s|%s|%d", q, engine, n)
if v, ok := cache.Load(ck); ok {
ce := v.(cacheEntry)
if time.Now().Unix() < ce.ExpiresAt {
writeJSON(w, http.StatusOK, SearchResponse{Success: true, Query: q, Total: len(ce.Results), Results: ce.Results})
return
}
cache.Delete(ck)
}
var results []SearchResult
switch engine {
case "google":
results = searchGoogle(q, n)
case "bing":
results = searchBing(q, n)
case "ddg":
results = searchDDG(q, n)
case "all":
results = parallelSearch(q, n)
default:
writeJSON(w, http.StatusBadRequest, SearchResponse{Success: false, Error: "unknown engine: " + engine})
return
}
cache.Store(ck, cacheEntry{Results: results, ExpiresAt: time.Now().Add(5 * time.Minute).Unix()})
writeJSON(w, http.StatusOK, SearchResponse{Success: true, Query: q, Total: len(results), Results: results})
}
func parallelSearch(q string, n int) []SearchResult {
ch := make(chan []SearchResult, 3)
go func() { ch <- searchGoogle(q, n) }()
go func() { ch <- searchBing(q, n) }()
go func() { ch <- searchDDG(q, n) }()
seen := map[string]bool{}
var results []SearchResult
for i := 0; i < 3; i++ {
for _, r := range <-ch {
key := strings.TrimRight(r.URL, "/")
if !seen[key] {
seen[key] = true
results = append(results, r)
}
}
}
if len(results) > n {
results = results[:n]
}
return results
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/search", handleSearch)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
log.Printf("search-proxy :%s (google/bing/ddg/all)", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=Search Proxy - Google/Bing/DDG API
After=network.target
[Service]
Type=simple
ExecStart=/opt/search-proxy/search-proxy
Restart=always
RestartSec=5
Environment=PORT=8080
[Install]
WantedBy=multi-user.target