当前位置: 首页 > news >正文

GLM-OCR文件处理进阶:C语言实现批量图片读取与识别结果输出

GLM-OCR文件处理进阶:C语言实现批量图片读取与识别结果输出

如果你已经用C语言调过几次GLM-OCR的接口,完成了单张图片的识别,可能会觉得不过瘾。毕竟,实际项目里,我们面对的更可能是一个塞满了几百张图片的文件夹,需要的是批量处理,然后把结果整整齐齐地保存下来,方便后续分析。

今天,我们就来把这个流程打通。我会带你用C语言,从零开始,一步步实现一个能读取文件夹里所有图片、调用GLM-OCR批量识别、最后把结果以JSON格式输出到文件的小工具。整个过程,我们会特别关注那些工程上容易踩坑的地方,比如怎么高效遍历文件、如何管理好内存避免泄漏,以及怎么给程序做个简单的“体检”(性能测试)。

准备好了吗?我们这就开始。

1. 环境准备与项目搭建

工欲善其事,必先利其器。我们先来把需要的“家伙事儿”准备好。

1.1 核心工具与库

首先,确保你的开发环境已经就绪。你需要一个C语言编译器,比如GCC或者Clang。在Linux或macOS上,它们通常默认就安装了。在Windows上,你可以使用MinGW或者MSVC。

接下来是几个关键的库:

  1. GLM-OCR的C语言接口库:这是主角,负责图片的文字识别。你需要从官方渠道获取其动态链接库(.so.dylib.dll)和对应的头文件(.h)。
  2. stb_image.h:一个非常出色的单头文件图像加载库。它轻量、简单,支持PNG、JPEG、BMP等多种格式,完美符合我们“用C语言读图片”的需求。你只需要去它的GitHub页面下载这一个文件,放到你的项目里就行。
  3. cJSON:一个超轻量级的C语言JSON解析器/生成器。我们要把识别结果输出成JSON格式,用它再合适不过。同样,它也是一个.c和一个.h文件,集成起来毫无压力。

你可以手动下载这些库,也可以使用包管理器。这里假设你把它们都放在了项目根目录下的一个libs文件夹里。

1.2 项目目录结构

一个清晰的项目结构能让后续开发省心不少。建议你这样组织:

glm_ocr_batch_demo/ ├── src/ │ ├── main.c # 主程序入口 │ ├── file_utils.c # 文件遍历相关函数 │ ├── image_loader.c # 图片加载(使用stb_image) │ └── ocr_engine.c # OCR引擎封装与结果处理 ├── libs/ │ ├── glm_ocr.h # GLM-OCR头文件 │ ├── glm_ocr.so # GLM-OCR库文件(Linux示例) │ ├── stb_image.h # 图像加载库 │ └── cJSON.h # JSON库头文件 │ └── cJSON.c # JSON库源文件 ├── input_images/ # 存放待识别的图片 ├── output/ # 程序输出的JSON文件目录 ├── Makefile # 编译脚本(或CMakeLists.txt) └── README.md

现在,打开你的代码编辑器,在src/main.c里,我们先引入必要的头文件,搭个架子。

// src/main.c #include <stdio.h> #include <stdlib.h> #include <string.h> // 引入我们自己的模块 #include "file_utils.h" #include "image_loader.h" #include "ocr_engine.h" int main(int argc, char *argv[]) { printf("GLM-OCR 批量处理工具启动...\n"); // TODO: 解析命令行参数,获取输入文件夹路径 // TODO: 遍历文件夹,获取图片文件列表 // TODO: 循环处理每张图片 // TODO: 将识别结果汇总并输出为JSON文件 printf("处理完成!\n"); return 0; }

框架搭好了,接下来我们逐个击破里面的“TODO”。

2. 核心模块实现:分而治之

我们把大任务拆成几个独立的小模块,这样代码好写,也好维护。

2.1 文件遍历模块

这个模块的任务是:给定一个文件夹路径,找出里面所有支持的图片文件(比如.jpg,.png,.bmp)。

在C语言里,遍历文件夹依赖于操作系统。我们这里以POSIX标准(Linux/macOS)为例,使用<dirent.h>。如果是Windows,需要使用<windows.h>里的FindFirstFile/FindNextFile,原理类似。

