在Go程序中使用Vosk进行语音识别STT

Vosk 介绍 Vosk是一款基于深度学习 的开源语音识别工具,能够在没有云连接的情况下进行高效的离线语音识别。它通过对语音信号进行预处理、特征提取和模型推断,将语音转换成文本。Vosk不仅支持多种主流编程语言,还覆盖了20多种语言和方言,包括英语、中文、法语、德语等,为跨语言应用提供了强大的支持。 工作原理 Vosk的语音识别过程可以分为以下几个关键步骤: 语音信号预处理:对输入的语音信号进行去噪、增强等处理,以提高识别准确性。 特征提取:从处理后的语音信号中提取出能够表征语音特性的关键特征。 模型推断:利用预训练的深度学习模型对提取的特征进行识别,输出对应的文本。 优势解析 隐私保护:Vosk的离线特性意味着用户的语音数据不会离开设备,有效保护了用户的隐私。 实时性:在设备端进行语音识别,减少了网络传输时间和延迟,使得识别过程更加实时。 跨平台:支持Windows、Linux、macOS以及嵌入式设备等多种平台,便于在不同场景下的应用。 可扩展性:作为开源项目,Vosk允许开发者根据自己的需求进行定制和优化,以适应不同的应用场景。 多语言支持:提供对多种语言和方言的识别能力,为跨国应用提供了便利。 官方 https://alphacephei.com/vosk/ 模型下载 https://alphacephei.com/vosk/models GO项目搭建 本文基于Linux,Windows请参考官方仓库的说明 创建go程序 创建go程序内容如下: package main import ( "encoding/json" "flag" "fmt" "io" "log" "os" vosk "github.com/alphacep/vosk-api/go" ) func main() { var filename string flag.StringVar(&filename, "f", "", "file to transcribe") flag.Parse() model, err := vosk.NewModel("model") if err != nil { log.Fatal(err) } // we can check if word is in the vocabulary // fmt.Println(model.FindWord("air")) sampleRate := 16000.0 rec, err := vosk.NewRecognizer(model, sampleRate) if err != nil { log.Fatal(err) } rec.SetWords(1) file, err := os.Open(filename) if err != nil { panic(err) } defer file.Close() buf := make([]byte, 4096) for { _, err := file.Read(buf) if err != nil { if err != io.EOF { log.Fatal(err) } break } if rec.AcceptWaveform(buf) != 0 { fmt.Println(rec.Result()) } } // Unmarshal example for final result var jres map[string]interface{} json.Unmarshal([]byte(rec.FinalResult()), &jres) fmt.Println(jres["text"]) } 下载模型 同时需要下载对应的模型,我这里使用中文的模型 https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip ...

2025-03-17 · 2 分钟 · czyt

AUFS简要笔记

文章内容为Claude生成,留作备忘 什么是 AUFS? AUFS 是一种联合文件系统(Union File System),最初是为 Linux 系统开发的。它的主要功能是将多个目录(称为分支或层)的内容虚拟地合并成一个统一的视图。这种技术在容器化技术(如早期的 Docker)中得到了广泛应用。 AUFS 的基本原理 联合挂载机制 AUFS 核心原理是联合挂载(Union Mount)技术,其工作原理如下: 多层结构:AUFS 允许将多个目录层叠在一起,形成一个统一的视图 读写分离:通常配置为一个可写层和多个只读层 写时复制(Copy-on-Write,CoW):当需要修改只读层中的文件时,AUFS 会将文件复制到可写层进行修改,保持底层文件不变 分支优先级 在 AUFS 中,每个分支都有优先级设置: 较高优先级的分支中的文件会覆盖低优先级分支中的同名文件 当访问一个文件时,AUFS 会从最高优先级的分支开始查找 AUFS 的主要特性 1. 写时复制(CoW) 当用户试图修改只读层中的文件时: ┌─────────────┐ │ 可写层 │ ← 修改后的文件被存储在这里 ├─────────────┤ │ 只读层 1 │ ← 原始文件位置 ├─────────────┤ │ 只读层 2 │ └─────────────┘ 2. 删除处理 当删除一个文件时,AUFS 会在可写层创建一个特殊的删除标记(whiteout 文件),以表示该文件已被删除。 3. 分支管理 AUFS 提供了动态添加、删除和重新排序分支的能力,使文件系统结构可以灵活调整。 AUFS 在容器技术中的应用 在容器技术早期发展中,AUFS 扮演了重要角色: 镜像分层:容器镜像由多个只读层组成,每层代表构建过程中的一步 容器实例:运行容器时,会在镜像上添加一个可写层 资源共享:多个容器可以共享基础镜像层,节省存储空间 AUFS 的优缺点 优点 高效的存储利用率(通过共享基础层) 快速的容器启动时间 成熟稳定的实现 缺点 较复杂的实现导致维护挑战 性能开销(尤其是在深层目录结构中) 在现代 Linux 内核中已被其他联合文件系统(如 OverlayFS)逐渐替代 实际应用场景 1. 软件测试而不污染原始环境 场景描述 假设你有一个稳定的应用程序环境,需要测试新版本软件但不想影响现有环境。 ...

