diff --git a/app/modules/indexer/parser/__init__.py b/app/modules/indexer/parser/__init__.py index 80893652c..da566f3d9 100644 --- a/app/modules/indexer/parser/__init__.py +++ b/app/modules/indexer/parser/__init__.py @@ -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 # 做种信息 diff --git a/app/modules/indexer/parser/nexus_php.py b/app/modules/indexer/parser/nexus_php.py index ac635fec7..58694cb3b 100644 --- a/app/modules/indexer/parser/nexus_php.py +++ b/app/modules/indexer/parser/nexus_php.py @@ -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 diff --git a/tests/fixtures/depthstudio_userdetails.html b/tests/fixtures/depthstudio_userdetails.html new file mode 100644 index 000000000..dbc63ffa4 --- /dev/null +++ b/tests/fixtures/depthstudio_userdetails.html @@ -0,0 +1,47 @@ + + + + +
+ + + 分享率 + 4.393 + + + + + 上传量 + + 323.04 GB + + + + + + 下载量 + + 73.54 GB + + + +
+ + + + + + + + + + + + + +
+ 分享率: 4.392 + (实际分享率:0.045) +
上传量: 323.04 GB  下载量: 73.54 GB
实际上传量: 317.04 GB  实际下载量: 6.838 TB实际上传/下载量 (仅用于记录, 不参与分享率计算)
+ + diff --git a/tests/test_discuz_parser.py b/tests/test_discuz_parser.py index 942a89d3a..7ba7c06e6 100644 --- a/tests/test_discuz_parser.py +++ b/tests/test_discuz_parser.py @@ -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]) diff --git a/tests/test_nexus_php_parser.py b/tests/test_nexus_php_parser.py index d707ce659..495c8c3fd 100644 --- a/tests/test_nexus_php_parser.py +++ b/tests/test_nexus_php_parser.py @@ -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( + "上传量: 1 GB 下载量: 2 GB 分享率: 0.5" + ) + original = parser.upload, parser.download, parser.ratio + + parser._parse_user_detail_info( + '' + '' + '
上传量: 323.04 GB下载量: 73.54 GB
分享率: 4.392
' + ) + + 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"{homepage}") + 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("上传量: 0 B 下载量: 0 B 分享率: 0") + + parser._parse_user_detail_info( + '' + '
上传量: 8 GB下载量: 2 GB
' + ) + + 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", [ + ('上传量: 1.5 TB' + '下载量: 512 GB', + round(1.5 * 1024 ** 4), 512 * 1024 ** 3, 3.0), + ('上傳量1,024 GB下載量512 GB' + '分享率2.001', + 1024 ** 4, 512 * 1024 ** 3, 2.001), + ('实际上传量: 1 TB' + '实际下载量: 1 TB', 0, 0, 0.0), + ('上传量0 B下载量0 B', 0, 0, 0.0), + ('上传量未知下载量未知', 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("") + + parser._parse_user_detail_info(f"{table}
") + + 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"上传量: 1.5 TB 下载量: {download} {ratio_text}" + ) + + 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 时应停止翻页而不是抛出异常。