// src/file_utils.h #ifndef FILE_UTILS_H #define FILE_UTILS_H // 定义一个结构体来存储文件信息链表 typedef struct ImageFileNode { char file_path[512]; // 文件的完整路径 struct ImageFileNode *next; // 指向下一个节点 } ImageFileNode; // 函数声明 ImageFileNode* find_image_files(const char *dir_path); void free_file_list(ImageFileNode *head); void print_file_list(const ImageFileNode *head); #endif
// src/file_utils.c #include "file_utils.h" #include <dirent.h> #include <string.h> #include <sys/stat.h> #include <stdio.h> #include <stdlib.h> // 检查文件扩展名是否为图片格式 static int is_image_file(const char *filename) { const char *ext = strrchr(filename, '.'); if (!ext) return 0; ext++; // 跳过点号 // 转换为小写进行比较(简单处理,生产环境需更严谨) char lower_ext[10]; for(int i=0; ext[i] && i<9; i++){ lower_ext[i] = tolower(ext[i]); } return (strcmp(lower_ext, "jpg") == 0 || strcmp(lower_ext, "jpeg") == 0 || strcmp(lower_ext, "png") == 0 || strcmp(lower_ext, "bmp") == 0); } // 遍历目录,寻找图片文件 ImageFileNode* find_image_files(const char *dir_path) { DIR *dir = opendir(dir_path); if (!dir) { perror("无法打开目录"); return NULL; } ImageFileNode *head = NULL; ImageFileNode *tail = NULL; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // 跳过当前目录(.)和上级目录(..) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // 构建完整路径 char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); // 检查是否是普通文件且为图片格式 struct stat path_stat; if (stat(full_path, &path_stat) == 0 && S_ISREG(path_stat.st_mode)) { if (is_image_file(entry->d_name)) { // 创建新节点 ImageFileNode *new_node = (ImageFileNode*)malloc(sizeof(ImageFileNode)); if (!new_node) { perror("内存分配失败"); closedir(dir); free_file_list(head); // 清理已分配的内存 return NULL; } strncpy(new_node->file_path, full_path, sizeof(new_node->file_path)-1); new_node->file_path[sizeof(new_node->file_path)-1] = '\0'; new_node->next = NULL; // 添加到链表尾部 if (!head) { head = new_node; tail = new_node; } else { tail->next = new_node; tail = new_node; } } } } closedir(dir); return head; } // 释放文件列表内存 void free_file_list(ImageFileNode *head) { ImageFileNode *current = head; while (current) { ImageFileNode *next = current->next; free(current); current = next; } } // 打印文件列表(调试用) void print_file_list(const ImageFileNode *head) { int count = 0; const ImageFileNode *current = head; while (current) { printf("%d: %s\n", ++count, current->file_path); current = current->next; } printf("共找到 %d 个图片文件。\n", count); }

这个模块的核心是find_image_files函数,它返回一个链表,链表的每个节点都存储了一个图片文件的完整路径。记得用完后要调用free_file_list来释放内存,这是避免内存泄漏的第一步。

2.2 图片加载模块

有了文件路径,下一步就是把图片数据读进内存,转换成GLM-OCR接口能接受的格式。我们用stb_image.h来做这件事,它简单到只需包含一个头文件。

// src/image_loader.h #ifndef IMAGE_LOADER_H #define IMAGE_LOADER_H // 图片数据结构 typedef struct { unsigned char *data; // 图像像素数据 (通常是RGB或RGBA) int width; // 图像宽度 int height; // 图像高度 int channels; // 颜色通道数 (3 for RGB, 4 for RGBA) } ImageData; // 函数声明 ImageData load_image(const char *file_path); void free_image(ImageData *img); #endif
// src/image_loader.c #include "image_loader.h" #define STB_IMAGE_IMPLEMENTATION // 重要:必须在包含头文件前定义这个宏 #include "../libs/stb_image.h" #include <stdio.h> #include <stdlib.h> ImageData load_image(const char *file_path) { ImageData img = {NULL, 0, 0, 0}; // stbi_load会自动分配内存,记得最后要用stbi_image_free释放 // 这里我们强制加载为3通道(RGB),因为大多数OCR模型期望RGB输入 img.data = stbi_load(file_path, &img.width, &img.height, &img.channels, 3); if (!img.data) { fprintf(stderr, "错误:无法加载图片 %s。原因:%s\n", file_path, stbi_failure_reason()); img.channels = 3; // 即使失败,也设置期望的通道数 } else { img.channels = 3; // 因为我们要求加载为3通道 printf("成功加载图片:%s (%d x %d, %d channels)\n", file_path, img.width, img.height, img.channels); } return img; } void free_image(ImageData *img) { if (img && img->data) { stbi_image_free(img->data); img->data = NULL; img->width = img->height = img->channels = 0; } }

