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

【C语言进阶】文件操作

文件操作

    • 1. 为什么使用文件
    • 2. 什么是文件
      • 2.1程序文件
      • 2.2 数据文件
      • 2.3 文件名
    • 3. 文件的打开和关闭
      • 3.1 文件指针
      • 3.2 文件的打开和关闭
    • 4. 文件的顺序读写
      • 4.1 对比一组函数
    • 5. 文件的随机读写
      • 5.1 fseek
      • 5.2 ftell
      • 5.3 rewind
    • 6. 文本文件和二进制文件
    • 7. 文件读取结束的判定
      • 7.1 被错误使用的feof
    • 8. 文件缓冲区

1. 为什么使用文件

我们前面学习结构体时,写了通讯录的程序,当通讯录运行起来的时候,可以给通讯录中增加、删除数据,此时数据是存放在内存中,当程序退出的时候,通讯录中的数据自然就不存在了,等下次运行通讯录程序的时候,数据又得重新录入,如果使用这样的通讯录就很难受。
我们在想既然是通讯录就应该把信息记录下来,只有我们自己选择删除数据的时候,数据才不复存在。这就涉及到了数据持久化的问题,我们一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。
使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化

2. 什么是文件

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

2.1程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

2.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章讨论的是数据文件。

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。

2.3 文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名

3. 文件的打开和关闭

3.1 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

在这里插入图片描述

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

struct _iobuf {char* _ptr;int _cnt;char* _base;int _flag;int _file;int _charbuf;int _bufsiz;char* _tmpfname;
};
typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

下面我们可以创建一个FILE*的指针变量:

FILE* pf;//文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
在这里插入图片描述

3.2 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

//打开文件
FILE * fopen ( const char * filename, const char * mode );

在这里插入图片描述

//关闭文件
int fclose ( FILE * stream );

在这里插入图片描述
打开方式如下:
在这里插入图片描述

代码实例:
int main()
{//打开文件FILE* pf = fopen("data.txt", "r");if (pf == NULL){perror("pc");return 1;}//读文件//关闭文件fclose(pf);pf = NULL;return 0;
}

4. 文件的顺序读写

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//写入一个字符
//写文件    fputc('a', pf);

在这里插入图片描述

//读取一个字符int ch = fgetc(pf);if (ch != EOF){printf("%c\n", ch);}
//连续每次读取一个字符
//文件中有abcdefgint ch = fgetc(pf);printf("%c\n", ch);	//ach = fgetc(pf);printf("%c\n", ch);	//bch = fgetc(pf);printf("%c\n", ch);	//cch = fgetc(pf);printf("%c\n", ch);	//d

在这里插入图片描述

//覆盖并写入一行数据:fputs("hello world", pf);v

在这里插入图片描述

//读取指定长度的数据
//定一一个数组char arr[10] = { 0 };fgets(arr, 5, pf);    //将所读取的数据放入arr中printf("%s\n", arr);

在这里插入图片描述

//将结构体信息写入文件中
//这里的结构体信息就是格式化的,那么就需要fprintf()函数了
#include<stdio.h>typedef struct S
{char name[10];int age;}Peo;
int main()
{FILE* pf = fopen("test.txt", "w");if (pf != NULL){Peo p = { "zhangsan", 18 };fprintf(pf, "%s %d\n", p.name, p.age);fclose(pf);pf = NULL;}return 0;
}

在这里插入图片描述

//读取文件信息到结构体变量中
#include<stdio.h>
typedef struct S
{char name[10];int age;
}Peo;
int main()
{FILE* pf = fopen("test.txt", "r");if (pf != NULL){Peo p = { 0 };fscanf(pf, "%s %d", p.name, &p.age);printf("%s %d", p.name, p.age);fclose(pf);pf = NULL;}return 0;
}

二进制输出
在这里插入图片描述

#include<stdio.h>
#include<string.h>
#include<errno.h>
typedef struct S
{char name[10];int age;
}Peo;
int main()
{FILE* pf = fopen("test.txt", "wb+");if (pf != NULL){Peo p = { "lisi", 19};fwrite(&p, sizeof(Peo), 1, pf);fclose(pf);pf = NULL;}return 0;
}