2025-02-25 · 2 分钟 · czyt

iter.Seq实践【译】

原文链接 https://blog.vertigrated.com/iterseq-in-practice FirstN 返回序列中的前 N 个项。 // FirstN takes an iter.Seq[int] and returns a new iter.Seq[int] that // yields only the first 'limit' items without creating intermediate slices. func FirstN[T any](original iter.Seq[T], limit int) iter.Seq[T] { return iter.Seq[T](func(yield func(T) bool) { count := 0 for item := range original { if count < limit { if !yield(item) { return } count++ } else { return } } }) } SkipFirstN 跳过前 N 个元素并返回剩余的序列。 ...

2025-02-19 · 4 分钟 · czyt

懒猫微服开发简明教程

最近入手了懒猫微服,简单记录下开发相关的内容。 环境配置 先决条件 你必须有一台懒猫微服,购买地址 安装基本环境lzc-cli,请参考官方说明地址 如果你要发布程序,必须要申请成为懒猫微服开发者,申请地址 设备上必须安装懒猫开发者工具 应用。这个应用主要用来通过lzc-cli进入devshell容器的开发以及将本地的测试镜像推送到盒子进行测试。 开发机器上安装懒猫微服客户端,这和懒猫微服的网络机制有关,参考官方文档 。开启客户端并且设备需要联网开机。 如果上面的条件都已经满足,那么我们进入下一步。 不同类型应用的注意事项 Docker应用 对于公网的docker应用如果要使用,需要先进行copy-image来利用懒猫官方提供的镜像源,参考官方说明 。下面是我的一个执行例子: 我在没copy操作之前lzc-cli project devshell cmd: install --uid czyt --pkgId cloud.lazycat.app.gokapi Error: rpc error: code = Unknown desc = "time=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_ID\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DEPLOY_UID\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DEPLOY_UID\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DOMAIN\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DEPLOY_UID\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DEPLOY_UID\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_DOMAIN\\\" variable is not set. Defaulting to a blank string.\"\ntime=\"2025-02-08T00:18:51+08:00\" level=warning msg=\"The \\\"LAZYCAT_APP_ID\\\" variable is not set. Defaulting to a blank string.\"\n app Pulling \n gokapi Pulling \n a0bed814693a Already exists \n b4e16c7102ef Already exists \n b23adc163656 Pulling fs layer \n 99db376b5073 Pulling fs layer \n 5ed8719dcb50 Pulling fs layer \n 99db376b5073 Downloading [==================================================>] 251B/251B\n 99db376b5073 Verifying Checksum \n 99db376b5073 Download complete \n b23adc163656 Downloading [> ] 134.7kB/11.46MB\n 5ed8719dcb50 Downloading [> ] 265.8kB/23.48MB\n b23adc163656 Verifying Checksum \n b23adc163656 Download complete \n 5ed8719dcb50 Verifying Checksum \n 5ed8719dcb50 Download complete \n b23adc163656 Extracting [> ] 131.1kB/11.46MB\n b23adc163656 Extracting [======================> ] 5.243MB/11.46MB\n b23adc163656 Extracting [==================================================>] 11.46MB/11.46MB\n gokapi Error unknown: {\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"name\":\"f0rc3/gokapi\",\"revision\":\"v1.9.6\"}}]}\nError response from daemon: unknown: {\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"name\":\"f0rc3/gokapi\",\"revision\":\"v1.9.6\"}}]}\n" with exit status 18 Copy ...

