零、前言

Array starting at Zero ——XXX

0.0 什么是JSON?

想必进行过牌堆编写,或稍微阅读过牌堆内容的用户应该对JSON这个格式不陌生毕竟后缀都是.json了谁会想不到嘛
或者在Minecraft游戏里玩过命令方块指令的,可能也对此稍微有所了解。

引用

什么是 JSON ?

  • JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  • JSON 是轻量级的文本数据交换格式
  • JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。 目前非常多的动态(PHP,JSP,.NET)编程语言都支持JSON。
  • JSON 具有自我描述性,更易理解

在需要有格式地存取大量信息的场合下,使用JSON的存取会更为便捷。
JSON的内容通常以{}包含首尾,长得像下面这样:
{"xxx":"yyy"}
{"xxx":["yyy","yyy"]}
{"xxx":[{"yyy":"zzz","yyy":"zzz","a":1},{"yyy":"zzz","yyy":"zzz","a":0}]}
此类大量套娃的文本。
这样的内容就是JSON。

现在看起来可能没那么复杂,但俗话说,前略,三生万物……
……于是,如果是这样呢?!
不想开链接的可以看下面:

{"code":0,"message":"0","ttl":1,"data":{"bvid":"BV1tY411b7wE","aid":253310569,"videos":1,"tid":28,"tname":"原创音乐","copyright":1,"pic":"http://i0.hdslb.com/bfs/archive/9abc024367fa50676db5aa42427204cf7fe860f7.jpg","title":"【V.W.P】《言霊/言灵》 / V.W.P #6【Original MV】","pubdate":1642762809,"ctime":1642738257,"desc":"爱啊、化作言灵拯救世界。\nLove, save the world as our spirit of words.\n\nV.W.P 4th Digital Single ”言霊” (Spirit of Words) \n2022.1.21(00:00)streaming&download(日本时间)\n2022.1.21(20:00)Music Video公開(日本时间)\navailable on streaming services and to download on 2022.1.21 (00:00)(日本时间)\nmusic video will be released on 2022.1.21 (20:00)(日本时间)\n\nLyric \u0026 Music \u0026 Arranged:IORI KANZAKI \nSinging:V.W.P(KAF / RIM / HARUSARUHI / ISEKAIJOUCHO / KOKO)\n\nDirector:Kenji Kawasaki(eallin)\nCG Artist:Eiji Takada(eallin)/ Wataru Kami(eallin)\nCinematographer:Hidehiko Kobayashi\nMotion Graphics Designer:yujurealworks / muen\nTitle Design:ZUMA\nProducer: Hideyuki Negishi\n──\n V.W.P are…\n・花譜_KAF\n・理芽_RIM\n・春猿火_HARUSARUHI\n・ヰ世界情緒_ISEKAIJOUCHO\n・幸祜_KOKO\n\n花谱、理芽、春猿火、异世界情绪、幸祜。\n五个具有电子头脑的魔女集结,开启新的故事篇章。\n从神椿诞生的命运相连的少女五人编织出的魔女现象。\n最强虚拟歌手组合「V.W.P」的活动必会引人瞩目。\nWe are V.W.P!!\n──\n■V.W.P\nhttps://weibo.com//7573179727","desc_v2":[{"raw_text":"爱啊、化作言灵拯救世界。\nLove, save the world as our spirit of words.\n\nV.W.P 4th Digital Single ”言霊” (Spirit of Words) \n2022.1.21(00:00)streaming&download(日本时间)\n2022.1.21(20:00)Music Video公開(日本时间)\navailable on streaming services and to download on 2022.1.21 (00:00)(日本时间)\nmusic video will be released on 2022.1.21 (20:00)(日本时间)\n\nLyric \u0026 Music \u0026 Arranged:IORI KANZAKI \nSinging:V.W.P(KAF / RIM / HARUSARUHI / ISEKAIJOUCHO / KOKO)\n\nDirector:Kenji Kawasaki(eallin)\nCG Artist:Eiji Takada(eallin)/ Wataru Kami(eallin)\nCinematographer:Hidehiko Kobayashi\nMotion Graphics Designer:yujurealworks / muen\nTitle Design:ZUMA\nProducer: Hideyuki Negishi\n──\n V.W.P are…\n・花譜_KAF\n・理芽_RIM\n・春猿火_HARUSARUHI\n・ヰ世界情緒_ISEKAIJOUCHO\n・幸祜_KOKO\n\n花谱、理芽、春猿火、异世界情绪、幸祜。\n五个具有电子头脑的魔女集结,开启新的故事篇章。\n从神椿诞生的命运相连的少女五人编织出的魔女现象。\n最强虚拟歌手组合「V.W.P」的活动必会引人瞩目。\nWe are V.W.P!!\n──\n■V.W.P\nhttps://weibo.com//7573179727","type":1,"biz_id":0}],"state":0,"duration":284,"mission_id":286690,"rights":{"bp":0,"elec":0,"download":1,"movie":0,"pay":0,"hd5":1,"no_reprint":1,"autoplay":1,"ugc_pay":0,"is_cooperation":1,"ugc_pay_preview":0,"no_background":0,"clean_mode":0,"is_stein_gate":0,"is_360":0,"no_share":0},"owner":{"mid":1636327445,"name":"VWP_虚拟魔女现象","face":"http://i2.hdslb.com/bfs/face/2c9dceb2fae106869d07cc6b4d77ae8d82330991.jpg"},"stat":{"aid":253310569,"view":70231,"danmaku":352,"reply":440,"favorite":6131,"coin":7292,"share":1307,"now_rank":0,"his_rank":0,"like":9244,"dislike":0,"evaluation":"","argue_msg":""},"dynamic":"《言霊/言灵》 / V.W.P 6【Original MV】#V.W.P##神椿工作室##言灵##花谱##理芽##春猿火##异世界情绪##幸祜#","cid":488882231,"dimension":{"width":1920,"height":1080,"rotate":0},"no_cache":false,"pages":[{"cid":488882231,"page":1,"from":"vupload","part":"言霊_220117_zhang","duration":284,"vid":"","weblink":"","dimension":{"width":1920,"height":1080,"rotate":0},"first_frame":"http://i0.hdslb.com/bfs/storyff/n220121a23b3hrqie384w81n9hjh1lj2_firsti.jpg"}],"subtitle":{"allow_submit":false,"list":[]},"staff":[{"mid":1636327445,"title":"UP主","name":"VWP_虚拟魔女现象","face":"http://i2.hdslb.com/bfs/face/2c9dceb2fae106869d07cc6b4d77ae8d82330991.jpg","vip":{"type":2,"status":1,"due_date":1647360000000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"年度大会员","label_theme":"annual_vip","text_color":"#FFFFFF","bg_style":1,"bg_color":"#FB7299","border_color":""},"avatar_subscript":1,"nickname_color":"#FB7299","role":3,"avatar_subscript_url":"http://i0.hdslb.com/bfs/vip/icon_Certification_big_member_22_3x.png"},"official":{"role":0,"title":"","desc":"","type":-1},"follower":26034,"label_style":0},{"mid":488970166,"title":"演唱","name":"花谱_kaf","face":"http://i1.hdslb.com/bfs/face/c0f213f5d06967bc74327a7d8886953013b8ffc0.jpg","vip":{"type":2,"status":1,"due_date":1673539200000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"年度大会员","label_theme":"annual_vip","text_color":"#FFFFFF","bg_style":1,"bg_color":"#FB7299","border_color":""},"avatar_subscript":1,"nickname_color":"#FB7299","role":3,"avatar_subscript_url":"http://i0.hdslb.com/bfs/vip/icon_Certification_big_member_22_3x.png"},"official":{"role":1,"title":"bilibili 知名音乐UP主","desc":"","type":0},"follower":210224,"label_style":0},{"mid":489046950,"title":"演唱","name":"理芽_RIM","face":"http://i0.hdslb.com/bfs/face/ab13251a9a58c35f620cae3794a79783d26fd7a1.jpg","vip":{"type":1,"status":0,"due_date":1643817600000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"","label_theme":"","text_color":"","bg_style":0,"bg_color":"","border_color":""},"avatar_subscript":0,"nickname_color":"","role":0,"avatar_subscript_url":""},"official":{"role":0,"title":"","desc":"","type":-1},"follower":76031,"label_style":0},{"mid":488978908,"title":"演唱","name":"异世界情绪","face":"http://i1.hdslb.com/bfs/face/5c0c7cb579ce12259d8262fbe4548a40c4df5898.jpg","vip":{"type":1,"status":1,"due_date":1652112000000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"大会员","label_theme":"vip","text_color":"#FFFFFF","bg_style":1,"bg_color":"#FB7299","border_color":""},"avatar_subscript":1,"nickname_color":"","role":1,"avatar_subscript_url":"http://i0.hdslb.com/bfs/vip/icon_Certification_big_member_22_3x.png"},"official":{"role":0,"title":"","desc":"","type":-1},"follower":84127,"label_style":0},{"mid":488976992,"title":"演唱","name":"春猿火_harusaruhi","face":"http://i2.hdslb.com/bfs/face/7207fd5f8c86ce0d2af96b052ed59fa8088ba92f.jpg","vip":{"type":1,"status":0,"due_date":1643817600000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"","label_theme":"","text_color":"","bg_style":0,"bg_color":"","border_color":""},"avatar_subscript":0,"nickname_color":"","role":0,"avatar_subscript_url":""},"official":{"role":0,"title":"","desc":"","type":-1},"follower":53241,"label_style":0},{"mid":701522855,"title":"演唱","name":"幸祜-koko-","face":"http://i1.hdslb.com/bfs/face/3b07090bdaa7809b1632c78de94c86980889af0a.jpg","vip":{"type":2,"status":1,"due_date":1666972800000,"vip_pay_type":0,"theme_type":0,"label":{"path":"","text":"年度大会员","label_theme":"annual_vip","text_color":"#FFFFFF","bg_style":1,"bg_color":"#FB7299","border_color":""},"avatar_subscript":1,"nickname_color":"#FB7299","role":3,"avatar_subscript_url":"http://i0.hdslb.com/bfs/vip/icon_Certification_big_member_22_3x.png"},"official":{"role":0,"title":"","desc":"","type":-1},"follower":73684,"label_style":0}],"is_season_display":false,"user_garb":{"url_image_ani_cut":""},"honor_reply":{}}}

