fix(indexer): 修复 NexusPHP 用户流量详情表格兜底解析 (#7008)

This commit is contained in:
Opportunity
2026-10-15 20:30:23 +08:00
committed by GitHub
parent 63c8c0d71a
commit b8eb66521b
5 changed files with 208 additions and 8 deletions

View File

@@ -98,9 +98,9 @@ class SiteParserBase(metaclass=ABCMeta):
self.join_at = None
self.bonus = 0.0
# 流量信息
self.upload = 0.0
self.download = 0.0
# 上传/下载量按整数字节计,分享率为浮点数
self.upload = 0
self.download = 0
self.ratio = 0.0
# 做种信息

View File

@@ -650,9 +650,42 @@ class NexusPhpSiteUserInfo(SiteParserBase):
return message_head_text, message_date_text, message_content_text
def _fixup_traffic_info(self, html):
"""从详情页补充首页缺失的魔力值。"""
"""从详情表格补读为零的流量、分享率和魔力值,非零值保持不变。"""
if not self.upload:
upload_text = self._traffic_table_value(html, r"上[传傳]量?")
if upload_text is not None:
self.upload = self.num_filesize(upload_text)
if not self.download:
download_text = self._traffic_table_value(html, r"下[载載]量?")
if download_text is not None:
self.download = self.num_filesize(download_text)
if not self.ratio:
ratio_text = self._traffic_table_value(html, "分享率")
if ratio_text is not None:
ratio = text_tools.parse_float(ratio_text)
else:
ratio = round(self.upload / self.download, 3) if self.download else 0.0
self.ratio = ratio
# fixup bonus
if not self.bonus:
bonus_text = html.xpath('//tr/td[text()="魔力值" or text()="猫粮"]/following-sibling::td[1]/text()')
if bonus_text:
self.bonus = text_tools.parse_float(bonus_text[0].strip())
@staticmethod
def _traffic_table_value(html: Any, field: str) -> Optional[str]:
"""按单元格字段名读取数值,支持同格或相邻列布局,排除“实际”流量。"""
for cell in html.xpath('//tr/td[not(.//td)]'):
cell_text = " ".join(cell.xpath("string(.)").split())
field_match = re.fullmatch(rf"{field}\s*[::]?\s*(.*)", cell_text)
if not field_match:
continue
value = field_match.group(1).strip()
if not value:
siblings = cell.xpath('following-sibling::td[1]')
value = " ".join(siblings[0].xpath("string(.)").split()) if siblings else ""
value_match = re.fullmatch(r"([\d,.]+(?:\s*[KMGTPI]i?B)?)(?:\s*[((].*)?", value, re.I)
if value_match:
return value_match.group(1)
return None

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<body>
<!-- 从用户提供的详情页摘取流量结构,省略身份、链接和其他个人信息。 -->
<div class="info-card-section info-card-stats info-card-stats-count-6">
<span class="info-stat info-stat-primary info-stat-ratio">
<span class="info-stat-copy">
<span class="info-stat-label color_ratio">分享率</span>
<span class="info-stat-value">4.393</span>
</span>
</span>
<span class="info-stat info-stat-primary info-stat-uploaded">
<span class="info-stat-copy">
<span class="info-stat-label color_uploaded">上传量</span>
<span class="info-stat-value">
323.04 <span class="info-stat-unit">GB</span>
</span>
</span>
</span>
<span class="info-stat info-stat-primary info-stat-downloaded">
<span class="info-stat-copy">
<span class="info-stat-label color_downloaded">下载量</span>
<span class="info-stat-value">
73.54 <span class="info-stat-unit">GB</span>
</span>
</span>
</span>
</div>
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="embedded">
<strong>分享率</strong>: <font color="">4.392</font>
(<strong>实际分享率</strong>:0.045)
</td>
</tr>
<tr>
<td class="embedded"><strong>上传量</strong>: 323.04 GB</td>
<td class="embedded">&nbsp;&nbsp;<strong>下载量</strong>: 73.54 GB</td>
</tr>
<tr>
<td class="embedded"><strong>实际上传量</strong>: 317.04 GB</td>
<td class="embedded">&nbsp;&nbsp;<strong>实际下载量</strong>: 6.838 TB</td>
<td class="embedded text-muted">实际上传/下载量 (仅用于记录, 不参与分享率计算)</td>
</tr>
</table>
</body>
</html>

View File

@@ -19,10 +19,13 @@ def parser() -> Iterator[DiscuzUserInfo]:
instance.close()
def test_discuz_traffic_defaults_are_floats(parser: DiscuzUserInfo) -> None:
for value in (parser.upload, parser.download, parser.ratio):
assert value == 0.0
assert isinstance(value, float)
def test_discuz_traffic_defaults_use_integer_bytes(parser: DiscuzUserInfo) -> None:
"""上传/下载量使用整数字节,分享率保持浮点数。"""
for value in (parser.upload, parser.download):
assert value == 0
assert type(value) is int
assert parser.ratio == 0.0
assert isinstance(parser.ratio, float)
@pytest.mark.parametrize("include_menu", [True, False])

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
from lxml import etree
from app.modules.indexer.parser.nexus_php import NexusPhpSiteUserInfo
@@ -19,6 +21,121 @@ def _build_parser() -> NexusPhpSiteUserInfo:
)
def test_nexus_php_depthstudio_traffic_falls_back_to_details():
"""首页解析失败后从详情表格补齐流量,不依赖卡片类名或实际流量。"""
html_text = (Path(__file__).parent / "fixtures" / "depthstudio_userdetails.html").read_text(encoding="utf-8")
html = etree.HTML(html_text)
homepage = etree.tostring(html.xpath("//div")[0], encoding="unicode")
homepage = homepage.replace("info-stat", "other-style")
parser = _build_parser()
parser._parse_user_base_info(homepage)
assert parser.upload == 0
assert parser.download == 0
parser._parse_user_detail_info(html_text)
assert parser.upload == round(323.04 * 1024 ** 3)
assert parser.download == round(73.54 * 1024 ** 3)
assert parser.ratio == 4.393
def test_nexus_php_detail_traffic_preserves_nonzero_values():
"""详情兜底不能覆盖已有的非零流量和分享率。"""
parser = _build_parser()
parser._parse_user_base_info(
"<html><body>上传量: 1 GB 下载量: 2 GB 分享率: 0.5</body></html>"
)
original = parser.upload, parser.download, parser.ratio
parser._parse_user_detail_info(
'<table><tr><td><strong>上传量</strong>: 323.04 GB</td>'
'<td><strong>下载量</strong>: 73.54 GB</td></tr>'
'<tr><td><strong>分享率</strong>: 4.392</td></tr></table>'
)
assert (parser.upload, parser.download, parser.ratio) == original
@pytest.mark.parametrize("homepage, expected_upload, expected_download, expected_ratio", [
("上传量: 1 GB", 1024 ** 3, round(73.54 * 1024 ** 3), 4.392),
("下载量: 0 B 分享率: 0", round(323.04 * 1024 ** 3), round(73.54 * 1024 ** 3), 4.392),
("上传量: 0 B 下载量: 0 B 分享率: 0", round(323.04 * 1024 ** 3), round(73.54 * 1024 ** 3), 4.392),
("上传量: 1 GB 下载量: 2 GB 分享率: 0", 1024 ** 3, 2 * 1024 ** 3, 4.392),
])
def test_nexus_php_detail_traffic_fills_zero_fields(
homepage, expected_upload, expected_download, expected_ratio
):
"""零值字段允许从详情补读,分享率优先取详情表格的显示值。"""
parser = _build_parser()
parser._parse_user_base_info(f"<html><body>{homepage}</body></html>")
detail = (Path(__file__).parent / "fixtures" / "depthstudio_userdetails.html").read_text(encoding="utf-8")
parser._parse_user_detail_info(detail)
assert parser.upload == expected_upload
assert parser.download == expected_download
assert parser.ratio == expected_ratio
def test_nexus_php_zero_ratio_uses_final_traffic_without_displayed_ratio():
"""详情未显示分享率时,按补读后的上传/下载量重新计算零分享率。"""
parser = _build_parser()
parser._parse_user_base_info("<html><body>上传量: 0 B 下载量: 0 B 分享率: 0</body></html>")
parser._parse_user_detail_info(
'<table><tr><td><strong>上传量</strong>: 8 GB</td>'
'<td><strong>下载量</strong>: 2 GB</td></tr></table>'
)
assert parser.upload == 8 * 1024 ** 3
assert parser.download == 2 * 1024 ** 3
assert parser.ratio == 4.0
@pytest.mark.parametrize("table, expected_upload, expected_download, expected_ratio", [
('<tr><td><strong>上传量</strong>: 1.5 <span>TB</span></td>'
'<td><strong>下载量</strong>: 512 <span>GB</span></td></tr>',
round(1.5 * 1024 ** 4), 512 * 1024 ** 3, 3.0),
('<tr><td>上傳量</td><td>1,024 GB</td></tr><tr><td>下載量</td><td>512 GB</td></tr>'
'<tr><td>分享率</td><td>2.001</td></tr>',
1024 ** 4, 512 * 1024 ** 3, 2.001),
('<tr><td><strong>实际上传量</strong>: 1 TB</td>'
'<td><strong>实际下载量</strong>: 1 TB</td></tr>', 0, 0, 0.0),
('<tr><td>上传量</td><td>0 B</td></tr><tr><td>下载量</td><td>0 B</td></tr>', 0, 0, 0.0),
('<tr><td>上传量</td><td>未知</td></tr><tr><td>下载量</td><td>未知</td></tr>', 0, 0, 0.0),
])
def test_nexus_php_detail_traffic_table(table, expected_upload, expected_download, expected_ratio):
"""按表格字段识别流量,支持嵌套单位和分列布局,并排除实际流量。"""
parser = _build_parser()
parser._parse_user_base_info("<html><body></body></html>")
parser._parse_user_detail_info(f"<html><body><table>{table}</table></body></html>")
assert parser.upload == expected_upload
assert parser.download == expected_download
assert parser.ratio == expected_ratio
@pytest.mark.parametrize("download, ratio_text, expected_ratio", [
("512 GB", "分享率: 3.001", 3.001),
("512 GB", "", 3.0),
("0 B", "", 0.0),
])
def test_nexus_php_legacy_traffic(download, ratio_text, expected_ratio):
"""保留传统页面解析、分享率计算及零下载量行为。"""
parser = _build_parser()
parser._parse_user_traffic_info(
f"<html><body>上传量: 1.5 TB 下载量: {download} {ratio_text}</body></html>"
)
assert parser.upload == round(1.5 * 1024 ** 4)
assert parser.download == (512 * 1024 ** 3 if download == "512 GB" else 0)
assert parser.ratio == expected_ratio
def test_nexus_php_seeding_next_page_stops_when_userid_missing():
"""
userid 未识别且下一页也缺少 userid 时应停止翻页而不是抛出异常。