当前位置: 首页 > news >正文

使用Visual C++创建Windows快捷方式:IShellLink与IPersistFile接口详解

1. 项目概述:为什么我们需要在程序中创建快捷方式?

在Windows桌面应用开发中,程序安装后自动在桌面或开始菜单创建快捷方式,是一个提升用户体验的经典功能。这不仅仅是安装程序(如InstallShield、Advanced Installer)的专利,对于需要独立部署的绿色软件、内部工具或需要特殊配置的应用程序,通过代码动态创建快捷方式,能赋予程序更强的灵活性和专业性。想象一下,你的工具软件在首次运行时,询问用户“是否在桌面创建快捷方式?”,这种贴心的设计远比让用户自己去安装目录里找EXE文件要友好得多。

然而,很多开发者,尤其是刚接触Windows原生开发的C++程序员,往往觉得这是一个“黑盒”操作,依赖于复杂的COM接口和神秘的GUID,不知从何下手。实际上,使用Visual C++(这里主要指基于Win32 API或ATL/COM的C++开发)实现这一功能,核心是调用Windows Shell提供的IShellLinkIPersistFile这两个COM接口。整个过程逻辑清晰,一旦掌握,你会发现它比你想象的要简单。本文将带你从零开始,深入理解其原理,并提供一个可直接集成到你项目中的、健壮的实现方案。

2. 核心原理与接口解析:IShellLink与IPersistFile是如何工作的?

在Windows系统中,快捷方式(.lnk文件)本质上是一个包含了目标路径、参数、工作目录、图标、描述等元数据的特殊文件。操作系统通过一系列COM组件来读写这些数据。我们编程创建快捷方式,就是与这些组件进行交互。

2.1 关键COM接口介绍

  1. IShellLink (Shell Link Object)这是创建和修改快捷方式的核心接口。它提供了一系列方法(如SetPath,SetArguments,SetWorkingDirectory,SetIconLocation)来设置快捷方式的所有属性。你可以把它理解为一个“快捷方式配置器”,我们先通过它配置好所有选项。

  2. IPersistFile (Persistence Interface)光有配置器还不够,我们需要把配置好的信息持久化保存为一个实际的.lnk文件。IPersistFile接口就是干这个的。IShellLink对象通常也会实现这个接口。我们通过QueryInterfaceIShellLink对象获取其IPersistFile接口,然后调用Save方法,将内存中的配置写入磁盘文件。

2.2 创建流程的“四步法”

整个创建过程可以概括为四个标准步骤,这也是COM编程的常见模式:

  1. 初始化COM库: 任何COM操作之前,必须调用CoInitializeCoInitializeEx。对于GUI程序,通常在程序启动时做一次。
  2. 创建IShellLink实例: 使用CoCreateInstance函数,传入CLSID_ShellLink,请求一个IShellLink接口指针。
  3. 配置快捷方式属性: 使用获得的IShellLink指针,调用SetPath,SetArguments等方法,设置目标程序、启动参数等信息。
  4. 保存为.lnk文件: 从IShellLink查询IPersistFile接口,然后调用IPersistFile::Save,指定完整的.lnk文件路径(如C:\\Users\\Name\\Desktop\\MyApp.lnk)。

注意: 务必进行完整的错误检查。每一步COM调用(CoCreateInstance,QueryInterface,SetPath,Save)都可能失败,返回HRESULT类型的错误码。使用SUCCEEDEDFAILED宏来判断,并在失败时妥善释放已申请的资源(如调用Release()),这是写出稳定代码的关键。

2.3 路径处理与权限考量

这是实操中最容易出问题的地方。你需要明确几个路径:

  • 目标路径 (Target Path): 你的应用程序EXE文件的完整路径。可以使用GetModuleFileName来获取当前运行程序的路径,或让用户指定。
  • 快捷方式存放路径 (Link Path): 你想把.lnk文件放在哪里。常见位置有:
    • 桌面:CSIDL_DESKTOPDIRECTORYFOLDERID_Desktop(Vista+)
    • 开始菜单程序目录:CSIDL_PROGRAMS
    • 当前用户启动文件夹:CSIDL_STARTUP获取这些特殊文件夹路径,推荐使用SHGetFolderPath(XP/2000+) 或更现代的SHGetKnownFolderPath(Vista+)。绝对不要硬编码如”C:\\Users\\Public\\Desktop”这样的路径,因为系统盘符和用户名会变。
  • 权限问题: 在Windows Vista及以后版本,写入C:\\ProgramData(所有用户的桌面)或C:\\Program Files目录需要管理员权限。如果你的安装程序或主程序以管理员身份运行,可以尝试创建“所有用户”的快捷方式。但对于普通单用户程序,更安全、更常见的做法是创建在当前用户的目录下(如%USERPROFILE%\\Desktop),这不需要提升权限。

