您的位置:首页 > 编程语言 > Java开发

Java小白修炼手册--第二阶段 Java SE--File、RandomAccessFile

2020-06-06 06:59 573 查看

目录

 

File:

常用方法:

FileFilter接口:

巩固练习:

递归:

lambda 表达式:

RandomAccessFile:

概述:

只读模式:

读写模式:

字节数据读写操作:

综合练习:

File:

  1. java.io.File用于表示文件(目录) , 也就是说程序员可以通过File类在程序中操作硬盘上的文件和目录。
  2. File类只用于表示文件(目录)的信息(名称、大小等)不能对文件的内容进行访问。
  3. 构造方法:- File(String pathname)通过将给定路径名字符串转换成抽象路径名来创建一个新File 实例,抽象路径应尽量使用相对路径,并且目录的层级分隔符不要直接写”/"或”\”,应使用File.separator这个常量表示,以避免不同系统带来的差异。"./"是当前目录的意思,而eclipse中运行程序时,当前目录是指的当前程序所在目录
  4. 其每一个实例就用于表示一个文件或目录。

常用方法:

File表示文件信息:

方法 说明
String getName( ) 获取文件的名字
long length( ) 获取文件的大小(单位: Byte字节)
boolean canRead( ) 文件是否可读
boolean canWrite( ) 文件是否可写
boolean isHidden( ) 文件是否隐藏
boolean exists ( ) 文件是否存在
 booelan isLife() 判断当前File表示是否为一个文件
createNewFile( ) 创建文件
delete( ) 删除文件

 

 

 

 

 

 

 

 

 

 

[code]public class FileDemo {
public static void main(String[] args) {
/*
* 绝对路径不推荐,不能很好的做到跨平台使用
*/
//		File file = new File("F:/mywork/eclipse-workspace/JSD2003_SE/demo.txt");
/*
* 相对路径可以更好的实现跨平台。
* "./"是当前目录的意思,而eclipse中运行程序时,当前目录指的是当前程序
* 所在的项目目录。这里相当于是JSD2003_SE这个项目的目录。
*/
File file = new File("./demo.txt");

String name = file.getName();
System.out.println("文件名:"+name);

long len = file.length();
System.out.println("大小:"+len+"字节");

boolean cr = file.canRead();
System.out.println("可读:"+cr);

boolean cw = file.canWrite();
System.out.println("可写:"+cw);

boolean ih = file.isHidden();
System.out.println("是否隐藏:"+ih);
}
}
[code]public class CreateNewFileDemo {
public static void main(String[] args) throws IOException {
/*
* 在当前目录下新建一个文件:test.txt
*/
File file = new File("./test.txt");
/*
* boolean exists()
* 该方法是用来判断当前File表示的路径对应的文件或目录是否已经存在
* 存在则返回true
*/
if(file.exists()) {
System.out.println("该文件已存在!");
}else {
//在File表示的路径上将该文件真实创建出来
file.createNewFile();
System.out.println("文件已创建!");
}

}
}

 

[code]public class DeleteFileDemo {
public static void main(String[] args) {
/*
* 将当前目录下的test.txt文件删除
*/
File file = new File("./test.txt");
if(file.exists()) {
file.delete();
System.out.println("文件已删除!");
}else {
System.out.println("文件不存在!");
}

}
}

 

File表示目录信息:

方法 说明
boolean isDirectory() 判断当前File是否为目录
mkdir() 创建目录
mkdirs() 创建多级目录
delete() 删除目录(目录为空才能删除)
File[] listFies() File[] listFies()
boolean exists() 判断该目录是否存在

 

 

 

 

 

 

 

 

[code]public class MkDirDemo {
public static void main(String[] args) {
/*
* 在当前目录下新建目录:demo
*/
//		File file = new File("./demo");

File file = new File("./a/b/c/d/e/f");
if(file.exists()) {
System.out.println("目录已存在!");
}else {
/*
* mkdir方法创建目录的前提是该目录所在的目录必须存在,否则无法创建
* mkdirs方法创建时会将所有不存在的父目录自动创建出来。
*/
//			file.mkdir();//创建该目录
file.mkdirs();
System.out.println("目录已创建!");
}
}
}

 

