likes
comments
collection
share

探索 Node 读取大文件与写入那些事

作者站长头像
站长
· 阅读数 57

开始之前

环境介绍

  • node环境20.0.0,语言 TypeScrupt
  • vscode 安装 Code Runner 插件
  • 配置环境:
    1. npm init
    2. npm install --save-dev @types/node
    3. 编写tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["./src/**/*.ts"],
  "exclude": ["node_modules"]
}

代码地址:github.com/SouthernPen…

代码中出现的文件自行创建

基础阶段

基础阶段————基本的使用之【读】

读取文件我们要使用fs模块中的readFile是个异步读取文件的方法,还有个同步的方法readFilesyc,这里我们使用异步的读取方法。

// 01读取文件
import { promises as fs } from 'fs';
import * as path from 'path';

// 读取文件
(async () => {
  const filePath = path.resolve(__dirname, '2024-5-2.log');
  const readFileDate = await fs.readFile(filePath, 'utf-8').catch((err) => console.log(err));
  console.log(readFileDate);
})();

基础阶段————基本的使用之【写】

写文件我们要使用fs模块的writeFilewrite它们是一个异步写入的方法,还有个同步写入的方法writeFileSync,writeSync,这里我们使用异步的方法

  • fs.writeFile,fs.write 是异步的,不会阻塞事件循环,适用于不希望阻塞其他操作的情况。
  • fs.writeFileSync,fs.writeSync 是同步的,会阻塞事件循环,适用于需要立即完成写入操作的情况,但会影响性能和响应时间
  • [中文文档 写](fs 文件系统 | Node.js v20 文档 (nodejs.cn))
// 02文件写入1
import { promises as fs } from 'fs';
import * as path from 'path';

(async () => {
  try {
    // 取个名字
    const newFileName = `${new Date().getFullYear()}-${new Date().getMonth() + 1}-${new Date().getDate()}.log`;
    const neFIlePath = path.resolve(__dirname, newFileName);

    // 打开文件
    const fsOpen = await fs.open(neFIlePath, 'a+');

    // 开始写入
    const writeText = { timer: new Date().getTime() };
    // await fs.writeFile(neFIlePath, JSON.stringify(writeText) + '\n'); // 覆盖之前的写入
    await fs.appendFile(neFIlePath, JSON.stringify(writeText) + '\n'); // 将新的内容追加到最后面

    // 关闭文件
    await fsOpen.close();
  } catch (error) {
    console.error(error);
  }
})();

// 02文件写入2
import { promises as fsPromises } from 'fs';
import * as fs from 'fs';
import * as path from 'path';

(async () => {
  try {
    // 取个名字
    const newFileName = `${new Date().getFullYear()}-${new Date().getMonth() + 1}-${new Date().getDate()}.log`;
    const neFIlePath = path.resolve(__dirname, newFileName);

    // 打开文件
    const fsOpen = await fsPromises.open(neFIlePath, 'a+');
    // 开始写入
    const writeText = { timer: new Date().getTime() };
    fs.write(fsOpen.fd, JSON.stringify(writeText) + '\n', (err) => {
      console.log(err);
    }); // 将新的内容追加到最后面

    // 关闭文件
    await fsOpen.close();
  } catch (error) {
    console.error(error);
  }
})();

我们会发现使用fs.writeFilefs.write都可以用来写入的操作,该怎么选择?

如果你需要频繁地向同一个文件中写入数据,那么 fs.write 方法会更加高效。相反,如果你只是想一次性地向一个文件写入文件,那么可以选择 fs.writeFile 方法。

进阶——流(Stream)

在Node.js中,fs模块提供了一种可以用于处理不适合一次性全读到内存的大文件或数据的方法,这就是流(Stream)

探索 Node 读取大文件与写入那些事

进阶阶段———— 流(Stream)の读操作

使用流(Stream)读文件和使用fs.readFile差不多,在文件小的情况下其实没有什么区别,如果你要读取一个大型文件比如好几个G的文件使用fs.readFile一次性读取,可能会让你的服务宕机,如果用流的方式一点一点读,就不会出现宕机【PS:凡是没有绝对】

// 03.流读文件
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

(async () => {
  const filePath = path.resolve(__dirname, 'bigSql.sql');

  // 创建读流
  const readerStream = fs.createReadStream(filePath, {
    encoding: 'utf-8',
    highWaterMark: 4028, // 设置每次读取的大小
  });

  // 创建逐行读取
  const readFileLine = readline.createInterface({
    input: readerStream,
    crlfDelay: Infinity,
  });

  for await (const line of readFileLine) {
    // 每个100毫秒打印一次,模拟文件的流动
    await new Promise((res) => setTimeout(res, 100));
    console.log(line);
  }
})();

进阶阶段———— 流(Stream)の写操作

既然读的操作已经完成,那我们就将读到的内容,通过流的方式,流到另外一个文件中 【PS:你可以直接复制文件并重命名啊!!!如果你杠那你都是正确的】

逐行写入

//04流写文件1
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

(async () => {
  const filePath = path.resolve(__dirname, 'j.js');
  const writeFilePath = path.resolve(__dirname, 'jCopy.js');

  // 创建读流
  const readerStream = fs.createReadStream(filePath, {
    encoding: 'utf-8',
    highWaterMark: 4028 * 10, // 设置每次读取的大小
  });

  // 创建写流
  const writeStream = fs.createWriteStream(writeFilePath, {
    encoding: 'utf-8',
    flags: 'a+',
    autoClose: true,
  });

  // 创建逐行读取
  const readFileLine = readline.createInterface({
    input: readerStream,
    crlfDelay: Infinity,
  });

  try {
    for await (const line of readFileLine) {
      await new Promise((res) => setTimeout(res, 100));
      writeStream.write(line + '\n');
    }
    console.log('写入完成');
  } catch (error) {
    console.log(error);
  }
})();

