【问题标题】:Iterate over byte vector and save pointers to strings遍历字节向量并保存指向字符串的指针
【发布时间】:2016-06-18 12:42:00
【问题描述】:

我有一个Vec<u8>,其中包含由'\0' as u8 分隔的UTF-8 字符串。固定长度的二进制打包数据放在每个字符串之后,如下所示:

b"abc\0" 10_bytes_of_data b"ufhd\0" 10_bytes_data,

我想解析这样一个Vec<u8> 并填充一个Vec<&str>。为了简化事情,我将10_bytes_of_data 替换为0_bytes_of_data

let cnt = b"abc\0efg\0";
let mut cnt_vec = Vec::<u8>::new();
for ch in cnt {
    cnt_vec.push(*ch);
}
let mut str_start_it = cnt_vec.iter();
let mut strings = Vec::<&str>::new();
let mut cur_it = cnt_vec.iter();
let mut counter: usize = 0;
loop {
    match cur_it.next() {
        Some(ch) => {
            if *ch == 0u8 {
                {
                    let slice = str_start_it.as_slice();
                    let s = std::str::from_utf8(&slice[0..counter]).unwrap();
                    strings.push(s);
                }
                str_start_it = cur_it.clone();
                counter = 0;
            } else {
                counter += 1;
            }
        }
        None => {
            break;
        }
    }
}

for s in strings {
    println!("s {}", s);
}

我有几个问题:

  1. 有没有更简单的方法可以将cnt 转换为cnt_vec

  2. 是否可以用更少的代码行编写主算法?在现实世界中,字符串之间有 10 个字节;我打算用Iterator::nth

  3. 我的代码中是否有任何不必要的堆分配?我只想在cnt_vecstrings 中进行堆分配,也许除了push 之外的某些行在堆上分配内存?