二进制输入
在这里插入图片描述

#include<stdio.h>
typedef struct S
{char name[10];int age;
}Peo;
int main()
{FILE* pf = fopen("test.txt", "rb+");if (pf != NULL){Peo p = { 0 };fread(&p, sizeof(Peo), 1, pf);printf("%s %d\n", p.name, p.age);fclose(pf);pf = NULL;}return 0;
}

4.1 对比一组函数

scanf/fscanf/sscanf
printf/fprintf/sprintf

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

5. 文件的随机读写

5.1 fseek

根据文件指针的位置和偏移量来定位文件指针

int fseek ( FILE * stream, long int offset, int origin );

在这里插入图片描述

#include <stdio.h>
int main()
{FILE* pFile;pFile = fopen("example.txt", "wb");fputs("This is an apple.", pFile);fseek(pFile, 9, SEEK_SET);fputs(" sam", pFile);fclose(pFile);return 0;
}

5.2 ftell

返回文件指针相对于起始位置的偏移量

long int ftell ( FILE * stream );

在这里插入图片描述

#include <stdio.h>
int main()
{FILE* pFile;long size;pFile = fopen("myfile.txt", "rb");if (pFile == NULL) perror("Error opening file");else{fseek(pFile, 0, SEEK_END); // non-portablesize = ftell(pFile);fclose(pFile);printf("Size of myfile.txt: %ld bytes.\n", size);}return 0;
}

5.3 rewind

让文件指针的位置回到文件的起始位置

void rewind ( FILE * stream );

在这里插入图片描述

#include <stdio.h>
int main()
{int n;FILE* pFile;char buffer[27];pFile = fopen("myfile.txt", "w+");for (n = 'A'; n <= 'Z'; n++)fputc(n, pFile);rewind(pFile);fread(buffer, 1, 26, pFile);fclose(pFile);buffer[26] = '\0';puts(buffer);return 0;
}

6. 文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文
本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而
二进制形式输出,则在磁盘上只占4个字节(VS2013测试)

在这里插入图片描述
由此可见:对于10000在数,如果以二进制形式存储占用4个字节,如果以ASCII码存储占用5个字节。试想:那对于数字1呢?

显而易见,二进制文件存储和文本文件存储对不同范围的数字可以做到节省空间。

#include<stdio.h>int main()
{FILE* pf = fopen("test.txt", "wb");int a = 10000;if (pf != NULL){fwrite(&a, 4, 1, pf);fclose(pf);pf = NULL;}return 0;
}

对于上面这段代码,我们知道是将数值10000放入了test.txt文件中,但我们无法直接看到它在文件中的真实值,于是使用vs的二进制编辑器即可查看:

在这里插入图片描述

7. 文件读取结束的判定

7.1 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

  1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
    例如:
    fgetc 判断是否为 EOF .
    fgets 判断返回值是否为 NULL .
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    fread判断返回值是否小于实际要读的个数。

文本文件案例:

#include <stdio.h>
#include <stdlib.h>
int main(void)
{int c; // 注意:int,非char,要求处理EOFFILE* fp = fopen("test.txt", "r");if (!fp) {perror("File opening failed");return EXIT_FAILURE;}//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOFwhile ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环{putchar(c);}//判断是什么原因结束的if (ferror(fp))puts("I/O error when reading");else if (feof(fp))puts("End of file reached successfully");fclose(fp);
}

二进制文件案例:

#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{double a[SIZE] = { 1.,2.,3.,4.,5. };FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组fclose(fp);double b[SIZE];fp = fopen("test.bin", "rb");size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组if (ret_code == SIZE) {puts("Array read successfully, contents: ");for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);putchar('\n');}else { // error handlingif (feof(fp))printf("Error reading test.bin: unexpected end of file\n");else if (ferror(fp)) {perror("Error reading test.bin");}}fclose(fp);
}

8. 文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
在这里插入图片描述

代码案例:
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{FILE* pf = fopen("test.txt", "w");fputs("abcdef", pf);//先将代码放在输出缓冲区printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");Sleep(10000);printf("刷新缓冲区\n");fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)//注:fflush 在高版本的VS上不能使用了printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");Sleep(10000);fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区pf = NULL;return 0;
}

