package util
import (
"net/url"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"pansou/model"
)
// normalizeUrl 标准化URL,将URL编码的中文部分解码为中文,用于去重
func normalizeUrl(rawUrl string) string {
// 解码URL中的编码字符
decoded, err := url.QueryUnescape(rawUrl)
if err != nil {
// 如果解码失败,返回原始URL
return rawUrl
}
return decoded
}
// isSupportedLink 检查链接是否为支持的网盘链接
func isSupportedLink(url string) bool {
lowerURL := strings.ToLower(url)
// 检查是否为百度网盘链接
if BaiduPanPattern.MatchString(lowerURL) {
return true
}
// 检查是否为天翼云盘链接
if TianyiPanPattern.MatchString(lowerURL) {
return true
}
// 检查是否为UC网盘链接
if UCPanPattern.MatchString(lowerURL) {
return true
}
// 检查是否为123网盘链接
if Pan123Pattern.MatchString(lowerURL) {
return true
}
// 检查是否为夸克网盘链接
if QuarkPanPattern.MatchString(lowerURL) {
return true
}
// 检查是否为迅雷网盘链接
if XunleiPanPattern.MatchString(lowerURL) {
return true
}
// 检查是否为115网盘链接
if Pan115Pattern.MatchString(lowerURL) {
return true
}
// 使用通用模式检查其他网盘链接
return AllPanLinksPattern.MatchString(lowerURL)
}
// normalizeBaiduPanURL 标准化百度网盘URL,确保链接格式正确并且包含密码参数
func normalizeBaiduPanURL(url string, password string) string {
// 清理URL,确保获取正确的链接部分
url = CleanBaiduPanURL(url)
// 如果URL已经包含pwd参数,不需要再添加
if strings.Contains(url, "?pwd=") {
return url
}
// 如果有提取到密码,且URL不包含pwd参数,则添加
if password != "" {
// 确保密码是4位
if len(password) > 4 {
password = password[:4]
}
return url + "?pwd=" + password
}
return url
}
// normalizeTianyiPanURL 标准化天翼云盘URL,确保链接格式正确
func normalizeTianyiPanURL(url string, password string) string {
// 清理URL,确保获取正确的链接部分
url = CleanTianyiPanURL(url)
// 天翼云盘链接通常不在URL中包含密码参数,所以这里不做处理
// 但是我们确保返回的是干净的链接
return url
}
// normalizeUCPanURL 标准化UC网盘URL,确保链接格式正确
func normalizeUCPanURL(url string, password string) string {
// 清理URL,确保获取正确的链接部分
url = CleanUCPanURL(url)
// UC网盘链接通常使用?public=1参数表示公开分享
// 确保链接格式正确,但不添加密码参数
return url
}
// normalize123PanURL 标准化123网盘URL,确保链接格式正确
func normalize123PanURL(url string, password string) string {
// 清理URL,确保获取正确的链接部分
url = Clean123PanURL(url)
// 123网盘链接通常不在URL中包含密码参数
// 但是我们确保返回的是干净的链接
return url
}
// normalize115PanURL 标准化115网盘URL,确保链接格式正确
func normalize115PanURL(url string, password string) string {
// 清理URL,确保获取正确的链接部分,只保留到password=后面4位密码
url = Clean115PanURL(url)
// 115网盘链接已经在Clean115PanURL中处理了密码部分
// 这里不需要额外添加密码参数
return url
}
// ParseSearchResults 解析搜索结果页面
func ParseSearchResults(html string, channel string) ([]model.SearchResult, string, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, "", err
}
var results []model.SearchResult
var nextPageParam string
// 查找消息块
doc.Find(".tgme_widget_message_wrap").Each(func(i int, s *goquery.Selection) {
messageDiv := s.Find(".tgme_widget_message")
// 提取消息ID
dataPost, exists := messageDiv.Attr("data-post")
if !exists {
return
}
parts := strings.Split(dataPost, "/")
if len(parts) != 2 {
return
}
messageID := parts[1]
// 生成全局唯一ID
uniqueID := channel + "_" + messageID
// 提取时间
timeStr, exists := messageDiv.Find(".tgme_widget_message_date time").Attr("datetime")
if !exists {
return
}
datetime, err := time.Parse(time.RFC3339, timeStr)
if err != nil {
return
}
// 获取消息文本元素
messageTextElem := messageDiv.Find(".tgme_widget_message_text")
// 获取消息文本的HTML内容
messageHTML, _ := messageTextElem.Html()
// 获取消息的纯文本内容
messageText := messageTextElem.Text()
// 提取标题
title := extractTitle(messageHTML, messageText)
// 提取网盘链接 - 使用更精确的方法
var links []model.Link
var foundLinks = make(map[string]bool) // 用于去重
var baiduLinkPasswords = make(map[string]string) // 存储百度链接和对应的密码
var tianyiLinkPasswords = make(map[string]string) // 存储天翼链接和对应的密码
var ucLinkPasswords = make(map[string]string) // 存储UC链接和对应的密码
var pan123LinkPasswords = make(map[string]string) // 存储123网盘链接和对应的密码
var pan115LinkPasswords = make(map[string]string) // 存储115网盘链接和对应的密码
var aliyunLinkPasswords = make(map[string]string) // 存储阿里云盘链接和对应的密码
// 1. 从文本内容中提取所有网盘链接和密码
extractedLinks := ExtractNetDiskLinks(messageText)
// 2. 从a标签中提取链接
messageTextElem.Find("a").Each(func(i int, a *goquery.Selection) {
href, exists := a.Attr("href")
if !exists {
return
}
// 使用更精确的方式匹配网盘链接
if isSupportedLink(href) {
linkType := GetLinkType(href)
password := ExtractPassword(messageText, href)
// 如果是百度网盘链接,记录链接和密码的对应关系
if linkType == "baidu" {
// 提取链接的基本部分(不含密码参数)
baseURL := href
if strings.Contains(href, "?pwd=") {
baseURL = href[:strings.Index(href, "?pwd=")]
}
// 记录密码
if password != "" {
baiduLinkPasswords[baseURL] = password
}
} else if linkType == "tianyi" {
// 如果是天翼云盘链接,记录链接和密码的对应关系
baseURL := CleanTianyiPanURL(href)
// 记录密码
if password != "" {
tianyiLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := tianyiLinkPasswords[baseURL]; !exists {
tianyiLinkPasswords[baseURL] = ""
}
}
} else if linkType == "uc" {
// 如果是UC网盘链接,记录链接和密码的对应关系
baseURL := CleanUCPanURL(href)
// 记录密码
if password != "" {
ucLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := ucLinkPasswords[baseURL]; !exists {
ucLinkPasswords[baseURL] = ""
}
}
} else if linkType == "123" {
// 如果是123网盘链接,记录链接和密码的对应关系
baseURL := Clean123PanURL(href)
// 记录密码
if password != "" {
pan123LinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := pan123LinkPasswords[baseURL]; !exists {
pan123LinkPasswords[baseURL] = ""
}
}
} else if linkType == "115" {
// 如果是115网盘链接,记录链接和密码的对应关系
baseURL := Clean115PanURL(href)
// 记录密码
if password != "" {
pan115LinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := pan115LinkPasswords[baseURL]; !exists {
pan115LinkPasswords[baseURL] = ""
}
}
} else if linkType == "aliyun" {
// 如果是阿里云盘链接,记录链接和密码的对应关系
baseURL := CleanAliyunPanURL(href)
// 记录密码
if password != "" {
aliyunLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := aliyunLinkPasswords[baseURL]; !exists {
aliyunLinkPasswords[baseURL] = ""
}
}
} else {
// 非特殊处理的网盘链接直接添加
// 使用标准化的URL进行去重
normalizedHref := normalizeUrl(href)
if !foundLinks[normalizedHref] {
foundLinks[normalizedHref] = true
links = append(links, model.Link{
Type: linkType,
URL: normalizedHref, // 使用标准化的URL
Password: password,
})
}
}
}
})
// 3. 处理从文本中提取的链接
for _, linkURL := range extractedLinks {
linkType := GetLinkType(linkURL)
password := ExtractPassword(messageText, linkURL)
// 如果是百度网盘链接,记录链接和密码的对应关系
if linkType == "baidu" {
// 提取链接的基本部分(不含密码参数)
baseURL := linkURL
if strings.Contains(linkURL, "?pwd=") {
baseURL = linkURL[:strings.Index(linkURL, "?pwd=")]
}
// 记录密码
if password != "" {
baiduLinkPasswords[baseURL] = password
}
} else if linkType == "tianyi" {
// 如果是天翼云盘链接,记录链接和密码的对应关系
baseURL := CleanTianyiPanURL(linkURL)
// 记录密码
if password != "" {
tianyiLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := tianyiLinkPasswords[baseURL]; !exists {
tianyiLinkPasswords[baseURL] = ""
}
}
} else if linkType == "uc" {
// 如果是UC网盘链接,记录链接和密码的对应关系
baseURL := CleanUCPanURL(linkURL)
// 记录密码
if password != "" {
ucLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := ucLinkPasswords[baseURL]; !exists {
ucLinkPasswords[baseURL] = ""
}
}
} else if linkType == "123" {
// 如果是123网盘链接,记录链接和密码的对应关系
baseURL := Clean123PanURL(linkURL)
// 记录密码
if password != "" {
pan123LinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := pan123LinkPasswords[baseURL]; !exists {
pan123LinkPasswords[baseURL] = ""
}
}
} else if linkType == "115" {
// 如果是115网盘链接,记录链接和密码的对应关系
baseURL := Clean115PanURL(linkURL)
// 记录密码
if password != "" {
pan115LinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := pan115LinkPasswords[baseURL]; !exists {
pan115LinkPasswords[baseURL] = ""
}
}
} else if linkType == "aliyun" {
// 如果是阿里云盘链接,记录链接和密码的对应关系
baseURL := CleanAliyunPanURL(linkURL)
// 记录密码
if password != "" {
aliyunLinkPasswords[baseURL] = password
} else {
// 即使没有密码,也添加到映射中,以便后续处理
if _, exists := aliyunLinkPasswords[baseURL]; !exists {
aliyunLinkPasswords[baseURL] = ""
}
}
} else {
// 非特殊处理的网盘链接直接添加
// 使用标准化的URL进行去重
normalizedLinkURL := normalizeUrl(linkURL)
if !foundLinks[normalizedLinkURL] {
foundLinks[normalizedLinkURL] = true
links = append(links, model.Link{
Type: linkType,
URL: normalizedLinkURL, // 使用标准化的URL
Password: password,
})
}
}
}
// 4. 处理百度网盘链接,确保每个链接只有一个版本(带密码的完整版本)
for baseURL, password := range baiduLinkPasswords {
normalizedURL := normalizeBaiduPanURL(baseURL, password)
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "baidu",
URL: normalizedURL,
Password: password,
})
}
}
// 5. 处理天翼云盘链接,确保每个链接只有一个版本
for baseURL, password := range tianyiLinkPasswords {
normalizedURL := normalizeTianyiPanURL(baseURL, password)
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "tianyi",
URL: normalizedURL,
Password: password,
})
}
}
// 6. 处理UC网盘链接,确保每个链接只有一个版本
for baseURL, password := range ucLinkPasswords {
normalizedURL := normalizeUCPanURL(baseURL, password)
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "uc",
URL: normalizedURL,
Password: password,
})
}
}
// 7. 处理123网盘链接,确保每个链接只有一个版本
for baseURL, password := range pan123LinkPasswords {
normalizedURL := normalize123PanURL(baseURL, password)
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "123",
URL: normalizedURL,
Password: password,
})
}
}
// 8. 处理115网盘链接,确保每个链接只有一个版本
for baseURL, password := range pan115LinkPasswords {
normalizedURL := normalize115PanURL(baseURL, password)
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "115",
URL: normalizedURL,
Password: password,
})
}
}
// 9. 处理阿里云盘链接,确保每个链接只有一个版本
for baseURL, password := range aliyunLinkPasswords {
normalizedURL := CleanAliyunPanURL(baseURL) // 阿里云盘URL通常不包含密码参数
// 确保链接不重复
if !foundLinks[normalizedURL] {
foundLinks[normalizedURL] = true
links = append(links, model.Link{
Type: "aliyun",
URL: normalizedURL,
Password: password,
})
}
}
// 提取标签
var tags []string
messageTextElem.Find("a[href^='?q=%23']").Each(func(i int, a *goquery.Selection) {
tag := a.Text()
if strings.HasPrefix(tag, "#") {
tags = append(tags, tag[1:])
}
})
// 提取图片链接(只从消息内容区域提取,排除用户头像)
var images []string
var foundImages = make(map[string]bool) // 用于去重
// 获取消息气泡区域,排除用户头像区域
messageBubble := messageDiv.Find(".tgme_widget_message_bubble")
// 1. 从消息内容中的图片包装元素提取图片
messageBubble.Find(".tgme_widget_message_photo_wrap").Each(func(i int, photoWrap *goquery.Selection) {
// 检查style属性中的background-image
style, exists := photoWrap.Attr("style")
if exists {
imageURL := extractImageURLFromStyle(style)
if imageURL != "" && !foundImages[imageURL] {
foundImages[imageURL] = true
images = append(images, imageURL)
}
}
})
// 2. 从消息内容中的其他可能包含图片的元素提取(排除用户头像)
messageBubble.Find("img").Each(func(i int, img *goquery.Selection) {
src, exists := img.Attr("src")
if exists && src != "" && !foundImages[src] {
foundImages[src] = true
images = append(images, src)
}
})
// 只有包含链接的消息才添加到结果中
if len(links) > 0 {
// 为每个链接提取作品标题
links = extractWorkTitlesForLinks(links, messageText, title)
results = append(results, model.SearchResult{
MessageID: messageID,
UniqueID: uniqueID,
Channel: channel,
Datetime: datetime,
Title: title,
Content: messageText,
Links: links,
Tags: tags,
Images: images,
})
}
})
return results, nextPageParam, nil
}
// CutTitleByKeywords 根据关键词进行裁剪,保留最前关键词前的部分
func CutTitleByKeywords(title string, keywords []string) string {
minIdx := -1
for _, kw := range keywords {
if idx := strings.Index(title, kw); idx >= 0 && (minIdx == -1 || idx < minIdx) {
minIdx = idx
}
}
if minIdx > 0 {
return strings.TrimSpace(title[:minIdx])
}
return strings.TrimSpace(title)
}
// extractImageURLFromStyle 从CSS样式字符串中提取background-image的URL
func extractImageURLFromStyle(style string) string {
// 查找background-image:url('...') 或 background-image:url("...")
startPattern := "background-image:url('"
endPattern := "')"
startIndex := strings.Index(style, startPattern)
if startIndex != -1 {
startIndex += len(startPattern)
endIndex := strings.Index(style[startIndex:], endPattern)
if endIndex != -1 {
return style[startIndex : startIndex+endIndex]
}
}
// 尝试双引号格式
startPattern = `background-image:url("`
endPattern = `")`
startIndex = strings.Index(style, startPattern)
if startIndex != -1 {
startIndex += len(startPattern)
endIndex := strings.Index(style[startIndex:], endPattern)
if endIndex != -1 {
return style[startIndex : startIndex+endIndex]
}
}
// 尝试无引号格式
startPattern = "background-image:url("
endPattern = ")"
startIndex = strings.Index(style, startPattern)
if startIndex != -1 {
startIndex += len(startPattern)
endIndex := strings.Index(style[startIndex:], endPattern)
if endIndex != -1 {
url := style[startIndex : startIndex+endIndex]
// 移除可能的引号
url = strings.Trim(url, "'\"")
return url
}
}
return ""
}
// extractTitle 从消息HTML和文本内容中提取标题
func extractTitle(htmlContent string, textContent string) string {
// 从HTML内容中提取标题
if brIndex := strings.Index(htmlContent, "
0 {
// 提取
前的HTML内容
firstLineHTML := htmlContent[:brIndex]
// 创建一个文档来解析这个HTML片段
doc, err := goquery.NewDocumentFromReader(strings.NewReader("