3. 完整实现步骤与代码详解

下面,我将提供一个封装好的C++函数CreateShortcut,它包含了完整的错误处理和资源管理。你可以直接将这个函数复制到你的项目中。

3.1 环境准备与头文件

确保你的Visual C++项目设置了正确的字符集(通常建议使用Unicode),并链接了必要的库。

#include <windows.h> #include <shlobj.h> // 用于SHGetFolderPath和Shell接口 #include <shobjidl.h> // 用于IShellLink和IPersistFile (如果shlobj.h版本较老) #include <strsafe.h> // 用于安全的字符串函数,如StringCchCopy #pragma comment(lib, "shell32.lib") // 链接Shell32库

如果你的开发环境(如Visual Studio 2019+)的shlobj.h已经包含了接口定义,可能不需要单独包含shobjidl.h。但为了更好的兼容性,可以都加上。

3.2 核心创建函数实现

/** * 创建一个Windows快捷方式(.lnk文件) * * @param pszTargetPath 目标文件的全路径 (例如: L"C:\\MyApp\\MyApp.exe") * @param pszLinkPath 要生成的.lnk文件的全路径 (例如: L"C:\\Users\\YourName\\Desktop\\MyApp.lnk") * @param pszArguments 启动参数 (可以为NULL或L"") * @param pszWorkingDir 工作目录 (可以为NULL,则使用目标路径所在目录) * @param pszDescription 快捷方式描述/注释 (鼠标悬停时显示) * @param pszIconPath 自定义图标路径 (可以为NULL,使用目标文件默认图标) * @param iIconIndex 图标资源索引 (通常为0) * @return bool true表示成功,false表示失败。可通过GetLastError()获取更多信息。 */ bool CreateShortcut( LPCWSTR pszTargetPath, LPCWSTR pszLinkPath, LPCWSTR pszArguments = NULL, LPCWSTR pszWorkingDir = NULL, LPCWSTR pszDescription = NULL, LPCWSTR pszIconPath = NULL, int iIconIndex = 0) { HRESULT hr = E_FAIL; IShellLink* pShellLink = NULL; IPersistFile* pPersistFile = NULL; // 1. 初始化COM (如果之前未初始化,这里进行初始化。对于已初始化的环境,此调用无害) // 注意:在程序退出前需要有对应的 CoUninitialize()。 // 更好的做法是在程序入口(如WinMain)初始化,这里假设已初始化。 // hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); // if (FAILED(hr)) { /* 处理错误 */ } // 2. 创建IShellLink实例 hr = CoCreateInstance( CLSID_ShellLink, // 快捷方式对象的CLSID NULL, // 不聚合 CLSCTX_INPROC_SERVER, // 在进程内创建 IID_IShellLink, // 请求的接口ID (LPVOID*)&pShellLink // 接收接口指针 ); if (FAILED(hr)) { SetLastError(hr); // 将COM错误码转换为Win32错误码(近似) return false; } // 3. 设置快捷方式属性 // 设置目标路径 (核心,必须设置) hr = pShellLink->SetPath(pszTargetPath); if (FAILED(hr)) { goto Cleanup; } // 设置启动参数 (可选) if (pszArguments != NULL && wcslen(pszArguments) > 0) { hr = pShellLink->SetArguments(pszArguments); if (FAILED(hr)) { goto Cleanup; } } // 设置工作目录 (可选) if (pszWorkingDir != NULL && wcslen(pszWorkingDir) > 0) { hr = pShellLink->SetWorkingDirectory(pszWorkingDir); } else { // 如果未指定,则使用目标文件所在目录作为工作目录 WCHAR szTargetDir[MAX_PATH]; wcscpy_s(szTargetDir, MAX_PATH, pszTargetPath); PathRemoveFileSpec(szTargetDir); // 移除文件名,得到目录 hr = pShellLink->SetWorkingDirectory(szTargetDir); } if (FAILED(hr)) { goto Cleanup; } // 设置描述 (可选) if (pszDescription != NULL && wcslen(pszDescription) > 0) { hr = pShellLink->SetDescription(pszDescription); if (FAILED(hr)) { goto Cleanup; } } // 设置图标 (可选) if (pszIconPath != NULL && wcslen(pszIconPath) > 0) { hr = pShellLink->SetIconLocation(pszIconPath, iIconIndex); // 即使图标设置失败,也不一定让整个创建失败,可以记录日志但继续 // if (FAILED(hr)) { goto Cleanup; } } // 4. 查询IPersistFile接口并保存文件 hr = pShellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&pPersistFile); if (SUCCEEDED(hr)) { hr = pPersistFile->Save(pszLinkPath, TRUE); // TRUE表示根据路径创建必要目录 pPersistFile->Release(); // 用完立即释放 } Cleanup: // 释放IShellLink接口 if (pShellLink) { pShellLink->Release(); } if (FAILED(hr)) { SetLastError(hr); return false; } return true; }

3.3 使用示例:在桌面创建快捷方式

如何调用上面这个函数?关键在于获取当前用户的桌面路径。我们使用SHGetFolderPath函数。

// 示例:为当前运行的EXE在桌面创建一个快捷方式 void CreateDesktopShortcutForSelf() { wchar_t szDesktopPath[MAX_PATH]; wchar_t szMyExePath[MAX_PATH]; wchar_t szLinkPath[MAX_PATH]; // 1. 获取当前用户的桌面文件夹路径 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, szDesktopPath))) { // 2. 获取当前可执行文件的完整路径 GetModuleFileNameW(NULL, szMyExePath, MAX_PATH); // 3. 构造完整的.lnk文件路径 (例如: C:\Users\YourName\Desktop\我的程序.lnk) // 注意:.lnk扩展名系统会自动添加,但显式写上更安全。 StringCchPrintfW(szLinkPath, MAX_PATH, L"%s\\我的应用程序.lnk", szDesktopPath); // 4. 调用我们的创建函数 if (CreateShortcut( szMyExePath, // 目标:当前EXE szLinkPath, // 快捷方式保存路径 L"--minimized", // 启动参数:以最小化方式启动 NULL, // 工作目录:使用EXE所在目录 L"这是我开发的超好用工具", // 描述 szMyExePath, // 图标:使用EXE自身图标 0 // 图标索引 )) { MessageBoxW(NULL, L"桌面快捷方式创建成功!", L"提示", MB_OK | MB_ICONINFORMATION); } else { DWORD dwErr = GetLastError(); wchar_t szErrMsg[256]; StringCchPrintfW(szErrMsg, 256, L"创建失败!错误代码: 0x%08X", dwErr); MessageBoxW(NULL, szErrMsg, L"错误", MB_OK | MB_ICONERROR); } } else { MessageBoxW(NULL, L"无法获取桌面路径!", L"错误", MB_OK | MB_ICONERROR); } }

将这个CreateDesktopShortcutForSelf函数放在你的某个菜单命令或按钮点击事件中,运行程序并触发,就能在桌面看到生成的快捷方式了。

4. 进阶技巧与疑难问题排查

掌握了基础创建后,我们来看看一些更深入的需求和常见的“坑”。

4.1 为所有用户创建快捷方式(需要管理员权限)

有时你需要为计算机的所有用户创建快捷方式(例如安装到公共桌面)。这需要写入CSIDL_COMMON_DESKTOPDIRECTORY路径,而该目录通常受保护。

bool CreateShortcutForAllUsers(LPCWSTR pszTargetPath, LPCWSTR pszLinkName) { wchar_t szCommonDesktop[MAX_PATH]; if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_DESKTOPDIRECTORY, NULL, 0, szCommonDesktop))) { return false; } wchar_t szLinkPath[MAX_PATH]; StringCchPrintfW(szLinkPath, MAX_PATH, L"%s\\%s.lnk", szCommonDesktop, pszLinkName); // 注意:写入CSIDL_COMMON_DESKTOPDIRECTORY通常需要管理员权限。 // 你的程序必须以管理员身份运行(通过清单文件requestedExecutionLevel level=”requireAdministrator”)。 // 否则,CreateShortcut会失败,错误码可能是ERROR_ACCESS_DENIED。 return CreateShortcut(pszTargetPath, szLinkPath, L"", NULL, L"供所有用户使用"); }