这里可以得出一个结论

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

相关文章:

【C语言进阶】文件操作

文件操作 1. 为什么使用文件2. 什么是文件2.1程序文件2.2 数据文件2.3 文件名 3. 文件的打开和关闭3.1 文件指针3.2 文件的打开和关闭 4. 文件的顺序读写4.1 对比一组函数 5. 文件的随机读写5.1 fseek5.2 ftell5.3 rewind 6. 文本文件和二进制文件7. 文件读取结束的判定7.1 被错…...

Redis学习(第八章缓存策略)

目录 RdisExample 课程介绍 1.Redis介绍 2.Redis 安装 3. Redis的数据结构 4. Redis缓存特性 5. Redis使用场景 6. Redis客户端-Jedis 7. Jedis Pipeline 8. Redis缓存策略 学习资料 QA 相关问题 http, socket ,tcp的区别 RdisExample 项目代码地址&#xff1a;htt…...

springboot+vue开发的视频弹幕网站动漫网站

springbootvue开发的视频弹幕网站动漫网站 演示视频 https://www.bilibili.com/video/BV1MC4y137Qk/?share_sourcecopy_web&vd_source11344bb73ef9b33550b8202d07ae139b 功能&#xff1a; 前台&#xff1a; 首页&#xff08;猜你喜欢视频推荐&#xff09;、轮播图、分类…...

【CSS】常见 CSS 布局

