news 2026/7/16 11:46:47

Python爬虫实战:逆向网易云音乐加密接口实现歌单下载

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:逆向网易云音乐加密接口实现歌单下载

1. 项目概述与核心价值

最近在技术社区和论坛里,经常看到有朋友在问,怎么才能把自己网易云音乐歌单里的歌“搬”到本地来。无论是出于备份收藏、离线收听,还是为了做一些音乐数据分析,这都算是一个挺普遍的需求。但稍微尝试过的朋友都知道,这事儿没那么简单。直接去网页上找下载按钮?不存在的。官方客户端下载的歌曲是加密格式,只能在自家App里播放。于是,很多人的目光就投向了爬虫技术。

这个项目,就是针对这个具体需求的一次深度实战。它不是一个泛泛而谈的“爬虫入门”,而是聚焦于“破解网易云音乐加密接口”这个核心难点,最终实现批量、自动化地下载标准MP3音频文件。整个过程,你会接触到现代Web应用常见的反爬策略,特别是参数加密和动态令牌,这比简单的静态页面抓取要硬核得多。对于已经掌握了Python和Requests库基础,想要挑战更复杂、更贴近真实生产环境爬虫的朋友来说,这是一个绝佳的练手项目。

通过这个项目,你不仅能学会如何分析一个复杂Ajax接口的调用链,逆向其核心加密逻辑,还能掌握一套应对类似加密接口的通用排查和破解思路。更重要的是,我会附上完整、可运行的代码,并详细解释每一行代码背后的意图和可能遇到的坑,让你不仅能“跑起来”,更能“懂得为什么这么写”。无论是为了技术学习,还是解决实际需求,这篇文章都将提供一条清晰的路径。

2. 核心思路与技术选型解析

2.1 为什么不能直接抓取音频链接?

很多新手的第一反应是:打开浏览器开发者工具(F12),在“Network”(网络)标签页里找.mp3.m4a的请求。如果你真的去网易云音乐的播放页试过,会发现根本找不到这样的直接链接。这是因为网易云音乐对音频资源采取了严格的保护措施:

  1. 动态加密链接:音频文件的真实URL不是静态的,它由服务器动态生成,并且包含了时间戳、用户令牌(如果登录)、歌曲ID以及最重要的——一个经过加密算法计算的参数(常被命名为params,encSecKey等)。这个链接通常有效期很短,过期即失效。
  2. 核心参数加密:用于请求音频链接或歌曲详情的关键参数(如歌曲ID列表)在发送前会被一套复杂的加密算法处理。你无法直接构造一个明文请求。
  3. 反爬虫机制:除了加密,还可能存在请求头校验、频率限制、行为验证等常见反爬手段。

因此,我们的核心任务从“找链接”变成了“模拟加密过程,成功向官方接口请求到解密后的音频真实地址”。

2.2 整体技术路线图

我们的技术路线非常明确,分为四个核心阶段:

  1. 接口定位与分析:找到负责提供歌曲详情和音频链接的那个“关键”API接口。
  2. 加密逻辑逆向:破解该接口请求参数(特别是paramsencSecKey)的生成算法。这是本项目最核心、最具挑战性的部分。
  3. 请求模拟与数据解析:用Python代码模拟浏览器,携带正确的加密参数去请求接口,并解析返回的JSON数据,提取出音频的真实URL。
  4. 音频下载与存储:根据获取的真实URL,下载音频二进制流,并保存为本地MP3文件,实现批量处理。

2.3 工具与库选型

工欲善其事,必先利其器。以下是本项目所需的核心Python库及其选型理由:

  • requests:HTTP库的不二之选。比内置的urllib更简洁易用,会话保持、请求头设置、代理支持等功能完善。我们将用它来发送所有网络请求。
  • json:Python标准库,用于解析接口返回的JSON数据。
  • re(正则表达式):标准库,在分析网页源码或JavaScript文件时,用于快速提取关键的函数定义或密钥。
  • hashlib:标准库,用于计算MD5、SHA等哈希值。某些加密步骤可能会用到。
  • Crypto(或pycryptodome):这是一个关键库。因为网易云音乐的加密算法很可能涉及AES、RSA等标准加密算法。pycryptodomePyCrypto的一个流行分支,维护更活跃,功能齐全。我们将用它来实现AES加密、CBC模式、PKCS7填充等操作。

    注意:安装时使用pip install pycryptodome。如果遇到与旧版Crypto模块的命名冲突,可能需要先卸载pycrypto

  • os/pathlib:标准库,用于创建本地目录、保存文件,处理文件路径。