[code]public class DeleteDirDemo {
public static void main(String[] args) {
/*
* 删除当前目录下的demo目录
*/
//		File file = new File("./demo");
File file = new File("./a");
if(file.exists()) {
/*
* delete方法在删除目录时有一个前提条件,就是该目录必须是一个
* 空目录,否则无法删除
*/
file.delete();
System.out.println("目录已删除!");
}else {
System.out.println("目录不存在!");
}

}
}

FileFilter接口:

FileFilter用于抽象路径名的过滤器

此接口的实例可传递给File 类的listFiles(FileFilter)方法。用于返回满足该过滤器要求的子项。

File[] listFiles(FileFilter filter)

过滤器accept方法,是用来定义过滤规则:
当参数file的文件或目录符合要求时该方法应当返回为true,否则返回为false
将返回true 的所有子项返回

[code]public class ListFilesDemo {
public static void main(String[] args) {
/*
* 访问当前目录下的所有子项
*/
File file = new File(".");
/*
* boolean isFile()
* 判断当前File表示的是否为一个文件
*
* boolean isDirectory()
* 判断当前File表示的是否为一个目录
*/
if(file.isDirectory()) {//首先判断是否为一个
/*
* File[] listFiles()
* 获取所有子项,数组长度由目录中的子项个数决定。每个元素是一个子项
*/
File[] subs = file.listFiles();
System.out.println(subs.length);
for(int i=0;i<subs.length;i++) {
File sub =  subs[i];
System.out.println(sub.getName());
}
}

}
}
[code]public class ListFilesDemo2 {
public static void main(String[] args) {
/*
* 获取当前目录下所有名字以"."开始的子项
*/
File file = new File(".");
if(file.isDirectory()) {
//			MyFilter filter = new MyFilter();

//匿名内部类的写法
FileFilter filter = new FileFilter() {
public boolean accept(File file) {
return file.getName().startsWith(".");
}
};

/*
* 重载的listFiles方法在获取该目录中所有子项时,会先将每一个子项
* (File对象)都调用一次给定的过滤器的accept方法,并最终将返回为
* 为true的所有子项返回。
*/
File[] subs = file.listFiles(filter);
System.out.println(subs.length);
for(int i=0;i<subs.length;i++) {
File sub = subs[i];
System.out.println(sub.getName());
}
}
}
}

class MyFilter implements FileFilter{
/**
* accept方法是用来定义过滤规则的。
* 当参数file表示的文件或目录符合过滤要求时该方法应当返回为true,否则返回
* 为false
*/
public boolean accept(File file) {
//名字是以"."开始的
return file.getName().startsWith(".");
}
}

巩固练习:

输出当前目录下所有文本文件(后缀为.txt)

[code]public class Test {
public static void main(String[] args) {
File file = new File(".");
if(file.isDirectory()) {
FileFilter filter = new FileFilter() {
public boolean accept(File f) {
return f.getName().endsWith(".txt");
}
};
File[] subs = file.listFiles(filter);
for(int i=0;i<subs.length;i++) {
File sub = subs[i];
System.out.println(sub.getName());
}
}
}
}

递归:

      删除一个目录
      递归:是整个流程重复执行
      递归在开发中尽量避免,开销大,运行慢
      使用递归的注意事项:
      1.方法内部调用当前方法称为递归
      2.递归调用必须依靠一个分支语句,不能必定执行,否则是死循环
      3.递归的层级尽可能少(开销大)     

[code]public class Test2 {
public static void main(String[] args) {
File dir = new File("./a");
delete(dir);
}

public static void delete(File file) {
/*
* 删除时要判定file表示的是否为一个目录,要是目录需要先清空目录(先把所有
* 子项删除),然后再删除该目录。
* 如果file表示的是一个文件,则直接删除
*/
if(file.isDirectory()) {
//先清空该目录
File[] subs = file.listFiles();//先获取该目录中的所有子项
for(int i=0;i<subs.length;i++) {
File sub = subs[i];
//一个方法内部调用自己这个方法的现象称为递归调用
delete(sub);
}
}
file.delete();
}
}

