IO.js File System

2018-11-28 22:34 更新

稳定度: 2 - 稳定

文件I/O是由标准POSIX函数的简单包装提供的。通过require('fs')来使用这个模块。所有的方法都有异步和同步两种形式。

异步形式的方法通常在最后一个参数上接受一个回调函数。回调函数的参数则取决于不同的方法,但是第一个参数总是为异常所保留。如果操作正常结束,那么第一个参数会是nullundefined

当同步形式的方法产生异常时,会立刻抛出。你可以使用try/catch捕获,或让它们冒泡。

下面是一个异步方法的例子:

var fs = require('fs');

fs.unlink('/tmp/hello', function (err) {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});

下面是一个同步方法的例子:

var fs = require('fs');

fs.unlinkSync('/tmp/hello');
console.log('successfully deleted /tmp/hello');

因为异步方法不能够保证执行顺序,所以下面的例子很容易出错:

fs.rename('/tmp/hello', '/tmp/world', function (err) {
  if (err) throw err;
  console.log('renamed complete');
});
fs.stat('/tmp/world', function (err, stats) {
  if (err) throw err;
  console.log('stats: ' + JSON.stringify(stats));
});

它需要在fs.rename后执行fs.stat。正确的执行方法应如下:

fs.rename('/tmp/hello', '/tmp/world', function (err) {
  if (err) throw err;
  fs.stat('/tmp/world', function (err, stats) {
    if (err) throw err;
    console.log('stats: ' + JSON.stringify(stats));
  });
});

在繁忙的进程中,十分推荐使用异步版本的方法。同步版本的方法会阻塞进程,直到它们完成,也就是说它们会暂停所有连接。

文件的相对路径也可以被使用,记住路径是相对于process.cwd()的。

大多数的fs函数允许你省略回调函数。如果你省略了,将会由一个默认的回调函数来重抛出(rethrows)错误。要获得原始调用地点的堆栈追踪信息,请设置NODE_DEBUG环境变量:

$ cat script.js
function bad() {
  require('fs').readFile('/');
}
bad();

$ env NODE_DEBUG=fs iojs script.js
fs.js:66
        throw err;
              ^
Error: EISDIR, read
    at rethrow (fs.js:61:21)
    at maybeCallback (fs.js:79:42)
    at Object.fs.readFile (fs.js:153:18)
    at bad (/path/to/script.js:2:17)
    at Object.<anonymous> (/path/to/script.js:5:1)
    <etc.>

fs.rename(oldPath, newPath, callback)

异步版本的rename(2)。回调函数只有一个可能的异常参数。

fs.renameSync(oldPath, newPath)

同步版本的rename(2)。返回undefined

fs.ftruncate(fd, len, callback)

异步版本的ftruncate(2)。回调函数只有一个可能的异常参数。

fs.ftruncateSync(fd, len)

同步版本的ftruncate(2)。返回undefined

fs.truncate(path, len, callback)

异步版本的truncate(2)。回调函数只有一个可能的异常参数。第一个参数也可以接受一个文件描述符,这样的话,fs.ftruncate()会被调用。

fs.truncateSync(path, len)

同步版本的truncate(2)。返回undefined

fs.chown(path, uid, gid, callback)

异步版本的chown(2)。回调函数只有一个可能的异常参数。

fs.chownSync(path, uid, gid)

同步版本的chown(2)。返回undefined

fs.fchown(fd, uid, gid, callback)

异步版本的fchown(2)。回调函数只有一个可能的异常参数。

fs.fchownSync(fd, uid, gid)

同步版本的fchown(2)。返回undefined

fs.lchown(path, uid, gid, callback)

异步版本的lchown(2)。回调函数只有一个可能的异常参数。

fs.lchownSync(path, uid, gid)

同步版本的lchown(2)。返回undefined

fs.chmod(path, mode, callback)

异步版本的chmod(2)。回调函数只有一个可能的异常参数。

fs.chmodSync(path, mode)

同步版本的chmod(2)。返回undefined

fs.fchmod(fd, mode, callback)

异步版本的fchmod(2)。回调函数只有一个可能的异常参数。

fs.fchmodSync(fd, mode)

同步版本的fchmod(2)。返回undefined

fs.lchmod(path, mode, callback)

