【问题标题】:Most efficient way to escape XML/HTML in C++ string?在 C++ 字符串中转义 XML/HTML 的最有效方法?
【发布时间】:2011-08-05 15:11:26
【问题描述】:

我不敢相信以前没有人问过这个问题。我有一个字符串需要插入到 HTML 文件中,但它可能包含特殊的 HTML 字符。我想用适当的 HTML 表示替换这些。

下面的代码可以工作,但是非常冗长和丑陋。性能对我的应用程序来说并不重要,但我想这里也存在可伸缩性问题。我该如何改进呢?我想这是 STL 算法或一些深奥的 Boost 函数的工作,但下面的代码是我自己能想到的最好的代码。

void escape(std::string *data)
{
    std::string::size_type pos = 0;
    for (;;)
    {
        pos = data->find_first_of("\"&<>", pos);
        if (pos == std::string::npos) break;
        std::string replacement;
        switch ((*data)[pos])
        {
        case '\"': replacement = "&quot;"; break;   
        case '&':  replacement = "&amp;";  break;   
        case '<':  replacement = "&lt;";   break;   
        case '>':  replacement = "&gt;";   break;   
        default: ;
        }
        data->replace(pos, 1, replacement);
        pos += replacement.size();
    };
}

【问题讨论】:

  • 你真的需要替换引号吗?我虽然它们是有效的 XML(我也会替换 \n 和 \r)。
  • 是的,这是一个不同的问题,也是一个很好的问题。哪些字符需要替换?
  • @Gionvanni:取决于上下文。如果将字符串粘贴到属性值的中间,例如tag = "&lt;select value=\"" + escaped(value) + "\"&gt;",则需要对引号进行转义。如果它被粘贴在任何标签之外,例如element = "&lt;p&gt;" + escaped(value) + "&lt;/p&gt;",则不需要转义引号,但假设输出是针对 HTML 或 XML 解析器的,转义它们不会造成任何伤害。 &amp;apos; 是 HTML 中的有效实体,但不是 XML。

标签: c++ algorithm string stl


【解决方案1】:

您可以使用动态替换进行复制,而不是仅仅替换原始字符串,从而避免移动字符串中的字符。这将有更好的复杂性和缓存行为,所以我期待一个巨大的改进。或者您可以使用boost::spirit::xml encodehttp://code.google.com/p/pugixml/

void encode(std::string& data) {
    std::string buffer;
    buffer.reserve(data.size());
    for(size_t pos = 0; pos != data.size(); ++pos) {
        switch(data[pos]) {
            case '&':  buffer.append("&amp;");       break;
            case '\"': buffer.append("&quot;");      break;
            case '\'': buffer.append("&apos;");      break;
            case '<':  buffer.append("&lt;");        break;
            case '>':  buffer.append("&gt;");        break;
            default:   buffer.append(&data[pos], 1); break;
        }
    }
    data.swap(buffer);
}

编辑:可以通过使用启发式方法来确定缓冲区的大小来实现小幅改进。将 buffer.reserve 行替换为 data.size()*1.1 (10%) 或类似内容,具体取决于预期的替换数量。

【讨论】:

  • 是的,除非很少有替代品。也感谢您的链接,但我认为两者都不实用。
  • 我不知道 CDATA,但看起来网络浏览器在 HTML 中不尊重它。
  • 如果要转义的是HTML,可能比简单的XML要难很多。如果您希望保护编码字符串免受奇怪字符的影响,您可能需要一个重量级库。检查这个:site.icu-project.org
  • 我使用的是 ASCII,所以肯定只有大约 100 个字符需要考虑,其中只有少数可能会导致问题?
  • 如果输入是 7 位 ascii,我认为我的函数非常安全。口音可能有问题。看到这个:w3.org/TR/html4/charset.html
【解决方案2】:

如果您追求处理速度,那么在我看来,最好的办法是在执行过程中创建第二个字符串,从第一个字符串复制到第二个字符串,然后附加 html 转义当你遇到他们。由于我假设替换方法首先涉及内存移动,然后是复制到替换位置,因此对于大字符串来说它会非常慢。如果您要使用 .append() 构建第二个字符串,它将避免内存移动。

就代码“干净”而言,我认为这与您将获得的一样漂亮。您可以创建一个字符数组及其替换,然后搜索该数组,但这可能会更慢而且不会更干净。