补充:

lambda 表达式:

lambda 表达式 JDK8退出后一个新特性
lambda 可以使更简短的语法定义匿名内部类
不是所有方法使用匿名内部类的情况都可以用lambda 代替,只有实现的接口中仅有一个抽象方法时,才可以使用lambda.
 语法:
 (参数列表)->{
    方法体代码
  }

[code]public class LambdaDemo {

public static void main(String[] args) {

FileFilter filter = new FileFilter(){
public boolean accept(File file) {
return file.getName().endsWith(".txt");
}

};
/*
* lambda 表达式是java编译器认可的,最终会被编译器改为内部类创建
* 并且编译器会结合程序上下问分析出实际上实例化是哪个类
* 以及重写的是哪个方法(因此接口只要求一个抽象方法,因此肯定是该方法)
* 并且方法的参数列表无须再定义参数的类型,只需要指定形参名即可
*
*/
FileFilter filter2 = (file)->{
return file.getName().endsWith(".txt");
};

/*
* 当lambda 表达式只有一句代码时,我们可以忽略"{}",如果这句代码含有
* return 关键字 ,那么 return 关键字必须忽略.
*/

FileFilter filter3 = (file)-> file.getName().endsWith(".txt");

File dir = new File(".");
//File[] sub = file.listFiles(filter3);
File[] sub = dir.listFiles(
(file)-> file.getName().endsWith(".txt")
);
System.out.println(sub.length);

RandomAccessFile:

概述:

java.io.RandomAccessFile
RAF是用来读写文件的API,其基于指针可以对文件任意位置进行读写操作。RandomAccessFile在对文件进行随机访问操作时有两个模式,分别为只读模式(只读取文件数据) ,和读写模式(对文件数据进行读写)。

只读模式:

在创建RandomAccessFile时,其提供的构造方法要求我们传入访问模式:
RandomAccessFile(File file,String mode)
RandomAccessFile(String filename,String mode)
-其中构造方法的第一个参数是需要访问的文件,而第二个参数则是访问模式:
"r” :字符串”r”表示对该文件的访问是只读的。

读写模式:

创建一个基于文件访问的读写模式的RandomAccessFile我们只需要在第二个参数中传入”rw"即可
RandomAccessFile raf = new RandomAccessFile(file,"' rw" );
那么这时在使用RandomAccessFile对该文件的访问就是又可读又可写的。

字节数据读写操作:

注:如果指定的文件不存在,那么当操作模式为读写模式时,会自动创建该文件, 如果是只读模式则会抛出异常(系统找不到指定文件) 

常用方法:

方法 功能
write( int  b) 写一个字节
int read() 读取1个字节,并以int形式返回。如果返回的int值为-1则表示文件末尾
write(byte[ ] b) 该方法会根据当前指针所在位置处连续写出给定数组中的所有字节。
void write(byte[] d,int offset,int len) 该方法会根据当前指十所在位置处连续写出给定数组中的部分字节,这个部分是从数组的offset处开始,连续len个字节,
read(byte[ ] b) 该方法会从指十位置处尝试最多读取给定数组的总长度的字节量,并从给定的字节数组第一个位置开始,将读取到的字节顺序存放至数组中,返回值为实际读取到的字节量
 
long length() 文件中字节的长度
byte[ ] getBytes(String csn) 将 当前字符串按照指定的字符集转换为字节(String提供)
String(byte[ ] data,String csn) 将给定的字节数组按照指定的字符集还原为字符串(String提供)
long getFilePointer() 获取当前RAF的指针位置(从o开始)
writelnt() 连续4个字节将int值写出(其余类型都可以写出)
seek(int index) 移动指针到指定位置
close( ) RandomAccessFile在对文件访问的操作全部结束后,要调用close()方法来释放与其关联的所有系统资源。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

方法实例:

1.write( )

[code]public class RAFDemo1 {
public static void main(String[] args) throws IOException {
/*
* 当前目录下的raf.dat文件操作
* 注:如果指定的文件不存在,那么当操作模式为读写模式时,会自动创建该文件
* 如果是只读模式则会抛出异常(系统找不到指定文件)
*/
RandomAccessFile raf = new RandomAccessFile("./raf.dat","rw");

/*
* 向文件中写入1个字节
* void write(int d)
* 写入的是给定的int值所对应的2进制的"低八位"
* 整数1,二进制:
*                            vvvvvvvv
* 00000000 00000000 00000000 00000001
*/
raf.write(1);//00000001
//多次调用write方法,可以写多个字节
raf.write(2);//00000010
raf.write(13);//00001101
/*
* 写完后,文件中的2进制为:
* 00000001 00000010 00001101
*/
System.out.println("写出完毕!");

raf.close();//读写完毕后必须关闭
}
}

2.int read( )

[code]public class RAFDemo2 {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("./raf.dat","r");
/*
* raf.dat文件中的内容:
* 00000001 00000010 00001101
*
* int read()
* 读取1个字节,并以int形式返回。如果返回的int值为-1则表示文件末尾
*/
//00000000 00000000 00000000 00000001
int d = raf.read();
System.out.println(d);//1
//00000000 00000000 00000000 00000010
d = raf.read();
System.out.println(d);//2
//00000000 00000000 00000000 00001101
d = raf.read();
System.out.println(d);//13

d = raf.read();
System.out.println(d);//-1

raf.close();
}
}

3.文件复制

[code]public class CopyDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile src = new RandomAccessFile("./movie.mkv","r");

RandomAccessFile desc = new RandomAccessFile("./movie_cp.mkv","rw");

int d = 0;//用来记录每次读取到的字节
long start = System.currentTimeMillis();
while( (d = src.read()) != -1  ) {
desc.write(d);
}
long end = System.currentTimeMillis();
System.out.println("复制完毕!耗时:"+(end-start)+"ms");
src.close();
desc.close();
}
}

 

