mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
feat(encoding): enhance encoding detection with confidence threshold
This commit is contained in:
@@ -219,6 +219,10 @@ class ConfigModel(BaseModel):
|
|||||||
BIG_MEMORY_MODE: bool = False
|
BIG_MEMORY_MODE: bool = False
|
||||||
# 全局图片缓存,将媒体图片缓存到本地
|
# 全局图片缓存,将媒体图片缓存到本地
|
||||||
GLOBAL_IMAGE_CACHE: bool = False
|
GLOBAL_IMAGE_CACHE: bool = False
|
||||||
|
# 是否启用编码探测的兼容模式
|
||||||
|
ENCODING_DETECTION_COMPATIBLE_MODE: bool = True
|
||||||
|
# 编码探测的最低置信度阈值
|
||||||
|
ENCODING_DETECTION_MIN_CONFIDENCE: float = 0.8
|
||||||
# 允许的图片缓存域名
|
# 允许的图片缓存域名
|
||||||
SECURITY_IMAGE_DOMAINS: List[str] = Field(
|
SECURITY_IMAGE_DOMAINS: List[str] = Field(
|
||||||
default_factory=lambda: ["image.tmdb.org",
|
default_factory=lambda: ["image.tmdb.org",
|
||||||
|
|||||||
@@ -344,11 +344,9 @@ class SiteParserBase(metaclass=ABCMeta):
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
f"{self._site_name} 检测到Cloudflare,请更新Cookie和UA")
|
f"{self._site_name} 检测到Cloudflare,请更新Cookie和UA")
|
||||||
return ""
|
return ""
|
||||||
if re.search(r"charset=\"?utf-8\"?", res.text, re.IGNORECASE):
|
return RequestUtils.get_decoded_html_content(res,
|
||||||
res.encoding = "utf-8"
|
settings.ENCODING_DETECTION_COMPATIBLE_MODE,
|
||||||
else:
|
settings.ENCODING_DETECTION_MIN_CONFIDENCE)
|
||||||
res.encoding = res.apparent_encoding
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import traceback
|
|||||||
from typing import List
|
from typing import List
|
||||||
from urllib.parse import quote, urlencode, urlparse, parse_qs
|
from urllib.parse import quote, urlencode, urlparse, parse_qs
|
||||||
|
|
||||||
import chardet
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from pyquery import PyQuery
|
from pyquery import PyQuery
|
||||||
from ruamel.yaml import CommentedMap
|
from ruamel.yaml import CommentedMap
|
||||||
@@ -250,27 +249,9 @@ class TorrentSpider:
|
|||||||
referer=self.referer,
|
referer=self.referer,
|
||||||
proxies=self.proxies
|
proxies=self.proxies
|
||||||
).get_res(searchurl, allow_redirects=True)
|
).get_res(searchurl, allow_redirects=True)
|
||||||
if ret is not None:
|
page_source = RequestUtils.get_decoded_html_content(ret,
|
||||||
# 使用chardet检测字符编码
|
settings.ENCODING_DETECTION_COMPATIBLE_MODE,
|
||||||
raw_data = ret.content
|
settings.ENCODING_DETECTION_MIN_CONFIDENCE)
|
||||||
if raw_data:
|
|
||||||
try:
|
|
||||||
result = chardet.detect(raw_data)
|
|
||||||
encoding = result['encoding']
|
|
||||||
# 解码为字符串
|
|
||||||
page_source = raw_data.decode(encoding)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"chardet解码失败:{str(e)}")
|
|
||||||
# 探测utf-8解码
|
|
||||||
if re.search(r"charset=\"?utf-8\"?", ret.text, re.IGNORECASE):
|
|
||||||
ret.encoding = "utf-8"
|
|
||||||
else:
|
|
||||||
ret.encoding = ret.apparent_encoding
|
|
||||||
page_source = ret.text
|
|
||||||
else:
|
|
||||||
page_source = ret.text
|
|
||||||
else:
|
|
||||||
page_source = ""
|
|
||||||
|
|
||||||
# 解析
|
# 解析
|
||||||
return self.parse(page_source)
|
return self.parse(page_source)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import re
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
import chardet
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
from requests import Response, Session
|
from requests import Response, Session
|
||||||
@@ -273,3 +275,108 @@ class RequestUtils:
|
|||||||
cache_headers["Cache-Control"] = f"max-age={max_age}"
|
cache_headers["Cache-Control"] = f"max-age={max_age}"
|
||||||
|
|
||||||
return cache_headers
|
return cache_headers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def detect_encoding_from_html_response(response: Response,
|
||||||
|
compatible_mode: bool = False, confidence_threshold: float = 0.8):
|
||||||
|
"""
|
||||||
|
根据HTML响应内容探测编码信息
|
||||||
|
|
||||||
|
:param response: HTTP 响应对象
|
||||||
|
:param compatible_mode: 是否使用兼容模式,默认为 False (性能模式)
|
||||||
|
:param confidence_threshold: chardet 检测置信度阈值,默认为 0.8
|
||||||
|
:return: 解析得到的字符编码
|
||||||
|
"""
|
||||||
|
fallback_encoding = None
|
||||||
|
try:
|
||||||
|
if compatible_mode:
|
||||||
|
# 兼容模式:使用chardet分析后,再处理 BOM 和 meta 信息
|
||||||
|
# 1. 使用 chardet 库进一步分析内容
|
||||||
|
detection = chardet.detect(response.content)
|
||||||
|
if detection["confidence"] > confidence_threshold:
|
||||||
|
return detection.get("encoding")
|
||||||
|
# 保存 chardet 的结果备用
|
||||||
|
fallback_encoding = detection.get("encoding")
|
||||||
|
|
||||||
|
# 2. 检查响应体中的 BOM 标记(例如 UTF-8 BOM)
|
||||||
|
if response.content[:3] == b"\xef\xbb\xbf": # UTF-8 BOM
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
|
# 3. 如果是 HTML 响应体,检查其中的 <meta charset="..."> 标签
|
||||||
|
if re.search(r"charset=[\"']?utf-8[\"']?", response.text, re.IGNORECASE):
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
|
# 4. 尝试从 response headers 中获取编码信息
|
||||||
|
content_type = response.headers.get("Content-Type", "")
|
||||||
|
if re.search(r"charset=[\"']?utf-8[\"']?", content_type, re.IGNORECASE):
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 性能模式:优先从 headers 和 BOM 标记获取,最后使用 chardet 分析
|
||||||
|
# 1. 尝试从 response headers 中获取编码信息
|
||||||
|
content_type = response.headers.get("Content-Type", "")
|
||||||
|
if re.search(r"charset=[\"']?utf-8[\"']?", content_type, re.IGNORECASE):
|
||||||
|
return "utf-8"
|
||||||
|
# 暂不支持直接提取字符集,仅提取UTF8
|
||||||
|
# match = re.search(r"charset=[\"']?([^\"';\s]+)", content_type, re.IGNORECASE)
|
||||||
|
# if match:
|
||||||
|
# return match.group(1)
|
||||||
|
|
||||||
|
# 2. 检查响应体中的 BOM 标记(例如 UTF-8 BOM)
|
||||||
|
if response.content[:3] == b"\xef\xbb\xbf":
|
||||||
|
return "utf-8"
|
||||||
|
|
||||||
|
# 3. 如果是 HTML 响应体,检查其中的 <meta charset="..."> 标签
|
||||||
|
if re.search(r"charset=[\"']?utf-8[\"']?", response.text, re.IGNORECASE):
|
||||||
|
return "utf-8"
|
||||||
|
# 暂不支持直接提取字符集,仅提取UTF8
|
||||||
|
# match = re.search(r"<meta[^>]+charset=[\"']?([^\"'>\s]+)", response.text, re.IGNORECASE)
|
||||||
|
# if match:
|
||||||
|
# return match.group(1)
|
||||||
|
|
||||||
|
# 4. 使用 chardet 库进一步分析内容
|
||||||
|
detection = chardet.detect(response.content)
|
||||||
|
if detection.get("confidence", 0) > confidence_threshold:
|
||||||
|
return detection.get("encoding")
|
||||||
|
# 保存 chardet 的结果备用
|
||||||
|
fallback_encoding = detection.get("encoding")
|
||||||
|
|
||||||
|
# 5. 如果上述方法都无法确定,信任 chardet 的结果(即使置信度较低),否则返回默认字符集
|
||||||
|
return fallback_encoding or "utf-8"
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error when detect_encoding_from_response: {str(e)}")
|
||||||
|
return fallback_encoding or "utf-8"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_decoded_html_content(response: Response,
|
||||||
|
compatible_mode: bool = False, confidence_threshold: float = 0.8) -> str:
|
||||||
|
"""
|
||||||
|
获取HTML响应的解码文本内容
|
||||||
|
|
||||||
|
:param response: HTTP 响应对象
|
||||||
|
:param compatible_mode: 是否使用兼容模式,默认为 False (性能模式)
|
||||||
|
:param confidence_threshold: chardet 检测置信度阈值,默认为 0.8
|
||||||
|
:return: 解码后的响应文本内容
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not response:
|
||||||
|
return ""
|
||||||
|
if response.content:
|
||||||
|
# 1. 获取编码信息
|
||||||
|
encoding = (RequestUtils.detect_encoding_from_html_response(response, compatible_mode,
|
||||||
|
confidence_threshold)
|
||||||
|
or response.apparent_encoding)
|
||||||
|
# 2. 根据解析得到的编码进行解码
|
||||||
|
try:
|
||||||
|
# 尝试用推测的编码解码
|
||||||
|
return response.content.decode(encoding)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Decoding failed, error message: {str(e)}")
|
||||||
|
# 如果解码失败,尝试 fallback 使用 apparent_encoding
|
||||||
|
response.encoding = response.apparent_encoding
|
||||||
|
return response.text
|
||||||
|
else:
|
||||||
|
return response.text
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error when getting decoded content: {str(e)}")
|
||||||
|
return response.text
|
||||||
|
|||||||
Reference in New Issue
Block a user