【讨论】:

    【解决方案3】:
    void escape(std::string *data)
    {
        using boost::algorithm::replace_all;
        replace_all(*data, "&",  "&amp;");
        replace_all(*data, "\"", "&quot;");
        replace_all(*data, "\'", "&apos;");
        replace_all(*data, "<",  "&lt;");
        replace_all(*data, ">",  "&gt;");
    }
    

    能以最少冗长的方式获奖吗?

    【讨论】:

    • 关心订单,应该以“&”开头:-)
    • 如果您只是想完成工作,绝对是最稳健的方式。但是,对 ASCII 文本 HTML 进行编码,引用 &、 就足够了。如果文本没有进入节点属性,则不需要引号。
    • 你的实现也应该赢得表现最差的奖,因为它将编码一个 N 长的 & 符号/引号/等字符串。复杂度为 O(N^2)。
    【解决方案4】:

    老实说,我会选择使用迭代器的更通用的版本,这样您就可以“流式传输”编码。考虑以下实现:

    #include <algorithm>
    
    namespace xml {
    
        // Helper for null-terminated ASCII strings (no end of string iterator).
        template<typename InIter, typename OutIter>
        OutIter copy_asciiz ( InIter begin, OutIter out )
        {
            while ( *begin != '\0' ) {
                *out++ = *begin++;
            }
            return (out);
        }
    
        // XML escaping in it's general form.  Note that 'out' is expected
        // to an "infinite" sequence.
        template<typename InIter, typename OutIter>
        OutIter escape ( InIter begin, InIter end, OutIter out )
        {
            static const char bad[] = "&<>";
            static const char* rep[] = {"&amp;", "&lt;", "&gt;"};
            static const std::size_t n = sizeof(bad)/sizeof(bad[0]);
    
            for ( ; (begin != end); ++begin )
            {
                // Find which replacement to use.
                const std::size_t i =
                    std::distance(bad, std::find(bad, bad+n, *begin));
    
                // No need for escaping.
                if ( i == n ) {
                    *out++ = *begin;
                }
                // Escape the character.
                else {
                    out = copy_asciiz(rep[i], out);
                }
            }
            return (out);
        }
    
    }
    

    然后,您可以使用一些重载来简化平均情况:

    #include <iterator>
    #include <string>
    
    namespace xml {
    
        // Get escaped version of "content".
        std::string escape ( const std::string& content )
        {
            std::string result;
            result.reserve(content.size());
            escape(content.begin(), content.end(), std::back_inserter(result));
            return (result);
        }
    
        // Escape data on the fly, using "constant" memory.
        void escape ( std::istream& in, std::ostream& out )
        {
            escape(std::istreambuf_iterator<char>(in),
                std::istreambuf_iterator<char>(),
                std::ostreambuf_iterator<char>(out));
        }
    
    }
    

    最后,测试整个批次:

    #include <iostream>
    
    int main ( int, char ** )
    {
        std::cout << xml::escape("<foo>bar & qux</foo>") << std::endl;
    }
    

    【讨论】:

    • 这看起来很流畅,而且应该可以工作。但是尝试使用它时出现编译器错误: std::cout bar & qux")
    【解决方案5】:

    或者只使用 stl :

     std::string& rep(std::string &s, std::string from, std::string to)
        {
          int pos = -1;
          while ( (pos = s.find(from, pos+1) ) != string::npos)
            s.erase(pos, from.length()).insert(pos, to);
    
          return s;
        }
    

    用法:

    rep(s, "&", "&quot;");
    rep(s, "\"", "&quot;");
    

    或:

    rep(s, "HTML","xxxx");
    

    【讨论】:

    • fromto 字符串正在通过副本传递。请改用const&amp;。此外,在执行find 的循环中链接eraseinsert 对性能非常不利,因为strings 是连续数组。你可能在 O(n^3)。
    【解决方案6】:

    这是一个简单的约 30 行 C 程序,它以相当好的方式完成了这个技巧。这里我假设 temp_str 将分配足够的内存来容纳额外的转义字符。

    void toExpatEscape(char *temp_str)
    {
        const char cEscapeChars[6]={'&','\'','\"','>','<','\0'};
        const char * const pEscapedSeqTable[] =
        {
            "&amp;",
            "&apos;",
            "&quot;",
            "&gt;",
            "&lt;",
        };
        unsigned int i, j, k, nRef = 0, nEscapeCharsLen = strlen(cEscapeChars), str_len = strlen(temp_str);
        int nShifts = 0; 
    
        for (i=0; i<str_len; i++)
        {
            for(nRef=0; nRef<nEscapeCharsLen; nRef++)
            {
                if(temp_str[i] == cEscapeChars[nRef])
                {
                    if((nShifts = strlen(pEscapedSeqTable[nRef]) - 1) > 0)
                    {
                        memmove(temp_str+i+nShifts, temp_str+i, str_len-i+nShifts); 
                        for(j=i,k=0; j<=i+nShifts,k<=nShifts; j++,k++)
                            temp_str[j] = pEscapedSeqTable[nRef][k];
                        str_len += nShifts;
                    }
                }
            }  
        }
        temp_str[str_len] = '\0';
    }
    

    【讨论】:

      【解决方案7】:

      我的测试显示this 答案提供了最佳性能(不足为奇,它的比率最高)。
      我已经为我的项目实现了相同的算法(我真的想要好的性能和内存使用)——我的测试表明我的实现有 ~2.6-3.25 更好的速度性能。此外,我不喜欢以前提供的最佳算法 bcs 的内存使用不佳 - 当应用 1.1 乘数“启发式”时,您将使用额外的内存,当 .append() 导致调整大小时。
      所以,把我的代码留在这里——也许有人觉得它有用。

      HtmlPreprocess.h:

      #ifndef _HTML_PREPROCESS_H_
      #define _HTML_PREPROCESS_H_
      
      #include <string>
      
      class HtmlPreprocess
      {
      public:
          HtmlPreprocess();
          ~HtmlPreprocess();
      
          static void htmlspecialchars(
              const std::string & in,
              std::string & out
              );
      };
      
      #endif // _HTML_PREPROCESS_H_
      

      HtmlPreprocess.cpp:

      #include "HtmlPreprocess.h"
      
      
      HtmlPreprocess::HtmlPreprocess()
      {
      }
      
      
      HtmlPreprocess::~HtmlPreprocess()
      {
      }
      
      
      const unsigned char map_char_to_final_size[] = 
      {
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   6,   1,   1,   1,   5,   6,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   4,   1,   4,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
         1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1
      };
      
      
      const unsigned char map_char_to_index[] = 
      {
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   2,      0xFF,   0xFF,   0xFF,   0,      1,      0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   4,      0xFF,   3,      0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,
         0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF,   0xFF
      };
      
      
      void HtmlPreprocess::htmlspecialchars(
          const std::string & in,
          std::string & out
          )
      {
          const char * lp_in_stored = &in[0];
          size_t in_size = in.size();
      
          const char * lp_in = lp_in_stored;
          size_t final_size = 0;
          for (size_t i = 0; i < in_size; i++)
              final_size += map_char_to_final_size[*lp_in++];
      
          out.resize(final_size);
      
          lp_in = lp_in_stored;
          char * lp_out = &out[0];
      
          for (size_t i = 0; i < in_size; i++)
          {
              char current_char = *lp_in++;
              unsigned char next_action = map_char_to_index[current_char];
      
              switch (next_action){
              case 0:
                  *lp_out++ = '&';
                  *lp_out++ = 'a';
                  *lp_out++ = 'm';
                  *lp_out++ = 'p';
                  *lp_out++ = ';';
                  break;
              case 1:
                  *lp_out++ = '&';
                  *lp_out++ = 'a';
                  *lp_out++ = 'p';
                  *lp_out++ = 'o';
                  *lp_out++ = 's';
                  *lp_out++ = ';';
                  break;
              case 2:
                  *lp_out++ = '&';
                  *lp_out++ = 'q';
                  *lp_out++ = 'u';
                  *lp_out++ = 'o';
                  *lp_out++ = 't';
                  *lp_out++ = ';';
                  break;
              case 3:
                  *lp_out++ = '&';
                  *lp_out++ = 'g';
                  *lp_out++ = 't';
                  *lp_out++ = ';';
                  break;
              case 4:
                  *lp_out++ = '&';
                  *lp_out++ = 'l';
                  *lp_out++ = 't';
                  *lp_out++ = ';';
                  break;
              default:
                  *lp_out++ = current_char;
              }
          }
      }
      

      【讨论】:

      • 当您使用查找表时,您应该使用无符号参数,例如map_char_to_index[static_cast(current_char)] 而不是 map_char_to_index[current_char]。
      • 超重的设计和语法。不要走这条路,太 90 年代的 C-stylish。
      【解决方案8】:

      如果你不想自己写,可以使用boost::property_tree::xml_parser::encode_char_entities

      供参考,这是boost 1.64.0中的代码:

      ```

      template<class Str>
      Str encode_char_entities(const Str &s)
      {
          // Don't do anything for empty strings.
          if(s.empty()) return s;
      
          typedef typename Str::value_type Ch;
      
          Str r;
          // To properly round-trip spaces and not uglify the XML beyond
          // recognition, we have to encode them IF the text contains only spaces.
          Str sp(1, Ch(' '));
          if(s.find_first_not_of(sp) == Str::npos) {
              // The first will suffice.
              r = detail::widen<Str>("&#32;");
              r += Str(s.size() - 1, Ch(' '));
          } else {
              typename Str::const_iterator end = s.end();
              for (typename Str::const_iterator it = s.begin(); it != end; ++it)
              {
                  switch (*it)
                  {
                      case Ch('<'): r += detail::widen<Str>("&lt;"); break;
                      case Ch('>'): r += detail::widen<Str>("&gt;"); break;
                      case Ch('&'): r += detail::widen<Str>("&amp;"); break;
                      case Ch('"'): r += detail::widen<Str>("&quot;"); break;
                      case Ch('\''): r += detail::widen<Str>("&apos;"); break;
                      default: r += *it; break;
                  }
              }
          }
          return r;
      }
      

      ```

      【讨论】:

        【解决方案9】:

        我使用 Visual Studio 2017 分析了 3 个解决方案。输入是 10 000 000 个大小为 5-20 的字符串,需要转义字符的概率为 9.4%。

        1. Giovanni Funchal 的解决方案
        2. HostageBrain 的解决方案
        3. 解决方案是我的

        结果:

        1. 需要 1.675 秒
        2. 需要 0.769 秒
        3. 需要 0.368 秒

        在我的解决方案中,仅在需要时才预先计算最终大小并完成字符串数据的副本。所以堆内存分配应该是最小的。

        const unsigned char calcFinalSize[] =
        {
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   6,   1,   1,   1,   5,   6,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   4,   1,   4,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,
        1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1
        };
        
        void escapeXml(std::string & in)
        {
            const char* dataIn = in.data();
            size_t sizeIn = in.size();
        
            const char* dataInCurrent = dataIn;
            const char* dataInEnd = dataIn + sizeIn;
            size_t outSize = 0;
            while (dataInCurrent < dataInEnd)
            {
                outSize += calcFinalSize[static_cast<uint8_t>(*dataInCurrent)];
                dataInCurrent++;
            }
        
        
            if (outSize == sizeIn)
            {
                return;
            }
            std::string out;
            out.resize(outSize);
        
            dataInCurrent = dataIn;
            char* dataOut = &out[0];
            while (dataInCurrent < dataInEnd)
            {
                switch (*dataInCurrent) {
                case '&':
                    memcpy(dataOut, "&amp;", sizeof("&amp;") - 1);
                    dataOut += sizeof("&amp;") - 1;
                    break;
                case '\'':
                    memcpy(dataOut, "&apos;", sizeof("&apos;") - 1);
                    dataOut += sizeof("&apos;") - 1;
                    break;
                case '\"':
                    memcpy(dataOut, "&quot;", sizeof("&quot;") - 1);
                    dataOut += sizeof("&quot;") - 1;
                    break;
                case '>':
                    memcpy(dataOut, "&gt;", sizeof("&gt;") - 1);
                    dataOut += sizeof("&gt;") - 1;
                    break;
                case '<':
                    memcpy(dataOut, "&lt;", sizeof("&lt;") - 1);
                    dataOut += sizeof("&lt;") - 1;
                    break;
                default:
                    *dataOut++ = *dataInCurrent;
                }
                dataInCurrent++;
            }
            in.swap(out);
        }
        

        编辑:将"&amp;quote;" 替换为"&amp;quot;"。旧的解决方案是覆盖内存,因为查找表包含的 "&amp;quote;" 长度为 6。

        【讨论】:

        • 这很好——逆运算怎么样?你这样做了吗?
        猜你喜欢
        • 1970-01-01
        • 2012-11-06
        • 2010-10-22
        • 1970-01-01
        • 2015-04-26
        • 2012-01-09
        • 2021-07-16
        • 2010-10-26
        相关资源
        最近更新 更多