为什么不选用Scrapy?Scrapy是一个强大的异步爬虫框架,适合大规模、结构固定的网站爬取。但本项目核心在于对单个复杂加密接口的逆向分析与破解,属于“攻坚”型任务,使用轻量级的requests库配合手动分析更加灵活、直观,也更利于学习和理解底层原理。在成功破解加密逻辑后,你可以很容易地将这套逻辑移植到Scrapy的下载器中间件中,以实现更高性能的批量爬取。

3. 逆向工程:定位与破解加密接口

这是整个项目最硬核的部分,需要一些耐心和技巧。我们主要依靠浏览器的开发者工具。

3.1 定位关键API接口

  1. 打开目标页面:在Chrome或Edge浏览器中,打开网易云音乐官网,并进入一个具体的歌曲播放页面,例如某首歌曲的详情页。
  2. 开启开发者工具:按F12,切换到Network(网络)标签页。务必勾选上 “Preserve log”(保留日志),以防止页面跳转时请求记录被清空。
  3. 触发数据请求:在页面中,点击播放按钮,或者查看歌曲详情。同时,仔细观察Network面板中新增的请求。
  4. 筛选与查找:在请求列表上方的筛选框内,输入关键词如songdetailplayerurlmusic等。同时,将请求类型XHR/Fetch选中,因为现代Web应用的数据交互大多通过这类Ajax请求完成。
  5. 识别目标接口:在众多请求中,寻找一个看起来像是获取歌曲信息或播放地址的请求。它的Name可能包含weapi/song/detailweapi/song/enhance/player/urlapi/song/url等路径。点击这个请求,查看它的Headers(标头)Payload(负载)

通常,weapi/song/enhance/player/url这个接口就是用来获取歌曲播放链接的。你会看到它的Request MethodPOST,并且Query String ParametersForm Data里有一堆看起来像乱码的参数,最主要的就是paramsencSecKey。这就是我们需要攻破的堡垒。

3.2 分析加密参数生成逻辑

找到目标接口后,点击它,查看Payload标签(如果是POST请求)。你会看到类似这样的数据:

params: x1B2c3D4e5F6... (很长一串密文) encSecKey: y7G8h9I0j1K2... (很长一串密文)

我们的目标是弄明白这两个字符串是怎么生成的。它们肯定是在浏览器端的JavaScript代码里计算出来的。

  1. 全局搜索:在开发者工具的Sources(源代码)标签页,或者直接在所有已加载的JS文件中(可以通过Network标签页筛选js文件后查看),使用搜索功能(Ctrl+Shift+F),搜索params:encSecKey:encSecKey =
  2. 查找加密函数:搜索后,你很可能会定位到一段JS代码,其中有一个函数,它接收一些明文参数(比如歌曲IDids),然后经过一系列处理,最终生成了paramsencSecKey。这个函数的名字可能叫window.asrseaencrypt或其它。
  3. 关键线索:固定公钥:在分析网易云音乐加密的众多资料中,一个被广泛验证的结论是,其加密核心采用了一套RSA公钥加密结合AES加密的模式。并且,RSA的公钥是固定不变的。这大大简化了我们的逆向工作。你通常可以在JS代码中找到类似这样的公钥字符串:
    -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB -----END PUBLIC KEY-----
    以及一个固定的AES密钥(如0CoJUm6Qyw8W8jud)和初始化向量IV(如0102030405060708)。
  4. 理解加密流程:典型的加密流程(简化版)可能是: a. 将请求的明文数据(如{"ids":"[123456]","level":"standard"})先进行一次AES加密,密钥是0CoJUm6Qyw8W8jud。 b. 将上一步得到的密文,再进行一次AES加密,但这次使用一个随机生成的16字节密钥。 c. 这个随机生成的AES密钥,会用上面那个固定的RSA公钥进行加密,得到的结果就是encSecKey。 d. 第二次AES加密得到的密文,就是params。 e. 此外,可能还会对明文进行JSON.stringifyzfill(补位)等预处理。

