Files
fanqie-go/internal/fqapi/content_parser.go
T
chenxin 1a4997e031 ✨ feat(api): 统一响应格式并完善参数体系与文档
- 正文参数支持语义值(plain/html/keep/remove)与别名(img_mode/review_mode),URL 参数优先于配置
- 修复纯文本被段评强制包回 HTML、HTML 模式段评注入失效的问题
- 修复 PKCS7 填充弱校验导致漫画/正文偶发解密乱码返回 200 的问题
- 搜索结果展平:去 search_tabs 层与 selected_tab_idx/server_ab_params,提升 has_more/next_offset
- 全接口解包字符串形式的 JSON 字段(category_schema 等),缓存出口幂等处理
- 序列化关闭 HTML 转义,video/audio 播放链接可直接复制播放
- video 响应透传 CENC 加密状态(key_seed/kid/encryption_method)
- 新增 gzip 中间件,JSON/文本响应压缩约 81%,二进制透传
- 漫画默认输出模式改为 proxy(url 模式链接 404 且内容加密不可用)
- openapi.yaml:补全 23 个路径参数定义、tab_type 完整枚举、CDN 签名说明,示例更新
2026-10-09 21:01:18 +08:00

186 lines
4.9 KiB
Go

package fqapi
import (
"fanqie/internal/common"
"fanqie/internal/config"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
)
var (
parserLogger = common.NewLogger("content_parser", "INFO", "")
)
// ContentParser 内容解析器
type ContentParser struct {
cfg config.ContentConfig
}
// NewContentParser 创建内容解析器
func NewContentParser() *ContentParser {
return &ContentParser{
cfg: getContentConfig(),
}
}
// Parse 解析内容(URL参数优先于配置)
func (p *ContentParser) Parse(content string, textMode bool, imageMode bool) string {
// 1. 清理XML声明和DOCTYPE
content = p.cleanXMLDeclaration(content)
// 2. 根据配置移除章节标题
if p.cfg.RemoveChapterTitle {
content = p.removeChapterTitle(content)
}
// 3. 根据配置处理图片描述
if p.cfg.RemoveImageDesc {
content = p.removeImageDesc(content)
}
// 4. 根据参数移除图片
// imageMode=false 时移除(URL 参数与配置 remove_images 已在调用方合成:URL 优先)
if !imageMode {
content = p.removeImages(content)
}
// 5. 根据参数处理内容模式
// textMode=true 时提取纯文本(URL 参数与配置 remove_html_tags 已在调用方合成:URL 优先)
if textMode {
return p.extractText(content)
}
// 6. HTML模式处理
return p.processHTMLMode(content)
}
// cleanXMLDeclaration 清理XML声明
func (p *ContentParser) cleanXMLDeclaration(content string) string {
content = regexp.MustCompile(`<\?xml[^>]*\?>`).ReplaceAllString(content, "")
content = regexp.MustCompile(`<!DOCTYPE[^>]*>`).ReplaceAllString(content, "")
return strings.TrimSpace(content)
}
// removeChapterTitle 移除章节标题
func (p *ContentParser) removeChapterTitle(content string) string {
// 移除 h1 和 h2 标签中的章节标题
content = regexp.MustCompile(`(?si)<h[12][^>]*class="[^"]*chapter[Tt]itle[^"]*"[^>]*>.*?</h[12]>`).ReplaceAllString(content, "")
// 同时移除 <title> 标签
content = regexp.MustCompile(`(?si)<title>.*?</title>`).ReplaceAllString(content, "")
return content
}
// removeImageDesc 移除图片描述
func (p *ContentParser) removeImageDesc(content string) string {
return regexp.MustCompile(`(?s)<p[^>]*class="pictureDesc"[^>]*>.*?</p>`).ReplaceAllString(content, "")
}
// removeImages 移除图片
func (p *ContentParser) removeImages(content string) string {
return regexp.MustCompile(`(?s)<div[^>]*data-fanqie-type="image"[^>]*>.*?</div>`).ReplaceAllString(content, "")
}
// extractText 提取文本
func (p *ContentParser) extractText(content string) string {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(content))
if err != nil {
parserLogger.Error("解析HTML失败: " + err.Error())
return content
}
finalResult := make([]string, 0)
// 查找body或article
body := doc.Find("body")
article := body.Find("article")
var elements *goquery.Selection
if article.Length() > 0 {
elements = article
} else {
elements = body
}
// 遍历所有直接子元素
elements.Children().Each(func(i int, item *goquery.Selection) {
// 检查是否是图片div
dataType, _ := item.Attr("data-fanqie-type")
isImageDiv := dataType == "image"
if isImageDiv {
// 这是图片div
if !p.cfg.RemoveImages {
// 保留完整的图片div结构
divHTML, _ := goquery.OuterHtml(item)
// 解码HTML实体
divHTML = decodeHTMLEntities(divHTML)
// 标准化img属性
divHTML = normalizeImgAttributes(divHTML)
finalResult = append(finalResult, divHTML)
}
return
}
// 普通文本元素
if item.Children().Length() == 0 {
text := strings.TrimSpace(item.Text())
if text != "" {
finalResult = append(finalResult, text)
}
} else {
// 递归提取文本
item.Find("p").Each(func(j int, p *goquery.Selection) {
text := strings.TrimSpace(p.Text())
if text != "" {
finalResult = append(finalResult, text)
}
})
}
})
// 拼接结果
var result string
// 根据配置决定段落间的换行符
separator := "\n"
if p.cfg.DoubleNewline {
separator = "\n\n"
}
if p.cfg.AddParagraphIndent && len(finalResult) > 0 {
// 添加段落缩进(两个全角空格)
result = "\u3000\u3000" + strings.Join(finalResult, separator+"\u3000\u3000")
} else {
// 不添加缩进
result = strings.Join(finalResult, separator)
}
// 根据配置决定是否移除空行
if p.cfg.RemoveBlankLines {
result = removeBlankLines(result)
}
return result
}
// processHTMLMode 处理HTML模式
func (p *ContentParser) processHTMLMode(content string) string {
// 标准化img属性
content = normalizeImgAttributes(content)
// 解码HTML实体
content = decodeHTMLEntities(content)
// 根据配置决定是否移除空行
if p.cfg.RemoveBlankLines {
// 对HTML内容移除空的<p>标签
content = regexp.MustCompile(`(?s)<p[^>]*>\s*</p>`).ReplaceAllString(content, "")
// 移除多余的空白
content = regexp.MustCompile(`\n\s*\n\s*\n`).ReplaceAllString(content, "\n\n")
}
return content
}