mirror of
https://gitee.com/blackfox/geekai.git
synced 2025-12-07 01:08:24 +08:00
333 lines
8.0 KiB
Go
333 lines
8.0 KiB
Go
package crawler
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"geekai/logger"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/go-rod/rod"
|
||
"github.com/go-rod/rod/lib/launcher"
|
||
"github.com/go-rod/rod/lib/proto"
|
||
)
|
||
|
||
// Service 网络爬虫服务
|
||
type Service struct {
|
||
browser *rod.Browser
|
||
}
|
||
|
||
// NewService 创建一个新的爬虫服务
|
||
func NewService() (*Service, error) {
|
||
// 启动浏览器
|
||
path, _ := launcher.LookPath()
|
||
u := launcher.New().Bin(path).
|
||
Headless(true). // 无头模式
|
||
Set("disable-web-security", ""). // 禁用网络安全限制
|
||
Set("disable-gpu", ""). // 禁用 GPU 加速
|
||
Set("no-sandbox", ""). // 禁用沙箱模式
|
||
Set("disable-setuid-sandbox", "").// 禁用 setuid 沙箱
|
||
MustLaunch()
|
||
|
||
browser := rod.New().ControlURL(u).MustConnect()
|
||
|
||
return &Service{
|
||
browser: browser,
|
||
}, nil
|
||
}
|
||
|
||
// SearchResult 搜索结果
|
||
type SearchResult struct {
|
||
Title string `json:"title"` // 标题
|
||
URL string `json:"url"` // 链接
|
||
Content string `json:"content"` // 内容摘要
|
||
}
|
||
|
||
// WebSearch 网络搜索
|
||
func (s *Service) WebSearch(keyword string, maxPages int) ([]SearchResult, error) {
|
||
if keyword == "" {
|
||
return nil, errors.New("搜索关键词不能为空")
|
||
}
|
||
|
||
if maxPages <= 0 {
|
||
maxPages = 1
|
||
}
|
||
if maxPages > 10 {
|
||
maxPages = 10 // 最多搜索 10 页
|
||
}
|
||
|
||
results := make([]SearchResult, 0)
|
||
|
||
// 使用百度搜索
|
||
searchURL := fmt.Sprintf("https://www.baidu.com/s?wd=%s", url.QueryEscape(keyword))
|
||
|
||
// 设置页面超时
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
|
||
// 创建页面
|
||
page := s.browser.MustPage()
|
||
defer page.MustClose()
|
||
|
||
// 设置视口大小
|
||
err := page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||
Width: 1280,
|
||
Height: 800,
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("设置视口失败: %v", err)
|
||
}
|
||
|
||
// 导航到搜索页面
|
||
err = page.Context(ctx).Navigate(searchURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("导航到搜索页面失败: %v", err)
|
||
}
|
||
|
||
// 等待搜索结果加载完成
|
||
err = page.WaitLoad()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("等待页面加载完成失败: %v", err)
|
||
}
|
||
|
||
// 分析当前页面的搜索结果
|
||
for i := 0; i < maxPages; i++ {
|
||
if i > 0 {
|
||
// 点击下一页按钮
|
||
nextPage, err := page.Element("a.n")
|
||
if err != nil || nextPage == nil {
|
||
break // 没有下一页
|
||
}
|
||
|
||
err = nextPage.Click(proto.InputMouseButtonLeft, 1)
|
||
if err != nil {
|
||
break // 点击下一页失败
|
||
}
|
||
|
||
// 等待新页面加载
|
||
err = page.WaitLoad()
|
||
if err != nil {
|
||
break
|
||
}
|
||
}
|
||
|
||
// 提取搜索结果
|
||
resultElements, err := page.Elements(".result, .c-container")
|
||
if err != nil || resultElements == nil {
|
||
continue
|
||
}
|
||
|
||
for _, result := range resultElements {
|
||
// 获取标题
|
||
titleElement, err := result.Element("h3, .t")
|
||
if err != nil || titleElement == nil {
|
||
continue
|
||
}
|
||
|
||
title, err := titleElement.Text()
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
// 获取 URL
|
||
linkElement, err := titleElement.Element("a")
|
||
if err != nil || linkElement == nil {
|
||
continue
|
||
}
|
||
|
||
href, err := linkElement.Attribute("href")
|
||
if err != nil || href == nil {
|
||
continue
|
||
}
|
||
|
||
// 获取内容摘要 - 尝试多个可能的选择器
|
||
var contentElement *rod.Element
|
||
var content string
|
||
|
||
// 尝试多个可能的选择器来适应不同版本的百度搜索结果
|
||
selectors := []string{".content-right_8Zs40", ".c-abstract", ".content_LJ0WN", ".content"}
|
||
for _, selector := range selectors {
|
||
contentElement, err = result.Element(selector)
|
||
if err == nil && contentElement != nil {
|
||
content, _ = contentElement.Text()
|
||
if content != "" {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果所有选择器都失败,尝试直接从结果块中提取文本
|
||
if content == "" {
|
||
// 获取结果元素的所有文本
|
||
fullText, err := result.Text()
|
||
if err == nil && fullText != "" {
|
||
// 简单处理:从全文中移除标题,剩下的可能是摘要
|
||
fullText = strings.Replace(fullText, title, "", 1)
|
||
// 清理文本
|
||
content = strings.TrimSpace(fullText)
|
||
// 限制内容长度
|
||
if len(content) > 200 {
|
||
content = content[:200] + "..."
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加到结果集
|
||
results = append(results, SearchResult{
|
||
Title: title,
|
||
URL: *href,
|
||
Content: content,
|
||
})
|
||
|
||
// 限制结果数量,每页最多 10 条
|
||
if len(results) >= 10*maxPages {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取真实 URL(百度搜索结果中的 URL 是短链接,需要跳转获取真实 URL)
|
||
for i, result := range results {
|
||
realURL, err := s.getRedirectURL(result.URL)
|
||
if err == nil && realURL != "" {
|
||
results[i].URL = realURL
|
||
}
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// 获取真实 URL
|
||
func (s *Service) getRedirectURL(shortURL string) (string, error) {
|
||
// 创建页面
|
||
page, err := s.browser.Page(proto.TargetCreateTarget{URL: ""})
|
||
if err != nil {
|
||
return shortURL, err // 返回原始URL
|
||
}
|
||
defer func() {
|
||
_ = page.Close()
|
||
}()
|
||
|
||
// 导航到短链接
|
||
err = page.Navigate(shortURL)
|
||
if err != nil {
|
||
return shortURL, err // 返回原始URL
|
||
}
|
||
|
||
// 等待重定向完成
|
||
time.Sleep(2 * time.Second)
|
||
|
||
// 获取当前 URL
|
||
info, err := page.Info()
|
||
if err != nil {
|
||
return shortURL, err // 返回原始URL
|
||
}
|
||
|
||
return info.URL, nil
|
||
}
|
||
|
||
// Close 关闭浏览器
|
||
func (s *Service) Close() error {
|
||
if s.browser != nil {
|
||
err := s.browser.Close()
|
||
s.browser = nil
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SearchWeb 封装的搜索方法
|
||
func SearchWeb(keyword string, maxPages int) (string, error) {
|
||
// 添加panic恢复机制
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
log := logger.GetLogger()
|
||
log.Errorf("爬虫服务崩溃: %v", r)
|
||
}
|
||
}()
|
||
|
||
service, err := NewService()
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建爬虫服务失败: %v", err)
|
||
}
|
||
defer service.Close()
|
||
|
||
// 设置超时上下文
|
||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||
defer cancel()
|
||
|
||
// 使用goroutine和通道来处理超时
|
||
resultChan := make(chan []SearchResult, 1)
|
||
errChan := make(chan error, 1)
|
||
|
||
go func() {
|
||
results, err := service.WebSearch(keyword, maxPages)
|
||
if err != nil {
|
||
errChan <- err
|
||
return
|
||
}
|
||
resultChan <- results
|
||
}()
|
||
|
||
// 等待结果或超时
|
||
select {
|
||
case <-ctx.Done():
|
||
return "", fmt.Errorf("搜索超时: %v", ctx.Err())
|
||
case err := <-errChan:
|
||
return "", fmt.Errorf("搜索失败: %v", err)
|
||
case results := <-resultChan:
|
||
if len(results) == 0 {
|
||
return "未找到关于 \"" + keyword + "\" 的相关搜索结果", nil
|
||
}
|
||
|
||
// 格式化结果
|
||
var builder strings.Builder
|
||
builder.WriteString(fmt.Sprintf("为您找到关于 \"%s\" 的 %d 条搜索结果:\n\n", keyword, len(results)))
|
||
|
||
for i, result := range results {
|
||
// // 尝试打开链接获取实际内容
|
||
// page := service.browser.MustPage()
|
||
// defer page.MustClose()
|
||
|
||
// // 设置页面超时
|
||
// pageCtx, pageCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
// defer pageCancel()
|
||
|
||
// // 导航到目标页面
|
||
// err := page.Context(pageCtx).Navigate(result.URL)
|
||
// if err == nil {
|
||
// // 等待页面加载
|
||
// _ = page.WaitLoad()
|
||
|
||
// // 获取页面标题
|
||
// title, err := page.Eval("() => document.title")
|
||
// if err == nil && title.Value.String() != "" {
|
||
// result.Title = title.Value.String()
|
||
// }
|
||
|
||
// // 获取页面主要内容
|
||
// if content, err := page.Element("body"); err == nil {
|
||
// if text, err := content.Text(); err == nil {
|
||
// // 清理并截取内容
|
||
// text = strings.TrimSpace(text)
|
||
// if len(text) > 200 {
|
||
// text = text[:200] + "..."
|
||
// }
|
||
// result.Content = text
|
||
// }
|
||
// }
|
||
// }
|
||
|
||
builder.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, result.Title))
|
||
builder.WriteString(fmt.Sprintf(" 链接: %s\n", result.URL))
|
||
if result.Content != "" {
|
||
builder.WriteString(fmt.Sprintf(" 摘要: %s\n", result.Content))
|
||
}
|
||
builder.WriteString("\n")
|
||
}
|
||
|
||
return builder.String(), nil
|
||
}
|
||
} |