225 lines
6.4 KiB
Go
225 lines
6.4 KiB
Go
// Package info loads and serves auxiliary data: backlink scores, adjustment
|
||
// table, and blocked query words.
|
||
// info 包负责加载和管理辅助数据:繁荣表(反向链接分数)、调整表(人工权重调整)和屏蔽词表。
|
||
package info
|
||
|
||
import (
|
||
"encoding/json" // JSON 反序列化
|
||
"math" // 对数运算(Log2)
|
||
"os" // 文件读取
|
||
"path/filepath" // 路径拼接
|
||
"strings" // 字符串操作
|
||
"sync" // 读写锁
|
||
)
|
||
|
||
// Service 管理繁荣表、调整表和屏蔽词表,并提供只读快照。
|
||
type Service struct {
|
||
mu sync.RWMutex
|
||
prosperMap map[string]float64 // 繁荣表:域名 → 归一化反向链接分数
|
||
adjustTable map[string]float64 // 调整表:主机名 → 人工权重倍数(默认 1.0)
|
||
blockedWords map[string]bool // 屏蔽词集合:搜索时直接过滤
|
||
storagePath string // 存储根目录路径
|
||
}
|
||
|
||
// New 创建并加载 info Service,从 storagePath 目录读取数据文件。
|
||
func New(storagePath string) *Service {
|
||
s := &Service{storagePath: storagePath}
|
||
s.Reload()
|
||
return s
|
||
}
|
||
|
||
// Reload 从磁盘重新加载所有数据文件(支持热更新配置)。
|
||
func (s *Service) Reload() {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.prosperMap = loadProsperMap(s.storagePath)
|
||
s.adjustTable = loadAdjustTable()
|
||
s.blockedWords = loadBlockedWords()
|
||
}
|
||
|
||
// Prosper 返回指定 URL 的繁荣分数(对其所有路径段累计计算)。
|
||
// 分数越高表示该域名越"有价值"(反向链接越多)。
|
||
func (s *Service) Prosper(rawURL string) float64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
return prosperFor(rawURL, s.prosperMap)
|
||
}
|
||
|
||
// ProsperMap 返回繁荣表的完整只读快照(深拷贝)。
|
||
// 供爬虫调度算法使用。
|
||
func (s *Service) ProsperMap() map[string]float64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make(map[string]float64, len(s.prosperMap))
|
||
for k, v := range s.prosperMap {
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Adjust 返回指定主机名的人工权重倍数(默认 1.0)。
|
||
// 允许管理员通过调整表提升或降低某些域名的爬取/搜索优先级。
|
||
func (s *Service) Adjust(host string) float64 {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
if v, ok := s.adjustTable[host]; ok {
|
||
return v
|
||
}
|
||
return 1.0
|
||
}
|
||
|
||
// IsBlocked 判断某词是否在屏蔽词列表中(搜索时不返回含该词的结果)。
|
||
func (s *Service) IsBlocked(word string) bool {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
return s.blockedWords[word]
|
||
}
|
||
|
||
// ---- 数据加载函数 ----
|
||
|
||
// backlinkBaseline 繁荣表归一化的基准值(用于将原始链接数映射到固定区间)。
|
||
const backlinkBaseline = 200000.0
|
||
|
||
// loadProsperMap 从 storage/prosper.json 加载繁荣表,并进行归一化和域名树传播。
|
||
func loadProsperMap(storagePath string) map[string]float64 {
|
||
path := filepath.Join(storagePath, "prosper.json")
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return map[string]float64{}
|
||
}
|
||
defer f.Close()
|
||
var raw map[string]float64
|
||
if err := json.NewDecoder(f).Decode(&raw); err != nil {
|
||
return map[string]float64{}
|
||
}
|
||
return normalise(raw)
|
||
}
|
||
|
||
// normalise 对繁荣表进行归一化,并执行域名树传播。
|
||
// 归一化:将所有顶级域名的分数总和缩放到 backlinkBaseline。
|
||
// 传播:子域名分数向上传播到父域名(父域名分数不低于任何子域名)。
|
||
func normalise(d map[string]float64) map[string]float64 {
|
||
// 计算顶级域名(不含 "/")的分数总和
|
||
total := 0.0
|
||
for k, v := range d {
|
||
if !strings.Contains(k, "/") {
|
||
total += v
|
||
}
|
||
}
|
||
if total == 0 {
|
||
return d
|
||
}
|
||
// 按总和归一化
|
||
factor := backlinkBaseline / total
|
||
out := make(map[string]float64, len(d))
|
||
for k, v := range d {
|
||
out[k] = v * factor
|
||
}
|
||
// 域名树传播:子域名分数 ≥ 父域名分数
|
||
for k, v := range out {
|
||
now := k
|
||
for {
|
||
idx := strings.Index(now, ".")
|
||
if idx < 0 {
|
||
break
|
||
}
|
||
now = now[idx+1:] // 上移一级
|
||
if cur, ok := out[now]; ok && cur < v {
|
||
out[now] = v // 父域名分数不低于子域名
|
||
} else if !ok {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// loadAdjustTable 从 data/adjust.json 加载人工调整表(主机名 → 权重倍数)。
|
||
// 文件不存在时返回空 map(所有域名权重为默认 1.0)。
|
||
func loadAdjustTable() map[string]float64 {
|
||
f, err := os.Open(filepath.Join("data", "adjust.json"))
|
||
if err != nil {
|
||
return map[string]float64{}
|
||
}
|
||
defer f.Close()
|
||
var m map[string]float64
|
||
json.NewDecoder(f).Decode(&m)
|
||
return m
|
||
}
|
||
|
||
// loadBlockedWords 从 data/blocked_words.json 加载屏蔽词列表。
|
||
// 文件不存在时返回空集合。
|
||
func loadBlockedWords() map[string]bool {
|
||
f, err := os.Open(filepath.Join("data", "blocked_words.json"))
|
||
if err != nil {
|
||
return map[string]bool{}
|
||
}
|
||
defer f.Close()
|
||
var words []string
|
||
json.NewDecoder(f).Decode(&words)
|
||
m := make(map[string]bool, len(words))
|
||
for _, w := range words {
|
||
m[w] = true
|
||
}
|
||
return m
|
||
}
|
||
|
||
// prosperFor 对 URL 按路径段分解查询繁荣表,计算综合繁荣分数。
|
||
// 分数计算:对每段取 Log2 变换后累加,返回值范围约 [0.1, +∞)。
|
||
func prosperFor(rawURL string, pm map[string]float64) float64 {
|
||
segments := decomposeURL(rawURL)
|
||
s := 0.0
|
||
for _, seg := range segments {
|
||
t, ok := pm[seg]
|
||
if !ok {
|
||
t = 0
|
||
}
|
||
l := 0.0
|
||
if t > 0 {
|
||
l = math.Log2(2+t*2) - 1 // Log2(2+2t)-1,t=0 时为 0,随 t 增大而增大
|
||
}
|
||
if s == 0 {
|
||
if l == 0 {
|
||
return 0
|
||
}
|
||
s = l
|
||
} else {
|
||
s = l + math.Log((s-l)/2+1) // 累加并衰减
|
||
}
|
||
}
|
||
if s > 0 {
|
||
return 0.1 + s
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// decomposeURL 将 URL 分解为递增的路径段。
|
||
// 例如:"https://zh.wikipedia.org/wiki/Go" → ["zh.wikipedia.org", "zh.wikipedia.org/wiki", "zh.wikipedia.org/wiki/Go"]。
|
||
// 用于按从泛到精的顺序查繁荣表。
|
||
func decomposeURL(rawURL string) []string {
|
||
u := strings.ToLower(rawURL)
|
||
if strings.HasPrefix(u, "https://") {
|
||
u = u[8:]
|
||
} else if strings.HasPrefix(u, "http://") {
|
||
u = u[7:]
|
||
} else {
|
||
return nil
|
||
}
|
||
u = strings.ReplaceAll(u, "?", "/") // 查询参数转路径
|
||
u = strings.ReplaceAll(u, "#", "/") // 锚点转路径
|
||
u = strings.TrimRight(u, "/")
|
||
// 过滤无效格式
|
||
if u == "" || u[0] == '/' || u[0] == '%' || u[0] == ' ' {
|
||
return nil
|
||
}
|
||
parts := strings.Split(u, "/")
|
||
var out []string
|
||
current := parts[0]
|
||
out = append(out, current) // 第一段:顶级域名
|
||
for _, p := range parts[1:] {
|
||
current = current + "/" + p // 逐步拼接路径段
|
||
out = append(out, current)
|
||
}
|
||
return out
|
||
}
|