stb_image帮我们处理了不同格式图片的解码,返回统一的RGB数据。注意STB_IMAGE_IMPLEMENTATION宏只需要在一个源文件中定义一次。

2.3 OCR引擎与结果处理模块

这是连接我们代码和GLM-OCR库的桥梁。我们在这里初始化OCR引擎、调用识别函数,并把返回的识别结果转换成我们想要的格式。

// src/ocr_engine.h #ifndef OCR_ENGINE_H #define OCR_ENGINE_H #include "../libs/cJSON.h" // 我们使用cJSON来构建结果 // OCR识别结果结构 typedef struct { char **text_lines; // 识别出的文本行数组 int line_count; // 文本行数 float confidence; // 整体置信度(可选) } OcrResult; // 函数声明 int init_ocr_engine(const char *model_path); OcrResult recognize_image(const ImageData *img); void free_ocr_result(OcrResult *result); cJSON* build_json_result(const char *image_path, const OcrResult *result); #endif
// src/ocr_engine.c #include "ocr_engine.h" #include "../libs/glm_ocr.h" // GLM-OCR的C接口头文件 #include <stdio.h> #include <stdlib.h> #include <string.h> // 假设GLM-OCR的句柄和函数(具体函数名需根据实际接口调整) static void* ocr_handle = NULL; int init_ocr_engine(const char *model_path) { // 这里调用GLM-OCR提供的初始化函数 // 例如:ocr_handle = glm_ocr_init(model_path); // 如果实际接口不同,请参照官方文档修改 printf("正在初始化OCR引擎,模型路径:%s\n", model_path); // 模拟初始化成功 ocr_handle = (void*)0x1; // 伪句柄,实际应为库返回的指针 if (ocr_handle) { printf("OCR引擎初始化成功。\n"); return 0; // 成功 } else { fprintf(stderr, "OCR引擎初始化失败。\n"); return -1; // 失败 } } OcrResult recognize_image(const ImageData *img) { OcrResult result = {NULL, 0, 0.0f}; if (!ocr_handle || !img || !img->data) { fprintf(stderr, "错误:OCR引擎未初始化或图像数据无效。\n"); return result; } // 调用GLM-OCR识别函数 // 假设接口函数为:glm_ocr_recognize(handle, image_data, width, height, channels, ...) // 例如: // char** lines = NULL; // int line_count = 0; // float conf = glm_ocr_recognize(ocr_handle, img->data, img->width, img->height, img->channels, &lines, &line_count); printf("正在识别图像 (%d x %d)...\n", img->width, img->height); // --- 模拟识别过程(实际项目中替换为真实调用)--- // 这里我们模拟返回一些结果,用于演示流程 result.line_count = 2; result.confidence = 0.92f; result.text_lines = (char**)malloc(result.line_count * sizeof(char*)); if (result.text_lines) { result.text_lines[0] = strdup("这是第一行识别文本。"); result.text_lines[1] = strdup("这是第二行识别文本。"); } // --- 模拟结束 --- if (result.line_count > 0) { printf("识别完成,找到 %d 行文本。\n", result.line_count); } else { printf("未识别到文本。\n"); } return result; } void free_ocr_result(OcrResult *result) { if (result && result->text_lines) { for (int i = 0; i < result->line_count; i++) { if (result->text_lines[i]) { free(result->text_lines[i]); } } free(result->text_lines); result->text_lines = NULL; result->line_count = 0; result->confidence = 0.0f; } } // 构建单个图片识别结果的JSON对象 cJSON* build_json_result(const char *image_path, const OcrResult *result) { cJSON *img_json = cJSON_CreateObject(); if (!img_json) return NULL; cJSON_AddStringToObject(img_json, "file_path", image_path); cJSON_AddNumberToObject(img_json, "line_count", result->line_count); cJSON_AddNumberToObject(img_json, "confidence", (double)result->confidence); // 添加文本行数组 cJSON *lines_array = cJSON_CreateArray(); if (lines_array) { for (int i = 0; i < result->line_count; i++) { cJSON *line_item = cJSON_CreateString(result->text_lines[i]); cJSON_AddItemToArray(lines_array, line_item); } cJSON_AddItemToObject(img_json, "text_lines", lines_array); } return img_json; }

