【问题标题】:How can I trim empty/whitespace lines?如何修剪空/空白行?
【发布时间】:2021-08-04 09:14:03
【问题描述】:

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

例如,这个:

<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) 替换为空的正则表达式正是我想要的,但正则表达式有点矫枉过正,我担心在处理包含很多行但修剪不多的文本时可能会花费我一些时间。

另一方面,显式算法很容易制作(只需阅读到非空白/结尾,同时记住最后一个换行符,然后截断,并向后执行相同操作),但感觉就像我错过了很明显的东西。

我该怎么做?

【问题讨论】:

  • 正则表达式的开发成本低、运行速度快(足够)并且易于理解。为什么要创建定制算法?
  • @LjisaMoige,他们给出的示例使用 std::space ,其中包括 \n\r 而空行是例如连续换行\n\n。有关 std::space 中包含的内容,请参阅 en.cppreference.com/w/cpp/string/byte/isspace。因此,将 std::space 替换为检查 \n\r 如果这不能解决您的问题,请提供 MRE 或测试字符串。
  • 连续换行也不是解决方案,因为白线通常包含空格或制表符。这就是我提出这个问题的全部原因。
  • @LjisaMoige 您可以在问题正文中使用代码标签 (`) 或 &lt;pre&gt; 来保留空格格式并在必要时向我们展示一些确切的示例。

标签: c++ text trim


【解决方案1】:

正如您在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. 我想我会问如何应对这个!

【讨论】:

  • 对于第三个版本,最好先进行向后检查,然后修剪结尾,然后进行向前检查,以避免读取两次大的空白文本。对于正则表达式,我使用\A\Z 来避免它粘在单行的边缘(见问,它还必须处理完全白色的输入)。
  • \A\Z 在 C++ 正则表达式中可用吗?我找不到关于他们的任何信息。
【解决方案2】:

如果可以删除第一行前面或最后一个非纯空格行之后的空格,那么这个答案https://stackoverflow.com/a/217605/14258355 就足够了。

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

这是一个工作示例: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));

输出显示空白:

【讨论】:

  • 还有第一行开头和最后一行末尾的空格和\t
  • 我明白了。我会改的。
  • 它看起来比字符级解析要昂贵得多,如果只是因为读取了整个文本,并且在第一次解析后第二次读取了边界空白。它也不处理可能发生的完全空白输入。
  • 就目前而言,我已经准备好使用正则表达式解决方案,我还有一种方法可以在字符级别解析我的文本,但它很难看。我并不是真的要一个复杂的代码解决方案。我希望有一个内置的方法来实现我想要的。回答说它不存在就足够了。
  • @LjisaMoige,你有它。正则表达式获胜,您的问题的简短std 答案不存在(据我所知)
猜你喜欢
  • 2010-11-14
  • 2018-08-26
  • 2017-03-16
  • 1970-01-01
  • 1970-01-01
  • 2012-08-07
  • 2013-10-21
  • 1970-01-01
  • 2011-05-07
相关资源
最近更新 更多