2025-02-07 · 12 分钟 · czyt

Slint相关资源

视频 SurrealismUI Slint Easy Slint Advanced Slint With Rust 在线教程 Slint Learn 官方教程 UI 库 SurrealismUI JUI

2025-01-16 · 1 分钟 · czyt

Go程序自动升级的方案探究

前导 2024年的最后一天看见v站的这个 帖子,故整理下,备忘。 常用的库 cloudflare tableflip 仓库为 https://github.com/cloudflare/tableflip 官方示例 package tableflip_test import ( "context" "flag" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/cloudflare/tableflip" ) // This shows how to use the upgrader // with the graceful shutdown facilities of net/http. func Example_httpShutdown() { var ( listenAddr = flag.String("listen", "localhost:8080", "`Address` to listen on") pidFile = flag.String("pid-file", "", "`Path` to pid file") ) flag.Parse() log.SetPrefix(fmt.Sprintf("%d ", os.Getpid())) upg, err := tableflip.New(tableflip.Options{ PIDFile: *pidFile, }) if err != nil { panic(err) } defer upg.Stop() // Do an upgrade on SIGHUP go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGHUP) for range sig { err := upg.Upgrade() if err != nil { log.Println("Upgrade failed:", err) } } }() // Listen must be called before Ready ln, err := upg.Listen("tcp", *listenAddr) if err != nil { log.Fatalln("Can't listen:", err) } server := http.Server{ // Set timeouts, etc. } go func() { err := server.Serve(ln) if err != http.ErrServerClosed { log.Println("HTTP server:", err) } }() log.Printf("ready") if err := upg.Ready(); err != nil { panic(err) } <-upg.Exit() // Make sure to set a deadline on exiting the process // after upg.Exit() is closed. No new upgrades can be // performed if the parent doesn't exit. time.AfterFunc(30*time.Second, func() { log.Println("Graceful shutdown timed out") os.Exit(1) }) // Wait for connections to drain. server.Shutdown(context.Background()) } 这个示例实现了以下功能: ...

2024-12-31 · 2 分钟 · czyt

Ble Sniffer相关资料参考

硬件 nrf52840 sniffer 我只买了这个硬件,所以只加这部分。 E104-BT5040U 硬件说明书 E104-BT5040UA 硬件说明书 nRF Sniffer for Bluetooth LE 软件 bettercap

2024-12-11 · 1 分钟 · czyt

对称和反向gRPC连接【译】