(我**太长了6000多字)←第一次用字数统计看这玩意
这就是利用bilibili视频获取器中调用api所得返回值的全貌,我特地挑了一个联合投稿的视频让值丰富一点。
没错,一眼看上去非常复杂,这也是我想写这个教程的原因之一。
这仍然是JSON,人脑已经很难迅速理解了,当然你可以使用Notepad++、Visual Studio Code等编程软件对其手动进行格式化以便于分析及获取信息,但到这种程度了手工分拣费神费力,还是老老实实用工具吧。

0.1 json.lua是什么?(附文件下载)

如果需要在lua中对json格式进行编写或分析,就必须使用第三方库。
一种方法是下载lua-cjson库并编译,但这种方法并非通用,门槛略高并且不符合我的初衷XD
毕竟我原先也是MiraiAndroid使用者,还是华为,手机装库基本等于痴人说梦。
因此这里推荐不需要编译的json.lua,文件已附于本回复末尾。

你可以将该压缩包解压出的文件直接放在骰娘根目录的DiceQQ\plugin,或者2.6.1及以上也可以放在Diceki\lua
某些群友反馈放在plugin无法读取,如果不放心的话可以两边都放一份

DiceQQ是指类似于Dice1234567890的文件夹不是真的叫DiceQQ的文件夹
不可以放在根目录的plugins,那边甚至不是Dice项目的范围
等等我这不是面向有经验的骰主写的教程吗
jsonlua.zip
3kB