异步版本的lchmod(2)。回调函数只有一个可能的异常参数。

仅在Mac OS X中可用。

fs.lchmodSync(path, mode)

同步版本的lchmod(2)。返回undefined

fs.stat(path, callback)

异步版本的stat(2)。回调函数有两个参数(err, stats),stats是一个fs.Stats对象。更多信息请参阅fs.Stats章节。

fs.lstat(path, callback)

异步版本的lstat(2)。回调函数有两个参数(err, stats),stats是一个fs.Stats对象。lstat()stat()是相同的,除了path是一个符号链接,连接自己本身就是stat-ed,而不是引用一个文件。

fs.fstat(fd, callback)

异步版本的fstat(2)。回调函数有两个参数(err, stats),stats是一个fs.Stats对象。fstat()stat()是相同的,除了将要被stat-ed的文件是通过文件描述符fd来指定的。

fs.statSync(path)

同步版本的stat(2)。返回一个fs.Stats实例。

fs.lstatSync(path)

同步版本的lstat(2)。返回一个fs.Stats实例。

fs.fstatSync(fd)

同步版本的fstat(2)。返回一个fs.Stats实例。

fs.link(srcpath, dstpath, callback)

异步版本的link(2)。回调函数只有一个可能的异常参数。

fs.linkSync(srcpath, dstpath)

同步版本的link(2)。返回undefined

fs.symlink(destination, path[, type], callback)

异步版本的symlink(2)。回调函数只有一个可能的异常参数。type参数可以被设置为'dir''file''junction'(默认为'file'),并且仅在Windows平台下可用(其他平台下会被忽略)。注意Windows junction点 要求目标路径必须是绝对的。当使用'junction'时,destination参数会被自动转换为绝对路径。

fs.symlinkSync(destination, path[, type])

同步版本的symlink(2)。返回undefined

fs.readlink(path, callback)

异步版本的link(2)。回调函数有两个参数(err, linkString)。

fs.readlinkSync(path)

异步版本的readlink(2),返回一个符号链接字符串值。

fs.realpath(path[, cache], callback)

异步版本的realpath(2)。回调函数有两个参数(err, resolvedPath)。可能会使用process.cwd来解析相对路径。cache是一个包含了路径映射的对象,被用来 强制进行指定的路径解析 或 避免对真实路径调用额外的fs.stat

例子:

var cache = {'/etc':'/private/etc'};
fs.realpath('/etc/passwd', cache, function (err, resolvedPath) {
  if (err) throw err;
  console.log(resolvedPath);
});

fs.realpathSync(path[, cache])

同步版本的realpath(2),返回一个解析出的路径。

fs.unlink(path, callback)

异步版本的unlink(2)。回调函数只有一个可能的异常参数。

fs.unlinkSync(path)

同步版本的unlink(2)。返回undefined

fs.rmdir(path, callback)

异步版本的rmdir(2)。回调函数只有一个可能的异常参数。

fs.rmdirSync(path)

同步版本的rmdir(2)。返回undefined

fs.mkdir(path[, mode], callback)

异步版本的mkdir(2)。回调函数只有一个可能的异常参数。mode默认为0o777

fs.mkdirSync(path[, mode])

同步版本的mkdir(2)。返回undefined

fs.readdir(path, callback)

异步版本的readdir(3)。读取目录内容。回调函数有两个参数(err, files),files是一个目录中的文件名数组(不包括'.''..')。

fs.readdirSync(path)

同步版本的readdir(3)。返回一个文件名数组(不包括'.''..')。

fs.close(fd, callback)

异步版本的close(2)。回调函数只有一个可能的异常参数。

fs.closeSync(fd)

同步版本的close(2)。返回undefined

fs.open(path, flags[, mode], callback)

异步版本的文件打开。参阅open(2)flag可以是:

  • 'r' - 以只读的方式打开文件。如果文件不存在则抛出异常。

  • 'r+' - 以读写的方式打开文件。如果文件不存在则抛出异常。

  • 'rs' - 同步地以只读的方式打开文件。绕过操作系统的本地文件系统缓存。

该功能主要用于打开NFS挂载的文件,因为它允许你跳过潜在的过时的本地缓存。它对I/O性能有非常大的影响,所以除非需要它,否则不应使用这个flag