1. 响应式布局 <!DOCTYPE html> <html><head><title>简单的响应式布局</title><style>/* 全局样式 */body {font-family: Arial, sans-serif;margin: 0;padding: 0;}/* 头部样式 */header {background-color: #333;color: #fff;padding: …...

数据结构---HashMap和HashSet

HashMap和HashSet都是存储在哈希桶之中&#xff0c;我们可以先了解一些哈希桶是什么。 像这样&#xff0c;一个数组数组的每个节点带着一个链表&#xff0c;数据就存放在链表结点当中。哈希桶插入/删除/查找节点的时间复杂度是O(1) map代表存入一个key值&#xff0c;一个val值…...

SLAM中相机姿态估计算法推导基础数学总结

相机模型 基本模型 内参 外参 对极几何 对极约束 外积符号 基础矩阵F和本质矩阵E 相机姿态估计问题分为如下两步: 本质矩阵 E t ∧ R Et^{\wedge}R Et∧R因为 t ∧ t^{\wedge} t∧其实就是个3x3的反对称矩阵&#xff0c;所以 E E E也是一个3x3的矩阵 用八点法估计E…...

【RS】遥感影像/图片64位、16位(64bit、16bit)的意义和区别

在数字图像处理中&#xff0c;我们常常会听到不同的位数术语&#xff0c;比如64位、16位和8位&#xff08;64bit、16bit、8bit&#xff09;。这些位数指的是图像的深度&#xff0c;也就是图像中每个像素可以显示的颜色数。位数越高&#xff0c;图像可以显示的颜色数就越多&…...

【单元测试】--基础知识

一、什么是单元测试 单元测试是软件开发中的一种测试方法&#xff0c;用于验证代码中的单个组件&#xff08;通常是函数、方法或类&#xff09;是否按预期工作。它旨在隔离和测试代码的最小单元&#xff0c;以确保其功能正确&#xff0c;提高代码质量和可维护性。单元测试通常…...

golang 反射机制

在 go 语言中&#xff0c;实现反射能力的是 reflect包&#xff0c;能够让程序操作不同类型的对象。其中&#xff0c;在反射包中有两个非常重要的 类型和 函数&#xff0c;两个函数分别是&#xff1a; reflect.TypeOfreflect.ValueOf 两个类型是 reflect.Type 和 reflect.Value…...

【Javascript】创建对象的几种方式

通过字面量创建对象 通过构造函数创建对象 Object()-------------构造函数 通过构造函数来实例化对象 给person注入属性 Factory工厂 this指向的是对象的本身使⽤new 实例化⼀个对象&#xff0c;就像⼯⼚⼀样...

深度学习_3_实战_房价预测

梯度 实战 代码&#xff1a; # %matplotlib inline import random import torch import matplotlib.pyplot as plt # from d21 import torch as d21def synthetic_data(w, b, num_examples):"""生成 Y XW b 噪声。"""X torch.normal(0,…...

HCIA -- 动态路由协议之RIP

一、静态协议的优缺点&#xff1a; 缺点&#xff1a; 1、中大型网络配置量过大 2、不能基于拓扑的变化而实时的变化 优点&#xff1a; 1、不会额外暂用物理资源 2、安全问题 3、计算路径问题 简单、小型网络建议使用静态路由&#xff1b;中大型较复杂网络&#xff0c;建议使用…...

JS常用时间操作moment.js参考文档

Moment.js是一个轻量级的JavaScript时间库&#xff0c;它方便了日常开发中对时间的操作&#xff0c;提高了开发效率。日常开发中&#xff0c;通常会对时间进行下面这几个操作&#xff1a;比如获取时间&#xff0c;设置时间&#xff0c;格式化时间&#xff0c;比较时间等等。下面…...

基于 FFmpeg 的跨平台视频播放器简明教程(九):Seek 策略

系列文章目录 基于 FFmpeg 的跨平台视频播放器简明教程&#xff08;一&#xff09;&#xff1a;FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程&#xff08;二&#xff09;&#xff1a;基础知识和解封装&#xff08;demux&#xff09;基于 FFmpeg 的跨平台视频…...

设计模式_备忘录模式

备忘录模式 介绍 设计模式定义案例问题堆积在哪里解决办法备忘录模式行为型模式&#xff0c; 保存了数据某一个时间点的状态 在需要的时候进行回档单机游戏的角色 数据保存并且回档保存和回档加一个状态管理类 类图 代码 MomentData using UnityEngine;public class MomentD…...

双势阱模型

双势阱模型 原子钟 传统的原子钟利用氨分子 由于隧道效应&#xff0c;上顶点的氮原子可以贯穿三个氢原子形成的势垒&#xff0c;到达下顶点对体系注入微波能量后&#xff0c;氮原子在上下定点之间振荡&#xff0c;体系的能量在两个稳定态之间交替变换&#xff0c;其振荡频率决…...

文献阅读:The Reversal Curse: LLMs trained on “A is B” fail to learn “B is A”

文献阅读&#xff1a;The Reversal Curse: LLMs trained on “A is B” fail to learn “B is A” 1. 文章简介2. 实验 & 结果考察 1. finetune实验2. 真实知识问答 3. 结论 & 思考 文献链接&#xff1a;https://arxiv.org/abs/2309.12288 1. 文章简介 这篇文章是前…...

真实感受:是智能家居在选择合适的技术!

科技从来都是为了让我们的生活更加的简单、舒适&#xff0c;而智能家居的智能&#xff0c;体现在如何更更更方便的使用我需要控制的家居。 例如&#xff1a;下班躺在床上想休息&#xff0c;房间和大厅的灯还开着&#xff0c;这时你会选择什么产品躺着解决问题&#xff1f; 红外…...

前端 TS 快速入门之二:接口

1. 接口有什么用 通过 interface 定义接口。 检测对象的属性&#xff0c;不会去检查属性的顺序&#xff0c;只要相应的属性存在并且类型也是对的就可以。 interface IPerson {name: string;age: number; } function say(person: IPerson): void {console.log(my name is ${pers…...

论文生成器(论文、文献综述、开题报告……),Java、Python、C++

“让论文生成器为您省时省力&#xff0c;轻松写出高质量的论文&#xff01;” 2022年&#xff0c;腾讯全球数字生态大会腾讯云智能专场发布。 链接&#xff1a;http://xiezuo.saiertewl.cn/tb/xrWQed?dCodeh1xDrXmuhZbKPKgI&couponCodexiaoweilunwen...

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试

作者&#xff1a;Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位&#xff1a;中南大学地球科学与信息物理学院论文标题&#xff1a;BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接&#xff1a;https://arxiv.…...

uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖

在前面的练习中&#xff0c;每个页面需要使用ref&#xff0c;onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入&#xff0c;需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

AI语音助手的Python实现

引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...