This commit is contained in:
fish2018
2025-07-12 19:53:44 +08:00
commit 5004e4f99f
30 changed files with 7885 additions and 0 deletions

37
util/cache/cache_key.go vendored Normal file
View File

@@ -0,0 +1,37 @@
package cache
import (
"crypto/md5"
"encoding/hex"
"sort"
)
// GenerateCacheKey 根据查询和过滤器生成缓存键
func GenerateCacheKey(query string, filters map[string]string) string {
// 如果只需要基于关键词的缓存,不考虑过滤器
if filters == nil || len(filters) == 0 {
// 直接使用查询字符串生成键,添加前缀以区分
keyStr := "keyword_only:" + query
hash := md5.Sum([]byte(keyStr))
return hex.EncodeToString(hash[:])
}
// 创建包含查询和所有过滤器的字符串
keyStr := query
// 按字母顺序排序过滤器键,确保相同的过滤器集合总是产生相同的键
var keys []string
for k := range filters {
keys = append(keys, k)
}
sort.Strings(keys)
// 添加过滤器到键字符串
for _, k := range keys {
keyStr += "|" + k + "=" + filters[k]
}
// 计算MD5哈希
hash := md5.Sum([]byte(keyStr))
return hex.EncodeToString(hash[:])
}

341
util/cache/disk_cache.go vendored Normal file
View File

@@ -0,0 +1,341 @@
package cache
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
)
// 磁盘缓存项元数据
type diskCacheMetadata struct {
Key string `json:"key"`
Expiry time.Time `json:"expiry"`
LastUsed time.Time `json:"last_used"`
Size int `json:"size"`
}
// DiskCache 磁盘缓存
type DiskCache struct {
path string
maxSizeMB int
metadata map[string]*diskCacheMetadata
mutex sync.RWMutex
currSize int64
}
// NewDiskCache 创建新的磁盘缓存
func NewDiskCache(path string, maxSizeMB int) (*DiskCache, error) {
// 确保缓存目录存在
if err := os.MkdirAll(path, 0755); err != nil {
return nil, err
}
cache := &DiskCache{
path: path,
maxSizeMB: maxSizeMB,
metadata: make(map[string]*diskCacheMetadata),
}
// 加载现有缓存元数据
cache.loadMetadata()
// 启动周期性清理
go cache.startCleanupTask()
return cache, nil
}
// 加载元数据
func (c *DiskCache) loadMetadata() {
c.mutex.Lock()
defer c.mutex.Unlock()
// 遍历缓存目录
files, err := ioutil.ReadDir(c.path)
if err != nil {
return
}
for _, file := range files {
if file.IsDir() {
continue
}
// 跳过元数据文件
if file.Name() == "metadata.json" {
continue
}
// 读取元数据
metadataFile := filepath.Join(c.path, file.Name()+".meta")
data, err := ioutil.ReadFile(metadataFile)
if err != nil {
continue
}
var meta diskCacheMetadata
if err := json.Unmarshal(data, &meta); err != nil {
continue
}
// 更新总大小
c.currSize += int64(meta.Size)
// 存储元数据
c.metadata[meta.Key] = &meta
}
}
// 保存元数据
func (c *DiskCache) saveMetadata(key string, meta *diskCacheMetadata) error {
metadataFile := filepath.Join(c.path, c.getFilename(key)+".meta")
data, err := json.Marshal(meta)
if err != nil {
return err
}
return ioutil.WriteFile(metadataFile, data, 0644)
}
// 获取文件名
func (c *DiskCache) getFilename(key string) string {
hash := md5.Sum([]byte(key))
return hex.EncodeToString(hash[:])
}
// Set 设置缓存
func (c *DiskCache) Set(key string, data []byte, ttl time.Duration) error {
c.mutex.Lock()
defer c.mutex.Unlock()
// 如果已存在,先减去旧项的大小
if meta, exists := c.metadata[key]; exists {
c.currSize -= int64(meta.Size)
// 删除旧文件
filename := c.getFilename(key)
os.Remove(filepath.Join(c.path, filename))
os.Remove(filepath.Join(c.path, filename+".meta"))
}
// 检查空间
maxSize := int64(c.maxSizeMB) * 1024 * 1024
if c.currSize+int64(len(data)) > maxSize {
// 清理空间
c.evictLRU(int64(len(data)))
}
// 获取文件名
filename := c.getFilename(key)
filePath := filepath.Join(c.path, filename)
// 写入文件
if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
return err
}
// 创建元数据
now := time.Now()
meta := &diskCacheMetadata{
Key: key,
Expiry: now.Add(ttl),
LastUsed: now,
Size: len(data),
}
// 保存元数据
if err := c.saveMetadata(key, meta); err != nil {
// 如果元数据保存失败,删除数据文件
os.Remove(filePath)
return err
}
// 更新内存中的元数据
c.metadata[key] = meta
c.currSize += int64(len(data))
return nil
}
// Get 获取缓存
func (c *DiskCache) Get(key string) ([]byte, bool, error) {
c.mutex.RLock()
meta, exists := c.metadata[key]
c.mutex.RUnlock()
if !exists {
return nil, false, nil
}
// 检查是否过期
if time.Now().After(meta.Expiry) {
c.Delete(key)
return nil, false, nil
}
// 获取文件路径
filePath := filepath.Join(c.path, c.getFilename(key))
// 读取文件
data, err := ioutil.ReadFile(filePath)
if err != nil {
// 如果文件不存在,删除元数据
if os.IsNotExist(err) {
c.Delete(key)
}
return nil, false, err
}
// 更新最后使用时间
c.mutex.Lock()
meta.LastUsed = time.Now()
c.saveMetadata(key, meta)
c.mutex.Unlock()
return data, true, nil
}
// Delete 删除缓存
func (c *DiskCache) Delete(key string) error {
c.mutex.Lock()
defer c.mutex.Unlock()
meta, exists := c.metadata[key]
if !exists {
return nil
}
// 删除文件
filename := c.getFilename(key)
os.Remove(filepath.Join(c.path, filename))
os.Remove(filepath.Join(c.path, filename+".meta"))
// 更新元数据
c.currSize -= int64(meta.Size)
delete(c.metadata, key)
return nil
}
// Has 检查缓存是否存在
func (c *DiskCache) Has(key string) bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
meta, exists := c.metadata[key]
if !exists {
return false
}
// 检查是否过期
if time.Now().After(meta.Expiry) {
// 异步删除过期项
go c.Delete(key)
return false
}
return true
}
// 清理过期项
func (c *DiskCache) cleanExpired() {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
for key, meta := range c.metadata {
if now.After(meta.Expiry) {
// 删除文件
filename := c.getFilename(key)
err := os.Remove(filepath.Join(c.path, filename))
if err == nil || os.IsNotExist(err) {
os.Remove(filepath.Join(c.path, filename+".meta"))
c.currSize -= int64(meta.Size)
delete(c.metadata, key)
}
}
}
}
// 驱逐策略 - LRU
func (c *DiskCache) evictLRU(requiredSpace int64) {
// 按最后使用时间排序
type cacheItem struct {
key string
lastUsed time.Time
size int
}
items := make([]cacheItem, 0, len(c.metadata))
for k, v := range c.metadata {
items = append(items, cacheItem{
key: k,
lastUsed: v.LastUsed,
size: v.Size,
})
}
// 按最后使用时间排序
// 使用冒泡排序保持简单
for i := 0; i < len(items); i++ {
for j := 0; j < len(items)-i-1; j++ {
if items[j].lastUsed.After(items[j+1].lastUsed) {
items[j], items[j+1] = items[j+1], items[j]
}
}
}
// 从最久未使用开始删除,直到有足够空间
maxSize := int64(c.maxSizeMB) * 1024 * 1024
for _, item := range items {
if c.currSize+requiredSpace <= maxSize {
break
}
// 删除文件
filename := c.getFilename(item.key)
err := os.Remove(filepath.Join(c.path, filename))
if err == nil || os.IsNotExist(err) {
os.Remove(filepath.Join(c.path, filename+".meta"))
c.currSize -= int64(item.size)
delete(c.metadata, item.key)
}
}
}
// 启动定期清理任务
func (c *DiskCache) startCleanupTask() {
ticker := time.NewTicker(10 * time.Minute)
for range ticker.C {
c.cleanExpired()
}
}
// Clear 清空缓存
func (c *DiskCache) Clear() error {
c.mutex.Lock()
defer c.mutex.Unlock()
// 删除所有缓存文件
files, err := ioutil.ReadDir(c.path)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
continue
}
os.Remove(filepath.Join(c.path, file.Name()))
}
// 重置元数据
c.metadata = make(map[string]*diskCacheMetadata)
c.currSize = 0
return nil
}