注意这个flag不会将fs.open()变为一个同步调用。因为如果你想要同步调用,你应使用fs.openSync()

  • 'rs+' - 以读写的方式打开文件,告诉操作系统同步地打开它。注意事项请参阅'rs'

  • 'w' - 以只写的方式打开文件。如果文件不存在,将会创建它。如果已存在,将会覆盖它。

  • 'wx' - 类似于'w',但是路径不存在时会失败。

  • 'w+' - 以读写的方式打开文件。如果文件不存在,将会创建它。如果已存在,将会覆盖它。

  • 'wx+' - 类似于'w+',但是路径不存在时会失败。

  • 'a' - 以附加的形式打开文件。如果文件不存在,将会创建它。

  • 'ax' - 类似于'a',但是路径不存在时会失败。

  • 'a+' - 以读取和附加的形式打开文件。如果文件不存在,将会创建它。

  • 'ax+' - 类似于'a+',但是路径不存在时会失败。

参数mode用于设置文件模式(权限和sticky bits),但是前提是文件已被创建。它默认为0666,有可读和可写权限。

回调函数有两个参数(err, fd)。

排除标识'x'open(2)中的O_EXCL标识)保证了目录是被新创建的。在POSIX系统上,即使路径指向了一个不存在的符号链接,也会被认定为文件存在。排除标识不能保证在网络文件系统中有效。

在Linux下,无法对以追加形式打开的文件,在指定位置写入数据。内核忽略了位置参数并且总是将数据追加到文件的末尾。

fs.openSync(path, flags[, mode])

同步版本的fs.open(),返回代表文件描述符的一个整数。

fs.utimes(path, atime, mtime, callback)

更改path所指向的文件的时间戳。

fs.utimesSync(path, atime, mtime)

同步版本的fs.utimes()。返回undefined

fs.futimes(fd, atime, mtime, callback)

更改文件描述符fd所指向的文件的时间戳。

fs.futimesSync(fd, atime, mtime)

同步版本的fs.futimes()。返回undefined

fs.fsync(fd, callback)

异步版本的fsync(2)。回调函数只有一个可能的异常参数。

fs.fsyncSync(fd)

同步版本的fsync(2)。返回undefined

fs.write(fd, buffer, offset, length[, position], callback)

向文件描述符fd指向的文件写入buffer

offsetlength决定了buffer的哪一部分被写入文件。

position指定了文件中,数据被写入的开始位置的偏移量。如果typeof position !== 'number',那么数据将会在当前位置被写入。参阅pwrite(2)

回调函数有三个参数(err, written, buffer)。written指出了buffer中有多少字节被写入。

注意,不等待回调函数而多次执行fs.write是不安全的。这种情况下推荐使用fs.createWriteStream

在Linux下,无法对以追加形式打开的文件,在指定位置写入数据。内核忽略了位置参数并且总是将数据追加到文件的末尾。

fs.write(fd, data[, position[, encoding]], callback)

向文件描述符fd指向的文件写入data。如果data不是一个Buffer实例,那么其值将被强制转化为一个字符串。

position指定了文件中,数据被写入的开始位置的偏移量。如果typeof position !== 'number',那么数据将会在当前位置被写入。参阅pwrite(2)

encoding是期望的字符串编码。

回调函数有三个参数(err, written, buffer)。written指出了buffer中有多少字节被写入。注意,写入的字节与字符串字符是不同的。参阅Buffer.byteLength

与写入buffer不同,整个字符串都必须被写入。不能指定子字符串。因为字节的偏移量可能与字符串的偏移量不相同。

注意,不等待回调函数而多次执行fs.write是不安全的。这种情况下推荐使用fs.createWriteStream

在Linux下,无法对以追加形式打开的文件,在指定位置写入数据。内核忽略了位置参数并且总是将数据追加到文件的末尾。

fs.writeSync(fd, buffer, offset, length[, position])

fs.writeSync(fd, data[, position[, encoding]])

同步版本的fs.write()。返回被写入的字节数。

fs.read(fd, buffer, offset, length, position, callback)

从文件描述符fd指向的文件读取数据。

buffer是数据将要被写入的缓冲区。

offset是开始向buffer写入数据的缓冲区偏移量。

length是一个指定了读取字节数的整数。