实操心得:逆向时,不要试图完全理解整个庞大JS文件的所有逻辑。我们的目标是“找到并复现”。重点盯住生成paramsencSecKey的那个函数,把它里面调用的子函数逻辑也一并抠出来。可以尝试在开发者工具的Console(控制台)中,手动调用这个加密函数,传入测试数据,看输出是否与Network里捕获的请求参数一致,这是验证我们理解是否正确的最快方法。

4. Python代码实现:完整复现加密与下载

在彻底分析清楚加密流程后,我们就可以用Python来复现这一过程了。下面我将分模块给出代码并详细解释。

4.1 环境准备与依赖安装

首先,确保你的Python环境(建议3.6以上)并安装必要库:

pip install requests pycryptodome

4.2 核心加密模块实现

我们创建一个名为encryptor.py的文件,将逆向出来的加密逻辑用Python实现。

import base64 import json import random import string from Crypto.Cipher import AES from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 from Crypto.Util.Padding import pad import binascii class NeteaseMusicEncryptor: """网易云音乐API参数加密器""" # 固定的AES密钥和IV AES_KEY = '0CoJUm6Qyw8W8jud'.encode('utf-8') AES_IV = '0102030405060708'.encode('utf-8') # 固定的RSA公钥 RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB -----END PUBLIC KEY-----""" @staticmethod def _random_string(size): """生成指定长度的随机字符串""" return ''.join(random.choices(string.ascii_letters + string.digits, k=size)) @staticmethod def _aes_encrypt(text, key, iv): """AES加密,CBC模式,PKCS7填充""" cipher = AES.new(key, AES.MODE_CBC, iv) # 确保文本是bytes,并进行PKCS7填充 padded_text = pad(text.encode('utf-8'), AES.block_size) encrypted_bytes = cipher.encrypt(padded_text) # 结果进行base64编码 return base64.b64encode(encrypted_bytes).decode('utf-8') @staticmethod def _rsa_encrypt(text, public_key): """RSA加密,使用PKCS1_v1_5填充""" rsa_key = RSA.import_key(public_key) cipher = PKCS1_v1_5.new(rsa_key) # RSA加密的数据也需要是bytes encrypted_bytes = cipher.encrypt(text.encode('utf-8')) # 结果转换为16进制字符串 return binascii.b2a_hex(encrypted_bytes).decode('utf-8') @classmethod def encrypted_request(cls, data): """ 核心加密函数,模拟网易云音乐前端加密流程 :param data: 字典,请求的明文数据,如 {'ids': '[123456]', 'level': 'standard'} :return: 包含加密后params和encSecKey的字典 """ # 第一步:将数据转换为JSON字符串 text = json.dumps(data) # 第二步:生成一个16字节的随机密钥 secret_key = cls._random_string(16) # 第三步:第一次AES加密,使用固定密钥 params = cls._aes_encrypt(text, cls.AES_KEY, cls.AES_IV) # 第四步:第二次AES加密,使用随机密钥 params = cls._aes_encrypt(params, secret_key.encode('utf-8'), cls.AES_IV) # 第五步:用RSA公钥加密随机密钥,生成encSecKey enc_sec_key = cls._rsa_encrypt(secret_key, cls.RSA_PUBLIC_KEY) return {'params': params, 'encSecKey': enc_sec_key}