4.int read(byte[ ] data) 与void write(byte[ ] data)< 块读与块写>

 提高每次读写的数据量减少实际读写的次数可以提高读写效率
 * 一次读取一组字节称为:块读取
 * 一次写出一组字节称为:块写出

[code]public class CopyDemo2 {
public static void main(String[] args) throws IOException {
RandomAccessFile src = new RandomAccessFile("./movie.mkv","r");
RandomAccessFile desc = new RandomAccessFile("./movie_cp.mkv","rw");
/*
* RandomAccessFile提供了一次读写一组字节的方法
* int read(byte[] data)
* 一次性读取给定的字节数组总长度的字节量,并将读取到的字节放入该数组中
* 此时返回值表示实际读取到的字节数,如果返回值为-1则表示文件末尾
* 注意:
* 这里的返回值int不再是实际读取到的字节内容,而仅表示读取到的字节数。
* 实际读取到的内容存在数组里了。
*
* void write(byte[] data)
* 一次性将给定的字节数组中的所有字节写入文件
*
* void write(byte[] data,int start,int len)
* 将给定的字节数组从下标start处的连续len个字节一次性写入文件		 *
*/
/*
* 8位2进制      1byte
* 1024byte  1kb
* 1024kb    1mb
* 1024mb    1gb
* 1024gb	 1tb
*/
byte[] buf = new byte[1024*10];//10kb
int len = 0;//记录每次读取到的字节数
long start = System.currentTimeMillis();
while((len = src.read(buf)) != -1) {
desc.write(buf,0,len);
}
long end = System.currentTimeMillis();
System.out.println("复制完毕,耗时:"+(end-start)+"ms");

src.close();
desc.close();
}
}

5.byte getBytes(String csn)
 
将当前字符串按照指定的字符集转换为一组字节 

[code]public class WriteStringDemo {
public static void main(String[] args) throws IOException {
//相对路径中的"./"可以不写,不写默认就是./
RandomAccessFile raf = new RandomAccessFile("raf.txt","rw");
String str = "回手,掏~";
/*
* String提供了将字符串转换为字节的方法:
* byte getBytes()
* 当当前字符串按照系统默认的字符集转换为一组字节
*
* byte getBytes(String csn)
* 将当前字符串按照指定的字符集转换为一组字节
*/
byte[] data = str.getBytes("utf-8");
raf.write(data);

str = "呦~~~难受~";
data = str.getBytes("utf-8");
raf.write(data);

System.out.println("写出完毕!");
raf.close();
}
}

