在前面的水文中,一方面,老周向各位同学介绍了通过 TCP 连接来访问 MPD 服务;另一方面,也简单演示了 ASP.NET Core 的“极简 API”(Mini API)。本篇老周就简单说一下如何用 Mini API 来封装对 MPD 服务器的访问。内容仅供参考,也许你会想到更好的方案。

你可能会问:老周,你这个懒B,上次写完之后,咋等了这么久才写这一篇?实不相瞒,因为遇到问题了……这问题主要出在了“add”命令上。

这个命令的功能是把某一曲目添加到当前播放列表中(不管你是否加载以前保存的列表,总之就是当前正在用的播放列表),其格式为:

add <音频文件URL>

还记得前面的内容乎?咱们在配置 MPD 时,会指定一个专门放置音乐文件的目录,因此,这个音频URL一般使用相对路径,即相对音乐目录的相对路径。

比如,你配置的音乐目录是 /home/pi/mpd/music,然后,你在 music 目录下放了一个子目录叫“装逼2021全新专辑”,里面有三个文件,结构大致如下:

装逼2021全新专辑
|-- 千年装逼魂.wav
|-- 每天装一逼.wav
|-- 装逼的人儿真无奈.wav

即,“千年装逼魂.wav”的全路径是 /home/pi/mpd/music/装逼2021全新专辑/千年装逼魂.wav,但是,使用 add 命令时,只使用相对路径即可,相对于音乐目录。

add "装逼2021全新专辑/千年装逼魂.wav"

URL最好加上双引号,因为路径中带有空格的概率很高。

那么,老周遇到的问题是啥?因为这个 add 命令会引用音频文件路径,这文本中避免不了会出现汉字字符。说到这里你估计明白了,对的,让人头痛的老问题——文本编码问题。有汉字字符就不能使用 ASCII 编码了,但显式使用 UTF-8 编码也不行,经多次尝试,还是报错。

终于,被老周测出一个完美解决方法——直接使用 Encoding.Default,让运行时自动使用与系统一致的编码。真 TM 没想到,这一招居然把所有问题全解决了,不再报错了。果然,默认的是最使的。

----------------------------------------------------------------------------------------------------

既然问题解决了,那么这篇水文就能写了。

为了方便操作,咱们不妨先单独封装一个类,这个类专用来与 MPD 服务进程通信。现在我把整个类的代码先放出来,然后老周再说一下核心部分。

