mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
update
This commit is contained in:
37
util/cache/cache_key.go
vendored
Normal file
37
util/cache/cache_key.go
vendored
Normal 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
341
util/cache/disk_cache.go
vendored
Normal 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
222
util/cache/two_level_cache.go
vendored
Normal 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
135
util/compression.go
Normal 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
18
util/convert.go
Normal 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
126
util/http_util.go
Normal 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
42
util/json/json.go
Normal 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
547
util/parser_util.go
Normal 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
75
util/pool/object_pool.go
Normal 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
178
util/pool/worker_pool.go
Normal 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
753
util/regex_util.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user