From fc6cc24529b347c11f5c42a4562a8e53566f30a5 Mon Sep 17 00:00:00 2001 From: Harold <8866033@gmail.com> Date: Sat, 25 Apr 2026 08:59:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20scaffold=20AAZ=E9=9F=B3=E4=B9=90=20spid?= =?UTF-8?q?er?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- py/AAZ音乐.py | 143 +++++++++++++++++++++++++++++++++++++++ py/tests/test_AAZ音乐.py | 113 +++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 py/AAZ音乐.py create mode 100644 py/tests/test_AAZ音乐.py diff --git a/py/AAZ音乐.py b/py/AAZ音乐.py new file mode 100644 index 0000000..794ac16 --- /dev/null +++ b/py/AAZ音乐.py @@ -0,0 +1,143 @@ +# coding=utf-8 +import re +import sys +from urllib.parse import urljoin + +from base.spider import Spider as BaseSpider + +sys.path.append("..") + + +class Spider(BaseSpider): + def __init__(self): + self.name = "AAZ音乐" + self.host = "https://www.aaz.cx" + self.headers = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ), + "Referer": self.host + "/", + } + self.classes = [ + {"type_id": "new", "type_name": "新歌榜"}, + {"type_id": "top", "type_name": "TOP榜单"}, + {"type_id": "singer", "type_name": "歌手"}, + {"type_id": "playtype", "type_name": "歌单"}, + {"type_id": "album", "type_name": "专辑"}, + {"type_id": "mv", "type_name": "高清MV"}, + ] + self.category_paths = { + "new": "/list/new.html", + "top": "/list/top.html", + "singer": "/singerlist/index/index/index/index.html", + "playtype": "/playtype/index.html", + "album": "/albumlist/index.html", + "mv": "/mvlist/index.html", + } + + def init(self, extend=""): + return None + + def getName(self): + return self.name + + def _build_url(self, path): + return urljoin(self.host + "/", str(path or "").strip()) + + def _clean_text(self, text): + return re.sub(r"\s+", " ", str(text or "")).strip() + + def _load_html(self, html): + return self.html(str(html or "").strip() or "") + + def _fetch_html(self, path): + response = self.fetch(self._build_url(path), headers=dict(self.headers), timeout=10, verify=False) + return response.text if getattr(response, "status_code", 0) == 200 else "" + + def _empty_result(self, page=1): + return {"page": int(page), "limit": 0, "total": 0, "list": []} + + def _extract_song_id(self, href): + matched = re.search(r"/m/([^.?#/]+)\.html", str(href or "").strip()) + return matched.group(1) if matched else "" + + def _extract_folder_id(self, href, prefix): + matched = re.search(r"/%s/([^/?#]+)" % re.escape(prefix), str(href or "").strip()) + return matched.group(1) if matched else "" + + def _encode_vod_id(self, href): + raw = str(href or "").strip() + song_id = self._extract_song_id(raw) + if song_id: + return "song:" + song_id + for prefix, label in [("s", "singer"), ("p", "playlist"), ("a", "album"), ("v", "mv")]: + folder_id = self._extract_folder_id(raw, prefix) + if folder_id: + return label + ":" + folder_id + return "" + + def _parse_song_cards(self, html): + root = self._load_html(html) + items = [] + seen = set() + for node in root.xpath("//li"): + href = "".join(node.xpath(".//div[contains(@class,'name')]//a[1]/@href")).strip() + vod_id = self._encode_vod_id(href) + if not vod_id.startswith("song:") or vod_id in seen: + continue + seen.add(vod_id) + has_mv = bool(node.xpath(".//div[contains(@class,'mv')]//a")) + items.append( + { + "vod_id": vod_id, + "vod_name": self._clean_text("".join(node.xpath(".//div[contains(@class,'name')]//a[1]/@title"))), + "vod_pic": "", + "vod_remarks": "高清MV" if has_mv else "", + } + ) + return items + + def _parse_folder_cards(self, html, expected_prefix): + root = self._load_html(html) + items = [] + seen = set() + for node in root.xpath("//li"): + href = "".join(node.xpath(".//div[contains(@class,'name')]//a[1]/@href")).strip() + vod_id = self._encode_vod_id(href) + if not vod_id.startswith(expected_prefix + ":") or vod_id in seen: + continue + seen.add(vod_id) + items.append( + { + "vod_id": vod_id, + "vod_name": self._clean_text("".join(node.xpath(".//div[contains(@class,'name')]//a[1]/@title"))), + "vod_pic": self._build_url("".join(node.xpath(".//img[1]/@src"))), + "vod_remarks": "", + } + ) + return items + + def homeContent(self, filter): + items = self._parse_song_cards(self._fetch_html(self.category_paths["new"])) + return {"class": list(self.classes), "list": items} + + def homeVideoContent(self): + return {"list": self.homeContent(False).get("list", [])} + + def categoryContent(self, tid, pg, filter, extend): + if tid not in self.category_paths: + return self._empty_result(pg) + html = self._fetch_html(self.category_paths[tid]) + if tid in ("new", "top"): + items = self._parse_song_cards(html) + elif tid == "singer": + items = self._parse_folder_cards(html, "singer") + elif tid == "playtype": + items = self._parse_folder_cards(html, "playlist") + elif tid == "album": + items = self._parse_folder_cards(html, "album") + else: + items = self._parse_folder_cards(html, "mv") + return {"page": int(pg), "limit": len(items), "total": len(items), "list": items} diff --git a/py/tests/test_AAZ音乐.py b/py/tests/test_AAZ音乐.py new file mode 100644 index 0000000..f7d6585 --- /dev/null +++ b/py/tests/test_AAZ音乐.py @@ -0,0 +1,113 @@ +import unittest +from importlib.machinery import SourceFileLoader +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +MODULE = SourceFileLoader("aaz_music_spider", str(ROOT / "AAZ音乐.py")).load_module() +Spider = MODULE.Spider + + +HOME_HTML = """ + + + +""" + +SINGER_HTML = """ + +
  • +
    +
    周杰伦
    +
  • + +""" + +PLAYLIST_HTML = """ + +
  • +
    +
    华语热歌
    +
  • + +""" + +ALBUM_HTML = """ + +
  • +
    +
    范特西
    +
  • + +""" + +MV_HTML = """ + +
  • +
    +
    晴天MV
    +
  • + +""" + + +class TestAAZMusicSpider(unittest.TestCase): + def setUp(self): + Spider._instance = None + self.spider = Spider() + self.spider.init() + + @patch.object(Spider, "fetch") + def test_home_content_returns_fixed_classes_and_song_cards(self, mock_fetch): + mock_fetch.return_value = SimpleNamespace(status_code=200, text=HOME_HTML) + result = self.spider.homeContent(False) + self.assertEqual( + [item["type_id"] for item in result["class"]], + ["new", "top", "singer", "playtype", "album", "mv"], + ) + self.assertEqual([item["vod_id"] for item in result["list"]], ["song:1001", "song:1002"]) + self.assertEqual(result["list"][0]["vod_name"], "晴天") + self.assertEqual(result["list"][0]["vod_remarks"], "高清MV") + + @patch.object(Spider, "fetch") + def test_home_video_content_reuses_home_list(self, mock_fetch): + mock_fetch.return_value = SimpleNamespace(status_code=200, text=HOME_HTML) + result = self.spider.homeVideoContent() + self.assertEqual([item["vod_id"] for item in result["list"]], ["song:1001", "song:1002"]) + + @patch.object(Spider, "fetch") + def test_category_content_maps_song_and_folder_types(self, mock_fetch): + mock_fetch.side_effect = [ + SimpleNamespace(status_code=200, text=HOME_HTML), + SimpleNamespace(status_code=200, text=HOME_HTML), + SimpleNamespace(status_code=200, text=SINGER_HTML), + SimpleNamespace(status_code=200, text=PLAYLIST_HTML), + SimpleNamespace(status_code=200, text=ALBUM_HTML), + SimpleNamespace(status_code=200, text=MV_HTML), + ] + self.assertEqual(self.spider.categoryContent("new", "1", False, {})["list"][0]["vod_id"], "song:1001") + self.assertEqual(self.spider.categoryContent("top", "1", False, {})["list"][1]["vod_id"], "song:1002") + self.assertEqual(self.spider.categoryContent("singer", "1", False, {})["list"][0]["vod_id"], "singer:jay") + self.assertEqual(self.spider.categoryContent("playtype", "1", False, {})["list"][0]["vod_id"], "playlist:top100") + self.assertEqual(self.spider.categoryContent("album", "1", False, {})["list"][0]["vod_id"], "album:fantasy") + self.assertEqual(self.spider.categoryContent("mv", "1", False, {})["list"][0]["vod_id"], "mv:qingtianmv") + + def test_category_content_returns_empty_for_unknown_type(self): + self.assertEqual( + self.spider.categoryContent("bad", "1", False, {}), + {"page": 1, "limit": 0, "total": 0, "list": []}, + ) + + +if __name__ == "__main__": + unittest.main()