已有json.lua的不必重复下载。

0.2 我应该如何使用json.lua?我能用它干什么?

现在你已经有了json.lua库,可以开始使用它了!
确保自己的库已经安装完毕,在所需的函数中插入这样一行:
j = require("json")
这样就为名为j的变量赋予了json库中的编码与解码函数,当然这个变量的名字可以任意,只要自己看得懂就行。
想要编码时,就使用j.encode(text)
想要解码时,就使用j.decode(text)
*这里的text为一个存储数据的变量名,在decode时应为一个字符串(string),在encode时应为一个(table)。在后续的相应章节将详细阐释。

 

“但是你说了这么多也不告诉我我为什么要学这个!”

使用JSON配合文件I/O来存取数据,比起直接存储数据,更加整齐且灵活,代码的存取遵循统一规则,无法被用户从外部攻破,并且可以辅助脚本让内容更为生动有趣……
你可以通过它同时记录多个参数,同时摆脱了setUserToday只能记录数字值的桎梏,想写什么写什么。包括之前论坛中有人提出过但未实现的故事接龙,总之JSON就是很香啦。

不过,我之所以写这个,另一方面还是出于有不止一个友人需要用到这个教程。
那我怎么好意思拒绝嘛~

*更正:SetUserConf可记录string值,是我记错了。在重新浏览链接内容并确认后改为setUserToday

Lily Black 将标题更改为 「【教程】json.lua第三方库的下载及使用」。

一、JSON的decode解码

无论你是用http.get(url)获取网络api、还是用文件io.read(path)读取文件内的文本,你读到的文本最原始的状态都只是一串大量套娃的普通的字符串。只有在decode后才能变成易于理解的模样。

1.0 原理介绍

Shiki的 【功能介绍】Dice!2.6.1的简易http访问函数&复杂图库接口的调用 中的例子:

图库接口实例

并不是所有随机图库都提供url直接随机跳转,有的图库返回json来提供图片url,以接口https://www.dmoe.cc/random.php?return=json为例,返回的数据格式如下:

{
"code":"200",
"imgurl":"https:\/\/tva2.sinaimg.cn\/large\/0072Vf1pgy1foxlhv1sxmj31hc0u07i9.jpg",
"width":"1920",
"height":"1080"
}

这就需要在lua中取到json并在解析后提取"imgurl"

res,info=http.get("https://www.dmoe.cc/random.php?return=json")
json = require "cjson" --调用第三方库
j = json.decode(info)
return "[CQ:image,url="..j.imgurl.."]"

(其实访问后会发现返回的是压缩的json,这里Shiki已经将其格式化了)

这就是一个比较简单的JSON实例,上述内容可以视为该JSON含有4个键(key),分别名为code imgurl width height,每个key储存的值(value)都是一个字符串。
逐行解释Shiki的代码,是这样的:

  1. 调用在2.6.1版本中预置的http第三方库,获取网址的返回值并储存在resinfo两个变量中(前者储存网页访问成功与否的布尔值,这里不会用到。后者储存访问网页后获得的数据,即上述JSON实例)
  2. 调用cjson第三方库,将其中的代码赋值给json这个变量。本教程使用的是json.lua,所以实际调用时需要改为require("json")
  3. info变量进行JSON解析,将解析后的内容存储在变量j中,变量j会变为一个表(table)
  4. 返回图片,其中图片网址为表j中索引为imgurl一项所对应的字符串

其中,j在第三行解析后,会成为一个存在四个索引的表,索引名就是JSON数据中存在的四个key。
以键code为例,需要调用该项值时使用j["code"]j.code皆可,推荐较为简单直观的后者,同时也能与数组进行良好的区分。

1.1 JSON对象与语法

因为教程是面向对JSON略有理解的用户,所以顺序略微靠后了。

对象语法 <来源>

实例:{ "name":"runoob", "alexa":10000, "site":null }
JSON 对象使用在大括号{}中书写。
对象可以包含多个 key/value(键/值)对。
key 必须是字符串,value 可以是合法的 JSON 数据类型(字符串, 数字, 对象, 数组, 布尔值或 null)。
key 和 value 中使用冒号:分割。
每个 key/value 对使用逗号,分割。