namespace MpcApi
{
using System;
using System.IO;
using System.Net;
using System.Collections.ObjectModel;
using System.Net.Sockets;
using static System.Text.Encoding;
using System.Text; internal class MPDTCPClient : IDisposable
{
const string LOCAL_HOST = "localhost"; // 本机地址
const int LOCAL_PORT = 6600; // 默认端口 TcpClient _client; /// <summary>
/// 构造函数
/// </summary>
public MPDTCPClient()
{
_client = new TcpClient(LOCAL_HOST, LOCAL_PORT);
// 判断MPD服务器是否有应答
using StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: UTF8,
leaveOpen: true
); string resp = sr.ReadLine();
if (resp == null || !resp.StartsWith("OK MPD"))
{
throw new Exception("服务器未正确响应");
}
} public void Dispose()
{
_client?.Close();
} private TextReader SendCommand(string cmd)
{
StreamWriter wr = new(
stream: _client.GetStream(),
encoding: Default,
leaveOpen: true);
wr.NewLine = "\n"; //换行符避免出现“\r\n”
// 写命令
wr.WriteLine(cmd);
wr.Flush();
wr.Dispose();
// 读响应
StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: Default,
leaveOpen: true);
return sr; //留给其他方法进一步处理
} #region 以下方法为公共成员
/*
* 为了用起来方便,封装一下
*/ /// <summary>
/// 获取可用命令
/// </summary>
public async Task<IReadOnlyList<string>> GetAvalidCommands()
{
List<string> files = new();
using TextReader reader = SendCommand("commands");
string msg = await reader.ReadLineAsync();
while (msg != null && msg != "OK")
{
files.Add(msg);
msg = await reader.ReadLineAsync();
}
return new ReadOnlyCollection<string>(files);
} /// <summary>
/// 获取所有歌曲列表
/// </summary>
public async Task<IReadOnlyList<string>> GetAllSongs()
{
List<string> list = new();
using TextReader reader = SendCommand("listall");
string line = await reader.ReadLineAsync();
while(line != null && line != "OK")
{
// 这里我们只需要文件,不需要目录
if (line.StartsWith("file:"))
{
list.Add(line);
}
line = await reader.ReadLineAsync();
}
return new ReadOnlyCollection<string>(list);
} /// <summary>
/// 播放(指定曲目)
/// </summary>
/// <param name="n">曲目编号,-1表示省略</param>
/// <returns>true:成功;否则失败S</returns>
public async Task<bool> Play(int n = -1)
{
string c = "play";
if(n >= 0)
{
c += $" {n}";
}
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 暂停
/// </summary>
/// <returns></returns>
public async Task<bool> Pause()
{
using TextReader reader = SendCommand("pause");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 下一首
/// </summary>
/// <returns></returns>
public async Task<bool> Next()
{
using TextReader reader = SendCommand("next");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 上一首
/// </summary>
/// <returns></returns>
public async Task<bool> Previous()
{
using TextReader reader = SendCommand("previous");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 停止播放
/// </summary>
/// <returns></returns>
public async Task<bool> Stop()
{
using TextReader reader = SendCommand("stop");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 设置音量
/// </summary>
/// <param name="v">音量值,可以为正负值</param>
/// <returns></returns>
public async Task<bool> SetVolume(string v)
{
string c = $"volume {v}";
using TextReader reader = SendCommand(c);
if(await reader.ReadLineAsync() == "OK")
{
return true;
}
return false;
} /// <summary>
/// 显示播放列表中的曲目
/// </summary>
/// <returns></returns>
public async Task<IReadOnlyList<string>> ShowPlaylist()
{
string c = "playlist";
using TextReader reader = SendCommand(c);
string msg = await reader.ReadLineAsync();
List<string> items = new();
while(msg != null && msg != "OK")
{
items.Add(msg);
msg = await reader.ReadLineAsync();
}
return new ReadOnlyCollection<string>(items);
} /// <summary>
/// 清空当前正在播放的列表
/// </summary>
/// <returns></returns>
public async Task<bool> ClearList()
{
using TextReader reader = SendCommand("clear");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 加载以前保存的播放列表
/// </summary>
/// <param name="lsname">播放列表的名称</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> LoadList(string lsname)
{
if (string.IsNullOrWhiteSpace(lsname))
throw new Exception("列表名称无效"); // 列表名称一定要有效
string c = $"load {lsname}";
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 将当前播放列表保存
/// </summary>
/// <param name="newname">新列表的名称</param>
/// <returns></returns>
public async Task<bool> SaveList(string newname)
{
if (string.IsNullOrWhiteSpace(newname))
throw new Exception("新列表名无效");
string cmd = $"save {newname}";
using TextReader rd = SendCommand(cmd);
if (await rd.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 删除播放列表
/// </summary>
/// <param name="lsname">要删除的播放列表名称</param>
/// <returns></returns>
public async Task<bool> DeleteList(string lsname)
{
if(string.IsNullOrWhiteSpace(lsname))
{
throw new Exception("播放列表名称是必要参数");
}
using TextReader reader = SendCommand($"rm {lsname}");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 将歌曲添加到当前播放列表
/// </summary>
/// <param name="url">歌曲URL</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<bool> AddToList(string url)
{
if (url == null)
throw new Exception("URL无效");
using TextReader rd = SendCommand($"add {url}");
if (await rd.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 获取正在播放的曲目
/// </summary>
/// <returns></returns>
public async Task<IReadOnlyList<string>> GetCurrent()
{
List<string> results = new();
using TextReader rd = SendCommand("currentsong");
string line = await rd.ReadLineAsync();
while (line != null && line != "OK")
{
results.Add(line);
line = await rd.ReadLineAsync();
}
return new ReadOnlyCollection<string>(results);
}
#endregion
}
}

我这个类并没有实现所有的命令,只包装了常用的命令,你只要明白其原理后,你自己也可以扩展。

对了,这里得纠正一点:老周在前面的文章中演示TCP协议访问 MPD,是直接发送文本的。由于前面我演示的只有 listall 一个命令,所以在连接 MPD 服务器后就马上发送 listall 命令,然后就接收服务器回应。上次老周说的是:服务器先回复了一句 OK + MPD 版本号,再发文件列表,最后一句 OK。

其实这里老周弄错了,MPD 服务器回复的第一句 OK + MPD版本号并不是响应 listall 命令的,而是当客户端与它建立TCP连接成功后就马上回复的,所以,MPD 对 listall 命令的回复的文件列表 + OK。

所以,再回过头来看刚刚那个类,在构造函数中,我让 TcpClient 对象连接MPD服务(服务器在本机)。

// new 之后会自动调用 Connect 方法请求连接
_client = new TcpClient(LOCAL_HOST, LOCAL_PORT);
// 判断MPD服务器是否有应答
using StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: UTF8,
leaveOpen: true
); // 一旦连接成功,MPD 会马上回你一句“OK MPD <版本号>”
// 只要判断“OK MPD”开头就行,版本号可以不管它,这里我们不关心
string resp = sr.ReadLine();
if (resp == null || !resp.StartsWith("OK MPD"))
{
throw new Exception("服务器未正确响应");
}

另一个核心方法是 SendCommand,它的功能是向 MPD 服务器发送命令,然后返回一个 TextReader 对象,这个 reader 可以读取 MPD 服务器的响应消息。

        private TextReader SendCommand(string cmd)
{
StreamWriter wr = new(
stream: _client.GetStream(),
encoding: Default,//默认编码能解万般忧愁
leaveOpen: true);
wr.NewLine = "\n"; //换行符避免出现“\r\n”
// 写命令
wr.WriteLine(cmd);
wr.Flush(); //一定要这句,不然不会发送
wr.Dispose();
// 读响应
StreamReader sr = new StreamReader(
stream: _client.GetStream(),
encoding: Default,//默认编码
leaveOpen: true);
return sr; //留给其他方法进一步处理
}

接着,各种控制方法都是调用这个方法与 MPD 服务器难信,封装后对外公开。

        /// <summary>
/// 获取所有歌曲列表
/// </summary>
public async Task<IReadOnlyList<string>> GetAllSongs()
{
List<string> list = new();
using TextReader reader = SendCommand("listall");
string line = await reader.ReadLineAsync();
while(line != null && line != "OK")
{
// 这里我们只需要文件,不需要目录
if (line.StartsWith("file:"))
{
list.Add(line);
}
line = await reader.ReadLineAsync();
}
return new ReadOnlyCollection<string>(list);
} /// <summary>
/// 播放(指定曲目)
/// </summary>
/// <param name="n">曲目编号,-1表示省略</param>
/// <returns>true:成功;否则失败S</returns>
public async Task<bool> Play(int n = -1)
{
string c = "play";
if(n >= 0)
{
c += $" {n}";
}
using TextReader reader = SendCommand(c);
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
} /// <summary>
/// 暂停
/// </summary>
/// <returns></returns>
public async Task<bool> Pause()
{
using TextReader reader = SendCommand("pause");
if (await reader.ReadLineAsync() == "OK")
return true;
return false;
}
…………

这个 MPDTCPClient 封装类在实例化时建立连接,在释放/清理时关闭连接。接着我们把这个类注册为依赖注入服务,并且是短暂实例模式(每次注入时都实例化,用完就释放),这可以避免 TCP 连接被长期占用导致环境污染。

var builder = WebApplication.CreateBuilder(args);

// 添加服务
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTransient<MPDTCPClient>();
builder.WebHost.UseUrls("http://*:888", "http://*:886"); var app = builder.Build();

接下来,咱们就可以使用 MapXXX 扩展方法来定义 Mini API。

/**     列出所有可用命令    **/
app.MapGet("/commands", async (MPDTCPClient client) =>
{
return await client.GetAvalidCommands();
}); /** 列出所有歌曲 **/
app.MapGet("/listall", async (MPDTCPClient client) =>
{
return await client.GetAllSongs();
}); /** 列出某个播放列表中的曲目 **/
app.MapGet("/lsplaylist", async (MPDTCPClient client) =>
{
return await client.ShowPlaylist();
}); /** 添加到当前播放列表 */
app.MapPost("/add", async (string url, MPDTCPClient cl) =>
{
var res = await cl.AddToList(url);
return res ? Results.Ok() : Results.StatusCode(500);
}); /** 播放 **/
app.MapGet("/play", async (MPDTCPClient cl) =>
{
bool res = await cl.Play();
return res ? Results.Ok() : Results.StatusCode(500);
}); /** 暂停 **/
app.MapGet("/pause", async (MPDTCPClient client) =>
{
bool res = await client.Pause();
return res ? Results.Ok() : Results.StatusCode(500);
}); /** 停止播放 **/
app.MapGet("/stop", async (MPDTCPClient client) =>
{
bool r = await client.Stop();
return r ? Results.Ok() : Results.StatusCode(500);
}); /** 上一首 **/
app.MapGet("/prev", async (MPDTCPClient cl) =>
{
bool procres = await cl.Previous();
if (procres)
return Results.Ok();
return Results.StatusCode(500);
}); /** 下一首 **/
app.MapGet("/next", async (MPDTCPClient client) =>
{
return (await client.Next()) ? Results.Ok() : Results.StatusCode(500);
}); /** 设定音量 **/
app.MapPost("/setvol", async (string vol, MPDTCPClient client) =>
{
bool res = await client.SetVolume(vol);
return res ? Results.Ok() : Results.StatusCode(500);
}); /** 清空当前播放列表 **/
app.MapGet("/clear", async (MPDTCPClient cl) =>
{
return (await cl.ClearList()) ? Results.Ok() : Results.StatusCode(500);
}); /** 加载指定列表 **/
app.MapPost("/loadlist", async (string lsname, MPDTCPClient client) =>
{
bool r = await client.LoadList(lsname);
if (r)
return Results.Ok();
return Results.StatusCode(500);
}); /** 删除播放列表 **/
app.MapGet("/rmlist", async (string lsname, MPDTCPClient cl) =>
{
bool r = await cl.DeleteList(lsname);
return r ? Results.Ok() : Results.StatusCode(500);
}); /** 保存当前列表 **/
app.MapPost("/savelist", async (string listname, MPDTCPClient cl) =>
{
bool res = await cl.SaveList(listname);
return res ? Results.Ok() : Results.StatusCode(500);
});

这个 API 的基本套路就是:若成功执行,返回 200(OK);若执行失败,返回 500。

MapXXX 方法的第二个参数是一个【万能】委托对象,注意在定义委托时,需要一个 MPDTCPClient 类型的参数,这个参数会自动获取到依赖注入进来的对象引用。

大体就是这样,你可以根据需要,自行补充其他 MPD 命令的封装。

有了这个 API 的封装,实现 MPD 客户端就灵活多了,你可以做移动App,也可以做成 Web App,也可以做成桌面程序。反正你爱咋整就咋整,不管用啥做客户端程序,只要调用这些 Web API 即可。

最后,拿几个 API 测试一下。

先测一下列出所有命令的 API。

返回的结果如下:

[
"command: add",
"command: addid",
"command: addtagid",
"command: albumart",
"command: binarylimit",
"command: channels",
"command: clear",
"command: clearerror",
"command: cleartagid",
"command: close",
"command: commands",
"command: config",
"command: consume",
"command: count",
"command: crossfade",
"command: currentsong",
"command: decoders",
"command: delete",
"command: deleteid",
"command: delpartition",
"command: disableoutput",
"command: enableoutput",
"command: find",
"command: findadd",
"command: getfingerprint",
"command: idle",
"command: kill",
"command: list",
"command: listall",
"command: listallinfo",
"command: listfiles",
"command: listmounts",
"command: listpartitions",
"command: listplaylist",
"command: listplaylistinfo",
"command: listplaylists",
"command: load",
"command: lsinfo",
"command: mixrampdb",
"command: mixrampdelay",
"command: mount",
"command: move",
"command: moveid",
"command: moveoutput",
"command: newpartition",
"command: next",
"command: notcommands",
"command: outputs",
"command: outputset",
"command: partition",
"command: password",
"command: pause",
"command: ping",
"command: play",
"command: playid",
"command: playlist",
"command: playlistadd",
"command: playlistclear",
"command: playlistdelete",
"command: playlistfind",
"command: playlistid",
"command: playlistinfo",
"command: playlistmove",
"command: playlistsearch",
"command: plchanges",
"command: plchangesposid",
"command: previous",
"command: prio",
"command: prioid",
"command: random",
"command: rangeid",
"command: readcomments",
"command: readmessages",
"command: readpicture",
"command: rename",
"command: repeat",
"command: replay_gain_mode",
"command: replay_gain_status",
"command: rescan",
"command: rm",
"command: save",
"command: search",
"command: searchadd",
"command: searchaddpl",
"command: seek",
"command: seekcur",
"command: seekid",
"command: sendmessage",
"command: setvol",
"command: shuffle",
"command: single",
"command: stats",
"command: status",
"command: sticker",
"command: stop",
"command: subscribe",
"command: swap",
"command: swapid",
"command: tagtypes",
"command: toggleoutput",
"command: unmount",
"command: unsubscribe",
"command: update",
"command: urlhandlers",
"command: volume"
]

再测一下 listall 命令。

向当前播放列表中添加一首曲子,注意:MPD 服务返回的文件名是有“file: ”开头的,而咱们传递给 add 命令时,不需要"file:",直接用相对路径即可(建议加上双引号)。

再测试一下 playlist 接口,列出当前播放列表中的曲目。

返回的播放列表如下:

[
"0:file: 化蝶/1/卓依婷vs周伟杰 - 化蝶.wav",
"1:file: 我的中国心/张明敏 - 龙的传人.wav",
"2:file: 化蝶/1/卓依婷 - 花好月圆.wav"
]

“file:”前面的数字是曲目在播放列表中的位置,从 0 开始计算,这样一来,在使用 play 命令时就可以通过这个数字来指定要播放的曲目,比如要播放第二首(位置1)。

不过,刚才老周写的 play API是没有参数的,默认播放整个列表,咱们可以改一下。

app.MapGet("/play", async (int? pos, MPDTCPClient cl) =>
{
bool res = await cl.Play(pos ?? -1);
return res ? Results.Ok() : Results.StatusCode(500);
});

如果 pos 参数为 -1,表示从头播放整个列表。

现在,可以调用了,播放第二首曲子。

好了,今天的文章就水到这里了。预告一下,下一篇水文中,咱们玩玩 LED 彩色灯带。

【.NET 与树莓派】MPD 的 Mini-API 封装的更多相关文章

  1. C#开发微信门户及应用(32)--微信支付接入和API封装使用

    在微信的应用上,微信支付是一个比较有用的部分,但也是比较复杂的技术要点,在微商大行其道的年代,自己的商店没有增加微信支付好像也说不过去,微信支付旨在为广大微信用户及商户提供更优质的支付服务,微信的支付 ...

  2. 接口API封装中常见的HTTP状态码

    在进行后端接口API封装的过程中,需要考虑各种错误信息的输出.一般情况下,根据相应问题输出适合的HTTP状态码,可以方便前端快速定位错误,减少沟通成本. HTTP状态码有很多,每个都有对应的含义,下面 ...

  3. 阿里云人脸比对API封装

    这是根据封装是根据阿里云官方给的Demo进行修改的,当时是因为编写微信小程序云函数需要使用到阿里云人脸比对接口,才对其进行封装的. 记录下来,方便下次使用. 复制下来可以直接使用. 用到的依赖如下: ...

  4. poium测试库之JavaScript API封装原理

    poium一直我在维护的一个开源项目,它的定位是以极简的方式在自动化项目中Page Objects设计模式.我在之前的文章中也有介绍. 本篇文章主要介绍一个JavaScript元素操作的封装原理. 为 ...

  5. VBA/VB6/VBS/VB.NET/C#/Python/PowerShell都能调用的API封装库

    API函数很强大,但是声明的时候比较繁琐. 我开发的封装库,包括窗口.键盘.鼠标.消息等常用功能.用户不需要添加API函数的声明,就可以用到API的功能. 在VBA.VB6的引用对话框中引用API.t ...

  6. vue学习过程总结(07) - vue的后台服务API封装及跨域问题的解决

    以登录流程为例说明接口的封装. 1.登录调用后台的登录api 登录界面的代码 <template> <div class="login-page"> < ...

  7. PopupWindow 的常用api封装

    对PopupWindow常用API的简单封装,几行代码就搞定PopupWindow弹窗,使用Builder模式,链式调用,像使用AlertDialog 一样 封装通用PopupWindow,Custo ...

  8. 微信小程序api封装

    写多 之后,吸取之前的经验,瞎写了一个简单的封装api,有幸看到的朋友,就随便看看哈,如果能给到你帮助就一直棒了,额呵呵呵! 新建constant.js和api.js文件 在constant.js中统 ...

  9. 个人博客开发之blog-api项目统一结果集api封装

    前言 由于返回json api 格式接口,所以我们需要通过java bean封装一个统一数据返回格式,便于和前端约定交互, 状态码枚举ResultCode package cn.soboys.core ...

  10. 吐槽贴:百度地图 api 封装 的实用功能 [源码下载]

    ZMap 类 功能介绍 ZMap 是学习百度地图 api 接口,开发基本功能后整的一个脚本类,本类方法功能大多使用 prototype 原型 实现: 包含的功能有:轨迹回放,圈画区域可编辑,判断几个坐 ...

随机推荐

  1. struts2漏洞复现分析合集

    struts2漏洞复现合集 环境准备 tomcat安装 漏洞代码取自vulhub,使用idea进行远程调试 struts2远程调试 catalina.bat jpda start 开启debug模式, ...

  2. SyntaxError: Non-UTF-8 code starting with '\xbb' in file D:\流畅学python\ex32.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

    1. 报错如下: SyntaxError: Non-UTF-8 code starting with '\xd3' in file D:\流畅学python\ex34.py on line 4, bu ...

  3. 如何再一台电脑上配置多个tomcat同时运行

    1.配置运行tomcat 首先要配置java的jdk环境,这个就不在谢了  不懂去网上查查,这里主要介绍再jdk环境没配置好的情况下 如何配置运行多个tomcat 2.第一个tomcat: 找到&qu ...

  4. 一文读懂Android进程及TCP动态心跳保活

    一直以来,APP进程保活都是 各软件提供商 和 个人开发者 头疼的问题.毕竟一切的商业模式都建立在用户对APP的使用上,因此保证APP进程的唤醒,提升用户的使用时间,便是软件提供商和个人开发者的永恒追 ...

  5. word-break leetcoder C++

    Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separa ...

  6. Linux 显示ip、dns、网关等命令

    在新版的ubuntu 终端里输入命令nm-tool, 想查看网络参数设置, 没想到却返回如下内容:   未找到 'nm-tool' 命令,您要输入的是否是:  命令 'dm-tool' 来自于包 'l ...

  7. Linux网卡bond模式

    Bond模式 交换机配置 mode=0 balance-rr 轮询均衡模式 LACP mode on 强制链路聚合 mode=1 active-backup 主备模式 无 mode=2 balance ...

  8. Go 跳出 for-switch 和 for-select 代码块

    原文:https://segmentfault.com/a/1190000013739000 没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出 ...

  9. 【pycharm】Python pip升级及升级失败解决方案,报错:You are using pip version 10.0.1, however version 21.3.1 is available. You should consider upgrading via the 'python -m pip install --upgrade pip' command.

    我已经升级到了最新的版本 安装其他模块过程中出现下面提示,便说明你需要升级pip You are using pip version 10.0.1, however version 21.3.1 is ...

  10. robot framework 常用关键字介绍

    1.log 打印所有内容 log hello word 2.定义变量 ${a} Set variable 92 log ${a}   3.连接对象 ${a} Catenate hello word l ...