代码关键点解析

  1. _aes_encrypt函数:我们使用Crypto.Cipher.AESMODE_CBC模式。pad函数用于进行PKCS7填充,这是AES CBC模式的标准要求。加密后输出Base64字符串,这与前端观察到的结果一致。
  2. _rsa_encrypt函数:使用PKCS1_v1_5填充方案(这是网易云音乐使用的)。加密后,我们将字节结果转换为十六进制字符串,这也是前端encSecKey的格式。
  3. encrypted_request类方法:这是对逆向流程的忠实复现。注意两次AES加密的对象:第一次加密明文JSON,第二次加密第一次加密后的结果(Base64字符串)。

4.3 主爬虫程序实现

接下来,我们创建主文件main.py,它负责组织请求、调用加密器、解析数据并下载。

import requests import json import os from encryptor import NeteaseMusicEncryptor class NeteaseMusicDownloader: def __init__(self): self.session = requests.Session() # 设置通用请求头,模拟浏览器 self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://music.163.com/', 'Content-Type': 'application/x-www-form-urlencoded', } self.session.headers.update(self.headers) def get_song_urls(self, song_ids, quality='standard'): """ 获取一批歌曲的播放链接 :param song_ids: 歌曲ID列表,如 [123456, 789012] :param quality: 音质,可选 'standard' (标准), 'higher' (较高), 'exhigh' (极高), 'lossless' (无损), 'hires' (Hi-Res) :return: 字典,{song_id: url, ...} """ api_url = 'https://music.163.com/weapi/song/enhance/player/url' # 构造请求明文数据 data = { 'ids': json.dumps(song_ids), # 需要是JSON字符串格式的数组 'level': quality, 'encodeType': 'aac', # 或 'mp3',根据实际情况调整 'csrf_token': '' # 如果未登录,通常为空 } # 加密参数 encrypted_data = NeteaseMusicEncryptor.encrypted_request(data) # 发送POST请求 resp = self.session.post(api_url, data=encrypted_data) resp.raise_for_status() # 检查请求是否成功 result = resp.json() url_map = {} if result['code'] == 200: for song in result['data']: if song['code'] == 200 and song['url']: # 确保请求成功且有链接 url_map[song['id']] = song['url'] else: print(f"歌曲 {song['id']} 获取链接失败,原因: {song.get('msg', '未知')}") url_map[song['id']] = None else: print(f"接口请求失败: {result}") return url_map def get_song_detail(self, song_ids): """ 获取歌曲详细信息(如名称、歌手) :param song_ids: 歌曲ID列表 :return: 字典,{song_id: {'name':..., 'artists':...}, ...} """ api_url = 'https://music.163.com/weapi/v3/song/detail' data = { 'c': json.dumps([{'id': sid} for sid in song_ids]), 'ids': json.dumps(song_ids), } encrypted_data = NeteaseMusicEncryptor.encrypted_request(data) resp = self.session.post(api_url, data=encrypted_data) resp.raise_for_status() result = resp.json() detail_map = {} if result['code'] == 200: for song in result['songs']: sid = song['id'] name = song['name'] artists = ' & '.join([ar['name'] for ar in song['ar']]) # 拼接所有歌手名 detail_map[sid] = {'name': name, 'artists': artists} return detail_map def download_song(self, url, song_id, song_name, artist, save_dir='./downloads'): """ 下载单首歌曲 """ if not url: print(f"[跳过] {song_name} - {artist} 无有效链接") return False # 创建保存目录 os.makedirs(save_dir, exist_ok=True) # 清理文件名中的非法字符 def sanitize_filename(filename): illegal_chars = '<>:"/\\|?*' for char in illegal_chars: filename = filename.replace(char, '_') return filename safe_name = sanitize_filename(f"{song_name} - {artist}") filepath = os.path.join(save_dir, f"{safe_name}.mp3") # 发送请求下载 print(f"[下载中] {song_name} - {artist}") try: resp = self.session.get(url, stream=True, timeout=30) resp.raise_for_status() with open(filepath, 'wb') as f: for chunk in resp.iter_content(chunk_size=1024*1024): # 1MB chunks if chunk: f.write(chunk) print(f"[成功] 已保存至: {filepath}") return True except Exception as e: print(f"[失败] 下载 {song_name} 时出错: {e}") # 如果下载失败,删除可能不完整的文件 if os.path.exists(filepath): os.remove(filepath) return False def batch_download(self, song_ids, quality='standard', save_dir='./downloads'): """ 批量下载主函数 """ print(f"开始处理 {len(song_ids)} 首歌曲...") # 1. 获取歌曲详情 print("正在获取歌曲信息...") detail_map = self.get_song_detail(song_ids) if not detail_map: print("获取歌曲信息失败,请检查网络或歌曲ID。") return # 2. 获取播放链接 print("正在获取播放链接...") url_map = self.get_song_urls(song_ids, quality) # 3. 逐个下载 success_count = 0 for sid in song_ids: if sid in detail_map and sid in url_map and url_map[sid]: info = detail_map[sid] success = self.download_song(url_map[sid], sid, info['name'], info['artists'], save_dir) if success: success_count += 1 else: print(f"[错误] 歌曲ID {sid} 信息或链接缺失,跳过。") print(f"\n批量下载完成!成功 {success_count}/{len(song_ids)} 首。") if __name__ == '__main__': downloader = NeteaseMusicDownloader() # 示例:下载歌曲ID为 1330348068 (起风了) 和 526464293 (平凡之路) 的歌曲 # 你可以从网易云音乐网页版分享链接中获取歌曲ID,通常是链接中的 `id=` 后面的数字 song_id_list = [1330348068, 526464293] # 开始批量下载,音质选择 'standard'(标准), 'higher'(较高), 'exhigh'(极高), 'lossless'(无损) downloader.batch_download(song_id_list, quality='higher', save_dir='./my_music')