222
util/cache/two_level_cache.go vendored Normal file
View File

@@ -0,0 +1,222 @@
package cache
import (
"sync"
"time"
"pansou/config"
)
// 简单的内存缓存项
type memoryCacheItem struct {
data []byte
expiry time.Time
lastUsed time.Time
size int
}
// 内存缓存
type MemoryCache struct {
items map[string]*memoryCacheItem
mutex sync.RWMutex
maxItems int
maxSize int64
currSize int64
}
// 创建新的内存缓存
func NewMemoryCache(maxItems int, maxSizeMB int) *MemoryCache {
return &MemoryCache{
items: make(map[string]*memoryCacheItem),
maxItems: maxItems,
maxSize: int64(maxSizeMB) * 1024 * 1024,
}
}
// 设置缓存
func (c *MemoryCache) Set(key string, data []byte, ttl time.Duration) {
c.mutex.Lock()
defer c.mutex.Unlock()
// 如果已存在,先减去旧项的大小
if item, exists := c.items[key]; exists {
c.currSize -= int64(item.size)
}
// 创建新的缓存项
now := time.Now()
item := &memoryCacheItem{
data: data,
expiry: now.Add(ttl),
lastUsed: now,
size: len(data),
}
// 检查是否需要清理空间
if len(c.items) >= c.maxItems || c.currSize+int64(len(data)) > c.maxSize {
c.evict()
}
// 存储新项
c.items[key] = item
c.currSize += int64(len(data))
}
// 获取缓存
func (c *MemoryCache) Get(key string) ([]byte, bool) {
c.mutex.RLock()
item, exists := c.items[key]
c.mutex.RUnlock()
if !exists {
return nil, false
}
// 检查是否过期
if time.Now().After(item.expiry) {
c.mutex.Lock()
delete(c.items, key)
c.currSize -= int64(item.size)
c.mutex.Unlock()
return nil, false
}
// 更新最后使用时间
c.mutex.Lock()
item.lastUsed = time.Now()
c.mutex.Unlock()
return item.data, true
}
// 驱逐策略 - LRU
func (c *MemoryCache) evict() {
// 找出最久未使用的项
var oldestKey string
var oldestTime time.Time
// 初始化为当前时间
oldestTime = time.Now()
for k, v := range c.items {
if v.lastUsed.Before(oldestTime) {
oldestKey = k
oldestTime = v.lastUsed
}
}
// 如果找到了最久未使用的项,删除它
if oldestKey != "" {
item := c.items[oldestKey]
c.currSize -= int64(item.size)
delete(c.items, oldestKey)
}
}
// 清理过期项
func (c *MemoryCache) CleanExpired() {
c.mutex.Lock()
defer c.mutex.Unlock()
now := time.Now()
for k, v := range c.items {
if now.After(v.expiry) {
c.currSize -= int64(v.size)
delete(c.items, k)
}
}
}
// 启动定期清理
func (c *MemoryCache) StartCleanupTask() {
ticker := time.NewTicker(5 * time.Minute)
go func() {
for range ticker.C {
c.CleanExpired()
}
}()
}
// 两级缓存
type TwoLevelCache struct {
memCache *MemoryCache
diskCache *DiskCache
}
// 创建新的两级缓存
func NewTwoLevelCache() (*TwoLevelCache, error) {
// 内存缓存大小为磁盘缓存的60%
memCacheMaxItems := 5000
memCacheSizeMB := config.AppConfig.CacheMaxSizeMB * 3 / 5
memCache := NewMemoryCache(memCacheMaxItems, memCacheSizeMB)
memCache.StartCleanupTask()
diskCache, err := NewDiskCache(config.AppConfig.CachePath, config.AppConfig.CacheMaxSizeMB)
if err != nil {
return nil, err
}
return &TwoLevelCache{
memCache: memCache,
diskCache: diskCache,
}, nil
}
// 设置缓存
func (c *TwoLevelCache) Set(key string, data []byte, ttl time.Duration) error {
// 先设置内存缓存
c.memCache.Set(key, data, ttl)
// 再设置磁盘缓存
return c.diskCache.Set(key, data, ttl)
}
// 获取缓存
func (c *TwoLevelCache) Get(key string) ([]byte, bool, error) {
// 优先检查内存缓存
if data, found := c.memCache.Get(key); found {
return data, true, nil
}
// 内存未命中,检查磁盘缓存
data, found, err := c.diskCache.Get(key)
if err != nil {
return nil, false, err
}
if found {
// 磁盘命中,更新内存缓存
ttl := time.Duration(config.AppConfig.CacheTTLMinutes) * time.Minute
c.memCache.Set(key, data, ttl)
return data, true, nil
}
return nil, false, nil
}
// 删除缓存
func (c *TwoLevelCache) Delete(key string) error {
// 从内存缓存删除
c.memCache.mutex.Lock()
if item, exists := c.memCache.items[key]; exists {
c.memCache.currSize -= int64(item.size)
delete(c.memCache.items, key)
}
c.memCache.mutex.Unlock()
// 从磁盘缓存删除
return c.diskCache.Delete(key)
}
// 清空所有缓存
func (c *TwoLevelCache) Clear() error {
// 清空内存缓存
c.memCache.mutex.Lock()
c.memCache.items = make(map[string]*memoryCacheItem)
c.memCache.currSize = 0
c.memCache.mutex.Unlock()
// 清空磁盘缓存
return c.diskCache.Clear()
}