你看JSON选对象的标准这么高难怪我没对象(?
需要注意的是,这里的JSON对象实例,也就是一整个以{}括起的,是一个对象。
对象里还能包含多个对象,每个对象里还能包含多个对象,子子孙孙无穷匮也……
但我还是没对象

在JSON解析中,对象类型并非需要特别关注的点,因此这里将暂时跳过部分value中数据类型的名词解析,只讲重点的几项内容:
对象:即形同实例中的内容。在对象中嵌套一个对象,表现形式为{"xxx":{略}}
数组:只会是一维数组。为被[]中括号括起的内容,表现形式为{"xxx":[{略},{略}]}
null:如果在Lua中调用了这个数据,将会返回nil。(但编码时若使用nil会连相应索引一起消失)
(其余的名词解析可参考这里

JSON 语法规则 <来源>

JSON 语法是 JavaScript 对象表示语法的子集。

  • 数据在名称/值对中
  • 数据由逗号,分隔
  • 大括号{}保存对象
  • 中括号[]保存数组,数组可以包含多个对象

*对象中也可以包含0个键值对、数组中也可以包含0个对象,这都是合法的。若在工具中调试,其会分别显示为(empty object)(empty array)的形式。如果使用#xxtonumber(xx)获取空对象或空数组的数字,将会返回0。

进行过牌堆创作的作者应该对JSON语法并不陌生。同时你也可以参考JSON牌堆教程中的2.0.1 JSON的标准格式章节的示例牌堆,其中有整洁的JSON语法示例。

1.2 Lua表与数组

其实一开始写的时候没有这章节直接跳到了1.3,想着和操作相关的内容在encode部分再写也不迟,结果写完1.3.2立刻发现不得不先在这里将概念厘清,才特意插入这一章。但这里只作为与调用相关的概念性内容,对于表操作仍然会留到后面再写。
呜呜呜别骂了我逻辑超差的

JSON中的对象{}数组[],在转换为Lua可识别的内容后会分别对应数组
表即为形同abc["def"]abc.defabc[123]的字符(最后一种也可能是数组)
其中,abcdef 123索引。如果在调用abc.def后返回的值为xyz,则称xyz是“abc的索引为def的元素”。
一个元素也可能是一个新的表或数组,因此在调用时形同ab.cd.ef.gh(表的索引的索引的索引)的元素也是存在的。
表与数组相似,都需要索引才能调用,但区别在于数组只能以数字作为索引,而表可以用nil以外的一切作为索引。

Lua table(表)<来源>

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。
Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。
Lua table 是不固定大小的,你可以根据自己需要进行扩容。
Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用"format"来索引table string。

Lua 数组<来源>

数组,就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组和多维数组。
Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。

放在一起就可以看出区别了,不过由于JSON转换后的数组只会是一维数组,数组惨遭史诗级削弱,于是数组就沦为了缩水版的表。
虽说如此,我个人仍然强烈建议一切以数字为索引的列表还是好好地用数组来整理。

两者在调用上存在区别:

  • 表的调用通过aaa["bbb"]aaa.bbb均可,但数字索引不可以使用后者的形式
  • 数组的调用只能通过aaa[123]的形式

这两种调用方法也可以混用:
aaa["bbb"][1]["ccc"] 等于 aaa["bbb"][1].ccc 等于 aaa.bbb[1]["ccc"] 等于 aaa.bbb[1].ccc
但是,aaa.bbb.1.ccc是不行的,如果你想使用一个字符串格式的数字作为索引,必须老老实实地用["1"]这种形式

1.3 我应该怎么获取JSON中自己想要的部分?

虽然只要几行代码就可以将JSON数据完全获取,但我还是建议拥有一定的分析能力,这有利于快速找到指定内容。

1.3.1 分析JSON数据

在理解JSON语法后,就可以着手对一段JSON进行分析了。
先来一段简单的。

{"text":"喵"}

很明显,这段JSON数据只含一个键/值(key/value)对,其中key是text,value是,存储格式是字符串(string)。
value的几种数据类型(字符串, 数字, 对象, 数组, 布尔值, null)中,只有字符串是用""英文双引号括起的,可以通过这一点辨认。

下面这段内容为我的私人脚本存储的用户留言。
(脚本还在测试阶段,由于特殊设置只有我自己的QQ号可以调用,故qqname全部相同。)

{"usagi":[{"qq":"1142145792","message":"喵喵喵","times":1,"name":"兔兔零号机"},{"qq":"1142145792","message":"汪汪汪","times":1,"name":"兔兔零号机"},{"qq":"1142145792","name":"兔兔零号机","times":0,"message":"[图片]"}]}

你也可以将它格式化以便于辨识:

{
  "usagi": [
    {
      "qq": "1142145792",
      "message": "喵喵喵",
      "times": 1,
      "name": "兔兔零号机"
    },
    {
      "qq": "1142145792",
      "message": "汪汪汪",
      "times": 1,
      "name": "兔兔零号机"
    },
    {
      "qq": "1142145792",
      "name": "兔兔零号机",
      "times": 0,
      "message": "[图片]"
    }
  ]
}

对这段内容进行分析,可以得知:

  1. 整个JSON包含一个名为usagi的键(key),对应的值是一个[]括起的数组。
  2. 这个数组中有多个对象,每个对象都含四个key,分别名为qq message timesname
  3. 每个对象中,除了times对应的值是数字(number)外,其余三个key对应的值都是字符串(string)。

可以发现其实实际应用中的JSON在格式上与牌堆有一定区别——通常牌堆不会在[]中再嵌套哪怕一个{}
有时候调用牌堆会有{牌堆名}这样的内容,但这其实是一整个字符串中的一部分,更何况{牌堆名}这种形式本身就不符合JSON的语法规则。

1.3.2 使用decode()进行解析

还记得1.2中的内容吗?来看看这段JSON数据:{"ab":{"cd":{"ef":{"gh":"ij"}}}}
——剧透,它在decode后可以通过调用ab.cd.ef.gh获得一个内容为ij的字符串。

作为这一段的测试,你可以复制1.01.3.1中任意一段代码,或是在2.6.1及以上版本参照此方法使用http.get()调取会返回JSON数据的网络api链接。(网址例:1/2,部分网址存在连接不稳定等问题,请自行斟酌)
更新:删除了挂掉的全部优客云api(疑似跑路)、修改了lolicon api的地址

下面会以一段同时存在空对象、空数组等内容的数据作为例子
(实际调用简单JSON时很少出现这种缺少秩序的状况,而是1.3.1中提到的形式。这段内容是人为作成的,教程仅针对此特定内容,故未设置预防报错的判定)

{"usagi":[{},{"name":"卡戎","qq":"0","text":"咕呃啊啊啊啊啊啊啊啊"},{"name":"兔兔","qq":"1142145792","text":{"message1":"卡戎说得对","message2":"我举双手赞成"}},{"name":"不愿透露姓名的围观群众","qq":[],"text":[{"message":"太精彩了","repeats":114},{"message":"听君一席话少读十年书","repeats":514}]}]}

但是,这段JSON数据是无法直接写入括号内的,因为它还不是一段字符串。
但最常用的表示字符串的""双引号,由于其中已经含有大量的双引号,又会失效,这时候怎么办捏——
答案是lua的字符串表示方法不止一种,而另外两种''[[]]都容易被忽略。这里只能使用后者。
个人感觉[[]]最不容易出错,但很多时候出于便捷性与习惯考虑仍然会使用""

abc = [[内容太长了会看不见右边的括号,你就当我把上面那块代码都粘贴在这里了。]]

 
或者使用http.get()的可以参考如下:

aaa , abc = http.get("https://api.lolicon.app/setu/v2")

*第一个变量用于存储链接访问成功与否的布尔值,第二个才会存储链接返回的内容。故这里只需要使用到第二个变量。

无论你使用哪种方法,到这一步都才刚刚将JSON数据作为字符串存储进一个变量里。

下一步,你需要调用json.lua,将其中的函数“赋值”给一个参数。
推荐将该参数直接命名为json,这样就不容易忘记了。

json = require("json")

require函数会遍历大量目录下的.lua.dll文件,其中包括先前提到的DiceQQ\plugin\Diceki\lua\,在找到指定名称的文件后,会将其中的函数都写入这个名叫json的参数中。

现在你拥有了有能力将JSON数据解码的函数,下一步是将数据解码并作为表(table)存入一个变量中。

j = json.decode(abc)

同样地,这里的j也可以改为任何名字,只要记得住。
到这一步为止,解码就算是完成了——接下来就是调用的问题。

1.3.3 对解析出的内容进行调用

以上面提到的JSON数据为例:

{"usagi":[{},{"name":"卡戎","qq":"0","text":"咕呃啊啊啊啊啊啊啊啊"},{"name":"兔兔","qq":"1142145792","text":{"message1":"卡戎说得对","message2":"我举双手赞成"}},{"name":"不愿透露姓名的围观群众","qq":[],"text":[{"message":"太精彩了","repeats":114},{"message":"听君一席话少读十年书","repeats":514}]}]}

对这段数据进行decode并存入j中,会获得如下内容(这里逐级给出):

  • 一个表,名叫j,其中只有一个索引,为字符串usagi
  • 一个名为j.usagi的数组,其中含有四个表:
    ◆ 一个表,在j.usagi中索引为1,含有0个元素
    ◆ 一个表,在j.usagi中索引为2,含有3个元素,分别以name qq text为索引,都只含有一个字符串元素
    ◆ 一个表,在j.usagi中索引为3,含有3个元素,分别以name qq text为索引,其中j.usagi[3].text又是一个表,含有2个索引,分别为message1message2
    ◆ 一个表,在j.usagi中索引为4,含有3个元素,分别以name qq text为索引,其中name索引下只含一个字符串元素、索引qq是含有0个元素的数组、text索引下是一个数组,含有2个表:
      ◆ j.usagi[4].text[1]含有2个元素,索引分别是messagerepeats,其中message索引下是一个字符串,repeat索引下是一个数字
      ◆ j.usagi[4].text[2]同上

程序已经将上述内容写入相应的变量中。如果想要让骰娘返回一个数据,必须从上至下精确地定位到该数据所在的位置。
例如我想让骰娘返回其中的“卡戎”这个字符串,就必须输入:

return j.usagi[2].name

当然你也可以加 点 细 节

return j.usagi[2].name .."(" .. j.usagi[2].qq .. ")说:" .. j.usagi[2].text .. "\n" .. j.usagi[3].name .. "(" .. j.usagi[3].qq .. ")说:" .. j.usagi[3].text.message1 .. "\n还说了:" .. j.usagi[3].text.message2 .. "\n" .. j.usagi[4].name .. "说了" .. j.usagi[4].text[1].repeats .. "次这句话:" .. j.usagi[4].text[1].message .. "\n还说了" .. j.usagi[4].text[2].repeats .. "次这句话:" .. j.usagi[4].text[2].message

于是本章的内容,写成函数后全貌是这样的:

function test(msg)
	json = require("json")
	text = [[{"usagi":[{},{"name":"卡戎","qq":"0","text":"咕呃啊啊啊啊啊啊啊啊"},{"name":"兔兔","qq":"1142145792","text":{"message1":"卡戎说得对","message2":"我举双手赞成"}},{"name":"不愿透露姓名的围观群众","qq":[],"text":[{"message":"太精彩了","repeats":114},{"message":"听君一席话少读十年书","repeats":514}]}]}]]
	j = json.decode(text)
	return j.usagi[2].name .."(" .. j.usagi[2].qq .. ")说:" .. j.usagi[2].text .. "\n" .. j.usagi[3].name .. "(" .. j.usagi[3].qq .. ")说:" .. j.usagi[3].text.message1 .. "\n还说了:" .. j.usagi[3].text.message2 .. "\n" .. j.usagi[4].name .. "说了" .. j.usagi[4].text[1].repeats .. "次这句话:" .. j.usagi[4].text[1].message .. "\n还说了" .. j.usagi[4].text[2].repeats .. "次这句话:" .. j.usagi[4].text[2].message
end

在添加触发词后骰娘会返回这样一个结果:
(祈祷我下次卡戎boss战的时候不要被干碎)←更新:卡戎也太难打了吧

这是1.3.2、1.3.3中完成的lua脚本。如果需要进行测试,将该脚本解压加入plugin文件夹后重载骰娘并发送1.3.3测试,即可获得图中的文本。

luajson133.zip
727B

二、JSON的encode编码

区别于只需要字符串的decode()encode()需要输入的是表(table)类型的数据。但Lua脚本编写的绝大多数时间不会用到数组或表,因此这里将会从表操作开始讲起。

由于数组的史诗级削弱我决定把数组当作数字变量的表一起讲。
感觉这样会被打,所以还是不这么做好了。

2.1 Lua table(表)和array(数组)操作

区别于可以直接赋值的变量,表和数组在操作前必须进行一步初始化的操作。
如果将表和数组看作是盒子的话,这一步就是让程序知道你这里有个盒子。

假设这个表的名字为usagi

usagi = {}

msg_order = {}一模一样~

现在你就获得了一个名为usagi的空表,可以往里面写入索引元素了。
(可以理解为:JSON中的/对=Lua表中的索引元素

索引可以用字符串数字来表达,由于JSON的特性,两者不能混用。后者在编码时会被视为数组变量,同时,空表在编码时也会被视为数组变量。

现在你可以往这个盒子里填充一些内容了。

usagi["A"] = "abc"
usagi["B"] = "def"

当然,内容也可以是另一个盒子……只是记得和程序说一声。

usagi["C"] = {}

如果想把索引C对应的元素做成一个数组变量,只要这样做即可。

usagi["C"][1] = "123"
usagi["C"][2] = "456"

同时考虑到JSON与Lua时,元素中可用的数据类型:

  • 字符串(string)——用""[[]]括起的内容
  • 数字(number)——就是数字,不需要被括起。如果添加了括号,它将变为字符串并失去数字的特性(数学计算等)
  • 布尔值(boolean)——TRUE和FALSE
  • 另一个表(table)或数组(array)——然后每个表里面又可以套一个表,每个表里面又可以…(下略)
  • nil——不完全对应JSON中的null。表示一个无效值,较少用到,通常用来移除表中的值。

2.2 使用encode()进行编码

在自学时本段主要参考对象为Lua利用cjson读写json使用Lua CJSON库进行encode与decode操作完成对Json数据转化,特此表达感谢。
与使用decode()时相同,首先需要调用json.lua

json = require("json")

然后利用自己建好的表的名称,例如我在上一篇中使用的是usagi
并将其存储在一个变量中:

json_text = json.encode(usagi)

返回的内容是:{"A":"abc","B":"def","C":["123","456"]}
等等,好像有哪里不对!!
噔 噔 咚
很明显,我忘记多嵌套一层了……散落在外的键像极了我破碎的心
(这段不是节目效果)

是的,最外层的表的名字不会被写入,而是直接读取表的所有索引与对应元素,并写成JSON数据,并作为字符串输出。
因此如果需要让它在写进JSON时也多套一层娃,正确的写法是这样的:

j= {}
j["usagi"] = {}
j["usagi"]["A"] = "abc"
j["usagi"]["B"] = "def"
j["usagi"]["C"] = {}
j["usagi"]["C"][1] = "123"
j["usagi"]["C"][2] = "456"
json = require("json")
json_text = json.encode(j)
return json_text

这样返回的东西就都储存于键usagi的值里了:{"usagi":{"C":["123","456"],"B":"def","A":"abc"}}
JSON编码时会随机打乱顺序,但不影响文件读取与路径

2.3 编写表时需要注意的逻辑

在这一章节中我将以一个正在进行中的脚本为例,来解释一些容易碰到的问题。
这是一个我想要做的脚本,首先我需要将想要达成的要求列成一个表(略去了部分不需要存储的内容):

  • 想要可以收到留言。留言内容就跟在指令的后面
  • 收到留言的同时,还想获取用户的QQ号、所在群号等信息
  • 想要让这些留言可以在脚本执行时随机抽取其中一条并发送
  • 由于这个指令每天限1次,肯定抽取的比发出的多,所以想让留言被抽取到一定次数后再销毁,而非阅后即焚
  • 但是有些留言我也想永久保存,大不了通过后台改

再对需求表进行细化,确认至少需要以下这些键用来存储值:

  • 收到的留言
  • QQ号、群号、用户名、群名
  • 一整个容器,用来容纳这些内容并随机抽取
  • 一个用来存储留言被抽取次数的变量
  • 一个用来记录是否需要让留言永久保存的变量

2.3.1 我应该怎么选择元素的类型?

首先还是列一下元素:

同时考虑到JSON与Lua时,元素中可用的数据类型:<我 引 用 我 自 己>
  • 字符串(string)——用""[[]]括起的内容
  • 数字(number)——就是数字,不需要被括起。如果添加了括号,它将变为字符串并失去数字的特性(数学计算等)
  • 布尔值(boolean)——TRUE和FALSE
  • 另一个表(table)或数组(array)——然后每个表里面又可以套一个表,每个表里面又可以…(下略)
  • nil——不完全对应JSON中的null。表示一个无效值,较少用到,通常用来移除表中的值。

那么,很明显,留言、昵称、群名称,肯定都是字符串形式的。
其次,QQ号与群号,由于不需要进行数学运算,所以也是字符串形式的。(实际上这些内容,即msg.fromQQmsg.fromGroup,本来就是字符串)
一个需要随机调用的容器?用ranint(min,max)数组解决吧!使用数组+随机数字作为索引值,刚好符合要求。
存储留言被抽取多少次的变量,因为需要进行简单的数学运算,所以用数字
对于识别留言永久保存与否,虽然可以用字符串或者数字强行匹配,不过肯定是用布尔值最方便。

——这样就对目标变量有了一个简单的心理认知,可以进行下一步了。

*特别备注:ranint(min,max)是Dice!预置的Lua函数之一,并不普遍地存在于其他不使用Dice!框架运行的Lua脚本中。

2.3.2 我应该如何将想要输入的元素归类?

有很多种归类的方法,不过我这里的话,由于已经确定了数组作为主要内容,因此大体框架肯定是这样的:

{"random_name":[{被省略的元素1},{被省略的元素2},{被省略的元素3}]}

这样如果我使用如下的方式:

num = ranint(1,#random_name)
xxx = random_name[num].xxx

就可以很方便的访问数组并调用想要的值了。
其他的内容由于并没有特别的包含关系,所以一股脑地塞进元素里也是可以的。

分类通常遵循范围上从大到小的原则,无论是地理或是其他方面。
如果不嫌瞎眼的,可以参考新冠疫情信息,其中有明显的省级-地级的排序。
(这个api分享自タブー術的【指令脚本】以一个用http函数调用api示例脚本的做的新脚本,感谢主动分享!)
(另外,此api返回信息量极大,即使是放进格式化工具也会卡顿好一会。但这种情况下更加推荐此工具,你可以在右边的树形结构图中更直观地看到以地区为主的分类规则,位置是list[n].city[n])

同时你也可以遵循重要度从高到低的排法。
例如,我想写一个新的脚本,一个用户会有很多不同的发言,我也可以将这些发言都包括在以用户QQ号为键的值内。
*注意这种情况下最好给索引取类似于qq12345678的字符串,而非使用纯数字,以免出现意想不到的错误。

三、额外内容-关于文件I/O的写法

很多用户在使用带有require的脚本时出现了这样的错误:
aaa是我自己做的函数库名称。它真的叫aaa……)

骰娘运行lua文件G:\Dice - 副本\Dice3306860448\plugin\测试.lua失败:G:\Dice - 副本\Dice3306860448\plugin\测试.lua:3: module 'aaa' not found:
	no field package.preload['aaa']
	no file 'G:\Dice - 鍓湰\Dice3306860448\plugin\aaa.lua'
	no file 'G:\Dice - 鍓湰\Dice3306860448\plugin\aaa\init.lua'
	no file 'G:\Dice - 鍓湰\Diceki\lua\aaa.lua'
	no file 'G:\Dice - 鍓湰\Diceki\lua\aaa\init.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\lua\aaa.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\lua\aaa\init.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\aaa.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\aaa\init.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\..\share\lua\5.4\aaa.lua'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\..\share\lua\5.4\aaa\init.lua'
	no file '.\aaa.lua'
	no file '.\aaa\init.lua'
	no file 'G:\Dice - 鍓湰\Diceki\lua\aaa.dll'
	no file 'G:\Dice - 鍓湰\Diceki\lib\aaa.dll'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\aaa.dll'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\..\lib\lua\5.4\aaa.dll'
	no file 'C:\Program Files\Java\jdk-17.0.1\bin\loadall.dll'
	no file '.\aaa.dll'

或者是在有文件I/O的脚本中,出现这样的问题:

骰娘调用G:\Dice - 副本\Dice3306860448\plugin\DriftingBottles.lua函数throw_bottle失败!
G:\Dice - 副本\Dice3306860448\plugin\DriftingBottles.lua:103: attempt to index a nil value (global 'file')

查看用户手册可以发现这样一条内容。

lua文件的字符编码问题 <来源>

Windows系统一般使用GBK字符集。Dice!支持utf-8及GBK两种字符集的lua文件,在读写字符串时将自动检测utf-8编码并转换。而出现以下情况时,编码并非二者皆可:
 

  • lua文件相互调用或读写其他文本文件,且字符串含有非ASCII字符时,关联文件字符集应保持一致
  • lua文件使用require或os等以文件名为参数的函数,且路径含有非ASCII字符时,必须使用GBK

出现上述错误的原因就是使用了UTF-8编码的脚本而自己的文件路径含有非ASCII字符,例如中文。
文件I/O也会出现同样的状况,是因为它调用的路径通常是以骰娘的getDiceDir()开始的。
所以路径和编码总得妥协一个。尤其是在json.lua本身也是UTF-8编码的、并且安装新脚本也需要和它编码一致的情况下,与其将所有脚本都转码为GBK,还不如干脆将骰娘文件夹移动到纯英文路径。

文件的I/O是涉及脚本本体外的内容的一种形式。它会在指定的路径创建并读写一个文档,通常来说使用txt就足够了。文末会放出读写文件时常用的三个函数,有需要的可以直接复制进相关脚本中。

需要注意的是,在利用含JSON的脚本时,第一次读取的文件无法直接交给json.lua解码,因为文档里空空荡荡什么都没有,甚至可能连文档都没有。
因此推荐读文件时添加一个类似于初始化的条件判定。可以利用read_file()读取空文件返回零长字符串的特点加以判定,若满足条件则不进行json.decode()的工作。

于是,在文件为空的情况下,如果我想实现前面提到的记录用户留言及其他信息,我必须这么做:

function ttbd_write(message,user_qq)
	local letter = read_file(ttbd_path) -- 读取
	json = require("json")
	if #letter==0 then -- 如果是空的
		j = {} -- 初始化表“j”
		j.usagi = {} -- 初始化数组“j.usagi”
		num = 1 -- 数组索引为1
	else
		j = json.decode(letter) -- 否则进行解码
		num = #j.usagi + 1 -- 数组索引为比原有数组的数字多1
	end
	j.usagi[num] = {} -- 初始化所在数组索引的表
	j.usagi[num].message = message -- 进行正常的存表内容工作
	j.usagi[num].name = getUserConf(user_qq,"nick","")
	j.usagi[num].qq = user_qq -- 最后放弃了记录群号的想法,因为意义不大
	j.usagi[num].times = 0
	j.usagi[num].isPermanent = false
	letter_full = json.encode(j) -- 编码
	overwrite_file(ttbd_path,letter_full) -- 覆写文件,由于JSON有特定格式因此无法使用追加写入
end

这样在没有文件的情况下,程序就会跳过解码,直接创建指定文件夹路径和文本文档。
我向骰娘添加的留言是喵喵喵,因此会写入如下内容:

{"usagi":[{"qq":"1142145792","name":"兔兔零号机","times":0,"message":"喵喵喵","isPermanent":false}]}

而在第二次及以上留言时,则会正常的进行解码-添加新表-编码的过程,再覆写文件。第二次我的留言是汪汪汪,文档内容会被修改为如下:

{"usagi":[{"times":1,"isPermanent":false,"qq":"1142145792","message":"喵喵喵","name":"兔兔零号机"},{"times":0,"isPermanent":false,"qq":"1142145792","message":"汪汪汪","name":"兔兔零号机"}]}

其中喵喵喵这一条已经被我写好的读取代码调用了一次,因此可以看见它的times值有增加。

以下为文件读、写、覆写函数,可直接复制使用:

-- 用于读文件,参数为文件路径
function read_file(path)
    local text = ""
    local file = io.open(path, "r") -- 打开了文件读写路径,以读取的方式
    if (file ~= nil) then -- 如果文件不是空的
        text = file.read(file, "*a") -- 读取内容
        io.close(file) -- 关闭文件
    end 
    return text -- 返回读取的内容
end

-- 用于写文件,参数为路径和需要写入的文本
function write_file(path, text)
	file = io.open(path, "a") -- 以追加的方式
    file.write(file, text) -- 写入内容
    io.close(file) -- 关闭文件
end

-- 用于覆写文件,参数与写文件相同
function overwrite_file(path, text)
	file = io.open(path, "w") -- 以只写的方式,会将原内容清空后写
	file.write(file, text)
	io.close(file)
end

四、常见报错信息及解决办法

同时推荐查看Dice!文档的附录:lua报错信息说明。这里只列举在该教程中会出现的报错信息并加以解释。

骰娘运行lua文件G:\Dice - 副本\Dice3306860448\plugin\测试.lua失败:G:\Dice - 副本\Dice3306860448\plugin\测试.lua:3: module 'aaa' not found:
	no field package.preload['aaa']
	no file 'G:\Dice - 鍓湰\Dice3306860448\plugin\aaa.lua'
	no file 'G:\Dice - 鍓湰\Dice3306860448\plugin\aaa\init.lua'
	...
	(下略)

产生原因:①UTF-8编码且存在requireloadLua的脚本,在含非ASCII字符集的路径中运行,见上一条
     ②放在了读取不到的地方,例如plugin\test\aaa.lua。这种情况不一定会产生如上的乱码。
解决办法:①将脚本全部转码或将骰娘转移至ASCII字符集的路径(可以简单理解为英文+数字+键盘上打得出的大部分英文符号)
     ②修改package.path,具体见->RainChain的回帖(非常感谢补充)
 

骰娘调用G:\Dice - 副本\Dice3306860448\plugin\DriftingBottles.lua函数throw_bottle失败!
G:\Dice - 副本\Dice3306860448\plugin\DriftingBottles.lua:103: attempt to index a nil value (global 'file')

产生原因:明明已经给file变量赋值了以getDiceDir()开头的路径,但是UTF-8编码的脚本在含非ASCII字符集的路径中运行,导致file变量未能正常写入,变成了nil
解决办法:如果只存在文件读写可以考虑转码,但更推荐修改骰娘的路径
 

骰娘调用G:\Dice\Dice3306860448\plugin\签到.lua函数ttbd失败!
G:\Dice\Dice3306860448\plugin\json.lua:184: unexpected character '' at line 1 col 1

注:上述文本中,''之间应当有一个符号,为黑色菱形中间带一个问号。因为实际上代表空字符所以可能在复制后消失了
产生原因:可能是让json.lua解码了nil内容
解决办法:重新看一下是不是脚本中的初始化部分没做对,或者在其前面加一个条件判断使其不进行解码
 

骰娘调用G:\Dice\Dice3306860448\plugin\签到.lua函数ttbd失败!
G:\Dice\Dice3306860448\plugin\签到.lua:29: attempt to index a nil value (global 'j')

产生原因:这一行的内容是j.usagi = {}。试图对空变量j使用索引
解决办法:声明j。在出现问题的行前面添加j = {}

五、结语

我可不可以在这里碎碎念点什么,大概行吧反正是我自己的东西我爱怎么写怎么写(?
总之,首先,感谢你一路看到这里。第一次写教程,也不知道写了多少字,感觉比写期末论文还长……能忍受我这么啰嗦的语言看到最后,真的辛苦你了(
至此应该是将能教的都教了,如果有没涉及到的地方,希望能够获得反馈。

另外,感谢タブー術对该教程结构方面的指点,还有Text_Koaku当了一期小白鼠(话说您完全不用论坛是吗)(笑死,地址忘了)

参考资料:
Lua 变量 | 菜鸟教程
Lua 字符串 | 菜鸟教程
Lua 数组 | 菜鸟教程
Lua table(表) | 菜鸟教程
Lua 文件I/O | 菜鸟教程
JSON 教程 | 菜鸟教程
JSON 语法 | 菜鸟教程
JSON 对象 | 菜鸟教程
JSON 数组 | 菜鸟教程
Lua利用cjson读写json - KAME - 博客园
使用Lua CJSON库进行encode与decode操作完成对Json数据转化 - echo111333 - 博客园
在线工具:
JSON在线解析 | 菜鸟工具
Lua在线工具 | 菜鸟工具

——碎碎念呢?
——想了想还是不写了,就祝大家虎年大吉吧。
然后,某人似乎打算把漂流瓶利用JSON重制一遍,期待ta的表现~
但为什么最后做漂流瓶重制版的人换了一个!某人呢某人呢?!