WPF数据爬取小工具-某宝推广位批量生成,及订单爬取 记:接单最痛一次的感悟
项目由来:上月闲来无事接到接到一个单子,自动登录 X宝平台,然后重定向到指定页面批量生成推广位信息;与此同时自动定时同步订单数据到需求提供方的Java服务。
当然期间遇到一个小小的问题就是界面样式的问题,起初使用的winform开发,但是样式,你懂的,所以后来索性直接使用wpf.
先声明:这里只做经验分享,不提供其他支持,毕竟,,,不安全。
1.首先看下我们的项目界面
说明:三张图分别是 登录,登录后主页面,和订单页面
(登录页面)界面整体就划分上中下尾四个部分,种下部分的灰色是一个webBrowser.可以很好地帮助我们解决重定向之后,通过重定向页面获取cookie,这个后面回说。
当然如果你觉得这个灰色很突兀,你可设置高宽为0,那么界面将会很简洁。我之所以显示出来是因为初次访问该网站的时候,会出现验证的问题,需要手动点击以及拖拽拼图。
(主页面) 依旧是头部上部中部下部尾部,
(订单页面)很明了。
界面插件:MetroWindow,请自行百度,谢谢。
2.主要逻辑
2.1.主页面内容
首先我们分析下,一般情况下,我们在登录某平台时候,如果使用第三方授权登陆之后,地址中会有一个redirectUrl,即授权成功之后从定向的页面,那么此时我们要获取的cokkie肯定是从重定向之后的页面获取
所以,这里也是一样的,我们这里的登录实现也是通过一个带有redirectUrl的登陆地址模拟post。
首先,我们在窗体初始化的时候,在webBrowser中初始化我们的登录页面,也就是 灰色部分。然后通过webbrowser获取相关dom元素,赋值,模拟登陆按钮的提交事件,代码如下
webBrowser代码:
在窗体的load事件中初始化,其中的 LoginUrl 就是我们的 带有重定向地址的 登录地址;eg:https://login.xxbao.com/login?redirectURL=www.baidu.com
webBrowser.Navigate(LoginUrl);
当然如此还没完,如果了解webBrowser的人肯定都知道,这个东西有一个常用的事件就是 LoadCompleted,每当页面加载完成或者重定向完成之后,都会执行,所以,继续在load中添加如下的代码,将 LoadCompleted 事件先设置了,
webBrowser.LoadCompleted += (wbSender, wbArgs) =>
{
if (_loginViewModel != null && !string.IsNullOrWhiteSpace(_loginViewModel.LoginAccount))
{
if (wbArgs.Uri.ToString().Contains("登录成功之后,跳转到的回调的网站的主页面地址"))
{
Log4netHelper.WriteLog(Log4netHelper.LogType.Info, "正在获取cookie...");
// TODO 获取cookie操作
try
{
_loginViewModel.StrCookies = CookieHelper.GetCookies(_loginViewModel.WebBrowser.Source.ToString());
_loginViewModel._tb_token_ = CookieHelper.GetFiled(_loginViewModel.StrCookies, "_tb_token_");
Log4netHelper.WriteLog(Log4netHelper.LogType.Info, "获取到TOKEN\n" + "\t" + _loginViewModel._tb_token_ + "");
}
catch (Exception ex)
{
........错误记录省略
}
this.WriteLog("cookie成功获取,即将跳转到主页面...");
this.GoTuiGuangWei();
}
}
};
}
简要说明下:其中红色部分为登录成功之后重新定向的网站的主页面,之所以在这里判断,上面有说了,loadCompleted会在webBrowser每次页面加载完成的时候都会被执行,所以在这里我们判断当前加载的页面是否是我们想要从中获取
cookie的网站的页面,如果是,那么我们执行cookie的获取操作
这里涉及到一个 重点问题就是cookie获取的问题,这里需要注意,下面提供的方法可以正常获取,其他方式 自行斟酌是否可行。
public class CookieHelper
{
[DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool InternetGetCookieEx(string pchURL, string pchCookieName, StringBuilder pchCookieData, ref System.UInt32 pcchCookieData, int dwFlags, IntPtr lpReserved); [DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int InternetSetCookieEx(string lpszURL, string lpszCookieName, string lpszCookieData, int dwFlags, IntPtr dwReserved); private static string GetCookieString(string url)
{
// Determine the size of the cookie
uint datasize = 256;
StringBuilder cookieData = new StringBuilder((int)datasize); if (!InternetGetCookieEx(url, null, cookieData, ref datasize, 0x2000, IntPtr.Zero))
{
if (datasize < 0)
return null; // Allocate stringbuilder large enough to hold the cookie
cookieData = new StringBuilder((int)datasize);
if (!InternetGetCookieEx(url, null, cookieData, ref datasize, 0x00002000, IntPtr.Zero))
return null;
}
return cookieData.ToString();
} public static string GetCookies(string requestUrl)
{
return GetCookieString(requestUrl);
} /// <summary>
/// 从cookie中获取指定键名称的对应的值
/// </summary>
/// <param name="cookies"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
public static string GetFiled(string cookies, string fieldName)
{
var cookieArray = cookies.Split(';');
foreach (var cookieStr in cookieArray)
{
if (cookieStr.Contains(fieldName))
{
return cookieStr.Split('=')[1];
}
}
return string.Empty;
}
}
涉及到的登录代码:
/// <summary>
/// 登录参数对象扩展方法
/// </summary>
public static class WebBrowserExtensions
{
/// <summary>
/// 登陆扩展
/// </summary>
/// <param name="loginViewModel"></param>
public static void LoginEx(this LoginViewModel loginViewModel)
{
var webBrowser = loginViewModel.WebBrowser;
#region 验证操作(登陆一次之后就不存在了,但是这里个 domID不确定是不是这个)
IHTMLDocument2 doc = (IHTMLDocument2)webBrowser.Document;
try
{
IHTMLElement jsLoginCheck = doc.all.item("J_SafeLoginCheck", 0);//id或者是name
jsLoginCheck.click();
Thread.Sleep(1000);
}
catch (Exception)
{ }
#endregion //账号dom ID
IHTMLElement elementAccount = doc.all.item("TPL_username_1", 0);
//密码dom ID
IHTMLElement elementPassword = doc.all.item("TPL_password_1", 0); //赋值操作
elementAccount.setAttribute("value", loginViewModel.LoginAccount);//绑定值
elementPassword.setAttribute("value", loginViewModel.LoginPassword); Thread.Sleep(100);
IHTMLElement buttonSubmit = doc.all.item("J_SubmitStatic", 0);
buttonSubmit.click();
}
}
这里需要注意的是wpf的ewbBrowser和winform的稍有不同,获取dom的方式,通过 IHTMLDocument2 doc = (IHTMLDocument2)webBrowser.Document;,需要引用命名空间:using mshtml;
2.2.主页面:
后台代码中定义了两个计时器 (System.Timers.Timer),这里是 Timers下的timer不是Threading下的,注意下。其他细节不便提供出来,如果感兴趣的我可以把源码改过后开放出来,
这里只说下控件数据绑定的问题,当多个线程同时操作某个控件时,虽说wpf的控件和winform有很大不同,但是一样的,存在子控件线程处理不当主线程(主界面)依旧会卡住的问题,
所以我们可像下面这样解决:
比如我们有一个label控件,定时刷新绑定 文本信息
this.label.Invoke(new Action(()=>{
this.Lable.Context ="我是好人";
}));
如此,是没有问题,但是看下面的写法,猜猜是否有问题呢?
this.label.Invoke(new Action(()=>{
//其他的请求操作,假设返回结果为 reuslt
this.Lable.Context =result;
}));
不用怀疑,主线程会卡住,最明显的示例就是使用wpf的 RitchTextBox,如下代码,会存在很严重的 问题:
private void WriteLog(string message){
this.RitchTextBox.Dispatcher.BeginInvoke(new Action(() =>
{
p = new Paragraph();
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r);
RitchTextBox.Document.Blocks.Add(p);
RitchTextBox.ScrollToEnd();
}));
}
假如反复的执行该方法,去给 RitchTextBox 追加内容,界面会卡到你想把自己的蛋蛋捏碎,然而,成败就在细节之间,下面的方式 是毫无问题的,
private void WriteLog(string message)
{
p = new Paragraph();
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r);
this.RitchTextBox.Dispatcher.BeginInvoke(new Action(() =>
{
RitchTextBox.Document.Blocks.Add(p);
RitchTextBox.ScrollToEnd();
}));
}
当然更进一步的优化一下可以像下面的方式:
void WriteLog(object message, bool isError = false)
{
this.Dispatcher.BeginInvoke(new Action(() =>
{
Paragraph p;
if (isError)
{
SolidColorBrush solidColorBrush = new SolidColorBrush(Color.FromRgb(255, 0, 0));
p = new Paragraph() { Foreground = solidColorBrush };
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r); }
else
{
p = new Paragraph();
Run r = new Run(message.ToString() + "\n");
p.TextAlignment = TextAlignment.Left;
p.Inlines.Add(r); } this.rtbLog.Dispatcher.BeginInvoke(new Action(() =>
{
rtbLog.Document.Blocks.Add(p);
rtbLog.ScrollToEnd();
}));
}));
}
其他:项目还使用了一个ini的文件的作为配置文件,相关实现类如下:
/// <summary>
/// ini文件操作类
/// </summary>
public class IniHelper
{
#region 动态链接库调用
/// <summary>
/// 调用动态链接库读取值
/// </summary>
/// <param name="lpAppName">ini节名</param>
/// <param name="lpKeyName">ini键名</param>
/// <param name="lpDefault">默认值:当无对应键值,则返回该值。</param>
/// <param name="lpReturnedString">结果缓冲区</param>
/// <param name="nSize">结果缓冲区大小</param>
/// <param name="lpFileName">ini文件位置</param>
/// <returns></returns>
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
StringBuilder lpReturnedString,
int nSize,
string lpFileName); /// <summary>
/// 调用动态链接库写入值
/// </summary>
/// <param name="mpAppName">ini节名</param>
/// <param name="mpKeyName">ini键名</param>
/// <param name="mpDefault">写入值</param>
/// <param name="mpFileName">文件位置</param>
/// <returns>0:写入失败 1:写入成功</returns>
[DllImport("kernel32")]
private static extern long WritePrivateProfileString(
string mpAppName,
string mpKeyName,
string mpDefault,
string mpFileName);
#endregion /// <summary>
/// 读ini文件
/// </summary>
/// <param name="section">节</param>
/// <param name="key">键</param>
/// <returns>返回读取值</returns>
public static string Ini_Read(string section, string key, string path)
{
StringBuilder stringBuilder = new StringBuilder(1024); //定义一个最大长度为1024的可变字符串
GetPrivateProfileString(section, key, "", stringBuilder, 1024, path); //读取INI文件
return stringBuilder.ToString(); //返回INI文件的内容
} /// <summary>
/// 写ini文件
/// </summary>
/// <param name="section">节</param>
/// <param name="key">键</param>
/// <param name="iValue">待写入值</param>
public static void Ini_Write(string section, string key, string iValue, string path)
{
WritePrivateProfileString(section, key, iValue, path); //写入
} /// <summary>
/// 根据文件名创建文件
/// </summary>
/// <param name="path">文件名称以及路径</param>
public static void ini_creat(string path)
{
if (!File.Exists(path)) //判断是否存在相关文件
{
FileStream _fs = File.Create(path); //不存在则创建ini文件
_fs.Close(); //关闭文件,解除占用
}
} /// <summary>
/// 删除ini文件中键
/// </summary>
/// <param name="section">节名称</param>
/// <param name="key">键名称</param>
/// <param name="path">ini文件路径</param>
public static void Ini_Del_Key(string section, string key, string path)
{
WritePrivateProfileString(section, key, null, path); //写入
} /// <summary>
/// 删除ini文件中节
/// </summary>
/// <param name="section">节名</param>
/// <param name="path">ini文件路径</param>
public static void Ini_Del_Section(string section, string path)
{
WritePrivateProfileString(section, null, null, path); //写入
} /// <summary>
/// 指定的ini文件是否存在,不存在就创建
/// </summary>
/// <param name="iniFileName">文件名(非路径,只是名称)</param>
/// <returns></returns>
public static void Ini_Init(string iniFileName = "app.ini")
{
var filePath = IniFilePath(iniFileName);
if (!File.Exists(filePath))
{
//初始化基础信息
IniHelper.ini_creat(filePath);
IniHelper.Ini_Write("INFO", "Preffix", "GWJ-", filePath);
IniHelper.Ini_Write("INFO", "DaoGouID", "", filePath);
IniHelper.Ini_Write("INFO", "Count", "0", filePath);
IniHelper.Ini_Write("INFO", "TAG", "29", filePath);
IniHelper.Ini_Write("INFO", "TimeRange", "8", filePath); //初始化 同步到Java 接口时候 爬取的分页,每爬取一次 更新一次
IniHelper.Ini_Write("DELIVERY", "TO_PAGE", "1", filePath);
IniHelper.Ini_Write("DELIVERY", "PER_PAGE_SIZE", "10", filePath);
//IniHelper.Ini_Write("DELIVERY", "TIMES", "0", filePath);//剩余请求次数 //初始化订单参数,
IniHelper.Ini_Write("ORDER", "SIZE", "2000", filePath); //初始化 服务器地址配置
var tempUrl = "http://xxxxx";
var tgwAddress = IniHelper.Ini_Read("SERVER", "TGWAddress", filePath);
if (tgwAddress != tempUrl)
{
if (string.IsNullOrWhiteSpace(tgwAddress))
IniHelper.Ini_Write("SERVER", "TGWAddress", tempUrl, filePath);//推广位
else
IniHelper.Ini_Write("SERVER", "TGWAddress", tgwAddress, filePath);//推广位
}
var qq = "21321321";
var initQQ = IniHelper.Ini_Read("QQ", "QQ", filePath);
if (qq != initQQ)
{
if (string.IsNullOrWhiteSpace(initQQ))
IniHelper.Ini_Write("QQ", "QQ", qq, filePath);//客服QQ
else
IniHelper.Ini_Write("QQ", "QQ", initQQ, filePath);//客服QQ
}
}
}
public static string IniFilePath(string iniFileName = "app.ini")
{
var filePath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, iniFileName);
return filePath;
}
}
http帮助类:(context-type哪里可以合成一个方法
public class HttpRequestHelper
{ /// <summary>
/// 默认的头
/// </summary>
public static string defaultHeaders = @"Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:no-cache
Connection:keep-alive
Pragma:no-cache
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36"; public static string DoRequest(string alimamaUrl, string method, string postDataStr, Encoding encoding, string cookiesStr)
{
var html = string.Empty;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(alimamaUrl);
request.Method = method;
request.AllowAutoRedirect = true;
request.ContentType = "application/x-www-form-urlencoded";
request.KeepAlive = true;
//request.CookieContainer.Add(cc);
request.Headers[HttpRequestHeader.Cookie] = cookiesStr; if (method.ToUpper() == "POST")
{
byte[] data = Encoding.UTF8.GetBytes(postDataStr);
request.ContentLength = data.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(data, 0, data.Length);
requestStream.Close();
} try
{
HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse();
using (System.IO.Stream dataStream = httpResponse.GetResponseStream())
{
using (System.IO.StreamReader sr = new System.IO.StreamReader(dataStream, Encoding.GetEncoding("utf-8")))
{
html = sr.ReadToEnd();
sr.Close();
}
}
httpResponse.Close();
}
catch (System.Net.WebException ex)
{
html = ex.Message;
}
return html;
} public static string DoJsonRequest(string alimamaUrl, string method, string postDataStr, Encoding encoding, string strCookies = "")
{
var html = string.Empty;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(alimamaUrl);
request.Method = method;
request.AllowAutoRedirect = true;
request.ContentType = "application/json";
request.KeepAlive = true;
//request.CookieContainer.Add(cc);
if (!string.IsNullOrWhiteSpace(strCookies))
request.Headers[HttpRequestHeader.Cookie] = strCookies; if (method.ToUpper() == "POST")
{
byte[] data = Encoding.UTF8.GetBytes(postDataStr);
request.ContentLength = data.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(data, 0, data.Length);
requestStream.Close();
} try
{
HttpWebResponse httpResponse = (HttpWebResponse)request.GetResponse();
using (System.IO.Stream dataStream = httpResponse.GetResponseStream())
{
using (System.IO.StreamReader sr = new System.IO.StreamReader(dataStream, Encoding.GetEncoding("utf-8")))
{
html = sr.ReadToEnd();
sr.Close();
}
}
httpResponse.Close();
}
catch (System.Net.WebException ex)
{
html = ex.Message;
}
return html;
} }
3.注意点
1.wpf窗体间传值问题;
2.cookie(cookieCollection)的传递问题,在上面获取到cookie(是一个字符串),不用做任何处理,在后面每次的请求(使用http的帮助类),都需要带上,不需要转换成cookiCollection,直接拼接到header中即可(http帮助类中有做判断),
3.控件绑定绑定值问题,避免子线程阻塞主线程(界面)
源码稍后提供到git
4.感悟:多么痛的领悟。
自从业以来,打过工、创过业(尽管失败了),接过单,但是这次接的这破东西是最坑的一次,按以往的经验和习惯,就这样类似的东西,仅仅四五个功能的,基本3-4个小时不出意外的话,毫无疑问的就可以完成,其中会有1-2小时的测试和数据分析。
然而,就这三个界面的东西,前前后后花了我三四天时间,每天都要花在上面3小时左右,平心而论,儿了这千把块的东西珍惜不值得,第二天时候我已经和需求方提出了,你们重新找人吧,需求一再的变动,前前后后不下4次,价格却一成不变,咱拜拜吧。但是一再的求我,
“兄弟,帮个忙吧,好处会有的,,,,”,完全是一堆废话,后又赶上订亲等一些事宜是真的很忙,这人竟然和我火了,大发雷霆,发短信打电话威胁我,我说你这么嚣张威胁我?回:对,我就是威胁你。。。。如此之嚣张,无奈之举,,找了几个兄弟,
同时报了警,做两手准备,钱可以不要,尊严一定要有,不能让技术显得这么廉价。,,,然而我想多了,事后第二天这人又打我电话表态与我和好,说自己是个傻子,让我把它当成个傻子,求我一定要把他这个东西做好,,,,
抛开其他不说,格局已经定了,这就是格局,小的很,虽然这人和其他两人创业的,但是格局决定了这个人以后的道路。
最后当然是把东西做完了,也就是上面的截图,交给了他,这人硬塞给了我1k,,,,1k,,,1k啊,尽管我早说了不要这钱了。
我就是想说明下,不论单子大小,不论生人熟人(我接这活的人是一个老乡),一定先见到钱,其他的都是扯淡,生意上,有钱老子跟你混,没钱少跟老子扯淡。更不要跟我谈什么亲戚朋友或者是狗屁老乡,我跟钱才是老乡。
其次是需求文档,无文档不开发,每次业务变更需求变动,白纸黑字,明明白白的写清楚,咱该加价加价,该加工期加工期。
WPF数据爬取小工具-某宝推广位批量生成,及订单爬取 记:接单最痛一次的感悟的更多相关文章
- 网页抓取小工具(IE法)
网页抓取小工具(IE法)—— 吴姐 http://club.excelhome.net/thread-1095707-1-1.html 用IE提取网页资料的好处在于:所见即所得,网页上能看到的信息一般 ...
- WPF开发查询加班小工具
先说一下,我们公司是六点下班,超过7点开始算加班,但是加班的时间是从六点开始计算,以0.5个小时为计数,就是你到了六点半,不算加班半小时,但是加班到七点半,就是加班了一个半小时. 一.打卡记录 首先, ...
- Winform数据导出Execl小工具
前台界面.cs文件 using System; using System.Collections.Generic; using System.ComponentModel; using System. ...
- c 小工具的使用
1. 这是一个gps 数据过滤的小工具,目的是过滤到gps数据中不符合要求的数据,然后转为json 数据 需要两个小工具 bermuda.c ------> 过滤一定范围的数据 geo2j ...
- java小工具:通过URL连接爬取资源(图片)
java语言编写一个简单爬取网站图片工具,实现简单: 通过 java.net.HttpURLConnection 获取一个URL连接 HttpURLConnection 连接成功返回一个java.io ...
- WPF做的迁移文件小工具
客户这边需要往服务器上传PDF文件.然后PDF文件很多,需要挑出来的PDF文件也不少.因此做了个小工具. 功能很简单,选定源文件夹,选定记录着要提取的文件的excel 文件.OK ,界面如下. XAM ...
- 小白突破百度翻译反爬机制,33行Python代码实现汉译英小工具!
表弟17岁就没读书了,在我家呆了差不多一年吧. 呆的前几个月,每天上网打游戏,我又不好怎么在言语上管教他,就琢磨着看他要不要跟我学习Python编程.他开始问我Python编程什么?我打开了我给学生上 ...
- python爬虫爬取京东、淘宝、苏宁上华为P20购买评论
爬虫爬取京东.淘宝.苏宁上华为P20购买评论 1.使用软件 Anaconda3 2.代码截图 三个网站代码大同小异,因此只展示一个 3.结果(部分) 京东 淘宝 苏宁 4.分析 这三个网站上的评论数据 ...
- 用Python写一个向数据库填充数据的小工具
一. 背景 公司又要做一个新项目,是一个合作型项目,我们公司出web展示服务,合作伙伴线下提供展示数据. 而且本次项目是数据统计展示为主要功能,并没有研发对应的数据接入接口,所有展示数据源均来自数据库 ...
随机推荐
- ES--01
ES概念: 垂直搜索(站内搜索) 什么是全文检索和Lucene? 1 全文检索 倒排索引 2 Lucene 就是一个jar包 里面包含了封装好的各种简历倒排索引 以及进行搜索的代码 包括各种算法 我们 ...
- $Django 聚合函数、分组查询、F,Q查询、orm字段以及参数
一.聚合函数 from django.db.models import Avg,Sum,Max,Min,Count,F,Q #导入 # .查询图书的总价,平均价,最大价,最小价 ...
- python获取esxi的磁盘使用率信息
#!/usr/bin/python3 #coding:utf-8 #Author: ziming """ 只用于模拟开发功能测试 """ f ...
- centos7 docker使用https_proxy 代理配置
centos7 docker使用https_proxy 代理配置 背景: 内网的centos主机不能上网,通过同网段的windows设置代理上网,yum.conf配置http代理是可以的,但是dock ...
- 【原创】大数据基础之Hive(4)hive元数据库核心表结构
1 dbs +-------+-----------------------+----------------------------------------------+------------+- ...
- 面向对象(metaclass继承高级用法)
方法一:# class MyType(type):# def __init__(self,*args,**kwargs):# print('132')# super(MyType,self).__in ...
- 洛谷P4707 重返现世 [DP,min-max容斥]
传送门 前置知识 做这题前,您需要认识这个式子: \[ kthmax(S)=\sum_{\varnothing\neq T\subseteq S}{|T|-1\choose k-1} (-1)^{|T ...
- Confluence 6 跟踪你安装中的自定义修改
在 Confluence 中的系统信息(System Information)部分,有一个 修改(Modification)的选项.在这个选项中列出了自你 Confluence 安装以来,你 Conf ...
- OC对象本质
@interface person:NSObject{ @public int _age; } @end @implementation person @end @interface student: ...
- python之vscode配置开发调试环境
在vscode中下载python插件,下载量最多的就是 打开launch.json,把以下代码粘贴进去即可 { // 使用 IntelliSense 了解相关属性. // 悬停以查看现有属性的描述. ...