重要提醒: 尝试写入系统级目录前,务必检查并请求管理员权限。对于安装程序,这是标准操作。对于普通应用程序,应谨慎使用,并做好权限失败后的降级处理(例如,改为创建到当前用户目录)。

4.2 设置快捷方式的“以管理员身份运行”属性

某些程序需要提升权限运行。你可以通过修改快捷方式指向的目标为一个“启动器”,或者在创建后修改.lnk文件的属性来实现。更直接的方法是通过代码设置快捷方式的“运行方式”。这需要操作快捷方式文件中的“运行方式”标志位,通常涉及到更底层的属性存储(IPropertyStore)。一个相对简单的方法是创建一个批处理文件或VBScript来设置,但不够优雅。

一个更“原生”的方法是使用IShellLinkDataList接口,但这属于更高级的Shell编程。对于大多数场景,如果目标程序自身通过清单文件声明了requireAdministrator,当用户双击快捷方式时,系统会自动触发UAC提示,无需在快捷方式上额外设置。

4.3 处理快捷方式图标不显示或显示为白板

这是一个非常常见的问题,通常由以下原因导致:

  1. 图标路径无效或索引错误SetIconLocation指定的文件路径不存在,或者该文件不包含有效的图标资源,或者iIconIndex超出了有效范围。建议:如果使用EXE自身的图标,确保pszIconPathpszTargetPath一致,且索引为0。对于DLL或ICO文件,确保路径正确。
  2. 图标缓存未更新: Windows会缓存图标以加速显示。当你创建或更改快捷方式图标后,缓存可能还是旧的。解决方法:
    • 重启Windows资源管理器(Explorer.exe)。
    • 手动清除图标缓存:删除%USERPROFILE%\\AppData\\Local\\IconCache.db文件(需在任务管理器中结束explorer.exe进程后操作),然后重启资源管理器。
    • 更温和的方式:在资源管理器按F5刷新桌面,有时需要多等几秒。
  3. 创建的.lnk文件本身损坏: 确保你的代码逻辑正确,特别是字符串处理部分(避免缓冲区溢出、确保Unicode字符串正确)。使用上面提供的带有错误检查的代码能极大避免此问题。

