目录
方法一:用 coroutine.wrap + xpcall 包裹你的业务逻辑
方法二:用 luv 的 makeCallback 思路(如果你能改底层)
6.2 涉及 C 扩展(FFI 或 Native Module):需要编译,但不用重编 Luvit
1. Lua版本的Node.js
Luvit是一个在github.com上开源的(近 4000 Star)Lua工具包;它的主要用途是让你能用 Lua 语言,以类似 Node.js 的方式构建高性能的网络服务或命令行工具。
换句话说,luaJit没有异步能力,添加了Libuv以后就有了事件驱动的异步能力,再扩展http,json,zip,等能力,就可以方便的编写简单的小程序了。那么作者为啥要重新造轮子呢?答案是:
-
启动速度:Node.js 启动约 35ms,而 LuaJIT 只要 0.64ms。如果你的脚本是“跑完就退”的短任务,Luvit 的启动速度是数量级的优势。
-
内存占用:Node.js 基础内存约 44MB,LuaJIT 约 2.6MB。如果你要在资源极其受限的环境(比如嵌入式设备、或需要开几十个进程的场景)运行,Luvit 的优势很明显。
代码仓库:GitHub - luvit/luvit: Lua + libUV + jIT = pure awesomesauce · GitHub
预编译版本:GitHub - truemedian/luvit-bin: Luvit build scripts and weekly binary releases · GitHub
📊 用的人多么?—— 社区规模与影响力
它属于“小而精”的项目,在 GitHub 上获得了不错的口碑,但活跃度和普及度还不能和 Node.js 相提并论。
-
社区认可度高:在 GitHub 上拥有约 3,950+ 个 Star 和 376 个 Fork,在代码仓库中排名前 2%。这说明它在对异步 I/O 和 Lua 感兴趣的开发者群体中,是一个受关注和有价值的项目。
-
并非大众工具:它的核心用户群是 Lua 开发者或需要嵌入式脚本能力的人。与 Node.js 动辄百万的周下载量相比,Luvit 的用户基数非常小。
-
项目成熟但更新缓慢:项目本身已有超过 10 年的历史,技术栈非常稳定。最近的代码提交在几个月前,但正式的版本发布停留在 2021 年(v2.18.1),这表明项目已进入维护状态,而非高速迭代期。
🛠️ 目前能干啥?—— 核心用途与场景
Luvit 的核心价值在于,它把 Node.js 那种“事件驱动、非阻塞 I/O”的编程模型带到了 Lua 生态中。你可以把它看作一个可以独立运行 Lua 脚本的“服务器引擎”。
-
构建网络服务:你可以用 Luvit 直接编写 HTTP 服务器,无需 Nginx 或 Apache。代码风格非常像 Node.js,很适合用来快速开发 API 服务、微服务或网络代理。
-
异步编程与脚本工具:它既是一个库,也是一个可执行文件。你可以用它编写需要处理大量并发连接(如 WebSocket 服务、TCP 客户端)的工具,或者任何需要高效文件操作、系统调用的自动化脚本。它同时支持 回调(Callbacks) 和 协程(Coroutines) 两种异步风格,后者能让代码看起来像同步写法,更易于阅读。
-
灵活的运行时定制:Luvit 的生态是模块化的,你可以按需组合。比如,你可以只使用它底层的
luv(Lua 的 libuv 绑定)库,或者只使用它的包管理工具lit,而不必引入整个框架。
总的来说,Luvit 是一个为 Lua 开发者设计的、成熟且专注的异步运行时。如果熟悉 Lua,并且想用它来做后端网络编程,它会是一个很趁手的工具。
2.💻 在 Windows 11 上的安装方法
目前主要有两种方式,推荐第一种:
1. 使用官方 PowerShell 脚本(官方推荐) 这是 Luvit 官方文档目前给出的 Windows 安装指引,它会自动下载所需的 lit 和 luvit 工具。
-
打开 PowerShell(建议以管理员身份)。
-
执行以下命令(注意,原文档中有一处笔误,这里的命令已修正):
PowerShell -NoProfile -ExecutionPolicy unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = 'Tls12'; iex ((new-object net.webclient).DownloadString('https://github.com/luvit/lit/raw/master/get-lit.ps1'))" -
脚本运行完成后,建议将生成的可执行文件所在目录(通常在当前用户目录下)添加到系统的 PATH 环境变量中,这样就能在任何地方使用
luvit命令了。
2. 使用社区维护的预编译包(备用方案) 如果上面的脚本因网络问题失败,可以考虑使用 truemedian/luvit-bin 项目,它每周都会自动构建适用于 Windows x64 的二进制文件。你可以在其 GitHub Releases 页面手动下载 luvit.exe 和 lit.exe,然后放入一个目录并同样添加到 PATH 中。
🚀 快速入门:从“Hello World”开始
安装成功后,就可以体验“像 Node.js 一样写 Lua”了。
-
验证安装:在终端输入
luvit,如果进入一个带>提示符的交互式界面(REPL),就说明安装成功了。 -
创建第一个脚本:新建一个文件
server.lua,写入以下代码,这是一个简单的 HTTP 服务器:local http = require('http') http.createServer(function (req, res) local body = "Hello world\n" res:setHeader("Content-Type", "text/plain") res:setHeader("Content-Length", #body) res:finish(body) end):listen(1337, '127.0.0.1') print('Server running at http://127.0.0.1:1337/') -
运行脚本:在终端中执行
luvit server.lua,然后打开浏览器访问http://127.0.0.1:1337/,就能看到 “Hello world” 了。
📚 学习资源推荐
-
官方文档:访问 luvit.io 查看完整的 API 文档,其设计思路与 Node.js 高度相似,如果你有 Node.js 基础会非常容易上手。
-
交互式教程:
luvit-quest是一个有趣的交互式工作坊,通过“闯关”的形式引导你学习网络编程。
考虑到你的任务是开发状态监控,熟悉了基本的 HTTP 服务器后,下一步可以重点研究一下 Luvit 的 childprocess 模块,它对于执行系统命令来采集监控数据会很有帮助。
3. 三个基本工具
下载的 truemedian/luvit-bin 提供的三个文件,正好对应了 Luvit 技术栈的三层结构。
🔧 三个工具的区别:从引擎到工具集
你看到的三个可执行文件,构成了 Luvit 生态的完整链条,其分工非常明确:
-
luvi.exe:核心引擎。它是用 C 语言编写的底层运行时,打包了 LuaJIT、libuv(异步 I/O)、OpenSSL 等原生库。你可以把它理解为一个“专用的 Lua 解释器”,它本身不含 HTTP 等高级 API,只提供最基础的 I/O 和网络套接字能力。它还能将你的 Lua 代码和依赖打包成一个独立的可执行文件。
-
luvit.exe:Node.js 风格的平台。它本身是用 Lua 写的,构建在
luvi之上。它实现了一套与 Node.js 高度相似的 API(如http,fs,tls等),让你能用 Lua 写出类似 Node.js 的代码。你平时写代码、跑脚本,主要用的就是它。 -
lit.exe:包管理与构建工具。它是 Luvit 世界的 “npm + 构建器”。你用它来安装第三方库、管理项目依赖,以及将你的脚本打包成可独立分发的二进制文件。
简单来说:luvi 是发动机,luvit 是装好发动机和内饰的汽车,而 lit 是车库和改装工具。
3.1 🌐 配置 HTTPS 服务器
在 Luvit 中配置 HTTPS 服务器,核心是使用 https 模块(或 tls 模块)并传入包含证书和私钥的 options 对象。
首先,你需要用 OpenSSL 生成自签名证书(仅用于测试):
openssl genrsa -out server.key 2048 openssl req -new -x509 -key server.key -out server.crt -days 365
然后,编写你的 HTTPS 服务器脚本 https_server.lua:
OpenSsl下载地址:Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions
local https = require('https')
local fs = require('fs')
-- 1. 读取证书和私钥文件
local options = {
key = fs.readFileSync('server.key'),
cert = fs.readFileSync('server.crt')
}
-- 2. 创建 HTTPS 服务器
local server = https.createServer(options, function (req, res)
local body = "Hello from HTTPS!\n"
res:setHeader("Content-Type", "text/plain")
res:setHeader("Content-Length", #body)
res:finish(body)
end)
-- 3. 监听 8443 端口
server:listen(8443, '0.0.0.0', function()
print('HTTPS Server running at https://127.0.0.1:8443/')
end)
运行 luvit https_server.lua,然后在浏览器访问 https://localhost:8443 即可(自签名证书会触发安全警告,这是正常的)。
3.2 📊 编写 Windows CPU/内存采集脚本
对于最简单的 Windows 主机状态采集,最直接的方法是调用系统自带的 wmic 命令,解析其输出。
创建一个名为 win_monitor.lua 的脚本:
local http = require('http')
local json = require('json')
local childprocess = require('childprocess')
-- 解析 CPU 输出
local function parse_cpu(output)
for line in output:gmatch("[^\r\n]+") do
if not line:match("^Node") and not line:match("^$") then
local val = line:match(",(%d+)$")
if val then return tonumber(val) end
end
end
return nil
end
-- 解析内存输出
local function parse_mem(output)
for line in output:gmatch("[^\r\n]+") do
if not line:match("^Node") and not line:match("^$") then
local free, total = line:match(",(%d+),(%d+)$")
if total then
return {
mem_total_kb = tonumber(total),
mem_free_kb = tonumber(free),
mem_usage_percent = math.floor((tonumber(total) - tonumber(free)) / tonumber(total) * 100)
}
end
end
end
return nil
end
-- 异步执行命令,完成后调用 callback(output)
local function run_command_async(cmd, args, callback)
local child = childprocess.spawn(cmd, args)
local output = ""
child.stdout:on('data', function(chunk)
if chunk then output = output .. chunk end
end)
child:on('exit', function(code)
callback(output, code)
end)
end
-- 主逻辑:异步获取系统信息,然后返回响应
local function handle_metrics(res)
run_command_async('wmic', {
'path', 'Win32_PerfFormattedData_PerfOS_Processor',
'where', "Name='_Total'", 'get', 'PercentProcessorTime', '/format:csv'
}, function(cpu_out)
local cpu_usage = parse_cpu(cpu_out)
run_command_async('wmic', {
'OS', 'get', 'FreePhysicalMemory,TotalVisibleMemorySize', '/format:csv'
}, function(mem_out)
local mem_info = parse_mem(mem_out) or {}
local info = {
cpu_usage = cpu_usage,
mem_total_kb = mem_info.mem_total_kb,
mem_free_kb = mem_info.mem_free_kb,
mem_usage_percent = mem_info.mem_usage_percent
}
local body = json.stringify(info)
res:setHeader("Content-Type", "application/json")
res:setHeader("Content-Length", #body)
res:finish(body)
end)
end)
end
-- HTTP 服务器
local server = http.createServer(function (req, res)
if req.url == '/metrics' then
handle_metrics(res)
else
res:setHeader("Content-Type", "text/plain")
res:finish("Try /metrics\n")
end
end)
server:listen(8080, '0.0.0.0', function()
print('Monitoring agent running at http://localhost:8080/metrics')
end)
运行 luvit win_monitor.lua 后,访问 http://localhost:8080/metrics,你将看到类似
{"mem_usage_percent":65,"cpu_usage":0,"mem_total_kb":16495096,"mem_free_kb":5657780}
的 JSON 输出。
3.3 使用外部库简化代码
使用 coro-spawn 库确实能显著减少代码量,并且代码结构会清晰很多。它用协程(coroutine)把异步的子进程调用包装成了类似同步的写法,正好解决了你之前遇到的“协程边界”报错问题。
./lit.exe install luvit/coro-spawn
会在deps目录下添加相关的lua文件。
📉 代码量对比
| 方面 | 原生 childprocess(修正版) | 使用 coro-spawn |
|---|---|---|
| 核心逻辑 | 需要手动管理 stdout:on('data') 和 exit 事件,通过回调嵌套处理 | 一行 for 循环即可读取输出,waitExit() 直接等待结束 |
| 代码结构 | 深层回调嵌套,异步流程不直观 | 线性同步风格,易于阅读和维护 |
| 依赖 | 无 | 需要 lit install luvit/coro-spawn |
🛠️ 使用 coro-spawn 的修正版脚本
安装库后,代码可以大幅简化:
local http = require('http')
local json = require('json')
local spawn = require('coro-spawn') -- 需要先 lit install luvit/coro-spawn
-- 解析函数保持不变
local function parse_cpu(output)
for line in output:gmatch("[^\r\n]+") do
if not line:match("^Node") and not line:match("^$") then
local val = line:match(",(%d+)$")
if val then return tonumber(val) end
end
end
return nil
end
local function parse_mem(output)
for line in output:gmatch("[^\r\n]+") do
if not line:match("^Node") and not line:match("^$") then
local free, total = line:match(",(%d+),(%d+)$")
if total then
return {
mem_total_kb = tonumber(total),
mem_free_kb = tonumber(free),
mem_usage_percent = math.floor((tonumber(total) - tonumber(free)) / tonumber(total) * 100)
}
end
end
end
return nil
end
-- 用 coro-spawn 执行命令,返回完整输出
local function run_command(cmd, args)
local child, err = spawn(cmd, {
args = args,
stdio = {nil, true, true} -- 关键:忽略 stdin,开启 stdout/stderr 管道
})
if not child then
return "", err or "spawn failed"
end
local output = ""
-- 同步风格遍历读取 stdout
for chunk in child.stdout.read do
if chunk then output = output .. chunk end
end
child.waitExit() -- 等待进程结束
return output
end
-- 现在 get_system_info 变成了同步写法,非常干净
local function get_system_info()
local info = {}
local cpu_out = run_command('wmic', {
'path', 'Win32_PerfFormattedData_PerfOS_Processor',
'where', "Name='_Total'", 'get', 'PercentProcessorTime', '/format:csv'
})
info.cpu_usage = parse_cpu(cpu_out)
local mem_out = run_command('wmic', {
'OS', 'get', 'FreePhysicalMemory,TotalVisibleMemorySize', '/format:csv'
})
local mem = parse_mem(mem_out) or {}
info.mem_total_kb = mem.mem_total_kb
info.mem_free_kb = mem.mem_free_kb
info.mem_usage_percent = mem.mem_usage_percent
return info
end
local server = http.createServer(function (req, res)
if req.url == '/metrics' then
-- 关键:用 coroutine.wrap 创建新协程来执行
coroutine.wrap(function()
local info = get_system_info()
local body = json.stringify(info)
res:setHeader("Content-Type", "application/json")
res:setHeader("Content-Length", #body)
res:finish(body)
end)()
else
res:setHeader("Content-Type", "text/plain")
res:finish("Try /metrics\n")
end
end)
server:listen(8080, '0.0.0.0', function()
print('Monitoring agent running at http://localhost:8080/metrics')
end)
4. 崩溃即退出的问题
Luvit 的设计哲学就是“未捕获错误直接崩溃”,没有内置类似 Node.js 的全局 uncaughtException 机制。 但可以在自己的代码层面实现类似的效果。
为什么 Luvit 默认会崩溃?
Luvit 的底层是 luv(libuv 的 Lua 绑定),它从 C 层调用 Lua 回调时,没有用 pcall 保护。这意味着一旦回调里出错,错误会直接冒泡到 C 层,最终导致进程终止。
这与 Node.js 不同:Node.js 在事件循环的每个回调外层都做了保护,所以才能提供 process.on('uncaughtException')。Luvit 社区曾讨论过加类似的机制,但最终决定维持“硬错误”行为,强制开发者自己处理错误。
在 Luvit 里实现“不崩溃”的两种方法
方法一:用 coroutine.wrap + xpcall 包裹你的业务逻辑
既然问题出在 HTTP handler 是 C 回调,那就在 handler 内部把业务逻辑“隔离”到受保护的协程里:
local server = http.createServer(function (req, res)
if req.url == '/metrics' then
coroutine.wrap(function()
local ok, err = xpcall(function()
local info = get_system_info()
local body = json.stringify(info)
res:setHeader("Content-Type", "application/json")
res:setHeader("Content-Length", #body)
res:finish(body)
end, function(e)
-- 错误处理器:返回堆栈信息
return debug.traceback(e)
end)
if not ok then
-- 出错时给客户端返回 500,而不是让进程崩溃
res:setHeader("Content-Type", "text/plain")
res:finish("Internal Server Error:\n" .. tostring(err))
end
end)()
else
res:setHeader("Content-Type", "text/plain")
res:finish("Try /metrics\n")
end
end)
这样做的效果:get_system_info() 里的错误会被 xpcall 捕获,你可以在错误处理器里记录日志或返回错误响应,进程不会崩溃。
方法二:用 luv 的 makeCallback 思路(如果你能改底层)
Luvit 的 init.lua 里其实有一个全局的 xpcall 包装主流程,但它不覆盖由 C 层(libuv)触发的回调。理论上你可以通过修改 luv 的 C 代码,给所有回调加上保护,但这需要重新编译 luvi,代价较大。
实际建议
对于的监控脚本项目,方法一足够了。把每个 HTTP handler 里的核心逻辑用 xpcall 包起来,错误时返回 500 并打印日志,就能达到类似 Node.js 的效果。唯一的代价是每个入口处都要记得加这层包装,不像 Node.js 那样有一个全局兜底。
5. http客户端与爬虫
Luvit 有内置的 JSON 模块,但没有内置的 XML 解析器。对于你“做简单爬虫”的需求,Luvit 的 HTTP 客户端能力是够用的,但 XML 解析需要额外安装第三方库。
📦 内置模块情况
5.1 JSON:有内置模块
Luvit 现在已经有了原生的 json 模块,基于 yajl 实现,这个是内置的不需要安装。你可以直接使用:
local json = require('json')
-- 解析 JSON 字符串
local obj = json.parse('{"name": "test", "value": 123}')
p(obj.name) -- 输出: test
-- 生成 JSON 字符串
local str = json.stringify({name = "test", value = 123})
p(str) -- 输出: {"name":"test","value":123}
需要注意,Lua 的 table 和 JSON 之间有一些语义差异(比如空 table 默认会被编码为 {} 还是 []),编码时可以给 table 设置 __jsontype 元字段来明确指定。
XML:没有内置
搜索结果中没有发现 Luvit 有内置的 XML 解析器。如果你需要解析 XML,只能通过 lit install 安装第三方包,或者自己用字符串模式匹配做简单的解析。
5.2 🕷️ 做简单爬虫的可行性
HTTP 客户端能力:够用
Luvit 的内置 http 模块提供了类似 Node.js 的 HTTP 客户端和服务端 API。你可以用它发起 HTTP 请求。不过,http.request 的回调风格比较繁琐,社区更推荐使用 coro-http 这个辅助库,它用协程包装了请求流程,代码会简洁很多:
.\lit.exe install luvit/coro-http
local coro = require('coro-http')
coroutine.wrap(function()
local result, body = coro.request('GET', 'https://example.com/api/data')
-- body 就是响应内容
local json = require('json')
local data = json.parse(body)
p(data)
end)()
做爬虫的实际建议
对于“简单爬虫”来说,Luvit 是可行的:
-
发起请求:用
coro-http获取页面内容。 -
解析内容:
-
如果目标 API 返回 JSON,直接用内置
json模块解析,非常顺畅。 -
如果目标页面是 HTML/XML,Luvit 没有内置解析器。你需要:
-
用
lit install找社区库(比如搜索 XML/HTML 解析相关的包); -
或者用 Lua 的
string.match/gmatch做简单的正则提取(只适合结构非常固定的页面)。
-
-
-
处理数据:用
json.stringify生成结构化输出。
一个需要留意的坑:你之前遇到的 attempt to yield across C-call boundary 错误,在爬虫场景下同样会出现。如果你在 HTTP 服务器的 handler 里发起爬虫请求,记得用 coroutine.wrap 包裹爬虫逻辑。
5.3 📊 总结
| 能力 | Luvit 支持情况 |
|---|---|
| JSON 解析/生成 | ✅ 内置 json 模块 |
| XML 解析 | ❌ 无内置,需第三方库或手动解析 |
| HTTP 客户端 | ✅ 内置 http,推荐 coro-http |
| 简单爬虫 | ⚠️ 可行,但 HTML/XML 解析需自行解决 |
所以,如果你的爬虫目标主要是 JSON API,Luvit 用起来会很顺手。如果目标网页是 HTML 且需要提取结构化数据,就需要额外安装 XML/HTML 解析库,或者自己写简单的字符串提取逻辑。
5.4 下载B站的音乐与视频
坦白说,用 Luvit 写一个能直接跑的 B 站下载器,会遇到不少棘手的工程问题。不过,我为你准备了一套可运行的脚本框架,它能完成“获取视频信息 → 下载音视频流 → 调用 FFmpeg 合并”的核心流程,你可以拿它作为起点来研究。
🎯 核心思路与前置准备
B 站视频采用 DASH 协议,视频和音频是分离的 .m4s 文件,必须下载后合并才能正常播放。整个流程分为三步:
-
获取 CID 和播放地址:通过视频 BV 号调用
view接口拿到 CID,再调用playurl接口获取音视频流地址。 -
下载音视频流:分别请求视频和音频的 URL,保存为本地文件。关键点:请求 CDN 资源时必须带上
Referer: https://www.bilibili.com,否则会返回 403。 -
FFmpeg 合并:用
ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4进行无损封装,不重新编码,速度极快。
前置准备:
-
安装
coro-http:./lit.exe install luvit/coro-http -
确保
ffmpeg已加入系统 PATH。 -
准备好你的 B 站 Cookie(
SESSDATA字段),否则大概率只能拿到 480P,且可能因风控返回-412。
📝 Luvit bilibili 爬虫脚本
将以下代码保存为 bili_downloader.lua:
./lit.exe install luvit/secure-socket winget install --id GnuWin32.Gzip -e chcp 65001
目前有个问题,就是这个版本的livit没有编译gzip模块,导致了不能直接解码,需要使用外部的gzip.exe来执行解码,
local http = require('coro-http')
local json = require('json')
local fs = require('fs')
local spawn = require('coro-spawn')
-- ============ 配置 ============
local VIDEO_URL = "https://www.bilibili.com/video/BV1TKha67Eob?vd_source=46553807f77d6bc390bf1eb40d81874d"
local OUTPUT_DIR = "d:\\mp3"
local pathJoin = require('luvi').path
local SESSDATA = ""
-- ==============================
local UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
-- 从 res 中提取指定 header 的值(res[1..n] 是 {name, value} 形式的 table)
local function get_header(res, name)
name = name:lower()
for i = 1, #res do
local h = res[i]
if type(h) == "table" and h[1] and h[2] then
if h[1]:lower() == name then
return h[2]
end
end
end
return nil
end
-- 收集响应中的 Set-Cookie
local function collect_cookies(res, cookies)
for i = 1, #res do
local h = res[i]
if type(h) == "table" and h[1] and h[1]:lower() == "set-cookie" then
local name, value = h[2]:match("^([^=]+)=([^;]+)")
if name then cookies[name] = value end
end
end
end
-- 带重定向跟随的 GET
local function get_with_redirect(url, extra_headers, max_redirects)
max_redirects = max_redirects or 5
local current_url = url
local cookies = {}
if SESSDATA ~= "" then
cookies["SESSDATA"] = SESSDATA
end
for i = 1, max_redirects do
local headers = {
{"User-Agent", UA},
{"Accept", "*/*"},
{"Accept-Language", "en-US,en;q=0.5"},
{"Accept-Encoding", "identity"},
}
if next(cookies) then
local cookie_str = {}
for k, v in pairs(cookies) do
table.insert(cookie_str, k .. "=" .. v)
end
table.insert(headers, {"Cookie", table.concat(cookie_str, "; ")})
end
if extra_headers then
for _, h in ipairs(extra_headers) do table.insert(headers, h) end
end
local res, body = http.request("GET", current_url, headers)
collect_cookies(res, cookies)
-- 处理 gzip 压缩:写临时文件 -> 调用 gzip 解压 -> 读回
local ce = get_header(res, "content-encoding")
if ce and ce:lower() == "gzip" then
local tmp_gz = "temp_body.gz"
local tmp_out = "temp_body.out"
fs.writeFileSync(tmp_gz, body)
-- gzip -d -c temp_body.gz > temp_body.out
local proc = spawn("C:\\Program Files\\Git\\usr\\bin\\gzip.exe", {
args = {"-d", "-c", tmp_gz},
stdio = {nil, true, true}
})
-- 读取 stdout 到文件
local out_chunks = {}
for chunk in proc.stdout.read do
if chunk then table.insert(out_chunks, chunk) end
end
proc.waitExit()
local decompressed = table.concat(out_chunks)
body = decompressed
print(" 已解压 gzip: " .. #body .. " bytes")
-- 清理临时文件
pcall(fs.unlinkSync, tmp_gz)
pcall(fs.unlinkSync, tmp_out)
end
if res.code == 301 or res.code == 302 or res.code == 303 or res.code == 307 or res.code == 308 then
local location = get_header(res, "location")
if not location then error("重定向但无 Location 头") end
if location:sub(1, 1) == "/" then
local base = current_url:match("^(https?://[^/]+)")
location = base .. location
end
print(" 重定向 -> " .. location)
current_url = location
elseif res.code == 200 then
return body, current_url, cookies
else
error("HTTP " .. res.code .. " at " .. current_url)
end
end
error("重定向次数过多")
end
-- 对应 Python 的 re.search + json.loads
local function my_match(text, prefix, suffix)
local function esc(s)
return s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
end
local pattern = esc(prefix) .. "(.-)" .. esc(suffix)
local match = text:match(pattern)
if match then
local ok, result = pcall(json.parse, match)
if ok then return result end
end
return nil
end
-- 对应 Python 的 clean_title
local function clean_title(text)
text = text:gsub("|", " ")
text = text:gsub("%s+", " ")
for _, char in ipairs({"<", ">", ":", '"', "/", "\\", "?", "*"}) do
text = text:gsub("%" .. char, "")
end
return text
end
-- 对应 Python 的 merge_video_and_audio
local function merge_video_and_audio(video_path, audio_path, output_path)
local proc = spawn("ffmpeg", {
args = {
"-i", video_path,
"-i", audio_path,
"-c", "copy",
"-map", "0:v:0",
"-map", "1:a:0",
"-shortest",
"-y",
output_path
},
stdio = {nil, true, true}
})
local code = proc.waitExit()
if code == 0 then
print("已合并: " .. output_path)
return true
else
print("ffmpeg 执行失败, 退出码: " .. tostring(code))
return false
end
end
-- 主流程
coroutine.wrap(function()
print("提取到的URL: " .. VIDEO_URL)
-- 1. 获取页面 HTML(自动跟随重定向)
print("请求页面...")
local html, final_url = get_with_redirect(VIDEO_URL)
print("最终页面: " .. final_url .. " (" .. #html .. " bytes)")
-- 2. 提取内嵌 JSON
local playinfo = my_match(html, "__playinfo__=", "</script><script>")
local initial_state = my_match(html, "__INITIAL_STATE__=", ";(function()")
if not playinfo then
print("__playinfo__ 未匹配,尝试备用后缀...")
playinfo = my_match(html, "__playinfo__=", "</script>")
end
if not initial_state then
print("__INITIAL_STATE__ 未匹配,尝试备用后缀...")
initial_state = my_match(html, "__INITIAL_STATE__=", "</script>")
end
if not playinfo or not initial_state then
fs.writeFileSync("debug_page.html", html)
print("解析失败,已保存 debug_page.html")
error("未能解析页面数据")
end
-- 3. 提取音视频地址和标题
local video_url = playinfo.data.dash.video[1].baseUrl
local audio_url = playinfo.data.dash.audio[1].baseUrl
local video_name = initial_state.videoData.title
print("视频名字: " .. video_name)
print("视频地址: " .. video_url:sub(1, 100) .. "...")
print("音频地址: " .. audio_url:sub(1, 100) .. "...")
-- 4. 创建输出目录
pcall(fs.mkdirSync, OUTPUT_DIR)
video_name = clean_title(video_name)
local referer = VIDEO_URL
-- 5. 下载视频
local video_path = OUTPUT_DIR .. "\\" .. video_name .. "_video.mp4"
print("开始下载视频...")
local vres, vbody = http.request("GET", video_url, {
{"User-Agent", UA},
{"Referer", referer},
})
if vres.code ~= 200 then error("视频下载失败: HTTP " .. vres.code) end
fs.writeFileSync(video_path, vbody)
print("视频下载完成: " .. #vbody .. " bytes")
-- 6. 下载音频
local audio_path = OUTPUT_DIR .. "\\" .. video_name .. "_audio.mp4"
print("开始下载音频...")
local ares, abody = http.request("GET", audio_url, {
{"User-Agent", UA},
{"Referer", referer},
})
if ares.code ~= 200 then error("音频下载失败: HTTP " .. ares.code) end
fs.writeFileSync(audio_path, abody)
print("音频下载完成: " .. #abody .. " bytes")
-- 7. 合并
local output_path = OUTPUT_DIR .. "\\" .. video_name .. "_merged.mp4"
print("合并中...")
if merge_video_and_audio(video_path, audio_path, output_path) then
print("完成: " .. video_name)
else
print("合并失败,保留临时文件")
end
end)()
执行的结果类似这样的:
PS E:\2026work> ./luvit.exe .\bili_downloader.lua 提取到的URL: https://www.bilibili.com/video/BV1TKha67Eob?vd_source=46553807f77d6bc390bf1eb40d81874d 请求页面... 重定向 -> https://www.bilibili.com/video/BV1TKha67Eob/?vd_source=46553807f77d6bc390bf1eb40d81874d 已解压 gzip: 508704 bytes 最终页面: https://www.bilibili.com/video/BV1TKha67Eob/?vd_source=46553807f77d6bc390bf1eb40d81874d (508704 bytes) 视频名字: 周深湾区首唱粤语新歌《月亮桥》 视频地址: https://xy58x49x253x21xy.mcdn.bilivideo.cn:8082/v1/resource/upgcxcode/01/99/42061529901/42061529901-... 音频地址: https://xy60x190x133x152xy.mcdn.bilivideo.cn:8082/v1/resource/upgcxcode/01/99/42061529901/4206152990... 开始下载视频... 视频下载完成: 15151947 bytes 开始下载音频... 音频下载完成: 2062730 bytes 合并中... 已合并: d:\mp3\周深湾区首唱粤语新歌《月亮桥》_merged.mp4 完成: 周深湾区首唱粤语新歌《月亮桥》
这里没有真的写入cookie,如果需要高质量版本的视频,需要F12复制到自己的COOKIE,并添加清晰度选择的代码逻辑。
6. 扩展livit库
到目前为止,我们知道怎么使用这个东西了,那么我希望自己使用这个库,并扩展自己的函数,比如做加法,或者XML解析与编码,protobuf编解码。
6.1 纯 Lua 扩展:像写普通脚本一样简单
如果你要做的功能能用纯 Lua 实现(比如简单的加法、基础的字符串处理),你不需要编译任何东西。直接创建一个 .lua 文件,用 require 加载即可。
Luvit 本身就是用 Lua 编写的,它天然支持从文件系统或 deps 目录加载模块。你可以把自定义函数放在项目目录下的一个文件里,直接 local mylib = require('./mylib') 就能用。这就是扩展你自己的业务逻辑(比如之前写的监控采集函数)最直接的方式。
6.2 涉及 C 扩展(FFI 或 Native Module):需要编译,但不用重编 Luvit
如果你要做的功能涉及高性能计算或调用现有的 C 库(比如 XML 解析库、Protobuf 的 C 实现),就需要编写 C 扩展。Luvit 对此有明确的支持路径:
-
Luvit 支持 FFI 和基于 Lua 的二进制模块。官方文档提到有一个 Wiki 专门解释如何管理和包含二进制模块。这意味着你可以用 LuaJIT 的 FFI 功能直接调用已有的 C 动态库(比如
.dll或.so),而不必为 Luvit 专门写绑定代码。 -
如果必须写 C 扩展,流程和 Node.js 的 native addon 类似:编写 C 代码,编译成动态库(在 Windows 上是
.dll),然后通过require加载。你不需要重新编译整个 Luvit/luvi 运行时,只需要编译你这个模块本身。社区邮件列表里也有关于创建 native modules 的讨论,指向了官方的“Publishing-Compiled-Code”指南。
6.2.1 针对具体需求(XML/Protobuf)
-
XML 解析:纯 Lua 的 XML 解析库在 lit 生态里可能不多,但你可以:
-
寻找已有的 Lua XML 解析库(很多是纯 Lua 的),直接放进
deps目录使用。 -
或者用 FFI 调用系统里现成的 C 库(比如
libxml2),这需要你了解一些 LuaJIT FFI 的用法。
-
-
Protobuf 编解码:这是典型的适合用 C 扩展或 FFI 的场景。
-
纯 Lua 实现的 Protobuf 库性能通常较差。
-
最佳路径:用 FFI 加载官方的
libprotobuf-c或类似的 C 库。你只需要写几十行 Lua 代码来定义 FFI 接口,就能获得接近原生的性能,完全不用重新编译 Luvit。
-
是的,Luvit 官方明确支持使用 FFI(外部函数接口) 来调用 DLL 扩展功能,这是其二进制模块支持的核心方式。
6.2.2 官方对 FFI 的定位
Luvit 的 README 中直接写道:“Luvit supports FFI and Lua based binary modules.”。在 Luvit 的语境下,FFI 的核心价值在于:允许 Lua 直接加载并运行动态库(如 DLL)中的代码,而无需编写繁琐的 C 绑定代码。
官方的邮件列表讨论也印证了这一点:当你需要集成一个 C 库时,推荐的做法是把 C 库编译成 .so/.dll,然后用 FFI 从 Lua 调用其 API。相比传统的 C Lua API 绑定,FFI 方式通常更快,因为它是 JIT 引擎的核心部分,允许 JIT 优化器做更多假设。
6.2.3 如何用 FFI 调用 DLL
基本流程分三步,以调用一个名为 myffi.dll 的库为例:
E:\2026work\ ├── bili_downloader.lua # 你的主脚本 ├── deps/ │ ├── myffi.lua # FFI 定义文件(ffi.cdef 声明) │ └── myffi.dll # 你的 C 扩展动态库
-
声明函数签名:用
ffi.cdef描述 DLL 中函数的 C 语言签名。local ffi = require("ffi") ffi.cdef[[ int add(int x, int y); /* 声明函数 */ ]] -
加载 DLL:用
ffi.load加载动态库。在 Windows 上,如果名字里不带点号,会自动补上.dll后缀。local myffi = ffi.load('myffi') -- 会查找 myffi.dll -
调用函数:通过返回的命名空间直接调用。
local ffi = require('ffi') -- 加载 deps 下的 DLL(Luvit 的 require 会搜索 deps 目录) local myffi = ffi.load('deps/myffi') -- 或 'myffi',取决于搜索路径 -- 从 Lua 文件里读取 C 函数声明 ffi.cdef[[ int add(int x, int y); ]] local res = myffi.add(1, 2) print(res) -- 输出 3
关于官方接口”的注意事项
Luvit 本身不提供一个额外的“DLL 管理 API”,它依赖的是 LuaJIT 自带的 FFI 库。因为 Luvit 的运行时核心就是 LuaJIT,所以 LuaJIT 的 FFI 就是 Luvit 的“官方接口”。你之前用的 luvi 运行时也已经内置了 FFI 支持,所以 require("ffi") 可以直接使用。
TODO: 这里没有讨论如何将DLL打包到luv中。
7. 嵌入式使用luvi或者luv
在 C/C++ 代码中调用 Luvit 引擎并与之交互,核心不是直接调用 Luvit,而是使用它底层的 luvi 运行时或 luv 绑定。
Luvit 本质上是一个为 luvi 运行时编写的 Lua 库集合。
在 C 代码中嵌入完整 Luvit 较为复杂,更常见的做法是嵌入 luvi(它内置了 LuaJIT 和 libuv),并通过 LuaJIT FFI 来实现 C 与 Lua 的双向交互。
方案一:通过 luvi 嵌入(最接近 Luvit 的方式)
luvi 是一个“介于 luv 和 luvit 之间的项目”。
https://github.com/luvit/luvi
🧩 它和 Luvit 的关系
luvi 的官方定位是 “A project in-between luv and luvit”(位于 luv 和 luvit 之间的项目)。它把 LuaJIT、libuv、OpenSSL、miniz 等依赖打包成一个独立的运行时,目标是 让构建 Luvit 及其衍生应用变得更容易。
它的 README 里明确写道:
Luvi has a somewhat unique, but very easy workflow for creating self-contained binaries on systems that don't have a compiler.
这意味着,不需要一个完整的 C 编译器就能打包出可执行文件——这就是它作为“嵌入式运行时”的核心价值。
🔗 交互能力:正是我们需要的
你关心的“在自己的代码中调用引擎并与它交互”,luvi 提供了明确的机制:
-
C 的
main函数集成:C 侧的argc和argv会以 0 起始索引的字符串表形式暴露给 Lua 的全局args表。 -
退出码:如果
main.lua返回一个整数,它就是程序的退出码。 -
环境变量:
env模块提供env.get、env.put、env.set等接口来读写环境变量。 -
Bundle I/O:
luvi的bundle属性统一了磁盘目录和 zip 包的读取方式,提供bundle.stat、bundle.readdir、bundle.readfile等接口。
📦 版本选择
luvi 提供两个预置构建版本:
-
tiny:只包含 Lua、Libuv、miniz 和最小化模块。 -
regular:完整的 Luvit 体验,包含 OpenSSL、LPeg、lrexlib。
它把你的 main.lua 运行在一个“基本标准的 LuaJIT 环境”中,并内置了 uv(libuv 绑定)和 bundle(文件 I/O)等模块。
交互方式:
-
C 调用 Lua:在 C 侧初始化
luvi运行时并加载你的main.lua。这类似于标准的 LuaJIT 嵌入流程(luaL_newstate,luaL_openlibs,luaL_dofile)。 -
Lua 调用 C:在 Lua 代码中通过
ffi.load加载你编译好的 DLL,然后用ffi.cdef声明并调用 C 函数。这是官方推荐的方式,Luvit 明确支持 FFI 和二进制模块。
一个 C 代码的交互示例(概念性):
// 你的 C 程序
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main() {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 注册一个 C 函数给 Lua 调用,或者直接加载 Lua 脚本
luaL_dofile(L, "main.lua"); // 你的 Lua 逻辑入口
// 启动 luvi 的事件循环(如果使用 uv 模块)
// ... 从 Lua 侧调用 uv.run() 后,控制权会交回 C
lua_close(L);
return 0;
}
方案二:仅使用 luv 绑定(更轻量)
GitHub - luvit/luv: Bare libuv bindings for lua · GitHub
如果只需要异步 I/O 能力,而不需要 Luvit 的 HTTP 等高级 API,可以直接使用 luv——即 libuv 的 Lua 绑定。有社区回复明确指出:“你很可能想要的不是 luvit 本身,而是 luv 绑定,它们可以独立使用并发布到 luarocks”。
这意味着你可以将 luv 编译进你现有的 C/C++ 应用程序中(你的应用可能已经链接了 libuv),然后在 Lua 脚本中 require('luv') 来使用异步功能。
这两个项目,luv 和 luvi 是“零件”与“成品”的关系。它们同属 luvit 生态,但处于完全不同的抽象层级。
🔩 luv:底层的“零件”
luv 是一个纯粹的 libuv 绑定库。它的官方描述是 “Bare libuv bindings for lua”,核心任务只有一件事:把 libuv 的 C API 原封不动地暴露给 Lua。
你可以把它理解为一把“瑞士军刀”的刀刃。它只提供最基础的异步能力:
-
事件循环:
uv.run() -
定时器:
uv.new_timer() -
TCP/UDP 网络:
uv.new_tcp()、uv.new_udp() -
文件系统操作:
uv.fs_open()等
它的设计目标是可嵌入性:可以编译成静态库、共享库,或者作为 Lua 模块加载。它支持 LuaJIT 和 PUC Lua 5.1 到 5.5,甚至可以在同一个进程的不同线程、不同 lua_State 中使用(每个 state 拥有独立的 uv_loop_t)。
📦 luvi:打包好的“成品”
luvi 则是一个完整的、可独立运行的运行时。它的目标很明确:“make building luvit and derivatives much easier”——让构建 Luvit 及其衍生应用变得更容易。
它把多个组件打包成一个可执行文件:
-
LuaJIT(或 PUC Lua)
-
libuv(通过
luv绑定,暴露为uv或luv模块) -
OpenSSL(regular 版本)
-
miniz(用于 zip 包读取)
-
其他实用模块:
env、bundle、args等
换句话说,luvi 内部包含了 luv。当你用 luvi 运行一个应用时,你的 main.lua 可以直接 require('uv'),因为 luvi 已经把 luv 编译进去了。
⚖️ 核心区别一览
| 维度 | luv | luvi |
|---|---|---|
| 本质 | libuv 的 Lua 绑定库 | 独立的、自包含的 Lua 运行时 |
| 包含关系 | 被 luvi 包含 | 包含 luv、LuaJIT、OpenSSL 等 |
| 交付形态 | .so / .dll / 静态库 / Lua 模块 | 单个可执行文件(如 luvi.exe) |
| 使用方式 | 嵌入到已有 Lua 环境或 C 程序中 | 直接运行 Lua 脚本或打包应用 |
| API 层级 | 底层、接近 libuv 原始 API | 在 luv 之上增加了 env、bundle、args 等 |
| 适用场景 | 已有 C 程序需要异步 I/O,或想自定义运行时 | 快速开发、打包、分发独立的 Lua 应用 |
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/robinfoxnan/article/details/166894675