135
util/compression.go Normal file
View File

@@ -0,0 +1,135 @@
package util
import (
"bytes"
"compress/gzip"
"io/ioutil"
"strings"
"github.com/gin-gonic/gin"
"pansou/config"
)
// 压缩响应的包装器
type gzipResponseWriter struct {
gin.ResponseWriter
gzipWriter *gzip.Writer
}
// 实现Write接口
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.gzipWriter.Write(data)
}
// 实现WriteString接口
func (g *gzipResponseWriter) WriteString(s string) (int, error) {
return g.gzipWriter.Write([]byte(s))
}
// 关闭gzip写入器
func (g *gzipResponseWriter) Close() {
g.gzipWriter.Close()
}
// GzipMiddleware 返回一个Gin中间件用于压缩HTTP响应
func GzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用压缩,直接跳过
if !config.AppConfig.EnableCompression {
c.Next()
return
}
// 检查客户端是否支持gzip
if !strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") {
c.Next()
return
}
// 创建一个缓冲响应写入器
buffer := &bytes.Buffer{}
blw := &bodyLogWriter{body: buffer, ResponseWriter: c.Writer}
c.Writer = blw
// 处理请求
c.Next()
// 获取响应内容
responseData := buffer.Bytes()
// 如果响应大小小于最小压缩大小,直接返回原始内容
if len(responseData) < config.AppConfig.MinSizeToCompress {
c.Writer.Write(responseData)
return
}
// 设置gzip响应头
c.Header("Content-Encoding", "gzip")
c.Header("Vary", "Accept-Encoding")
// 创建gzip写入器
gz, err := gzip.NewWriterLevel(c.Writer, gzip.BestSpeed)
if err != nil {
c.Writer.Write(responseData)
return
}
defer gz.Close()
// 写入压缩内容
gz.Write(responseData)
}
}
// bodyLogWriter 是一个用于记录响应体的写入器
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
// Write 实现ResponseWriter接口
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// WriteString 实现ResponseWriter接口
func (w bodyLogWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
// CompressData 压缩数据
func CompressData(data []byte) ([]byte, error) {
var buf bytes.Buffer
// 创建gzip写入器
gz, err := gzip.NewWriterLevel(&buf, gzip.BestSpeed)
if err != nil {
return nil, err
}
// 写入数据
if _, err := gz.Write(data); err != nil {
return nil, err
}
// 关闭写入器
if err := gz.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// DecompressData 解压数据
func DecompressData(data []byte) ([]byte, error) {
// 创建gzip读取器
gz, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer gz.Close()
// 读取解压后的数据
return ioutil.ReadAll(gz)
}

18
util/convert.go Normal file
View File

@@ -0,0 +1,18 @@
package util
import (
"strconv"
)
// StringToInt 将字符串转换为整数如果转换失败则返回0
func StringToInt(s string) int {
if s == "" {
return 0
}
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}

126
util/http_util.go Normal file
View File

@@ -0,0 +1,126 @@
package util
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"net/url"
"time"
"golang.org/x/net/proxy"
"pansou/config"
)
// 全局HTTP客户端
var httpClient *http.Client
// InitHTTPClient 初始化HTTP客户端
func InitHTTPClient() {
// 创建传输配置
transport := &http.Transport{
// 启用HTTP/2
ForceAttemptHTTP2: true,
// TLS配置
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false, // 生产环境应设为false
},
// 连接池优化
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
MaxConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// TCP连接优化
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
}
// 如果配置了代理,设置代理
if config.AppConfig.UseProxy {
proxyURL, err := url.Parse(config.AppConfig.ProxyURL)
if err == nil {
// 根据代理类型设置不同的处理方式
if proxyURL.Scheme == "socks5" {
// 创建SOCKS5代理拨号器
dialer, err := proxy.FromURL(proxyURL, proxy.Direct)
if err == nil {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
}
} else {
// HTTP/HTTPS代理
transport.Proxy = http.ProxyURL(proxyURL)
}
}
}
// 创建客户端
httpClient = &http.Client{
Transport: transport,
Timeout: time.Duration(60) * time.Second,
}
}
// GetHTTPClient 获取HTTP客户端
func GetHTTPClient() *http.Client {
if httpClient == nil {
InitHTTPClient()
}
return httpClient
}
// FetchHTML 获取HTML内容
func FetchHTML(targetURL string) (string, error) {
// 使用优化后的HTTP客户端
client := GetHTTPClient()
// 创建请求
req, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
return "", err
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Upgrade-Insecure-Requests", "1")
// 发送请求
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
// BuildSearchURL 构建搜索URL
func BuildSearchURL(channel string, keyword string, nextPageParam string) string {
baseURL := "https://t.me/s/" + channel
if keyword != "" {
baseURL += "?q=" + url.QueryEscape(keyword)
if nextPageParam != "" {
baseURL += "&" + nextPageParam
}
}
return baseURL
}

42
util/json/json.go Normal file
View File

@@ -0,0 +1,42 @@
package json
import (
"github.com/bytedance/sonic"
)
// API是sonic的全局配置实例
var API = sonic.ConfigDefault
// 初始化sonic配置
func init() {
// 根据需要配置sonic选项
API = sonic.Config{
UseNumber: true,
EscapeHTML: true,
SortMapKeys: false, // 生产环境设为false提高性能
}.Froze()
}
// Marshal 使用sonic序列化对象到JSON
func Marshal(v interface{}) ([]byte, error) {
return API.Marshal(v)
}
// Unmarshal 使用sonic反序列化JSON到对象
func Unmarshal(data []byte, v interface{}) error {
return API.Unmarshal(data, v)
}
// MarshalString 序列化对象到JSON字符串
func MarshalString(v interface{}) (string, error) {
bytes, err := API.Marshal(v)
if err != nil {
return "", err
}
return string(bytes), nil
}
// UnmarshalString 反序列化JSON字符串到对象
func UnmarshalString(str string, v interface{}) error {
return API.Unmarshal([]byte(str), v)
}

547
util/parser_util.go Normal file
View File

@@ -0,0 +1,547 @@
package util
import (
"strings"
"time"
"github.com/PuerkitoBio/goquery"
"pansou/model"
)
// 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
// 查找分页链接 - 使用next而不是prev来获取下一页
doc.Find("link[rel='next']").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if exists {
// 从href中提取before参数
parts := strings.Split(href, "before=")
if len(parts) > 1 {
nextPageParam = strings.Split(parts[1], "&")[0]
}
}
})
// 查找消息块
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 {
// 非特殊处理的网盘链接直接添加
if !foundLinks[href] {
foundLinks[href] = true
links = append(links, model.Link{
Type: linkType,
URL: href,
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 {
// 非特殊处理的网盘链接直接添加
if !foundLinks[linkURL] {
foundLinks[linkURL] = true
links = append(links, model.Link{
Type: linkType,
URL: linkURL,
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:])
}
})
// 只有包含链接的消息才添加到结果中
if len(links) > 0 {
results = append(results, model.SearchResult{
MessageID: messageID,
UniqueID: uniqueID,
Channel: channel,
Datetime: datetime,
Title: title,
Content: messageText,
Links: links,
Tags: tags,
})
}
})
return results, nextPageParam, nil
}
// extractTitle 从消息HTML和文本内容中提取标题
func extractTitle(htmlContent string, textContent string) string {
// 从HTML内容中提取标题
if brIndex := strings.Index(htmlContent, "<br"); brIndex > 0 {
// 提取<br>前的HTML内容
firstLineHTML := htmlContent[:brIndex]
// 创建一个文档来解析这个HTML片段
doc, err := goquery.NewDocumentFromReader(strings.NewReader("<div>" + firstLineHTML + "</div>"))
if err == nil {
// 获取解析后的文本
firstLine := strings.TrimSpace(doc.Text())
// 如果第一行以"名称:"开头,则提取冒号后面的内容作为标题
if strings.HasPrefix(firstLine, "名称:") {
return strings.TrimSpace(firstLine[len("名称:"):])
}
return firstLine
}
}
// 如果HTML解析失败则使用纯文本内容
lines := strings.Split(textContent, "\n")
if len(lines) == 0 {
return ""
}
// 第一行通常是标题
firstLine := strings.TrimSpace(lines[0])
// 如果第一行以"名称:"开头,则提取冒号后面的内容作为标题
if strings.HasPrefix(firstLine, "名称:") {
return strings.TrimSpace(firstLine[len("名称:"):])
}
// 否则直接使用第一行作为标题
return firstLine
}

75
util/pool/object_pool.go Normal file
View File

@@ -0,0 +1,75 @@
package pool
import (
"sync"
"pansou/model"
)
// LinkPool 网盘链接对象池
var LinkPool = sync.Pool{
New: func() interface{} {
return &model.Link{}
},
}
// SearchResultPool 搜索结果对象池
var SearchResultPool = sync.Pool{
New: func() interface{} {
return &model.SearchResult{
Links: make([]model.Link, 0, 4),
Tags: make([]string, 0, 8),
}
},
}
// MergedLinkPool 合并链接对象池
var MergedLinkPool = sync.Pool{
New: func() interface{} {
return &model.MergedLink{}
},
}
// GetLink 从对象池获取Link对象
func GetLink() *model.Link {
return LinkPool.Get().(*model.Link)
}
// ReleaseLink 释放Link对象回对象池
func ReleaseLink(l *model.Link) {
l.Type = ""
l.URL = ""
l.Password = ""
LinkPool.Put(l)
}
// GetSearchResult 从对象池获取SearchResult对象
func GetSearchResult() *model.SearchResult {
return SearchResultPool.Get().(*model.SearchResult)
}
// ReleaseSearchResult 释放SearchResult对象回对象池
func ReleaseSearchResult(sr *model.SearchResult) {
sr.MessageID = ""
sr.Channel = ""
sr.Title = ""
sr.Content = ""
sr.Links = sr.Links[:0]
sr.Tags = sr.Tags[:0]
// 不重置时间,因为会被重新赋值
SearchResultPool.Put(sr)
}
// GetMergedLink 从对象池获取MergedLink对象
func GetMergedLink() *model.MergedLink {
return MergedLinkPool.Get().(*model.MergedLink)
}
// ReleaseMergedLink 释放MergedLink对象回对象池
func ReleaseMergedLink(ml *model.MergedLink) {
ml.URL = ""
ml.Password = ""
ml.Note = ""
// 不重置时间,因为会被重新赋值
MergedLinkPool.Put(ml)
}

178
util/pool/worker_pool.go Normal file
View File

@@ -0,0 +1,178 @@
package pool
import (
"context"
"sync"
"time"
)
// Task 表示一个工作任务
type Task func() interface{}
// WorkerPool 工作池结构体
type WorkerPool struct {
maxWorkers int
taskQueue chan Task
results chan interface{}
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
// NewWorkerPool 创建一个新的工作池
func NewWorkerPool(maxWorkers int) *WorkerPool {
ctx, cancel := context.WithCancel(context.Background())
pool := &WorkerPool{
maxWorkers: maxWorkers,
taskQueue: make(chan Task, maxWorkers*2), // 任务队列大小为工作者数量的2倍
results: make(chan interface{}, maxWorkers*2), // 结果队列大小为工作者数量的2倍
ctx: ctx,
cancel: cancel,
}
// 启动工作者
pool.startWorkers()
return pool
}
// NewWorkerPoolWithContext 创建一个带有指定上下文的新工作池
func NewWorkerPoolWithContext(ctx context.Context, maxWorkers int) *WorkerPool {
ctx, cancel := context.WithCancel(ctx)
pool := &WorkerPool{
maxWorkers: maxWorkers,
taskQueue: make(chan Task, maxWorkers*2), // 任务队列大小为工作者数量的2倍
results: make(chan interface{}, maxWorkers*2), // 结果队列大小为工作者数量的2倍
ctx: ctx,
cancel: cancel,
}
// 启动工作者
pool.startWorkers()
return pool
}
// startWorkers 启动工作者协程
func (p *WorkerPool) startWorkers() {
for i := 0; i < p.maxWorkers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for {
select {
case task, ok := <-p.taskQueue:
if !ok {
return
}
// 执行任务并发送结果
result := task()
p.results <- result
case <-p.ctx.Done():
return
}
}
}()
}
}
// Submit 提交一个任务到工作池
func (p *WorkerPool) Submit(task Task) {
p.taskQueue <- task
}
// GetResults 获取所有任务的结果
func (p *WorkerPool) GetResults(count int) []interface{} {
results := make([]interface{}, 0, count)
// 收集指定数量的结果
for i := 0; i < count; i++ {
select {
case result := <-p.results:
results = append(results, result)
case <-p.ctx.Done():
// 上下文取消,返回已收集的结果
return results
}
}
return results
}
// Close 关闭工作池
func (p *WorkerPool) Close() {
// 取消上下文
p.cancel()
// 关闭任务队列
close(p.taskQueue)
// 等待所有工作者完成
p.wg.Wait()
// 关闭结果队列
close(p.results)
}
// ExecuteBatch 批量执行任务并返回结果
func ExecuteBatch(tasks []Task, maxWorkers int) []interface{} {
if len(tasks) == 0 {
return []interface{}{}
}
// 如果任务数量少于工作者数量,调整工作者数量
if len(tasks) < maxWorkers {
maxWorkers = len(tasks)
}
// 创建工作池
pool := NewWorkerPool(maxWorkers)
defer pool.Close()
// 提交所有任务
for _, task := range tasks {
pool.Submit(task)
}
// 获取所有结果
return pool.GetResults(len(tasks))
}
// ExecuteBatchWithTimeout 批量执行任务,带有超时控制,并返回结果
func ExecuteBatchWithTimeout(tasks []Task, maxWorkers int, timeout time.Duration) []interface{} {
if len(tasks) == 0 {
return []interface{}{}
}
// 如果任务数量少于工作者数量,调整工作者数量
if len(tasks) < maxWorkers {
maxWorkers = len(tasks)
}
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// 创建工作池
pool := NewWorkerPoolWithContext(ctx, maxWorkers)
defer pool.Close()
// 提交所有任务
for _, task := range tasks {
select {
case pool.taskQueue <- task:
// 任务提交成功
case <-ctx.Done():
// 超时或取消,停止提交更多任务
return pool.GetResults(len(tasks))
}
}
// 获取所有结果GetResults方法会处理超时情况
return pool.GetResults(len(tasks))
}