4.4 常见错误码(HRESULT)解析与处理

CreateShortcut函数中,我们通过SetLastError(hr)将COM的HRESULT转换为Win32错误码。以下是可能遇到的常见错误及其含义:

HRESULT (十六进制)可能原因与解决方案
0x80004005 (E_FAIL)一般性失败。可能是CoCreateInstance失败(COM未初始化?),或Save时路径不可写。检查COM初始化,检查目标目录是否存在且有写入权限。
0x80070002 (E_FILENOTFOUND)文件未找到。SetPath中指定的pszTargetPath不存在。请检查路径是否正确、是否包含中文字符(确保使用宽字符版本)、程序是否有权限访问该路径。
0x80070005 (E_ACCESSDENIED)访问被拒绝。尝试写入受保护的系统目录(如所有用户的桌面)而没有管理员权限。或者目标文件被其他进程锁定。
0x80070050 (E_FILE_EXISTS)文件已存在。Save时如果文件已存在,通常会被覆盖。此错误不常见,除非有特殊的文件系统限制。
0x80040154 (REGDB_E_CLASSNOTREG)类未注册。CLSID_ShellLink对应的COM组件未注册。这在正常的Windows系统中极不可能发生,除非系统严重损坏。

调试建议: 在开发阶段,可以在每个COM调用后立即检查hr,并输出详细信息到调试窗口(使用OutputDebugString),这样可以快速定位是哪一步出了问题。

5. 实战:集成到MFC或Win32应用程序中

5.1 在MFC对话框中添加功能

假设你有一个MFC对话框程序,上面有一个按钮IDC_BTN_CREATE_SHORTCUT

  1. 使用ClassWizard或右键“添加事件处理程序”,为该按钮添加BN_CLICKED消息处理函数。
  2. 在对应的.cpp文件中,包含我们之前写的CreateShortcut函数声明和定义(或放在一个公共头文件/源文件中)。
  3. 在消息处理函数中调用CreateDesktopShortcutForSelf()
void CYourDlg::OnBnClickedBtnCreateShortcut() { // 简单调用示例函数 CreateDesktopShortcutForSelf(); // 或者更灵活地自定义 wchar_t szPath[MAX_PATH]; GetModuleFileNameW(NULL, szPath, MAX_PATH); wchar_t szDesktop[MAX_PATH]; if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, szDesktop))) { wchar_t szLink[MAX_PATH]; StringCchPrintfW(szLink, MAX_PATH, L"%s\\我的工具.lnk", szDesktop); if (CreateShortcut(szPath, szLink, L"/silent", NULL, L"我的专属工具")) { AfxMessageBox(L"快捷方式已创建到桌面!"); } else { AfxMessageBox(L"创建失败,请检查是否有权限或路径是否正确。"); } } }

5.2 在控制台程序或Win32 GUI程序中调用

对于非MFC项目,流程完全一样。确保在程序入口点(mainWinMain)初始化COM库,并在程序退出前反初始化。

#include <windows.h> #include <shlobj.h> #include <iostream> // ... 这里插入 CreateShortcut 函数定义 ... int main() { // 初始化COM库 (单线程公寓模型,适用于大多数GUI程序) HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (FAILED(hr)) { std::wcerr << L"COM初始化失败!" << std::endl; return -1; } // 获取当前程序路径和桌面路径 WCHAR szExePath[MAX_PATH]; WCHAR szDesktopPath[MAX_PATH]; WCHAR szLinkPath[MAX_PATH]; GetModuleFileNameW(NULL, szExePath, MAX_PATH); if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, szDesktopPath))) { StringCchPrintfW(szLinkPath, MAX_PATH, L"%s\\ConsoleTool.lnk", szDesktopPath); if (CreateShortcut(szExePath, szLinkPath)) { std::wcout << L"快捷方式创建成功: " << szLinkPath << std::endl; } else { DWORD err = GetLastError(); std::wcerr << L"创建失败! 错误码: 0x" << std::hex << err << std::endl; } } else { std::wcerr << L"无法获取桌面路径!" << std::endl; } // 反初始化COM库 CoUninitialize(); return 0; }

5.3 关于Visual C++运行库(MSVCRT)的注意事项

从你提供的网络热词中可以看到,很多开发者被“Microsoft Visual C++ Redistributable”相关问题困扰。你的程序如果使用动态链接到VC运行库(/MD或/MDd编译选项),那么目标机器上必须安装对应版本的运行库,否则程序无法启动。快捷方式只是指向你的EXE,如果EXE本身因为缺少运行库而无法运行,快捷方式也会失效。

给你的建议

  • 发布程序时: 如果使用动态链接,请将对应的vcredist_x86.exevcredist_x64.exe打包进你的安装程序,并静默安装(/quiet/passive参数)。
  • 排查用户问题: 当用户反馈“双击快捷方式没反应”时,除了检查快捷方式本身,一定要引导他们检查事件查看器(Event Viewer)中应用程序的错误日志,很可能就是缺少运行库(错误模块MSVCP140.dll,VCRUNTIME140.dll等)或vcbuild.exe(这是构建工具,与运行库不同)环境问题。
  • 静态链接: 对于小型工具,可以考虑使用静态链接(/MT或/MTd),这样会将运行库代码打包进你的EXE,无需用户额外安装,但EXE体积会增大。

6. 总结与扩展思路

通过上述步骤,你已经掌握了使用Visual C++原生API创建Windows快捷方式的完整技能。关键点在于理解IShellLinkIPersistFile这对COM接口的协作,以及妥善处理路径和错误。

扩展思考

  • 读取和修改现有快捷方式: 流程类似,使用IPersistFile::Load加载现有.lnk文件,然后通过IShellLinkGetPath,GetArguments等方法读取属性,或用Set系列方法修改后保存。
  • 创建URL快捷方式(.url): 这是另一种更简单的格式,本质是一个文本文件(.ini格式),可以直接用WritePrivateProfileString等文件API创建,无需COM。但对于程序关联和复杂属性,.lnk文件更强大。
  • 在开始菜单创建文件夹和快捷方式: 思路是先使用SHGetFolderPath获取CSIDL_PROGRAMS路径,然后用CreateDirectory创建你的程序文件夹,最后在这个文件夹内调用CreateShortcut生成.lnk文件。

最后,把创建快捷方式的功能做得更人性化一些:例如,在首次运行时询问用户,或者在设置界面提供一个复选框。良好的用户体验就藏在这些细节里。希望这篇指南能帮你彻底搞定这个功能,让你的程序看起来更专业、用起来更顺手。如果在集成过程中遇到任何具体问题,欢迎在社区交流,通常问题都出在路径字符串处理或COM初始化环节,仔细检查这两点,大部分难题都能迎刃而解。

http://www.jsqmd.com/news/1185059/

相关文章:

  • Windows环境iOS应用砸壳实战:Frida逆向QQ与淘宝完整指南
  • 2026年7月游览船企业推荐,画舫游览船/观光游览船/观光船/游船/新能源电动船/电动巡逻艇,游览船供应商哪家可靠 - 品牌推荐师
  • 2026年7月最新南宁百达翡丽官方售后热线及客户服务网点地址 - 百达翡丽服务中心
  • 2026年7月最新嘉兴帝舵官方售后服务网点地址及客服电话一览 - 帝舵中国官方服务中心
  • 21-缓存越用越脏-给本地数据加版本过期和重建策略
  • java数据类型:Long 和 long 在雪花算法ID下的陷阱
  • Java语言中的类
  • 多态与虚函数:面试官问“底层怎么实现的”,这样答直接拿满分
  • static关键字的四种用法:面试官问“static有几种用法”,90%的人答不全
  • Unity XR开发入门:从SDK导入到交互配置的完整指南
  • CSS字体样式(font)[实战指南]
  • 亲身探访广州泰格豪雅官方售后服务中心|详细网点地址与售后热线(2026年7月最新) - 亨得利官方服务中心
  • Unity BlendShape与Animation Clip实现角色动态表情完整指南
  • 2026年7月最新温州劳力士官方售后客服中心地址电话及服务网点分布 - 劳力士官方服务中心
  • 卫星视频目标跟踪:从相关滤波到深度学习的演进与挑战
  • Folium地图可视化实战:从数据到交互式地理看板
  • 宝珀手表网上预订售后保养服务指南权威公示(2026年7月最新) - 宝珀官方售后服务中心
  • Pandas数据清洗速查手册:12个高频函数实战指南
  • Gifski跨平台视频转GIF终极指南:高质量转换与自动化脚本
  • 基于C++20协程的异步网络编程:libfv框架解析与实践指南
  • YOLOv8道路坑洼检测实战:从数据到部署全流程解析
  • 图像锐度评分算法实战:从方差、点锐度到梯度法的选择与优化
  • C++跨平台开发中整数类型陷阱与<cstdint>解决方案
  • 劳力士中国官方售后服务中心|全部网点地址及24小时热线权威信息通告(2026年7月更新) - 劳力士服务中心
  • 如何基于用户需求生成高质量技术博文
  • 2026最新衡水本地漏水检测公司本地精选权威推荐:正规防水补漏公司优选口碑TOP5:卫生间/厨房/阳台/飘窗/地下室渗漏水维修师傅上门 - 即刻修防水
  • Unity游戏开发:3分钟实现角色轮廓高亮效果(Outline Effect)
  • 2026年7月行业内诚信的巴伦公司推荐,功率电感RH系列/磁胶NR电感/车规电感/排磁珠/耦合器,巴伦实力厂家有哪些 - 品牌推荐师
  • Level 2行情数据下载过程和笔记
  • `beautifulsoup4`(简称 BeautifulSoup)是 Python 中最常用、最强大的 HTML 和 XML 解析库之一