position是一个指定了从文件的何处开始读取数据的整数。如果positionnull,数据将会从当前位置开始读取。

回调函数有三个参数(err, bytesRead, buffer)。

fs.readSync(fd, buffer, offset, length, position)

同步版本的fs.read。返回读取字节的个数。

fs.readFile(filename[, options], callback)

  • filename String
  • options Object | String

  • encoding String | Null 默认为null
  • flag String 默认为'r'

  • callback Function

异步得读取文件的所有内容。例子:

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

回调函数有两个参数(err, data),data是文件的内容。

如果没有指定编码,那么将会返回源buffer

如果options是一个字符串,那么它将指定编码,例子:

fs.readFile('/etc/passwd', 'utf8', callback);

fs.readFileSync(filename[, options])

同步版本的fs.readFile。返回文件的内容。

如果指定了编码那么将会返回字符串。否则返回buffer

fs.writeFile(filename, data[, options], callback)

  • filename String
  • data String | Buffer
  • options Object | String

  • encoding String | Null 默认为'utf8'
  • mode Number 默认为0o666
  • flag String 默认为'w'

  • callback Function

异步地向文件写入数据,如果文件已经存在,那么会覆盖它。data可以是一个字符串或一个buffer

如果数据时一个buffer那么编码会被忽略。编码默认为'utf8'

例子:

fs.writeFile('message.txt', 'Hello io.js', function (err) {
  if (err) throw err;
  console.log('It\'s saved!');
});

如果options是一个字符串,那么它将指定编码,例子:

fs.writeFile('message.txt', 'Hello io.js', 'utf8', callback);

fs.writeFileSync(filename, data[, options])

同步版本的fs.writeFile。返回undefined

fs.appendFile(filename, data[, options], callback)

  • filename String
  • data String | Buffer
  • options Object | String

  • encoding String | Null 默认为'utf8'
  • mode Number 默认为0o666
  • flag String 默认为'a'

  • callback Function

异步地向文件追加数据,如果文件不存在将会创建它。data可以是一个字符串或一个buffer

例子:

fs.appendFile('message.txt', 'data to append', function (err) {
  if (err) throw err;
  console.log('The "data to append" was appended to file!');
});

如果options是一个字符串,那么它将指定编码,例子:

fs.appendFile('message.txt', 'data to append', 'utf8', callback);

fs.appendFileSync(filename, data[, options])

同步版本的fs.appendFile。返回undefined

fs.watchFile(filename[, options], listener)

监视文件变化。回调函数listener会在文件每一次被访问时调用。

第二参数是可选的。如果options被提供,那么它必须是一个含有两个成员persistentinterval的对象。persistent表明了进程是否在文件被监视时继续执行。interval表明了文件被轮询的间隔(毫秒)。默认是{ persistent: true, interval: 5007 }

listener有两个参数,当前状态对象和先前状态对象:

fs.watchFile('message.text', function (curr, prev) {
  console.log('the current mtime is: ' + curr.mtime);
  console.log('the previous mtime was: ' + prev.mtime);
});

这两个状态对象都是fs.Stat实例。

如果你想要在文件被修改时被通知,而不仅仅是在被访问时,你需要比较curr.mtimeprev.mtime

注意:fs.watchfs.watchFilefs.unwatchFile更高效。当可能时,请使用fs.watch替代它们。

fs.unwatchFile(filename[, listener])

停止监视filename的变化。如果指定了listener,那么仅仅会移除指定的listener。否则所有的监听器都会被移除,并且停止继续监视文件。

对一个没有被监视的文件调用fs.unwatchFile()将不会发生任何事,而不是报错。

注意:fs.watchfs.watchFilefs.unwatchFile更高效。当可能时,请使用fs.watch替代它们。

fs.watch(filename[, options][, listener])

监视filename的变化,filename指向的可以是文件也可以是目录。返回一个fs.FSWatcher对象。

第二个参数是可选的。options必须是一个对象。支持的布尔值属性是persistentrecursivepersistent表明了进程是否在文件被监视时继续执行。recursive表明了是否子目录也需要被监视,或仅仅监视当前目录。这只在支持的平台(参阅下方警告)下传递一个目录时有效。

默认是{ persistent: true, recursive: false }