753
util/regex_util.go Normal file
View File

@@ -0,0 +1,753 @@
package util
import (
"regexp"
"strings"
)
// 通用网盘链接匹配正则表达式 - 修改为更精确的匹配模式
var AllPanLinksPattern = regexp.MustCompile(`(?i)(?:(?:magnet:\?xt=urn:btih:[a-zA-Z0-9]+)|(?:ed2k://\|file\|[^|]+\|\d+\|[A-Fa-f0-9]+\|/?)|(?:https?://(?:(?:[\w.-]+\.)?(?:pan\.(?:baidu|quark)\.cn|(?:www\.)?(?:alipan|aliyundrive)\.com|drive\.uc\.cn|cloud\.189\.cn|caiyun\.139\.com|(?:www\.)?123(?:684|685|912|pan|592)\.(?:com|cn)|115\.com|115cdn\.com|anxia\.com|pan\.xunlei\.com|mypikpak\.com))(?:/[^\s'"<>()]*)?))`)
// 单独定义各种网盘的链接匹配模式,以便更精确地提取
// 修改百度网盘链接正则表达式,确保只匹配到链接本身,不包含后面的文本
var BaiduPanPattern = regexp.MustCompile(`https?://pan\.baidu\.com/s/[a-zA-Z0-9_-]+(?:\?pwd=[a-zA-Z0-9]{4})?`)
var QuarkPanPattern = regexp.MustCompile(`https?://pan\.quark\.cn/s/[a-zA-Z0-9]+`)
var XunleiPanPattern = regexp.MustCompile(`https?://pan\.xunlei\.com/s/[a-zA-Z0-9]+(?:\?pwd=[a-zA-Z0-9]+)?(?:#)?`)
// 添加天翼云盘链接正则表达式
var TianyiPanPattern = regexp.MustCompile(`https?://cloud\.189\.cn/t/[a-zA-Z0-9]+`)
// 添加UC网盘链接正则表达式
var UCPanPattern = regexp.MustCompile(`https?://drive\.uc\.cn/s/[a-zA-Z0-9]+(?:\?public=\d)?`)
// 添加123网盘链接正则表达式
var Pan123Pattern = regexp.MustCompile(`https?://(?:www\.)?123(?:684|685|912|pan|592)\.(?:com|cn)/s/[a-zA-Z0-9_-]+(?:\?(?:%E6%8F%90%E5%8F%96%E7%A0%81|提取码)[:][a-zA-Z0-9]+)?`)
// 添加115网盘链接正则表达式
var Pan115Pattern = regexp.MustCompile(`https?://(?:115\.com|115cdn\.com|anxia\.com)/s/[a-zA-Z0-9]+(?:\?password=[a-zA-Z0-9]{4})?(?:#)?`)
// 添加阿里云盘链接正则表达式
var AliyunPanPattern = regexp.MustCompile(`https?://(?:www\.)?(?:alipan|aliyundrive)\.com/s/[a-zA-Z0-9]+`)
// 提取码匹配正则表达式 - 增强提取密码的能力
var PasswordPattern = regexp.MustCompile(`(?i)(?:(?:提取|访问|提取密|密)码|pwd)[:]\s*([a-zA-Z0-9]{4})`)
var UrlPasswordPattern = regexp.MustCompile(`(?i)[?&]pwd=([a-zA-Z0-9]{4})`)
// 百度网盘密码专用正则表达式 - 确保只提取4位密码
var BaiduPasswordPattern = regexp.MustCompile(`(?i)(?:链接:.*?提取码:|密码:|提取码:|pwd=|pwd:|pwd)([a-zA-Z0-9]{4})`)
// GetLinkType 获取链接类型
func GetLinkType(url string) string {
url = strings.ToLower(url)
// 处理可能带有"链接:"前缀的情况
if strings.Contains(url, "链接:") || strings.Contains(url, "链接:") {
url = strings.Split(url, "链接")[1]
if strings.HasPrefix(url, "") || strings.HasPrefix(url, ":") {
url = url[1:]
}
url = strings.TrimSpace(url)
}
// 根据关键词判断ed2k链接
if strings.Contains(url, "ed2k:") {
return "ed2k"
}
if strings.HasPrefix(url, "magnet:") {
return "magnet"
}
if strings.Contains(url, "pan.baidu.com") {
return "baidu"
}
if strings.Contains(url, "pan.quark.cn") {
return "quark"
}
if strings.Contains(url, "alipan.com") || strings.Contains(url, "aliyundrive.com") {
return "aliyun"
}
if strings.Contains(url, "cloud.189.cn") {
return "tianyi"
}
if strings.Contains(url, "drive.uc.cn") {
return "uc"
}
if strings.Contains(url, "caiyun.139.com") {
return "mobile"
}
if strings.Contains(url, "115.com") || strings.Contains(url, "115cdn.com") || strings.Contains(url, "anxia.com") {
return "115"
}
if strings.Contains(url, "mypikpak.com") {
return "pikpak"
}
if strings.Contains(url, "pan.xunlei.com") {
return "xunlei"
}
// 123网盘有多个域名
if strings.Contains(url, "123684.com") || strings.Contains(url, "123685.com") ||
strings.Contains(url, "123912.com") || strings.Contains(url, "123pan.com") ||
strings.Contains(url, "123pan.cn") || strings.Contains(url, "123592.com") {
return "123"
}
return "others"
}
// CleanBaiduPanURL 清理百度网盘URL确保链接格式正确
func CleanBaiduPanURL(url string) string {
// 如果URL包含"https://pan.baidu.com/s/",提取出正确的链接部分
if strings.Contains(url, "https://pan.baidu.com/s/") {
// 找到链接的起始位置
startIdx := strings.Index(url, "https://pan.baidu.com/s/")
if startIdx >= 0 {
// 从起始位置开始提取
url = url[startIdx:]
// 查找可能的结束标记
endMarkers := []string{" ", "\n", "\t", "", "。", "", ";", "", ",", "?pwd="}
minEndIdx := len(url)
for _, marker := range endMarkers {
idx := strings.Index(url, marker)
if idx > 0 && idx < minEndIdx {
minEndIdx = idx
}
}
// 如果找到了结束标记,截取到结束标记位置
if minEndIdx < len(url) {
// 特殊处理pwd参数
if strings.Contains(url[:minEndIdx], "?pwd=") {
pwdIdx := strings.Index(url, "?pwd=")
pwdEndIdx := pwdIdx + 10 // ?pwd=xxxx 总共9个字符加上问号前的位置
if pwdEndIdx < len(url) {
return url[:pwdEndIdx]
}
}
return url[:minEndIdx]
}
// 如果没有找到结束标记但URL包含?pwd=确保只保留4位密码
if strings.Contains(url, "?pwd=") {
pwdIdx := strings.Index(url, "?pwd=")
if pwdIdx > 0 && pwdIdx+9 <= len(url) { // ?pwd=xxxx 总共9个字符
return url[:pwdIdx+9]
}
}
}
}
return url
}
// CleanTianyiPanURL 清理天翼云盘URL确保链接格式正确
func CleanTianyiPanURL(url string) string {
// 如果URL包含"https://cloud.189.cn/t/",提取出正确的链接部分
if strings.Contains(url, "https://cloud.189.cn/t/") {
// 找到链接的起始位置
startIdx := strings.Index(url, "https://cloud.189.cn/t/")
if startIdx >= 0 {
// 从起始位置开始提取
url = url[startIdx:]
// 查找可能的结束标记
endMarkers := []string{" ", "\n", "\t", "", "。", "", ";", "", ",", "实时", "天翼", "更多"}
minEndIdx := len(url)
for _, marker := range endMarkers {
idx := strings.Index(url, marker)
if idx > 0 && idx < minEndIdx {
minEndIdx = idx
}
}
// 如果找到了结束标记,截取到结束标记位置
if minEndIdx < len(url) {
return url[:minEndIdx]
}
}
}
return url
}
// CleanUCPanURL 清理UC网盘URL确保链接格式正确
func CleanUCPanURL(url string) string {
// 如果URL包含"https://drive.uc.cn/s/",提取出正确的链接部分
if strings.Contains(url, "https://drive.uc.cn/s/") {
// 找到链接的起始位置
startIdx := strings.Index(url, "https://drive.uc.cn/s/")
if startIdx >= 0 {
// 从起始位置开始提取
url = url[startIdx:]
// 查找可能的结束标记(包括常见的网盘名称,可能出现在链接后面)
endMarkers := []string{" ", "\n", "\t", "", "。", "", ";", "", ",", "网盘", "123", "夸克", "阿里", "百度"}
minEndIdx := len(url)
for _, marker := range endMarkers {
idx := strings.Index(url, marker)
if idx > 0 && idx < minEndIdx {
minEndIdx = idx
}
}
// 如果找到了结束标记,截取到结束标记位置
if minEndIdx < len(url) {
return url[:minEndIdx]
}
// 处理public参数
if strings.Contains(url, "?public=") {
publicIdx := strings.Index(url, "?public=")
if publicIdx > 0 {
// 确保只保留?public=1这样的参数不包含后面的文本
if publicIdx+9 <= len(url) { // ?public=1 总共9个字符
return url[:publicIdx+9]
}
return url[:publicIdx+8] // 如果参数不完整,至少保留?public=
}
}
}
}
return url
}
// Clean123PanURL 清理123网盘URL确保链接格式正确
func Clean123PanURL(url string) string {
// 检查是否为123网盘链接
domains := []string{"123684.com", "123685.com", "123912.com", "123pan.com", "123pan.cn", "123592.com"}
isDomain123 := false
for _, domain := range domains {
if strings.Contains(url, domain+"/s/") {
isDomain123 = true
break
}
}
if isDomain123 {
// 确保链接有协议头
hasProtocol := strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
// 找到链接的起始位置
startIdx := -1
for _, domain := range domains {
if idx := strings.Index(url, domain+"/s/"); idx >= 0 {
startIdx = idx
break
}
}
if startIdx >= 0 {
// 如果链接没有协议头,添加协议头
if !hasProtocol {
// 提取链接部分
linkPart := url[startIdx:]
// 添加协议头
url = "https://" + linkPart
} else if startIdx > 0 {
// 如果链接有协议头但可能包含前缀文本提取完整URL
protocolIdx := strings.Index(url, "://")
if protocolIdx >= 0 {
protocol := url[:protocolIdx+3]
url = protocol + url[startIdx:]
}
}
// 保留提取码参数,但需要处理可能的表情符号和其他无关文本
// 查找可能的结束标记(表情符号、标签标识等)
// 注意:我们不再将"提取码"作为结束标记因为它是URL的一部分
endMarkers := []string{" ", "\n", "\t", "", "。", "", ";", "", ",", "📁", "🔍", "标签", "🏷", "📎", "🔗", "📌", "📋", "📂", "🗂️", "🔖", "📚", "📒", "📔", "📕", "📓", "📗", "📘", "📙", "📄", "📃", "📑", "🧾", "📊", "📈", "📉", "🗒️", "🗓️", "📆", "📅", "🗑️", "🔒", "🔓", "🔏", "🔐", "🔑", "🗝️"}
minEndIdx := len(url)
for _, marker := range endMarkers {
idx := strings.Index(url, marker)
if idx > 0 && idx < minEndIdx {
minEndIdx = idx
}
}
// 如果找到了结束标记,截取到结束标记位置
if minEndIdx < len(url) {
return url[:minEndIdx]
}
// 标准化URL编码的提取码统一使用非编码形式
if strings.Contains(url, "%E6%8F%90%E5%8F%96%E7%A0%81") {
url = strings.Replace(url, "%E6%8F%90%E5%8F%96%E7%A0%81", "提取码", 1)
}
}
}
return url
}
// Clean115PanURL 清理115网盘URL确保链接格式正确
func Clean115PanURL(url string) string {
// 检查是否为115网盘链接
if strings.Contains(url, "115.com/s/") || strings.Contains(url, "115cdn.com/s/") || strings.Contains(url, "anxia.com/s/") {
// 找到链接的起始位置
startIdx := -1
if idx := strings.Index(url, "115.com/s/"); idx >= 0 {
startIdx = idx
} else if idx := strings.Index(url, "115cdn.com/s/"); idx >= 0 {
startIdx = idx
} else if idx := strings.Index(url, "anxia.com/s/"); idx >= 0 {
startIdx = idx
}
if startIdx >= 0 {
// 确保链接有协议头
hasProtocol := strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
// 如果链接没有协议头,添加协议头
if !hasProtocol {
// 提取链接部分
linkPart := url[startIdx:]
// 添加协议头
url = "https://" + linkPart
} else if startIdx > 0 {
// 如果链接有协议头但可能包含前缀文本提取完整URL
protocolIdx := strings.Index(url, "://")
if protocolIdx >= 0 {
protocol := url[:protocolIdx+3]
url = protocol + url[startIdx:]
}
}
// 如果链接包含password参数确保只保留到password=xxxx部分4位密码
if strings.Contains(url, "?password=") {
pwdIdx := strings.Index(url, "?password=")
if pwdIdx > 0 && pwdIdx+14 <= len(url) { // ?password=xxxx 总共14个字符
// 截取到密码后面4位
url = url[:pwdIdx+14]
return url
}
}
// 如果链接包含#,截取到#位置
hashIdx := strings.Index(url, "#")
if hashIdx > 0 {
url = url[:hashIdx]
return url
}
}
}
return url
}
// CleanAliyunPanURL 清理阿里云盘URL确保链接格式正确
func CleanAliyunPanURL(url string) string {
// 如果URL包含阿里云盘域名提取出正确的链接部分
if strings.Contains(url, "alipan.com/s/") || strings.Contains(url, "aliyundrive.com/s/") {
// 找到链接的起始位置
startIdx := -1
if idx := strings.Index(url, "alipan.com/s/"); idx >= 0 {
startIdx = idx
} else if idx := strings.Index(url, "aliyundrive.com/s/"); idx >= 0 {
startIdx = idx
}
if startIdx >= 0 {
// 确保链接有协议头
hasProtocol := strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
// 如果链接没有协议头,添加协议头
if !hasProtocol {
// 提取链接部分
linkPart := url[startIdx:]
// 添加协议头
url = "https://" + linkPart
} else if startIdx > 0 {
// 如果链接有协议头但可能包含前缀文本提取完整URL
protocolIdx := strings.Index(url, "://")
if protocolIdx >= 0 {
protocol := url[:protocolIdx+3]
url = protocol + url[startIdx:]
}
}
// 查找可能的结束标记(表情符号、标签标识等)
endMarkers := []string{" ", "\n", "\t", "", "。", "", ";", "", ",", "📁", "🔍", "标签", "🏷", "📎", "🔗", "📌", "📋", "📂", "🗂️", "🔖", "📚", "📒", "📔", "📕", "📓", "📗", "📘", "📙", "📄", "📃", "📑", "🧾", "📊", "📈", "📉", "🗒️", "🗓️", "📆", "📅", "🗑️", "🔒", "🔓", "🔏", "🔐", "🔑", "🗝️"}
minEndIdx := len(url)
for _, marker := range endMarkers {
idx := strings.Index(url, marker)
if idx > 0 && idx < minEndIdx {
minEndIdx = idx
}
}
// 如果找到了结束标记,截取到结束标记位置
if minEndIdx < len(url) {
return url[:minEndIdx]
}
}
}
return url
}
// normalizeAliyunPanURL 标准化阿里云盘URL确保链接格式正确
func normalizeAliyunPanURL(url string, password string) string {
// 清理URL确保获取正确的链接部分
url = CleanAliyunPanURL(url)
// 阿里云盘链接通常不在URL中包含密码参数
// 但是我们确保返回的是干净的链接
return url
}
// ExtractPassword 提取链接密码
func ExtractPassword(content, url string) string {
// 先从URL中提取密码
matches := UrlPasswordPattern.FindStringSubmatch(url)
if len(matches) > 1 {
// 确保百度网盘密码只有4位
if strings.Contains(strings.ToLower(url), "pan.baidu.com") && len(matches[1]) > 4 {
return matches[1][:4]
}
return matches[1]
}
// 特殊处理115网盘URL中的密码
if (strings.Contains(url, "115.com") ||
strings.Contains(url, "115cdn.com") ||
strings.Contains(url, "anxia.com")) &&
strings.Contains(url, "password=") {
// 尝试从URL中提取密码
passwordPattern := regexp.MustCompile(`password=([a-zA-Z0-9]{4})`)
passwordMatches := passwordPattern.FindStringSubmatch(url)
if len(passwordMatches) > 1 {
return passwordMatches[1]
}
}
// 特殊处理123网盘URL中的提取码
if (strings.Contains(url, "123684.com") ||
strings.Contains(url, "123685.com") ||
strings.Contains(url, "123912.com") ||
strings.Contains(url, "123pan.com") ||
strings.Contains(url, "123pan.cn") ||
strings.Contains(url, "123592.com")) &&
(strings.Contains(url, "提取码") || strings.Contains(url, "%E6%8F%90%E5%8F%96%E7%A0%81")) {
// 尝试从URL中提取提取码处理普通文本和URL编码两种情况
extractCodePattern := regexp.MustCompile(`(?:提取码|%E6%8F%90%E5%8F%96%E7%A0%81)[:]([a-zA-Z0-9]+)`)
codeMatches := extractCodePattern.FindStringSubmatch(url)
if len(codeMatches) > 1 {
return codeMatches[1]
}
}
// 检查123网盘URL中的提取码参数
if (strings.Contains(url, "123684.com") ||
strings.Contains(url, "123685.com") ||
strings.Contains(url, "123912.com") ||
strings.Contains(url, "123pan.com") ||
strings.Contains(url, "123pan.cn") ||
strings.Contains(url, "123592.com")) &&
strings.Contains(url, "提取码") {
// 尝试从URL中提取提取码
parts := strings.Split(url, "提取码")
if len(parts) > 1 {
// 提取码通常跟在冒号后面
codeStart := strings.IndexAny(parts[1], ":")
if codeStart >= 0 && codeStart+1 < len(parts[1]) {
// 提取冒号后面的内容,去除空格
code := strings.TrimSpace(parts[1][codeStart+1:])
// 如果提取码后面有其他字符(如表情符号、标签等),只取提取码部分
// 增加更多可能的结束标记
endIdx := strings.IndexAny(code, " \t\n\r;,🏷📁🔍📎🔗📌📋📂🗂🔖📚📒📔📕📓📗📘📙📄📃📑🧾📊📈📉🗒🗓📆<EFB88F><F09F9386>🗑🔒🔓🔏🔐🔑🗝")
if endIdx > 0 {
code = code[:endIdx]
}
// 去除可能的空格和其他无关字符
code = strings.TrimSpace(code)
// 确保提取码是有效的通常是4位字母数字
if len(code) > 0 && len(code) <= 6 && isValidPassword(code) {
return code
}
}
}
}
// 检查内容中是否包含"提取码"字样
if strings.Contains(content, "提取码") {
// 尝试从内容中提取提取码
parts := strings.Split(content, "提取码")
for _, part := range parts {
if len(part) > 0 {
// 提取码通常跟在冒号后面
codeStart := strings.IndexAny(part, ":")
if codeStart >= 0 && codeStart+1 < len(part) {
// 提取冒号后面的内容,去除空格
code := strings.TrimSpace(part[codeStart+1:])
// 如果提取码后面有其他字符,只取提取码部分
endIdx := strings.IndexAny(code, " \t\n\r;,🏷📁🔍📎🔗📌📋📂🗂️🔖📚📒📔📕📓📗📘📙📄📃📑🧾📊📈📉🗒️🗓️📆📅🗑️🔒🔓🔏🔐🔑🗝️")
if endIdx > 0 {
code = code[:endIdx]
} else {
// 如果没有明显的结束标记假设提取码是4-6位字符
if len(code) > 6 {
// 检查前4-6位是否是有效的提取码
for i := 4; i <= 6 && i <= len(code); i++ {
if isValidPassword(code[:i]) {
code = code[:i]
break
}
}
// 如果没有找到有效的提取码取前4位
if len(code) > 6 {
code = code[:4]
}
}
}
// 去除可能的空格和其他无关字符
code = strings.TrimSpace(code)
// 如果提取码不为空且是有效的,返回
if code != "" && isValidPassword(code) {
return code
}
}
}
}
}
// 再从内容中提取密码
// 对于百度网盘链接,尝试查找特定格式的密码
if strings.Contains(strings.ToLower(url), "pan.baidu.com") {
// 尝试匹配百度网盘特定格式的密码
baiduMatches := BaiduPasswordPattern.FindStringSubmatch(content)
if len(baiduMatches) > 1 {
return baiduMatches[1]
}
}
// 通用密码提取
matches = PasswordPattern.FindStringSubmatch(content)
if len(matches) > 1 {
return matches[1]
}
return ""
}
// isValidPassword 检查提取码是否有效(只包含字母和数字)
func isValidPassword(password string) bool {
for _, c := range password {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
return false
}
}
return true
}
// ExtractNetDiskLinks 从文本中提取所有网盘链接
func ExtractNetDiskLinks(text string) []string {
var links []string
// 提取百度网盘链接
baiduMatches := BaiduPanPattern.FindAllString(text, -1)
for _, match := range baiduMatches {
// 清理并添加百度网盘链接
cleanURL := CleanBaiduPanURL(match)
if cleanURL != "" {
links = append(links, cleanURL)
}
}
// 提取天翼云盘链接
tianyiMatches := TianyiPanPattern.FindAllString(text, -1)
for _, match := range tianyiMatches {
// 清理并添加天翼云盘链接
cleanURL := CleanTianyiPanURL(match)
if cleanURL != "" {
links = append(links, cleanURL)
}
}
// 提取UC网盘链接
ucMatches := UCPanPattern.FindAllString(text, -1)
for _, match := range ucMatches {
// 清理并添加UC网盘链接
cleanURL := CleanUCPanURL(match)
if cleanURL != "" {
links = append(links, cleanURL)
}
}
// 提取123网盘链接
pan123Matches := Pan123Pattern.FindAllString(text, -1)
for _, match := range pan123Matches {
// 清理并添加123网盘链接
cleanURL := Clean123PanURL(match)
if cleanURL != "" {
// 检查是否已经存在相同的链接比较完整URL
isDuplicate := false
for _, existingLink := range links {
// 标准化链接以进行比较(仅移除协议)
normalizedExisting := normalizeURLForComparison(existingLink)
normalizedNew := normalizeURLForComparison(cleanURL)
if normalizedExisting == normalizedNew {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, cleanURL)
}
}
}
// 提取115网盘链接
pan115Matches := Pan115Pattern.FindAllString(text, -1)
for _, match := range pan115Matches {
// 清理并添加115网盘链接
cleanURL := Clean115PanURL(match) // 115网盘链接的清理逻辑与123网盘类似
if cleanURL != "" {
// 检查是否已经存在相同的链接比较完整URL
isDuplicate := false
for _, existingLink := range links {
normalizedExisting := normalizeURLForComparison(existingLink)
normalizedNew := normalizeURLForComparison(cleanURL)
if normalizedExisting == normalizedNew {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, cleanURL)
}
}
}
// 提取阿里云盘链接
aliyunMatches := AliyunPanPattern.FindAllString(text, -1)
if aliyunMatches != nil {
for _, match := range aliyunMatches {
// 清理并添加阿里云盘链接
cleanURL := CleanAliyunPanURL(match)
if cleanURL != "" {
// 检查是否已经存在相同的链接
isDuplicate := false
for _, existingLink := range links {
normalizedExisting := normalizeURLForComparison(existingLink)
normalizedNew := normalizeURLForComparison(cleanURL)
if normalizedExisting == normalizedNew {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, cleanURL)
}
}
}
}
// 提取夸克网盘链接
quarkLinks := QuarkPanPattern.FindAllString(text, -1)
if quarkLinks != nil {
for _, match := range quarkLinks {
// 检查是否已经存在相同的链接
isDuplicate := false
for _, existingLink := range links {
if strings.Contains(existingLink, match) || strings.Contains(match, existingLink) {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, match)
}
}
}
// 提取迅雷网盘链接
xunleiLinks := XunleiPanPattern.FindAllString(text, -1)
if xunleiLinks != nil {
for _, match := range xunleiLinks {
// 检查是否已经存在相同的链接
isDuplicate := false
for _, existingLink := range links {
if strings.Contains(existingLink, match) || strings.Contains(match, existingLink) {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, match)
}
}
}
// 使用通用模式提取其他可能的链接
otherLinks := AllPanLinksPattern.FindAllString(text, -1)
if otherLinks != nil {
// 过滤掉已经添加过的链接
for _, link := range otherLinks {
// 跳过百度、夸克、迅雷、天翼、UC和123网盘链接因为已经单独处理过
if strings.Contains(link, "pan.baidu.com") ||
strings.Contains(link, "pan.quark.cn") ||
strings.Contains(link, "pan.xunlei.com") ||
strings.Contains(link, "cloud.189.cn") ||
strings.Contains(link, "drive.uc.cn") ||
strings.Contains(link, "123684.com") ||
strings.Contains(link, "123685.com") ||
strings.Contains(link, "123912.com") ||
strings.Contains(link, "123pan.com") ||
strings.Contains(link, "123pan.cn") ||
strings.Contains(link, "123592.com") {
continue
}
isDuplicate := false
for _, existingLink := range links {
normalizedExisting := normalizeURLForComparison(existingLink)
normalizedNew := normalizeURLForComparison(link)
// 使用完整URL比较包括www.前缀
if normalizedExisting == normalizedNew ||
strings.Contains(normalizedExisting, normalizedNew) ||
strings.Contains(normalizedNew, normalizedExisting) {
isDuplicate = true
break
}
}
if !isDuplicate {
links = append(links, link)
}
}
}
return links
}
// normalizeURLForComparison 标准化URL以便于比较
// 移除协议头,标准化提取码,保留完整域名用于比较
func normalizeURLForComparison(url string) string {
// 移除协议头
if idx := strings.Index(url, "://"); idx >= 0 {
url = url[idx+3:]
}
// 标准化URL编码的提取码统一使用非编码形式
if strings.Contains(url, "%E6%8F%90%E5%8F%96%E7%A0%81") {
url = strings.Replace(url, "%E6%8F%90%E5%8F%96%E7%A0%81", "提取码", 1)
}
return url
}