我怎样才能 trim empty/whitespace 行?

How can I trim empty/whitespace lines?

我必须处理管理不善且带有创意缩进的文本。我想删除文本开头和结尾的空(或空白)行而不触及任何其他内容;这意味着如果第一行或最后一行分别以空格开头或结尾,这些行将保留。

例如,这个:

<lines, empty or with whitespaces ...>
<text, maybe preceded by whitespace>
<lines with or without text...>
<text, maybe followed by whitespace>
<lines, empty or with whitespaces ...>

转为

<text, maybe preceded by whitespace>
<lines with or without text...>
<text, maybe followed by whitespace>

保留实际文本行开头和结尾的空格(文本也可能完全是空白)

用空性替换 (\A\s*(\r\n|\Z)|\r\n\s*\Z) 的正则表达式正是我想要的,但正则表达式有点矫枉过正,我担心在处理包含很多行但不多的文本时可能会花费我一些时间 trim.

另一方面,一个明确的算法很容易实现(只需阅读直到 non-whitespace/the 结束,同时记住最后的换行符,然后截断,然后向后做同样的事情)但感觉就像我'我遗漏了一些明显的东西。

我该怎么做?

如果可以删除第一行前面或最后一行非空白行之后的空格,那么这个答案 就足够了。

但是,由于这个限制,如果您不想使用正则表达式,我会建议将字符串转换为行,然后从第一行到最后一行非空白行重新构建字符串.

这是一个工作示例:https://godbolt.org/z/rozxj6saj

将字符串转换为行:

std::vector<std::string> StringToLines(const std::string &s) {
  // Create vector with lines (not using input stream to keep line break
  // characters)
  std::vector<std::string> result;
  std::string line;

  for (auto c : s) {
    line.push_back(c);

    // Check for line break
    if (c == '\n' || c == '\r') {
      result.push_back(line);
      line.clear();
    }
  }

  // add last bit
  result.push_back(line);

  return result;
}

从第一行到最后一行非空白行构建字符串:

bool IsNonWhiteSpaceString(const std::string &s) {
  return s.end() != std::find_if(s.begin(), s.end(), [](unsigned char uc) {
           return !std::isspace(uc);
         });
}

std::string TrimVectorEmptyEndsIntoString(const std::vector<std::string> &v) {
  std::string result;

  // Find first non-whitespace line
  auto it_begin = std::find_if(v.begin(), v.end(), [](const std::string &s) {
    return IsNonWhiteSpaceString(s);
  });

  // Find last non-whitespace line
  auto it_end = std::find_if(v.rbegin(), v.rend(), [](const std::string &s) {
    return IsNonWhiteSpaceString(s);
  });

  // Build the string
  for (auto it = it_begin; it != it_end.base(); std::advance(it, 1)) {
    result.append(*it);
  }

  return result;
}

用法示例:

 // Create a test string
  std::string test_string(
      "  \n\t\n  \n   TEST\n\tTEST\n\t\tTEST\n  TEST\t\n   \t");

  // Output result
  std::cout << TrimVectorEmptyEndsIntoString(StringToLines(test_string));

显示空白的输出:

正如您从 this discussion 中看到的那样,修剪空白需要在 C++ 中进行大量工作。这绝对应该包含在标准库中。

无论如何,我已经尽可能简单地检查了如何做到这一点,但没有什么比 RegEx 更紧凑。至于速度,那就另当别论了。

在下文中,您可以找到执行所需任务的程序的三个版本。使用正则表达式、标准函数和几个索引。最后一个也可以做得更快,因为你可以完全避免复制,但为了公平比较,我把它留了下来:

#include <string>
#include <sstream>
#include <chrono>
#include <iostream>
#include <regex>
#include <exception>

struct perf {
    std::chrono::steady_clock::time_point start_;
    perf() : start_(std::chrono::steady_clock::now()) {}
    double elapsed() const {
        auto stop = std::chrono::steady_clock::now();
        std::chrono::duration<double> elapsed_seconds = stop - start_;
        return elapsed_seconds.count();
    }
};

std::string Generate(size_t line_len, size_t empty, size_t nonempty) {
    std::string es(line_len, ' ');
    es += '\n';
    for (size_t i = 0; i < empty; ++i) {
        es += es;
    }

    std::string nes(line_len - 1, ' ');
    es += "a\n";
    for (size_t i = 0; i < nonempty; ++i) {
        nes += nes;
    }

    return es + nes + es;
}


int main()
{
    std::string test;
    //test = "  \n\t\n  \n  \tTEST\n\tTEST\n\t\t\n  TEST\t\n   \t\n \n  ";
    std::cout << "Generating...";
    std::cout.flush();
    test = Generate(1000, 8, 10);
    std::cout << " done." << std::endl;

    std::cout << "Test 1...";
    std::cout.flush();
    perf p1;
    std::string out1;
    std::regex re(R"(^\s*\n|\n\s*$)");
    try {
        out1 = std::regex_replace(test, re, "");
    }
    catch (std::exception& e) {
        std::cout << e.what() << std::endl;
    }
    std::cout << " done. Elapsed time: " << p1.elapsed() << "s" << std::endl;

    std::cout << "Test 2...";
    std::cout.flush();
    perf p2;
    std::stringstream is(test);
    std::string line;
    while (std::getline(is, line) && line.find_first_not_of(" \t\n\v\f\r") == std::string::npos);
    std::string out2 = line;
    size_t end = out2.size();
    while (std::getline(is, line)) {
        out2 += '\n';
        out2 += line;
        if (line.find_first_not_of(" \t\n\v\f\r") != std::string::npos) {
            end = out2.size();
        }
    }
    out2.resize(end);
    std::cout << " done. Elapsed time: " << p2.elapsed() << "s" << std::endl;

    if (out1 == out2) {
        std::cout << "out1 == out2\n";
    }
    else {
        std::cout << "out1 != out2\n";
    }

    std::cout << "Test 3...";
    std::cout.flush();
    perf p3;
    static bool whitespace_table[] = {
        1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
    };
    size_t sfl = 0; // Start of first line
    for (size_t i = 0, end = test.size(); i < end; ++i) {
        if (test[i] == '\n') {
            sfl = i + 1;
        }
        else if (whitespace_table[(unsigned char)test[i]]) {
            break;
        }
    }
    size_t ell = test.size(); // End of last line
    for (size_t i = test.size(); i-- > 0;) {
        if (test[i] == '\n') {
            ell = i;
        }
        else if (whitespace_table[(unsigned char)test[i]]) {
            break;
        }
    }
    std::string out3 = test.substr(sfl, ell - sfl);
    std::cout << " done. Elapsed time: " << p3.elapsed() << "s" << std::endl;

    if (out1 == out3) {
        std::cout << "out1 == out3\n";
    }
    else {
        std::cout << "out1 != out3\n";
    }

    return 0;
}

运行 它在 C++ Shell 你得到这些时间:

Generating... done.
Test 1... done. Elapsed time: 4.2288s
Test 2... done. Elapsed time: 0.0077323s
out1 == out2
Test 3... done. Elapsed time: 0.000695783s
out1 == out3

如果性能很重要,最好用真实文件进行测试。

附带说明一下,这个正则表达式在 MSVC 上不起作用,因为我找不到避免 ^$ 匹配行首和行尾的方法,即禁用多行操作模式。如果你 运行 这个,它会抛出一个异常说 regex_error(error_complexity): The complexity of an attempted match against a regular expression exceeded a pre-set level. 我想我会问如何处理这个问题!