listener回调函数有两个参数(event, filename)。event'rename''change'filename是触发事件的文件名。

警告

fs.watch API 不是在所有平台下都表现一致的,并且在一些情况下是不可用的。

recursive选项目前只支持OS X。只有FSEvents支持这种类型的文件监控,所有其他平台并不会很快都被支持。

可用性

这个特性依赖于底层操作系统提供的文件变化提示。

  • 在Linux系统下,它使用inotify
  • 在BSD系统下,它使用kqueue
  • 在OS X下,对于文件它使用kqueue,对于目录它使用FSEvents
  • 在SunOS系统(包括SolarisSmartOS)下,它使用事件端口(event ports)。
  • 在Windows系统下,这个特性依赖于ReadDirectoryChangesW

如果由于一些原因,底层功能不可用,那么fs.watch的功能也将不可用。例如,在网络文件系统(NFS,SMB等)中监视文件或目录变化,往往结果不可靠或完全不可用。

你仍可以使用fs.watchFile,它使用了状态轮询。但是性能更差且可靠性更低。

Filename 参数

回调函数中提供的filename参数不是在所有平台上都支持的(目前只支持Linux和Windows)。即使是在支持的平台上,filename也不是总会被提供。因此,不要假设filename参数总会在回调函数中被提供,需要有一些检测它是否为null的逻辑。

fs.watch('somedir', function (event, filename) {
  console.log('event is: ' + event);
  if (filename) {
    console.log('filename provided: ' + filename);
  } else {
    console.log('filename not provided');
  }
});

fs.exists(path, callback)

fs.exists()已被弃用。请使用fs.statfs.access替代。

检查文件系统来测试提供的路径是否存在。然后在回调函数的参数中提供结果truefalse

fs.exists('/etc/passwd', function (exists) {
  util.debug(exists ? "it's there" : "no passwd!");
});

fs.exists()是一个不符合潮流的函数,并且仅因一些历史原因所以仍然错在。在你的代码中,不应有任何原因要继续使用它。

特别的,在打开文件前检查文件是否存在 是一种反模式。因为竞态条件所以让你的代码十分脆弱:其他进程可能fs.exists()fs.open()之间删除文件。所以仅仅就去打开一个文件,并且当它不存在时处理错误。

fs.existsSync(path)

同步版本的fs.exists。当文件存在,返回true,否则返回false

fs.existsSync()已被弃用。请使用fs.statSyncfs.accessSync替代。

fs.access(path[, mode], callback)

对于指定的路径,检测用户的权限。mode是一个可选的整数,指定了要被执行的可访问性检查。以下是mode的一些可用的常量。可以通过“或”运算符(|)连接两个或以上的值。

  • fs.F_OK - 文件对于当前进程可见。这对于检查文件是否存在很有用,但是不提供任何rwx权限信息。这是默认值。
  • fs.R_OK - 文件对于当前进程可读。
  • fs.W_OK - 文件对于当前进程可写。
  • fs.X_OK - 文件对于当前进程可执行。这在Windows上无效(将会表现得像fs.F_OK一样)。

最后一个参数callback,是一个包含了潜在错误参数的回调函数。如果任何一个可访问检查失败了,错误参数就会被提供。以下是一个在当前进程中检查/etc/passwd可读性和可写性的例子。

fs.access('/etc/passwd', fs.R_OK | fs.W_OK, function(err) {
  util.debug(err ? 'no access!' : 'can read/write');
});

fs.accessSync(path[, mode])

同步版本的fs.access。如果任何一个可访问性检查失败了,它会抛出异常。否则什么都不做。

Class: fs.Stats

fs.stat()fs.lstat()fs.lstat()和它们的同步版本函数所返回的对象。

  • stats.isFile()
  • stats.isDirectory()
  • stats.isBlockDevice()
  • stats.isCharacterDevice()
  • stats.isSymbolicLink() (仅在调用fs.lstat()时有效)
  • stats.isFIFO()
  • stats.isSocket()

对于一个普通的文件,util.inspect(stats)可能会返回:

{ dev: 2114,
  ino: 48064969,
  mode: 33188,
  nlink: 1,
  uid: 85,
  gid: 100,
  rdev: 0,
  size: 527,
  blksize: 4096,
  blocks: 8,
  atime: Mon, 10 Oct 2011 23:24:11 GMT,
  mtime: Mon, 10 Oct 2011 23:24:11 GMT,
  ctime: Mon, 10 Oct 2011 23:24:11 GMT,
  birthtime: Mon, 10 Oct 2011 23:24:11 GMT }

请注意,atimemtimebirthtimectime都是Date对象实例,并且你可以通过合适的方法来比较它们的值。普遍的使用方式是,调用getTime()来获取unix时间戳并且这个整数可以被用来进行任何比较。但是还有一些可以展示模糊信息的方法。更多的详细信息请参阅MDN JavaScript Reference页。

Stat 时间值

stat对象中的各个时间有如下语义:

  • atime "访问时间" - 文件数据最后一次被访问时的时间。由mknod(2)utimes(2)read(2)系统调用改变。
  • mtime "修改时间" - 文件数据最后一次被修改的时间。由mknod(2)utimes(2)write(2)系统调用改变。
  • ctime "改变时间" - 文件状态最后一次被改变(索引节点改变)的时间。由chmod(2)chown(2)link(2)mknod(2)rename(2)unlink(2)utimes(2)read(2)write(2)系统调用改变。
  • birthtime "创建时间" - 文件的创建时间。在文件被创建时设置。在创建时间不可用的的文件系统上,这个值可能会被ctime或是1970-01-01T00:00Z(unix时间戳0)填充。在Darwin或其他FreeBSD系统变体上,如果使用utimes(2)系统调用设置atime为一个比当前birthtime更早的时间,birthtime也会被这样填充。

io.js v1.0 和 Node v0.12 前,Windows系统中ctime持有了birthtime值。但是在 v0.12 里,ctime不再是“创建时间”。在Unix系统中,它从来都不是。

fs.createReadStream(path[, options])

返回一个新的可读流对象(参阅Readable Stream)。

options是一个有以下默认值的对象或字符串:

{ flags: 'r',
  encoding: null,
  fd: null,
  mode: 0o666,
  autoClose: true
}

options可以包含startend值来读取指定范围的文件数据。startend这两个位置本身,也都是被包括的,并且start0开始。编码可以是'utf8''ascii''base64'

如果指定了fd,可读流将会忽略path参数并且将会使用指定的文件描述符。这意味open事件不再会触发。

如果autoClosefalse,那么文件描述符将不会被关闭,甚至是有错误发生时。关闭它将是你的责任,并且要确保没有文件描述符泄漏。如果autoClosetrue(默认),那么在发生错误时,或到达文件描述末端时,它会被自动关闭。

从一个100字节的文件中读取最后10字节数据的例子:

fs.createReadStream('sample.txt', {start: 90, end: 99});

如果options是一个字符串,那么它表示指定的编码。

Class: fs.ReadStream

ReadStream是一个可读流。

Event: 'open'

  • fd Integer 被可读流使用的文件描述符

当可读流文件被打开时触发。

fs.createWriteStream(path[, options])

返回一个新的可写流对象(参阅Writable Stream)。

options是一个有以下默认值的对象或字符串:

{ flags: 'w',
  encoding: null,
  fd: null,
  mode: 0o666 }

options可以包含一个start选项来允许从指定位置开始写入数据。修改一个文件而不是替换它,需要一个r+标识,而不是默认的w。编码可以是'utf8''ascii''binary''base64'

与上文的ReadStream类似,如果指定了fd,可写流会忽略path参数,并且使用指定的文件描述符。这意味open事件不再会触发。

如果options是一个字符串,那么它表示指定的编码。

Class: fs.WriteStream

WriteStream是一个可写流。

Event: 'open'

  • fd Integer WriteStream使用的文件描述符

当可写流文件被打开时触发。

file.bytesWritten

至今为止写入的字节数。不包括仍在写入队列中的数据。

Class: fs.FSWatcher

fs.watch()返回的对象。

watcher.close()

停止在指定的fs.FSWatcher上监视文件变化。

Event: 'change'

  • event String 文件的改变类型
  • filename String The filename that changed (if relevant/available)被改变的文件(如果有意义/可用的话)

当被监视的目录或文件发生了改变时触发。详情参阅fs.watch

Event: 'error'

  • error Error object

当错误发生时触发。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号