From ebcfac5f1e0d2efb1cd16a042e141c54a8d96bb8 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Sun, 19 Apr 2026 19:02:40 +0800 Subject: [PATCH] feat: add juquanquan detail parsing --- py/tests/test_剧圈圈.py | 48 +++++++++++++++++++ py/剧圈圈.py | 100 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/py/tests/test_剧圈圈.py b/py/tests/test_剧圈圈.py index 2c2f4f4..f155fd0 100644 --- a/py/tests/test_剧圈圈.py +++ b/py/tests/test_剧圈圈.py @@ -96,6 +96,54 @@ class TestJuQuanQuanSpider(unittest.TestCase): self.assertEqual(result["list"][0]["vod_id"], "vod/777") self.assertEqual(result["pagecount"], 1) + def test_parse_detail_page_extracts_metadata_and_playlists(self): + html = """ +

详情标题

+
+
+
导演
+
导演甲
+
+
+
主演
+
演员甲演员乙
+
+
+
备注
+
更新至3集
+
+
一段剧情简介
+ +
+
+
+
+
+ 第1集 + 第2集 +
+
+ 正片 +
+ """ + result = self.spider._parse_detail_page(html, "vod/123") + vod = result["list"][0] + self.assertEqual(vod["vod_id"], "vod/123") + self.assertEqual(vod["vod_name"], "详情标题") + self.assertEqual(vod["vod_pic"], "https://www.jqqzx.cc/poster.jpg") + self.assertEqual(vod["type_name"], "古装 / 剧情") + self.assertEqual(vod["vod_director"], "导演甲") + self.assertEqual(vod["vod_actor"], "演员甲 / 演员乙") + self.assertEqual(vod["vod_content"], "一段剧情简介") + self.assertEqual(vod["vod_play_from"], "线路A$$$线路B") + self.assertEqual(vod["vod_play_url"], "第1集$play/123-1-1#第2集$play/123-1-2$$$正片$play/123-2-1") + + @patch.object(Spider, "_request_html") + def test_detail_content_decodes_compact_vod_id(self, mock_request_html): + mock_request_html.return_value = '

详情标题

' + self.spider.detailContent(["vod/321"]) + self.assertEqual(mock_request_html.call_args.args[0], "https://www.jqqzx.cc/vod/321.html") + if __name__ == "__main__": unittest.main() diff --git a/py/剧圈圈.py b/py/剧圈圈.py index b1beacb..0357f9c 100644 --- a/py/剧圈圈.py +++ b/py/剧圈圈.py @@ -144,3 +144,103 @@ class Spider(BaseSpider): self._request_html(self._build_url(f"/index.php/ajax/suggest?mid=1&wd={quote(keyword)}")) ) return {"page": page, "pagecount": 1, "total": len(items), "list": items} + + def _parse_info_items(self, root): + info = {} + for item in root.xpath("//*[contains(@class,'module-info-item')]"): + title = self._clean_text( + "".join(item.xpath(".//*[contains(@class,'module-info-item-title')][1]//text()")) + ).rstrip("::") + if not title: + continue + links = [ + self._clean_text("".join(node.xpath(".//text()"))) + for node in item.xpath(".//*[contains(@class,'module-info-item-content')][1]//a") + ] + links = [value for value in links if value] + if links: + info[title] = " / ".join(links) + continue + info[title] = self._clean_text( + "".join(item.xpath(".//*[contains(@class,'module-info-item-content')][1]//text()")) + ) + return info + + def _parse_detail_page(self, html, vod_id): + root = self.html(html) + if root is None: + return {"list": []} + info = self._parse_info_items(root) + tab_names = [] + for node in root.xpath("//*[@id='y-playList']//*[contains(@class,'module-tab-item')]"): + tab_names.append( + self._clean_text( + (node.xpath("./@data-dropdown-value") or [""])[0] + or "".join(node.xpath(".//span[1]//text()")) + or "".join(node.xpath(".//text()")) + ) + ) + groups = [] + for index, box in enumerate(root.xpath("//*[contains(@class,'his-tab-list')]")): + episodes = [] + for anchor in box.xpath(".//a[contains(@class,'module-play-list-link') and @href]"): + play_id = self._encode_play_id((anchor.xpath("./@href") or [""])[0]) + ep_name = self._clean_text( + "".join(anchor.xpath(".//span[1]//text()")) or "".join(anchor.xpath(".//text()")) + ) + if play_id and ep_name: + episodes.append(f"{ep_name}${play_id}") + if episodes: + groups.append( + { + "from": tab_names[index] if index < len(tab_names) and tab_names[index] else f"线路{index + 1}", + "urls": "#".join(episodes), + } + ) + type_name = " / ".join( + [ + self._clean_text("".join(node.xpath(".//text()"))) + for node in root.xpath("//*[contains(@class,'module-info-tag-link')]//a") + if self._clean_text("".join(node.xpath(".//text()"))) + ] + ) + pic = ( + root.xpath( + "(//*[contains(@class,'module-item-pic')]//img/@data-original | " + "//*[contains(@class,'module-info-poster')]//img/@data-original | " + "//*[contains(@class,'module-item-pic')]//img/@src | " + "//*[contains(@class,'module-info-poster')]//img/@src)[1]" + ) + or [""] + )[0] + return { + "list": [ + { + "vod_id": vod_id, + "vod_name": self._clean_text( + "".join(root.xpath("//*[contains(@class,'module-info-heading')]//h1[1]//text()")) + ), + "vod_pic": self._build_url(pic), + "type_name": type_name, + "vod_remarks": info.get("备注") or info.get("状态", ""), + "vod_actor": info.get("主演", ""), + "vod_director": info.get("导演", ""), + "vod_content": self._clean_text( + "".join(root.xpath("//*[contains(@class,'module-info-introduction-content')][1]//text()")) + ), + "vod_play_from": "$$$".join(group["from"] for group in groups), + "vod_play_url": "$$$".join(group["urls"] for group in groups), + } + ] + } + + def detailContent(self, ids): + result = {"list": []} + for raw_id in ids: + vod_id = str(raw_id or "").strip() + detail_url = self._decode_vod_id(vod_id) + if not detail_url: + continue + parsed = self._parse_detail_page(self._request_html(detail_url), vod_id) + result["list"].extend(parsed.get("list", [])) + return result