这个模块里,init_ocr_enginerecognize_image这两个函数需要你根据GLM-OCR库提供的实际C语言API文档进行修改。我这里用注释和模拟数据展示了调用逻辑。重点是build_json_result函数,它使用cJSON库将识别结果构建成一个结构化的JSON对象,这是后续输出到文件的基础。

3. 主程序串联与进阶处理

现在,我们把所有模块像拼图一样组合起来,并在主程序中处理一些进阶问题。

3.1 整合主程序逻辑

回到main.c,我们把之前的TODO填上。

// src/main.c (完整版) #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "file_utils.h" #include "image_loader.h" #include "ocr_engine.h" // 简单的命令行参数解析 void print_usage(const char *program_name) { printf("用法: %s <输入图片目录> [输出JSON文件路径]\n", program_name); printf("示例: %s ./input_images ./output/results.json\n", program_name); } int main(int argc, char *argv[]) { // 1. 解析参数 if (argc < 2) { print_usage(argv[0]); return 1; } const char *input_dir = argv[1]; const char *output_file = (argc >= 3) ? argv[2] : "./output/ocr_results.json"; printf("GLM-OCR 批量处理工具启动...\n"); printf("输入目录: %s\n", input_dir); printf("输出文件: %s\n", output_file); // 2. 初始化OCR引擎 (假设模型在当前目录的`models`文件夹) if (init_ocr_engine("./models/glm_ocr_model.bin") != 0) { fprintf(stderr, "初始化OCR引擎失败,程序退出。\n"); return 1; } // 3. 查找图片文件 printf("\n正在扫描目录: %s\n", input_dir); ImageFileNode *file_list = find_image_files(input_dir); if (!file_list) { fprintf(stderr, "未找到图片文件或目录错误。\n"); // 注意:这里也应该清理OCR引擎资源 return 1; } print_file_list(file_list); // 4. 创建总的JSON结果对象 cJSON *root_json = cJSON_CreateObject(); cJSON_AddStringToObject(root_json, "project", "GLM-OCR Batch Processing"); cJSON_AddStringToObject(root_json, "input_dir", input_dir); cJSON_AddStringToObject(root_json, "output_file", output_file); cJSON_AddStringToObject(root_json, "timestamp", "2023-10-27T10:30:00Z"); // 应使用time库生成 cJSON *images_array = cJSON_CreateArray(); cJSON_AddItemToObject(root_json, "images", images_array); // 5. 遍历并处理每个图片文件 printf("\n开始批量识别处理...\n"); clock_t start_time = clock(); ImageFileNode *current = file_list; int processed_count = 0; int success_count = 0; while (current) { processed_count++; printf("\n[%d/%d] 处理: %s\n", processed_count, 0, current->file_path); // 总文件数需要先统计 // 5.1 加载图片 ImageData img = load_image(current->file_path); if (!img.data) { fprintf(stderr, " 跳过,图片加载失败。\n"); current = current->next; continue; } // 5.2 调用OCR识别 OcrResult ocr_result = recognize_image(&img); // 5.3 构建该图片的JSON结果并添加到数组 cJSON *img_result_json = build_json_result(current->file_path, &ocr_result); if (img_result_json) { cJSON_AddItemToArray(images_array, img_result_json); success_count++; } // 5.4 释放本张图片占用的资源 free_ocr_result(&ocr_result); free_image(&img); printf(" 处理完成。\n"); current = current->next; } clock_t end_time = clock(); double total_time = (double)(end_time - start_time) / CLOCKS_PER_SEC; // 6. 统计信息 cJSON_AddNumberToObject(root_json, "total_files", processed_count); cJSON_AddNumberToObject(root_json, "successful_files", success_count); cJSON_AddNumberToObject(root_json, "processing_time_seconds", total_time); // 7. 将JSON对象写入文件 printf("\n正在将结果写入文件: %s\n", output_file); char *json_str = cJSON_Print(root_json); // 格式化的JSON字符串 if (json_str) { // 确保输出目录存在(简单示例,生产环境需更健壮) FILE *fp = fopen(output_file, "w"); if (fp) { fputs(json_str, fp); fclose(fp); printf("结果已成功保存。\n"); } else { perror("无法打开输出文件"); } free(json_str); } else { fprintf(stderr, "生成JSON字符串失败。\n"); } // 8. 清理所有资源 cJSON_Delete(root_json); free_file_list(file_list); // 这里应调用GLM-OCR的清理函数,例如:glm_ocr_free(ocr_handle); printf("\n======= 处理报告 =======\n"); printf("总处理文件: %d\n", processed_count); printf("成功识别文件: %d\n", success_count); printf("总耗时: %.2f 秒\n", total_time); if (processed_count > 0) { printf("平均每张图片: %.2f 秒\n", total_time / processed_count); } printf("=========================\n"); return 0; }