主程序逻辑解析

  1. 会话保持:使用requests.Session()可以自动管理cookies,保持一些必要的会话状态。
  2. 请求头模拟:设置User-AgentReferer是绕过基础反爬的常见手段。
  3. 分步操作batch_download方法清晰地分为三步:获取详情、获取链接、逐个下载。结构清晰,易于调试。
  4. 错误处理:在下载和请求过程中加入了基本的异常捕获和错误提示,提高了程序的健壮性。
  5. 文件命名:使用歌曲名和歌手组合作为文件名,并清理了Windows文件名中不允许的字符。

5. 常见问题、排查技巧与伦理考量

5.1 常见问题与解决方案

在实际运行中,你可能会遇到以下问题:

  1. 接口返回-460错误码:这通常意味着“校验失败”。最可能的原因是加密算法复现不准确。

    • 排查:仔细核对encryptor.py中的每一个步骤。特别是AES的模式(CBC)填充(PKCS7)密钥和IV的编码(必须转为bytes),以及RSA加密后的输出格式(十六进制)。建议使用浏览器的Console,用相同的明文数据分别运行你的Python加密函数和网页上的JS加密函数,对比输出的paramsencSecKey是否完全一致。
  2. 获取到的音频URL为空或无法播放

    • 原因:该歌曲可能因版权限制,无法获取到免费播放链接。或者你请求的音质(level)该歌曲不支持。
    • 解决:尝试降低音质,如从lossless改为exhighstandard。在代码中,我们已经判断了song['url']是否存在。
  3. 下载速度慢或连接超时

    • 原因:网易云音乐的音频服务器可能对单个IP的请求频率有限制。
    • 解决:在download_song函数的session.get中增加timeout参数。对于大规模批量下载,必须在下载每个文件之间添加随机延时(例如time.sleep(random.uniform(1, 3))),并考虑使用代理IP池来分散请求。
  4. ModuleNotFoundError: No module named 'Crypto'

    • 原因pycryptodome库安装后,导入时使用的模块名仍然是Crypto
    • 解决:确保使用pip install pycryptodome安装。如果同时存在旧的pycrypto,可能需要先卸载它:pip uninstall pycrypto
  5. 歌曲信息获取失败

    • 排查:检查get_song_detail函数中构造的data字典格式,是否与通过浏览器捕获的请求负载格式完全一致。有时接口的字段名或结构会发生微调。

