likes
comments
collection
share

当我翻遍搜索引擎,找不到Rust怎么读取末尾几千条记录时

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

如题,最近有个需求想要获取最近的日志文件记录,又怕日志文件太大,太多我也要不起,那么就折中取最近的2k条记录,返回给前端意思下。

我以为这是个简单的搬砖任务,结果竟然找不到砖厂。这么简单的需求,竟然找不到地方搬砖,rust体验着实太差了。

对着sdk翻了半天,也没找到一个正经的能从末尾开始读数据的姿势。没办法,只能求助gpt了。

以下便是问答

ask: rust 读取文件的最后5000条记录

answer:

要读取一个文件的最后5000条记录,你可以使用Rust的标准库和文件操作来实现。下面是一个简单的示例代码,展示了如何读取文件的最后5000行:

rustCopy code
use std::fs::File;
use std::io::{self, BufRead, Seek, SeekFrom};

fn read_last_lines(filename: &str, num_lines: usize) -> io::Result<Vec<String>> {
    let file = File::open(filename)?;
    let mut reader = io::BufReader::new(file);

    let mut lines = Vec::with_capacity(num_lines);
    let mut buffer = String::new();
    let mut offset = 0;

    // 从文件末尾开始逐行读取,直到达到指定的行数或文件开始
    loop {
        let position = reader.seek(SeekFrom::End(offset))?;
        if position == 0 {
            break;
        }

        // 读取一行并将其添加到结果向量中
        buffer.clear();
        reader.read_line(&mut buffer)?;
        lines.push(buffer.clone());

        // 如果达到了指定的行数,则退出循环
        if lines.len() >= num_lines {
            break;
        }

        // 更新偏移量以读取下一行
        offset += 1;
    }

    // 将结果向量反转,使最后一行成为向量的第一个元素
    lines.reverse();
    Ok(lines)
}

请确保将 filename 替换为实际的文件路径。这个例子使用了 BufReaderseek 方法来逐行读取文件的最后5000行。读取的行存储在一个向量中,并在读取完成后进行反转,以使最后一行成为向量的第一个元素。最后,你可以遍历向量并对每行执行你想要的操作。

ps: 答案一本正经,看起来像那么回事,but随便一跑,什么乱七八糟。 不过至少给了个思路reader.seek

ask: seek的用法貌似不对

answer:


fn read_last_lines(filename: &str, num_lines: usize) -> io::Result<Vec<String>> {
    let file = File::open(filename)?;
    let reader = BufReader::new(file);

    let mut lines = Vec::with_capacity(num_lines);
    let mut buffer = String::new();

    // 逐行读取文件,并将每行添加到向量中
    for line in reader.lines().flatten() {
        if lines.len() >= num_lines {
            // 如果向量中的行数已达到指定的行数,则删除第一个元素,以保持最后的 num_lines 行
            lines.remove(0);
        }
        lines.push(line);
    }

    Ok(lines)
}

ps: 从上往下读,这尼玛效率是不是太低了。

ask: 这个方法如果文件大的话效率太低了

answer:

你对于大文件的效率问题提出了合理的关注。读取大文件的最后几行确实可能导致效率问题,因为需要逐行读取并保留最后的行数。如果文件非常大,这种方法可能会占用大量的内存并且运行时间较长。

use std::fs::File;
use std::io::{self, BufRead, BufReader, Seek, SeekFrom};

fn read_last_lines(filename: &str, num_lines: usize) -> io::Result<Vec<String>> {
    let file = File::open(filename)?;
    let mut reader = BufReader::new(file);

    // 获取文件的总字节数
    let file_size = reader.seek(SeekFrom::End(0))?;

    let mut lines = Vec::with_capacity(num_lines);
    let mut buffer = Vec::new();
    let mut lines_count = 0;

    // 从文件末尾开始逐行读取,直到达到指定的行数或文件开始
    for offset in (0..file_size).rev() {
        reader.seek(SeekFrom::Start(offset))?;
        reader.read_until(b'\n', &mut buffer)?;

        if lines_count >= num_lines {
            // 如果向量中的行数已达到指定的行数,则退出循环
            break;
        }

        // 如果读取到行末尾,则将行添加到结果向量中
        if buffer.ends_with(&[b'\n']) {
            let line = String::from_utf8_lossy(&buffer).into_owned();
            lines.push(line);
            lines_count += 1;
        }

        buffer.clear();
    }

    // 将结果向量反转,使最后一行成为向量的第一个元素
    lines.reverse();
    Ok(lines)
}

ps: 看着像那么回事。代码一跑,取到是末尾一行的数据 ["\n","\rn","e\r\n", ...],不过至少看着这个seek加read_util的方式可以从末尾取数了。思路可行,继续求教。

然后就没有更加像那么回事的代码了~,~

long times later!

算了,自己撸吧。

最终方案

pub async fn read_last_lines(&self, num_lines: usize) -> RetAny<Vec<String>> {
        let file = File::open(&self.0).await?;
        let mut reader = BufReader::new(file);
        // 获取文件的总字节数
        let file_size = reader.seek(SeekFrom::End(0)).await?;

        let mut lines = Vec::new();
        let mut pre_buffer: Option<Vec<u8>> = None;
        let mut buffer = Vec::new();
        let mut lines_count = 0;

        for offset in (0..file_size).rev() {
            reader.seek(SeekFrom::Start(offset)).await?;
            let result = reader.read_until(b'\n', &mut buffer).await?;
            if offset == 0 {
                lines.push(String::from_utf8_lossy(&buffer).into_owned());
                break;
            }
            if result == 1 && pre_buffer.is_some() {
                let line = String::from_utf8_lossy(&pre_buffer.unwrap()).into_owned();
                lines.push(line);
                lines_count += 1;
                if lines_count >= num_lines {
                    break;
                }
            }
            pre_buffer = Some(buffer.clone());
            buffer.clear();
        }
        lines.reverse();
        Ok(lines)
    }

大致讲下思路:

其实就是得到了文件的总字节数。然后从后往前判断字符。 比如总字节数2000,那么1999-2000是否为字节'\n',第一次循环肯定是,但是pre_buffer是空的,所以不用加入。那么丢入pre_buffer,等下一次找到'\n'时,把pre_buffer取出来,加入结果集。然后继续循环。

本文仅图一乐,若后来人搬砖搬到这,也算是为改善搬砖环境做了贡献吧。