【问题讨论】:

    标签: rust


    【解决方案1】:

    我会这样做(支持忽略固定数量的额外数据):

    fn main() {
        const DATA_LEN: usize = 3;
        let bytes = b"abc\0zzzefg\0zzzhij\0zzz".to_vec();
        let mut strs = Vec::new();
        for (i, chunk) in bytes.split(|&b| b == b'\0').enumerate() {
            // The first chunk doesn't have the extra data at the start.
            let skip = if i == 0 {
                0
            } else {
                DATA_LEN
            };
            strs.push(std::str::from_utf8(&chunk[skip..]).unwrap());
        }
        // Remove the last str. If the data is valid, it should be empty as the last
        // chunk only contains extra data.
        strs.pop();
        for str in strs {
            println!("{}", str);
        }
    }
    

    输出:

    abc
    efg
    hij
    

    这里唯一的堆分配(在bytes之后)是strs,它将使用大约2 words * number of strs的堆内存。

    编辑:更实用的版本(没有mut!),但这也会跳过所有 0 大小的字符串:

    fn main() {
        const DATA_LEN: usize = 3;
        let bytes = b"abc\0zzzefg\0zzzhij\0zzz".to_vec();
        let strs = bytes.split(|&b| b == b'\0')
            .enumerate()
            .flat_map(|(i, chunk)| {
                // The first chunk doesn't have the extra data at the start.
                let skip = if i == 0 {
                    0
                } else {
                    DATA_LEN
                };
                if skip == chunk.len() {
                    None
                } else {
                    Some(std::str::from_utf8(&chunk[skip..]).unwrap())
                }
            })
            .collect::<Vec<&str>>();
        for str in strs {
            println!("{}", str);
        }
    }
    

    如果您的数据没有空字符串,我会使用它。这应该和前一个一样快。

    【讨论】:

      【解决方案2】:

      您可以实现自己的迭代器:

      struct DataSplitter<'d> {
          data: &'d [u8],
          pos: usize,
          dlen: usize,
      }
      
      impl<'d> DataSplitter<'d> {
          fn new(data: &'d [u8], dlen: usize) -> DataSplitter {
              DataSplitter {
                  data: data,
                  pos: 0,
                  dlen: dlen,
              }
          }
      }
      
      impl<'d> Iterator for DataSplitter<'d> {
          type Item = &'d [u8];
      
          fn next(&mut self) -> Option<&'d [u8]> {
              if self.pos < self.data.len() {
                  if let Some(zpos) = self.data[self.pos..].iter().position(|&x| x == 0) {
                      // zero found. prepare slice
                      let res = &self.data[self.pos..self.pos + zpos];
                      // move current position
                      self.pos += zpos + self.dlen + 1;
                      Some(res)
                  } else {
                      // No more zeros found. Return all bytes
                      let res = &self.data[self.pos..];
                      self.pos = self.data.len();
                      Some(res)//You can return None if you want skip it
                  }
              } else {
                  None
              }
          }
      }
      
      fn main() {
          const DLEN: usize = 2;
          let bytes: Vec<u8> = b"abc\0\x01\0zxc\0\t\x02fgh\0\x01\n".to_vec();
      
          let res = DataSplitter::new(&bytes, DLEN)
              .map(|s| std::str::from_utf8(s))
              .collect::<Result<Vec<_>, _>>()
              .expect("UTF8 error");
      
          println!("{:?}", res);//["abc", "zxc", "fgh"]
      }
      

      【讨论】:

      • 如果fixed length packed data在utf-8字符串之间包含b'\0'怎么办?
      • 一切都出错了。我没有预见到这种情况
      【解决方案3】:

      对于Code Review,这更像是一个问题,因为您的代码有效,但您正在寻找更好的解决方案。

      您需要的唯一分配是让向量保存字符串。除此之外,您还可以将输入切片用作后备内存。

      const PADDING_LENGTH: usize = 1;
      
      use std::str;
      
      fn main() {
          let cnt = b"abc\0\x01efg\0\x01";
      
          let mut a = cnt as &[u8];
          let mut words = vec![];
      
          while let Some(idx) = a.iter().position(|&x| x == b'\0') {
              let (head, tail) = a.split_at(idx);
              let word = str::from_utf8(head).expect("Invalid UTF-8");
              words.push(word);
      
              // +1 to skip over the NUL
              if tail.len() > PADDING_LENGTH + 1 {
                  a = &tail[PADDING_LENGTH + 1..];
              } else {
                  break;
              }
          }
      
          assert_eq!(words, ["abc", "efg"]);
      }
      

      没有必要将输入切片转换为Vec,但更好的方法是to_vec

      fn main() {
          let cnt = b"abc\0efg\0";
          let mut cnt_vec = Vec::<u8>::new();
          for ch in cnt {
              cnt_vec.push(*ch);
          }
      
          let cnt_vec_better = cnt.to_vec();
          assert_eq!(cnt_vec, cnt_vec_better)
      }
      

      最好用b'0'来表示单字节字面量;它是多字节文字的b"" 的镜像。

      【讨论】:

      • 谢谢,我最近两天才使用 rust,所以我不需要“代码审查”,而是“代码重写”,就像你演示的那样。
      • @user1034749 查看code reviews for Rust;他们中的许多人都有相同数量的“检查这个标准库函数或你不知道的 crate”或“你在这里分配了太多内存”。那里的代码审查非常彻底,并且经常涉及重写大块。代码审查不仅限于“我的变量名好不好”。
      【解决方案4】:

      您也可以使用正则表达式箱:

      extern crate regex;
      
      fn main() {
          let data = b"abc\0123456789xfu\nhd\0123456789x";
          let re = regex::bytes::Regex::new(r"(?s)(.*?)\0.{10}").unwrap();
          let v: Vec<&str> = re.captures_iter(data)
              .map(|captures| std::str::from_utf8(captures.at(1).unwrap()).unwrap())
              .collect();
          println!("{:?}", v);
      }
      

      s 标志在这里非常重要,没有它. 将无法匹配换行符,并且连续的字符串可能最终不连续。

      如果您关心此代码的性能,请注意每个Captures 将分配一个Vec。您可以改用 find_iter 并手动切掉最后 11 个字节来摆脱这种分配。

      【讨论】:

        【解决方案5】:

        是否可以用更少的代码行编写主要算法?在现实世界中,字符串之间有 10 个字节;我打算用Iterator::nth

        这是另一种选择:

        fn main() {
            let bytes = b"abc\0???efg\0???hij\0".to_vec();
            let mut v = Vec::new();
            let mut chunks = bytes.split(|&b| b == b'\0');
            if let Some(first) = chunks.next().map(from_utf8_unwrap) {
                v.push(first);
                v.extend(chunks.map(skip_data).map(from_utf8_unwrap));
                v.pop(); // discard the last string
            }
            assert_eq!(vec!["abc", "efg", "hij"], v);
        }
        
        const PADDING: usize = 3;
        
        fn skip_data(s: &[u8]) -> &[u8] {
            &s[std::cmp::min(s.len(), PADDING)..]
        }
        
        fn from_utf8_unwrap(s: &[u8]) -> &str {
            std::str::from_utf8(s).unwrap()
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-11-23
          • 2012-06-13
          • 2020-03-25
          • 2013-04-26
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多