3.2 编译与运行

我们需要一个Makefile来编译这个项目,因为它链接了多个源文件和外部库。

# Makefile CC = gcc CFLAGS = -Wall -Wextra -O2 -g -I./libs LDFLAGS = -L. -lm -lpthread # 假设GLM-OCR动态库名为libglm_ocr.so LDLIBS = -lglm_ocr TARGET = glm_ocr_batch SRCS = src/main.c src/file_utils.c src/image_loader.c src/ocr_engine.c libs/cJSON.c OBJS = $(SRCS:.c=.o) all: $(TARGET) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(TARGET) $(OBJS) ./output/*.json run: $(TARGET) ./$(TARGET) ./input_images ./output/results.json .PHONY: all clean run

在终端中,进入项目目录,执行以下命令:

# 1. 编译程序 make # 2. 将GLM-OCR的动态库文件(如libglm_ocr.so)放到项目根目录或系统库路径 # 3. 在input_images目录下放入一些测试图片(jpg/png等) # 4. 运行程序 make run # 或者直接运行 # ./glm_ocr_batch ./input_images ./output/my_results.json

如果一切顺利,你会在output目录下看到一个results.json文件,里面包含了所有图片的识别结果。

3.3 进阶话题:错误处理与内存泄漏排查

写C程序,这两点至关重要。

错误处理:上面的代码已经加入了一些基本的错误检查(如文件打开失败、内存分配失败)。在生产环境中,你需要更细致地处理每一种可能的错误,并给出清晰的日志信息,方便定位问题。

内存泄漏排查:C语言需要手动管理内存。我们的代码中,所有通过mallocstrdupcJSON_Create*分配的内存,都必须有对应的freecJSON_Delete。可以使用工具来辅助检查:

  • Valgrind(Linux/macOS):valgrind --leak-check=full ./glm_ocr_batch ./input_images
  • AddressSanitizer(GCC/Clang): 在编译时加上-fsanitize=address标志。

运行这些工具,确保程序结束时没有“definitely lost”的内存块。

3.4 性能基准测试

想知道我们的程序处理速度如何?可以写个简单的测试脚本,或者直接在代码里加入更精细的计时。

// 在main.c的循环处理部分,可以针对每张图片计时 #include <time.h> // ... while (current) { clock_t img_start = clock(); // ... 处理图片的代码 ... clock_t img_end = clock(); double img_time = (double)(img_end - img_start) / CLOCKS_PER_SEC; printf(" 单张图片处理耗时: %.3f 秒\n", img_time); // 可以将img_time也记录到该图片的JSON结果中 // ... }

通过分析单张图片的处理时间,你可以判断瓶颈是在图片加载、OCR识别还是文件I/O上,从而进行有针对性的优化。