5.2 高级技巧与优化建议

  1. 并发下载优化:对于成百上千首歌的下载,单线程太慢。可以使用concurrent.futures库的ThreadPoolExecutor来实现多线程下载,但务必注意控制并发数(建议5-10个线程),并在每个线程的下载函数中保持独立的会话或妥善处理会话对象,避免线程安全问题。
  2. 断点续传:可以检查本地已存在的文件大小,然后在请求音频URL时通过设置请求头Range: bytes=已下载大小-来实现断点续传。但这需要服务器支持Range请求。
  3. 歌单解析:本示例核心是给定歌曲ID进行下载。一个更实用的扩展是:输入一个公开歌单的分享链接,程序自动解析出歌单内所有歌曲的ID。这需要你分析歌单页面的接口(如weapi/v3/playlist/detail),其参数加密方式与歌曲接口类似。
  4. 音质选择逻辑:可以先尝试请求最高音质(如lossless),如果返回的URL为空,则自动降级到下一档音质重新请求,直到成功。

5.3 重要的伦理与法律提醒

在享受技术带来的便利时,我们必须清醒地认识到边界和责任。

重要提示:本项目和所有相关技术分享,仅限于个人学习、研究和合理使用范畴。你必须严格遵守以下原则:

  1. 尊重版权:音乐是创作者的心血。通过此技术下载的音频文件,严禁用于任何商业用途、网络公开传播或盗版销售。请仅作为个人离线备份和欣赏。
  2. 遵守robots.txt:虽然技术上有能力抓取,但应尊重网站运营者的意愿。对目标网站保持友好,避免对其服务器造成过大压力。
  3. 控制请求频率:在代码中务必添加显著的延时(例如每请求几次后休眠数秒),避免高频请求对网易云音乐服务器造成干扰,这既是道德要求,也能防止你的IP被封锁。
  4. 个人使用边界:不要开发、传播或提供以此技术为核心的自动化下载服务或工具。技术的滥用会损害平台和内容创作者的合法权益,也可能让你面临法律风险。

技术是一把双刃剑。掌握爬虫和逆向工程技术,能极大地提升我们解决问题的能力,但我们必须用它来做正确的事。希望这个项目能成为你技术成长路上的一块坚实的垫脚石,而不是一个麻烦的开端。在实际操作中,如果遇到任何问题,回顾加密逻辑的比对和请求头的模拟这两个关键点,大部分难题都能迎刃而解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 11:43:45

抖音无水印批量下载终极指南:5分钟掌握专业级视频收藏技巧

抖音无水印批量下载终极指南&#xff1a;5分钟掌握专业级视频收藏技巧 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback s…

作者头像 李华
网站建设 2026/7/16 11:41:48

SAR ADC数字接口设计与可靠性优化实践

1. 逐次逼近型ADC的数字接口挑战 逐次逼近型ADC&#xff08;SAR ADC&#xff09;作为中高精度模数转换的主流方案&#xff0c;其数字接口的可靠性直接影响系统性能。与流水线型或Σ-Δ型ADC不同&#xff0c;SAR ADC的转换过程对数字接口的时序和噪声更为敏感。我曾在一个工业温…

作者头像 李华
网站建设 2026/7/16 11:40:35

AI编程工具套壳争议:Composer 2与Kimi K2.5技术解析

1. Composer 2套壳争议事件全貌2026年3月&#xff0c;AI编程工具领域爆发了一场关于"套壳"的激烈争论。Cursor团队高调发布了Composer 2&#xff0c;宣称其在终端基准测试中超越了Claude 4.6 Opus。但很快就有开发者发现&#xff0c;这个所谓的"全新"产品与…

作者头像 李华
网站建设 2026/7/16 11:40:16

Ubuntu安装White Sur主题打造macOS风格桌面

1. White Sur Theme主题简介与安装准备 White Sur Theme是一款高度还原macOS Big Sur风格的Ubuntu主题套件&#xff0c;包含GTK主题、图标包、光标样式和Shell主题。这个主题最大的特点是完美复刻了macOS的毛玻璃效果、圆角窗口和精致的图标设计&#xff0c;让Ubuntu用户也能享…

作者头像 李华