原文链接为 https://tilde.town/~hut8/post/grpc-connections/ 使用kimi进行翻译 背景 gRPC是一项出色的现代技术,用于远程过程调用。它允许你在客户端创建一个“存根”对象,该对象的目的是调用服务器上的方法。它是许多情况下REST或GraphQL的绝佳替代品,通常值得学习这项技术。更多信息可以在官方文档中找到。 网络 概念上,我们对客户端和服务器有两种不同的想法。 TCP服务器和客户端 - gRPC在HTTP/2之上运行,HTTP/2在TCP之上运行,所以我将讨论TCP中“服务器”和“客户端”的含义。在TCP连接中,客户端是连接的发起者,服务器是连接的接收者。然而,一旦建立了连接,连接就是对称的;客户端和服务器都可以发送和接收消息,直到一方通过shutdown(2)关闭,或通过close(2)关闭连接。 gRPC服务器和客户端 - gRPC客户端通过存根调用在服务器上运行的方法。这不是对称的。服务器不能在客户端上调用方法。 服务器/客户端类型耦合问题 如果你有一个gRPC客户端,它也是TCP客户端(它调用connect(2))。如果你有一个gRPC服务器,它也是TCP服务器(它调用listen(2)和accept(2))。 因此,如果你想让两台机器可以相互调用gRPC,那么每台机器都是客户端和服务器(在TCP和gRPC两种意义上),现在你有了两个与彼此无关的TCP连接。在有两朵云实例的场景中,这种架构通常并不复杂。但TCP通常很混乱。防火墙、NAT和动态分配的IP地址可能会使客户端到服务器的单向连接变得复杂。 这里有一个这样的情况的例子:假设你的“服务器”是一个在家庭路由器后面的笔记本电脑上运行的程序,它必须接收命令(例如,远程控制)来自“客户端”(例如,云计算实例)。根据gRPC,客户端必须是云计算实例(这很容易被称为“服务器”),因为那是创建远程过程调用的一方。服务器是笔记本电脑,因为那是实际发生过程调用的地方。 当你在客户端(即云计算实例)上创建一个“存根”时,你必须创建一个TCP连接并连接到服务器(即笔记本电脑)。现在你遇到了几个可能的问题: 你不知道笔记本电脑的IP地址,所以你现在需要一个反向服务,让笔记本电脑告诉云它的IP地址。 笔记本电脑可能在NAT后面,所以一旦你找到了IP地址,笔记本电脑将不得不配置端口转发。这在企业环境中很可能是不可能的。 笔记本电脑可能在防火墙后面,这将禁止传入连接。这可能是本地机器上的,也可能是在网关路由器上的。 这些问题中的一些可能是无法解决的,所以我们需要另一种方法。 gRPC独有的解决方案 服务器不能在客户端gRPC上调用方法。也许你可以让客户端在服务器上调用一个方法,其唯一目的是接收描述服务器希望客户端运行的方法的消息。这可以通过流式响应来完成。但这很复杂,需要大量的代码来绕过gRPC的设计。我在其他地方看到过这个建议,但我认为这是一个丑陋的临时解决方案,它制造的问题比它解决的还要多。考虑一下,你将如何在静态类型的方式中实际调用这些“客户端方法”。 基于隧道的解决方案 将TCP客户端/服务器从gRPC客户端/服务器中解耦的一个好方法是实现某种隧道。在上面的例子中,笔记本电脑可以向云计算实例发起一个TCP连接,然后通过实现特定于语言的接口,“拨号”操作让云计算实例(gRPC客户端)连接到笔记本电脑(gRPC服务器)可以简单地使用现有的TCP连接。 SSH是一个不可思议的协议,它的用途比大多数用户知道的还要多。在我们的情况下,它是将我们的TCP连接从gRPC连接中解耦的完美方式。它还有其他好处:尽管gRPC提供认证和加密,但如果更方便,你可以使用SSH提供的。 这些例子是Go语言特有的,但你可以在任何语言中做类似的事情。gRPC服务器不需要监听端口;你可以传入任何实现了Go的net.Listener的类型。所以我们可以做一个net.Listener,它将接受SSH连接,任何时候请求我们的自定义类型的新SSH通道,我们将接受它并返回一个新的net.Conn,这是我们将实现的另一个类型,它只是通过我们的隧道传输数据。 让我们从SSHDataTunnel开始,它是我们的net.Conn。 import ( "net" "time" "golang.org/x/crypto/ssh" ) // SSHDataTunnel实现了net.Conn type SSHDataTunnel struct { Chan ssh.Channel Conn net.Conn } func NewSSHDataTunnel(sshChan ssh.Channel, carrier net.Conn) *SSHDataTunnel { return &SSHDataTunnel{ Chan: sshChan, Conn: carrier, } } func (c *SSHDataTunnel) Read(b []byte) (n int, err error) { return c.Chan.Read(b) } func (c *SSHDataTunnel) Write(b []byte) (n int, err error) { return c.Chan.Write(b) } func (c *SSHDataTunnel) Close() error { return c.Chan.Close() } func (c *SSHDataTunnel) LocalAddr() net.Addr { return c.Conn.LocalAddr() } func (c *SSHDataTunnel) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() } func (c *SSHDataTunnel) SetDeadline(t time.Time) error { return c.Conn.SetDeadline(t) } func (c *SSHDataTunnel) SetReadDeadline(t time.Time) error { return c.Conn.SetReadDeadline(t) } func (c *SSHDataTunnel) SetWriteDeadline(t time.Time) error { return c.Conn.SetWriteDeadline(t) } // 静态检查类型 var _ net.Conn = &SSHDataTunnel{} 最后一行只是确保我们的类型实际上实现了net.Conn,如果它没有,它将无法编译。 ...

2024-12-10 · 3 分钟 · czyt

在Go语言中使用Arrow、Flight和Duckdb

从duckdb开始 DuckDB 是一个嵌入式分析型数据库,专为 OLAP(在线分析处理)工作负载设计。本文将基于 go-duckdb 项目的示例,详细介绍 DuckDB 在 Go 语言中的各种使用场景。 基础使用 简单查询 package main import ( "database/sql" _ "github.com/marcboeker/go-duckdb" ) func main() { db, err := sql.Open("duckdb", ":memory:") if err != nil { panic(err) } defer db.Close() // 执行查询 rows, err := db.Query("SELECT 42") if err != nil { panic(err) } defer rows.Close() } 高级特性 Copy COPY 函数可以用于导入导出数据: ...

2024-12-05 · 6 分钟 · czyt

在Go语言中处理带BOM的json数据

缘起 今天开发的时候遇到一个奇怪的问题,一个JSON文件,使用文本编辑器打开复制,并使用strings.NewReader来decode,是正常的,但是通过文件打开同样调用的方法来decode,却是失败的。后面通过打开IDE,发现文件前面有一些空白的内容。是一些bom信息。 关于BOM BOM (Byte Order Mark) 的历史原因和用途主要与字符编码和跨平台兼容性有关: 历史原因 Unicode 出现前: ASCII 只用 1 字节,没有字节序问题 各国有自己的编码标准(GB2312、Shift-JIS等) Unicode 引入后: UTF-16 使用 2 字节表示字符 不同CPU架构的字节序不同: Big Endian (大端序): 高位字节在前 Little Endian (小端序): 低位字节在前 跨平台问题: Intel x86 使用小端序 Motorola 68k 使用大端序 同一文件在不同平台解析可能出错 BOM 的作用 UTF-16 的字节序标记: FE FF: Big Endian FF FE: Little Endian UTF-8 的编码标识: EF BB BF: 表明这是 UTF-8 编码 UTF-8 实际不需要 BOM(字节序无关) Windows 添加 BOM 主要为了兼容性 实际例子 // 字符 "中" 在不同编码下的表示 text := "中" // UTF-8: E4 B8 AD // UTF-16BE: 4E 2D // UTF-16LE: 2D 4E // 示例代码 func showEncoding() { text := "中" utf8Bytes := []byte(text) // UTF-8 utf16beBytes := utf16.Encode([]rune(text)) // UTF-16BE fmt.Printf("UTF-8: % X\n", utf8Bytes) fmt.Printf("UTF-16BE: % X\n", utf16beBytes) } 常见问题场景 文本编辑器: Notepad++ 可选是否添加 BOM VS Code 可以识别和显示 BOM 某些编辑器可能显示 BOM 为特殊字符 Web 开发: PHP 文件开头的 BOM 可能导致 header 错误 HTML 文件的 BOM 可能影响布局 JavaScript 文件的 BOM 可能导致语法错误 配置文件: XML 文件的 BOM 可能影响解析 INI 文件的 BOM 可能导致配置读取错误 JSON 文件的 BOM 可能导致解析失败 建议 UTF-8 编码: 不建议使用 BOM UTF-8 是字节序无关的 已成为事实标准 新项目: 统一使用 UTF-8 无 BOM 明确规定编码要求 添加编码检测机制 老项目维护: 保持现有编码方式 添加编码处理代码 记录文件编码信息 解决方案 帮助函数 // 检测文件是否包含 BOM func HasBOM(filename string) (bool, string, error) { file, err := os.Open(filename) if err != nil { return false, "", err } defer file.Close() reader := bufio.NewReader(file) peek, err := reader.Peek(4) if err != nil && err != io.EOF { return false, "", err } // 检查不同的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { return true, encoding, nil } } return false, "", nil } // 添加 UTF-8 BOM func AddUTF8BOM(filename string) error { // 读取原文件内容 content, err := os.ReadFile(filename) if err != nil { return err } // 检查是否已有 BOM if len(content) >= 3 && bytes.Equal(content[:3], []byte{0xEF, 0xBB, 0xBF}) { return nil // 已有 BOM,不需要添加 } // 添加 BOM newContent := append([]byte{0xEF, 0xBB, 0xBF}, content...) return os.WriteFile(filename, newContent, 0644) } // 移除 BOM func RemoveBOM(filename string) error { content, err := os.ReadFile(filename) if err != nil { return err } // 检查并移除不同类型的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for _, bom := range boms { if len(content) >= len(bom) && bytes.Equal(content[:len(bom)], bom) { // 移除 BOM return os.WriteFile(filename, content[len(bom):], 0644) } } return nil // 没有 BOM,不需要处理 } 纯文本文件调用 func main() { // 创建示例文件 content := []byte("Hello, 世界!") tempDir := os.TempDir() filename := filepath.Join(tempDir, "test.txt") // 写入原始内容 if err := os.WriteFile(filename, content, 0644); err != nil { log.Fatalf("Error writing file: %v", err) } defer os.Remove(filename) // 1. 检测原始文件 hasBOM, encoding, err := DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("Original file has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 2. 添加 BOM if err := AddBOM(filename); err != nil { log.Fatalf("Error adding BOM: %v", err) } fmt.Println("Added UTF-8 BOM to file") // 3. 再次检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After adding BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 4. 移除 BOM if err :=RemoveBOM(filename); err != nil { log.Fatalf("Error removing BOM: %v", err) } fmt.Println("Removed BOM from file") // 5. 最后检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After removing BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 读取并显示最终内容 finalContent, err := os.ReadFile(filename) if err != nil { log.Fatalf("Error reading final content: %v", err) } fmt.Printf("Final content: %s\n", string(finalContent)) } JSON解析 package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "os" ) // 定义 JSON 结构 type Person struct { Name string `json:"name"` Age int `json:"age"` Hobbies []string `json:"hobbies"` } // BOM 处理函数 func skipBOM(reader *bufio.Reader) error { boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, } // 预读4个字节 peek, err := reader.Peek(4) if err != nil && err != io.EOF { return fmt.Errorf("reading BOM: %w", err) } // 检查并跳过 BOM for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { _, err = reader.Discard(len(bom)) if err != nil { return fmt.Errorf("skipping BOM: %w", err) } log.Printf("Skipped %s BOM", encoding) return nil } } return nil // 没有 BOM } // 读取并解析 JSON 文件 func ReadJSONFile(filename string) ([]Person, error) { // 打开文件 file, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("opening file: %w", err) } defer file.Close() // 创建带缓冲的读取器 reader := bufio.NewReader(file) // 处理 BOM if err := skipBOM(reader); err != nil { return nil, fmt.Errorf("handling BOM: %w", err) } // 解析 JSON var people []Person decoder := json.NewDecoder(reader) if err := decoder.Decode(&people); err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) } return people, nil } // 创建示例 JSON 文件 func createSampleJSONFile(filename string, withBOM bool) error { // 示例数据 people := []Person{ { Name: "张三", Age: 25, Hobbies: []string{"读书", "游泳"}, }, { Name: "李四", Age: 30, Hobbies: []string{"篮球", "音乐"}, }, } // 转换为 JSON jsonData, err := json.MarshalIndent(people, "", " ") if err != nil { return err } // 如果需要 BOM,添加 UTF-8 BOM if withBOM { jsonData = append([]byte{0xEF, 0xBB, 0xBF}, jsonData...) } // 写入文件 return os.WriteFile(filename, jsonData, 0644) } func main() { // 测试文件名 filename := "test.json" // 1. 创建带 BOM 的 JSON 文件 log.Println("Creating JSON file with BOM...") err := createSampleJSONFile(filename, true) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } defer os.Remove(filename) // 2. 读取并解析 JSON log.Println("Reading JSON file...") people, err := ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 3. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } // 4. 创建不带 BOM 的文件测试 log.Println("\nCreating JSON file without BOM...") err = createSampleJSONFile(filename, false) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } // 5. 再次读取 log.Println("Reading JSON file without BOM...") people, err = ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 6. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } }

2024-12-03 · 5 分钟 · czyt