6.String(byte[] data,String csn)
   将给定的字节数组中所有字节按照给定的字符集还原为字符串

[code]public class ReadStringDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("note.txt","r");

byte[] data = new byte[(int)raf.length()];

raf.read(data);
/*
* String提供的构造方法:
* String(byte[] data,String csn)
* 将给定的字节数组中所有字节按照给定的字符集还原为字符串
*/
String str = new String(data,"UTF-8");

System.out.println(str);

raf.close();

}
}

RAF读写基本类型数据,以及基于指针的读写操作:

[code]public class RAFDemo3 {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("raf.dat","rw");
/*
* long getFilePointer()
* 获取当前面RAF的指针位置
*/
long pos = raf.getFilePointer();
System.out.println("pos:"+pos);//0

/*
* 将int最大值写入文件
* int最大值的2进制:
*                            vvvvvvvv
* 01111111 11111111 11111111 11111111
*
* max>>>24                   vvvvvvvv
* 00000000 00000000 00000000 01111111
*
* raf.dat
* 01111111 11111111 11111111 11111111 01111111 11111111 11111111 11111111 00...
*/
int max = Integer.MAX_VALUE;
raf.write(max>>>24);
System.out.println("pos:"+raf.getFilePointer());
raf.write(max>>>16);
raf.write(max>>>8);
raf.write(max);
System.out.println("pos:"+raf.getFilePointer());//4
/*
* RAF提供了方便直接写出基本类型的方法
*/
raf.writeInt(max);//连续写4个字节,将int值写出,等同上面4句
System.out.println("pos:"+raf.getFilePointer());//8
raf.writeLong(123L);//连续写8个字节,将long值写出
System.out.println("pos:"+raf.getFilePointer());//16
raf.writeDouble(123.123);
System.out.println("pos:"+raf.getFilePointer());//24
System.out.println("写出完毕!");

/*
* void seek(long pos)
* 移动指针到指定位置
*/
//将指针移动到文件开始
raf.seek(0);
System.out.println("pos:"+raf.getFilePointer());//0
/*
* RAF也提供了方便读取基本类型数据的方法
* int readInt()
* 连续读取4字节,还原对应的int值
*/
int d = raf.readInt();
System.out.println(d);
System.out.println("pos:"+raf.getFilePointer());//4

//读取后面的long值
//1先将指针移动到long第一个字节的位置
raf.seek(8);
//2连续读取8字节还原long值
long lon = raf.readLong();
System.out.println(lon);//123
System.out.println("pos:"+raf.getFilePointer());//16

//读取后面的double值
double dou = raf.readDouble();
System.out.println(dou);
System.out.println("pos:"+raf.getFilePointer());//24

/*
* 修改long值为666
*/
//1先将指针移动到long第一个字节的位置
raf.seek(8);
//2重新写入8字节,将整数666对应的2进制的8个字节写入文件覆盖原内容
raf.writeLong(666);
System.out.println("pos:"+raf.getFilePointer());//16

raf.seek(8);
lon = raf.readLong();
System.out.println(lon);

raf.close();
}
}

综合练习:

要求:

 用户注册,注册信息分为四块:用户名,密码,昵称,年龄,除了年龄为int值之外,其余三个是字符串。将每个注册用户都写入user.dat文件中。用户登录操作,程序启动后要求输入用户名和密码如果用户名密码都输入正确则提示:登录成功!如果密码输入错误或者该用户在user.dat文件中不存在,则提示:用户名或密码错误!将user.dat文件所用用户输出到控制台,修改用户昵称,程序启动后要求用户输入用户名和新昵称。然后修改user.dat文件中该用户的昵称,如果输入的用户名在user.dat文件中不存在,则输出查无此人。循环读取每条记录的用户名,如果用户名不匹配则直接跳转指针到下一条记录开始位置 如果用户名匹配(注意,读取出的用户名要trim后再和用户输入的对比),则跳转指针 到该条记录的昵称位置,将新的昵称转换为字节后扩容到32字节后写入以达到覆盖原昵称的效果(注:一定要扩容,否则可能出现昵称覆盖不完全的情况)。