上面的代码在写入的时候控制台总是有一段时间卡在那里太丑了,程序没有告诉我们是卡住了,还是在写入状态,那应该怎么办呢?

加上进度条

实战一

问题又来怎么在控制台怎么打印进度条?进度条百分比怎么计算?

这里我们是用 process.stdout.write(str)在控制台打印,为什么不用console.log打印,因为console.log每次打印都会自动添加换行符,进度总不能换行打印把,必须在一行内完成

百分比计算思路:获取原文大小,然后在写入的时候实时获取写入文件大小,进行触发运行得出进度长度

 // 04流写文件2.ts
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

(async () => {
  try {
    const filePath = path.resolve(__dirname, 'j.js');
    const writeFilePath = path.resolve(__dirname, 'jCopy.js');

    // 判断文件是否已经被复制
    if (fs.existsSync(writeFilePath)) {
      console.log('文件已被复制');
      return;
    }
    // 创建读流
    const readerStream = fs.createReadStream(filePath, {
      encoding: 'utf-8',
      highWaterMark: 4028 * 10, // 设置每次读取的大小
    });

    // 创建写流
    const writeStream = fs.createWriteStream(writeFilePath, {
      encoding: 'utf-8',
      flags: 'a+',
    });

    // 创建逐行读取
    const readFileLine = readline.createInterface({
      input: readerStream,
      crlfDelay: Infinity,
    });

    // 原文件大小
    let originalSqlSize = 0;
    fs.stat(filePath, (err, stats) => {
      if (err) {
        console.error(err);
        return;
      }
      originalSqlSize = stats.size * 1;
    });

    // 进度条
    let str0 = '当前进度写入: ';
    let str02 = '';
    let str03 = '';

    // 记录写入完成没
    let isWriteEnd = false;

    readFileLine.on('close', () => {
      console.log('正在写入');
    });

    for await (const line of readFileLine) {
      await new Promise((res) => setTimeout(res, 100));
      isWriteEnd = writeStream.write(line + '\n', () => {
        fs.stat(writeFilePath, async (err, stats) => {
          if (err) {
            console.error(err);
            return;
          }
          let progress = (((stats.size * 1) / originalSqlSize) * 100).toFixed(12).toString();
          let progressInt = Math.floor(Number(progress));

          // 防止进度过长
          if (progressInt % 2 === 0) {
            str03 = str0 + str02 + '=> ' + progress + '%';
            await new Promise((res) => setTimeout(res, 500));
            process.stdout.clearLine(0);
            process.stdout.cursorTo(0);
            str02 = '='.repeat(progressInt / 2);
            //不换行打印
            process.stdout.write(str03);
          }
        });
      });
    }

    if (isWriteEnd) {
      // 当所有行都处理完毕,结束写入流
      writeStream.end();
    }

    writeStream.on('close', () => {
      console.log('\n' + '大文件内容复制完成!');
    });
  } catch (error) {
    console.log(error);
  }
})();

实战二

既然是复制文件,哪有一行一行复制,肯定是大批字节复制

//05流写入.ts
import * as fs from 'fs';
import * as path from 'path';

(async () => {
  try {
    const filePath = path.resolve(__dirname, 'bigSql.sql');
    const writeFilePath = path.resolve(__dirname, 'bigSqlCopy.sql');

    // 判断文件是否已经被复制
    if (fs.existsSync(writeFilePath)) {
      console.log('文件已被复制');
      return;
    }

    // 创建读流
    const readStream = fs.createReadStream(filePath, {
      encoding: 'utf8',
      highWaterMark: 1024 * 4, // 设置每次读取的大小
    });

    // 总文件大小
    let originalSqlSize = 0;
    fs.stat(filePath, (err, stats) => {
      if (err) {
        console.error(err);
        return;
      }
      originalSqlSize = stats.size * 1;
    });

    // 创建写流
    const writeStream = fs.createWriteStream(writeFilePath, {
      encoding: 'utf8',
      flags: 'a+',
      autoClose: true,
    });
    // 进度条
    let str0 = '当前进度写入: ';
    let str02 = '';
    let str03 = '';

    readStream.on('data', async (chunk) => {
      writeStream.write(chunk, async () => {
        fs.stat(writeFilePath, async (err, stats) => {
          if (err) {
            console.error(err);
            return;
          }
          let progress = (((stats.size * 1) / originalSqlSize) * 100).toFixed(12).toString();
          let progressInt = Math.floor(Number(progress));

          // 防止进度过长
          if (progressInt % 2 === 0) {
            str03 = str0 + str02 + '=> ' + progress + '%';
            await new Promise((res) => setTimeout(res, 500));
            process.stdout.clearLine(0);
            process.stdout.cursorTo(0);
            str02 = '='.repeat(progressInt / 2);
            //不换行打印
            process.stdout.write(str03);
          }
        });
      });
    });

    readStream.on('close', () => {
      writeStream.end();
    });

    writeStream.on('close', () => {
      console.log('\n' + '大文件内容复制完成!');
    });
  } catch (error) {}
})();

转载自:https://juejin.cn/post/7372463538681036812
评论
请登录