4. 总结与展望

走完这一趟,你应该已经拥有了一个功能完整的C语言批量图片OCR处理工具。从遍历文件夹、加载图片,到调用底层AI接口、结构化输出结果,我们覆盖了一个典型离线批处理任务的核心流程。

过程中,我们刻意强调了C语言编程中那些“琐碎”但关键的部分:手动管理内存、细致的错误检查、跨平台的文件操作考量。这些正是从“写个小demo”到“开发可靠工具”必须跨越的坎儿。

这个程序本身还有很多可以打磨的地方。比如,你可以加入多线程来并行处理图片,大幅提升吞吐量;可以设计更灵活的配置文件,来指定模型路径、支持的文件格式、输出格式等;还可以将结果直接导入数据库,或者增加一个简单的进度条来提升用户体验。

最重要的是,你现在有了一个清晰、模块化的代码框架。无论是想接入另一个OCR引擎,还是想把输出格式从JSON改成XML,或者增加图片预处理步骤,你都知道该去修改哪个文件里的哪个函数了。这就是结构化编程带来的好处。

希望这个实践能帮你更深入地理解如何用C语言驾驭AI模型,完成实实在在的工程任务。代码的世界,动手搭一遍,比看十遍都管用。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

http://www.cnnetsun.cn/news/1551228.html

相关文章:

  • 终极番茄小说下载器:Rust重构的高效电子书下载解决方案
  • 基于Matlab的语音信号加密解密传输系统:支持GUI界面与自定义密码保护
  • YOLOv9镜像快速上手:一行命令跑通推理,小白也能玩转目标检测
  • 3分钟上手!AI驱动的代码学习助手完全指南
  • Qwen2-VL-2B-Instruct应对“耦合过度”设计:从UML图中识别代码坏味道
  • 基于DWS构建RAG框架生成行业调研报告
  • PMSM无感ActiveFlux仿真模型:基于电流误差补偿的相电压重构与延时相角补偿技术实现及...
  • RCS调度系统:从架构蓝图到智能决策的AGV指挥中枢
  • FastAPI 2.0异步流式AI服务上线前必做的7项压力测试:并发流数、断连重试率、token吞吐拐点、内存增长斜率…(附自动化测试脚本)
  • 架构革新与纯粹体验:铜钟音乐平台的现代Web音频解决方案
  • 手机玩转Kali必看:Termux环境完整避坑指南(含文件校验/环境变量设置)
  • OpenClaw监控告警系统:Qwen3-32B-Chat实时日志分析
  • vLLM-v0.17.1技术解析:PagedAttention内存管理与显存优化技巧
  • Alpamayo-R1-10B详细步骤:从supervisorctl服务管理到日志实时监控
  • HY-Motion 1.0在医疗康复中的应用:患者动作评估与指导系统
  • 小白友好!Ollama部署GLM-4.7-Flash常见问题解决
  • M2LOrder模型实战:基于.NET框架的桌面端AI助手开发
  • AIGlasses OS Pro效果实测:纯本地视觉辅助系统,四大模式惊艳展示
  • 06_gstack发布运营:一键发布与文档同步机制
  • 如何通过md2pptx实现Markdown到PPT的高效转换与自动化办公
  • LabWindows/CVI文本框控件实战:从显示Hello World到动态时间更新
  • 构建边缘AI小语言模型
  • Qwen3.5-4B模型网络协议分析应用:模拟客户端与解析通信数据
  • 如何摆脱Armoury Crate的困扰?GHelper带来的轻量高效深度控制革命
  • 基于OpenCV与QT开发的卡尺工具:工具跟随、自动纠偏、图像处理与形状匹配集成应用
  • 一文讲透|盘点2026年全网爆红的一键生成论文工具
  • 零基础入门WeKnora:手把手教你搭建精准问答系统,告别AI幻觉
  • 东方美学人像生成神器:Asian Beauty Z-Image Turbo快速入门与实战体验
  • 核桃剥壳机的设计【说明书、11张CAD图纸、SW三维图、通用三维格式、外文翻译】 去壳机设计
  • 通义千问2.5-0.5B-Instruct汽车维修:故障代码解释系统实战