用户注册:

[code]public class RegDemo {

public static void main(String[] args) throws IOException {
Scanner scanner = new  Scanner(System.in);
System.out.println("请输入用户名:");
String username =scanner.nextLine();
System.out.println("请输入密码:");
String password =scanner.nextLine();
System.out.println("请输入昵称:");
String nickname =scanner.nextLine();
System.out.println("请输入年龄:");
int age = scanner.nextInt();

RandomAccessFile raf = new RandomAccessFile("user.dat","rw");
//将指针移动到末尾
raf.seek(raf.length());

/*
* 用户名 密码昵称各占32字节,字符串故意留白的好处是便于修改
* 其他数组,并且长度固定后格式统一,便于读取
*/
//写入用户名
byte[] data = username.getBytes("utf-8");
data = Arrays.copyOf(data, 32);
raf.write(data);
//写入密码
data = password.getBytes("utf-8");
data = Arrays.copyOf(data, 32);
raf.write(data);
//写入昵称
data = nickname.getBytes("utf-8");
data = Arrays.copyOf(data, 32);
raf.write(data);
//写入年龄
raf.writeInt(age);
System.out.println("注册完毕:");
raf.close();

}

}

用户登录:

[code]public class LoginDemo {

public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();

RandomAccessFile raf = new  RandomAccessFile("user.dat","r");
boolean check = false;//验证是否同通过
for(int i=0;i<raf.length();i++){
//先将指针移动到每条记录的开始位置
raf.seek(i*100);
byte[] data = new byte[32];
raf.read(data);
String name = new String(data,"UTF-8").trim();
if(name.equals(username)){
raf.read(data);
String pwd = new String(data,"UTF-8").trim();
if(pwd.equals(password)){
check = true;
}
break;
}
}
if(check){
System.out.println("登录成功!欢迎回来");
}else{
System.out.println("登录失败!用户名或密码不正确!");
}
raf.close();
}

}

修改用户昵称:

[code]public class UpdataDemo {

public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入新昵称:");
String nickname = scanner.nextLine();

RandomAccessFile raf = new RandomAccessFile("user.dat","rw");
boolean 	updata = false;//是否记录修改
for(int i=0;i<raf.length();i++){
raf.seek(i*100);
byte[] data = new  byte[32];
raf.read(data);
String name = new String(data,"UTF-8").trim();
if(name.equals(username)){
raf.seek(i*100+64);
data = nickname.getBytes("UTF-8");
data = Arrays.copyOf(data, 32);
raf.write(data);
updata = true;
break;
}
}
if(updata){
System.out.println("修改完毕!");
}else{
System.out.println("查无此人!");
}
raf.close();
}
}

输出所有用户到控制台:

[code]public class ShowAllUserDemo {

public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("user.dat","r");
for(int i=0;i<raf.length()/100;i++){
//读取用户名
byte[] data = new byte[32];
raf.read(data);
String username = new String(data,"utf-8").trim();

raf.read(data);
String password = new String(data,"utf-8").trim();

raf.read(data);
String nickname = new String(data,"utf-8").trim();

int age  = raf.readInt();
System.out.println(username+","+password+","+nickname+","+age);
System.out.println("pos:"+raf.getFilePointer());

}
raf.close();
}

}

简易记事本:

程序启动后,要求输入一个文件名,然后创建RAF对文件进行操作,之后在控制台输出的每一行字符串都写入到该文件中(统一字符集用UTF-8), 当单独输入exit时程序结束。

[code]public class Test {

public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入文件名:");
String filename =scanner.nextLine();
RandomAccessFile raf = new RandomAccessFile(filename,"rw");
System.out.println("输入内容,exit退出");
while(true){
String line = scanner.nextLine();
if("exit".equals(line)){
break;
}
byte[] data = line.getBytes("utf-8");
raf.write(data);
}

System.out.println("再见!");
raf.close();

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: