使用C++将字符串拆分为键值对

Split string into key-value pairs using C++

本文关键字:键值对 拆分 字符串 C++ 使用      更新时间:2023-10-16

我有一个这样的字符串:

"CA: ABCDnCB: ABFGnCC: AFBVnCD: 4567"

现在": "从值中分离键,而n则分离对。我想将键值对添加到C++中的映射中。

考虑到优化,有什么有效的方法可以做到这一点吗?

这里有两个方法。第一个是我一直使用的简单、明显的方法(性能很少是问题(。第二种方法可能更有效,但我还没有进行任何正式的计时

在我的测试中,第二种方法大约快3倍。

#include <map>
#include <string>
#include <sstream>
#include <iostream>
std::map<std::string, std::string> mappify1(std::string const& s)
{
    std::map<std::string, std::string> m;
    std::string key, val;
    std::istringstream iss(s);
    while(std::getline(std::getline(iss, key, ':') >> std::ws, val))
        m[key] = val;
    return m;
}
std::map<std::string, std::string> mappify2(std::string const& s)
{
    std::map<std::string, std::string> m;
    std::string::size_type key_pos = 0;
    std::string::size_type key_end;
    std::string::size_type val_pos;
    std::string::size_type val_end;
    while((key_end = s.find(':', key_pos)) != std::string::npos)
    {
        if((val_pos = s.find_first_not_of(": ", key_end)) == std::string::npos)
            break;
        val_end = s.find('n', val_pos);
        m.emplace(s.substr(key_pos, key_end - key_pos), s.substr(val_pos, val_end - val_pos));
        key_pos = val_end;
        if(key_pos != std::string::npos)
            ++key_pos;
    }
    return m;
}
int main()
{
    std::string s = "CA: ABCDnCB: ABFGnCC: AFBVnCD: 4567";
    std::cout << "mappify1: " << 'n';
    auto m = mappify1(s);
    for(auto const& p: m)
        std::cout << '{' << p.first << " => " << p.second << '}' << 'n';
    std::cout << "mappify2: " << 'n';
    m = mappify2(s);
    for(auto const& p: m)
        std::cout << '{' << p.first << " => " << p.second << '}' << 'n';
}

输出:

mappify1: 
{CA => ABCD}
{CB => ABFG}
{CC => AFBV}
{CD => 4567}
mappify2: 
{CA => ABCD}
{CB => ABFG}
{CC => AFBV}
{CD => 4567}

这种格式被称为"标记值"。

行业中使用这种编码的性能最关键的地方可能是金融FIX协议(=表示键值分隔符,'01'表示条目分隔符(。因此,如果你使用的是x86硬件,那么你最好的选择是在谷歌上搜索"SSE4 FIX协议解析器github",并重用HFT商店的开源发现。

如果您仍然想将矢量化部分委托给编译器,并且可以留出几纳秒的可读性,那么最优雅的解决方案是将结果存储在std::string(数据(+boost::flat_map<boost::string_ref, boost::string_ref>(视图(中。解析是一个品味问题,而循环或strtok对编译器来说是最容易解析的。对于人类(熟悉boostspirit(来说,基于BoostSpirit的解析器将是最容易阅读的。

C++用于基于循环的解决方案

#include <boost/container/flat_map.hpp> 
#include <boost/range/iterator_range.hpp>
#include <boost/range/iterator_range_io.hpp> 
#include <iostream>
// g++ -std=c++1z ~/aaa.cc
int main()
{
    using range_t = boost::iterator_range<std::string::const_iterator>;
    using map_t = boost::container::flat_map<range_t, range_t>;
    char const sep = ':';
    char const dlm = 'n';
    // this part can be reused for parsing multiple records
    map_t result;
    result.reserve(1024);
    std::string const input {"hello:worldn bye: world"};
    // this part is per-line/per-record
    result.clear();
    for (auto _beg = begin(input), _end = end(input), it = _beg; it != _end;)
    {
        auto sep_it = std::find(it, _end, sep);
        if (sep_it != _end)
        {
            auto dlm_it = std::find(sep_it + 1, _end, dlm);
            result.emplace(range_t {it, sep_it}, range_t {sep_it + 1, dlm_it});
            it = dlm_it + (dlm_it != _end);
        }
        else throw std::runtime_error("cannot parse");
    }
    for (auto& x: result)
        std::cout << x.first << " => " << x.second << 'n';
    return 0;
}

格式足够简单,"手动"解析IMO是最好的选择,总体而言仍然可读。

这也应该是相当有效的(keyvalue字符串总是相同的——尽管是cleared,所以主循环内部的重新分配应该在几次迭代后停止(;ret也应该符合NRVO、OTOH的条件,以防出现问题,您可以随时更改为输出参数。

当然,std::map可能不是西方最快的枪,但这是问题文本中的一个要求。

std::map<std::string, std::string> parseKV(const std::string &sz) {
    std::map<std::string, std::string> ret;
    std::string key;
    std::string value;
    const char *s=sz.c_str();
    while(*s) {
        // parse the key
        while(*s && *s!=':' && s[1]!=' ') {
            key.push_back(*s);
            ++s;
        }
        // if we quit due to the end of the string exit now
        if(!*s) break;
        // skip the ": "
        s+=2;
        // parse the value
        while(*s && *s!='n') {
            value.push_back(*s);
            ++s;
        }
        ret[key]=value;
        key.clear(); value.clear();
        // skip the newline
        ++s;
    }
    return ret;
}

如果担心性能,您可能应该重新考虑最终结果是否需要映射。这可能会导致内存中出现大量的字符缓冲区。理想情况下,只跟踪char*和每个子字符串的长度会更快/更小。

这里有一个解决方案,使用strtok作为拆分手段。请注意,strtok会更改您的字符串,它会将"\0"放在拆分字符处。

#include <iostream>
#include <string>
#include <map>
#include <string.h>
using namespace std;

int main (int argc, char *argv[])
{
    char s1[] = "CA: ABCDnCB: ABFGnCC: AFBVnCD: 4567";
    map<string, string> mymap;
    char *token;
    token = strtok(s1, "n");
    while (token != NULL) {
        string s(token);
        size_t pos = s.find(":");
        mymap[s.substr(0, pos)] = s.substr(pos + 1, string::npos);
        token = strtok(NULL, "n");
    }
    for (auto keyval : mymap) 
        cout << keyval.first << "/" << keyval.second << endl;
    return 0;
}

我怀疑您是否应该担心读取此字符串并将其转换为std::map的优化问题。如果您真的想优化这个固定内容映射,请将其更改为std::vector<std::pair<>>并对其进行一次排序。

也就是说,创建具有标准C++功能的std::map最优雅的方法如下:

std::map<std::string, std::string> deserializeKeyValue(const std::string &sz) {
    constexpr auto ELEMENT_SEPARATOR = ": "s;
    constexpr auto LINE_SEPARATOR = "n"s;
    std::map<std::string, std::string> result;
    std::size_t begin{0};
    std::size_t end{0};
    while (begin < sz.size()) {
        // Search key
        end = sz.find(ELEMENT_SEPARATOR, begin);
        assert(end != std::string::npos); // Replace by error handling
        auto key = sz.substr(begin, /*size=*/ end - begin);
        begin = end + ELEMENT_SEPARATOR.size();
        // Seach value
        end = sz.find(LINE_SEPARATOR, begin);
        auto value = sz.substr(begin, end == std::string::npos ? std::string::npos : /*size=*/ end - begin);
        begin = (end == std::string::npos) ? sz.size() : end + LINE_SEPARATOR.size();
        // Store key-value
        [[maybe_unused]] auto emplaceResult = result.emplace(std::move(key), std::move(value));
        assert(emplaceResult.second); // Replace by error handling
    }
    return result;
}

尽管每个c++程序员都理解这段代码,但它的性能可能并不理想。

使用boost的一个非常简单的解决方案如下,它也适用于部分令牌(例如,没有值或空对的密钥(。

#include <string>
#include <list>
#include <map>
#include <iostream>
#include <boost/foreach.hpp>
#include <boost/algorithm/string.hpp>
using namespace std;
using namespace boost;
int main() {
    string s = "CA: ABCDnCB: ABFGnCC: AFBVnCD: 4567";
    list<string> tokenList;
    split(tokenList,s,is_any_of("n"),token_compress_on);
    map<string, string> kvMap;
    BOOST_FOREACH(string token, tokenList) {
        size_t sep_pos = token.find_first_of(": ");
        string key = token.substr(0,sep_pos);
        string value = (sep_pos == string::npos ? "" : token.substr(sep_pos+2,string::npos));
        kvMap[key] = value;
        cout << "[" << key << "] => [" << kvMap[key] << "]" << endl;
    }
    return 0;
}
void splitString(std::map<std::string, std::string> &mymap, const std::string &text, char sep)
{
    int start = 0, end1 = 0, end2 = 0;
    while ((end1 = text.find(sep, start)) != std::string::npos && (end2 = text.find(sep, end1+1)) != std::string::npos) {
        std::string key = text.substr(start, end1 - start);
        std::string val = text.substr(end1 + 1, end2 - end1 - 1);
        mymap.insert(std::pair<std::string,std::string>(key, val));
        start = end2 + 1;
    }
}

例如:

std::string text = "key1;val1;key2;val2;key3;val3;";
std::map<std::string, std::string> mymap;
splitString(mymap, text, ';');

将产生大小为3的映射:{key1="val1",key2="val2",key3="val3"}

更多示例:

"key1;val1;key2;"=>{key1="val1"}(没有第二个val,所以第二个密钥不计算在内(

"key1;val1;key2;val2"=>{key1="val1"}(第二个val末尾没有delim,因此不计数(

"key1;val1;key2;;"=>{key1="val1",key2="}(key2包含空字符串(

查看了已接受的答案,并尝试扩展一点,这似乎在更常见的情况下有效。测试运行可以在这里找到。欢迎任何意见或修改。

#include <iostream>
#include <string>
#include <sstream>
#include <map>
#include <algorithm>
#include <vector>
size_t find(const std::string& line, std::vector<std::string> vect, int pos=0) {
    int eol1;
    eol1 = 0;
    for (std::vector<std::string>::iterator iter = vect.begin(); iter != vect.end(); ++iter) {
        //std::cout << *iter << std::endl;
        int eol2 = line.find(*iter, pos);
        if (eol1 == 0 && eol2 > 0)
            eol1 = eol2;
        else if (eol2 > 0 && eol2 < eol1)
            eol1 = eol2;
    }
    return eol1;
}
std::map<std::string, std::string> mappify(std::string const& s, char delim='=') {
    std::map<std::string, std::string> m;
    std::string::size_type key_pos = 0, i, j;
    std::string::size_type key_end;
    std::string::size_type val_pos;
    std::string::size_type lim_pos;
    std::string::size_type val_end;
    while ((key_end = s.find(delim, key_pos)) != std::string::npos) {
        if ((val_pos = s.find_first_not_of(delim, key_end + 1)) == std::string::npos)break;
        while (key_end - 1 > 0 && (s[key_end - 1] <= 32 || s[key_end - 1] == ';'))
            key_end--;
        while (val_pos < s.size() && (s[val_pos] <= 32 || s[val_pos] == ';'))
            val_pos++;
        val_end = s.find('n', val_pos);
        i = s.find('"', val_pos);
        if (i != std::string::npos)
            j = s.find('"', i + 1);
        else
            j = 0;
        lim_pos = find(s.substr(0, i), { " ",";","t" }, val_pos + 1);
        //std::cout << "s.substr(j):" << s.substr(j)<<std::endl;
        if (lim_pos == 0 && j != std::string::npos)lim_pos = find(s.substr(j), { " ",";","t" }) + j;
        if (lim_pos < val_pos)lim_pos = val_pos + 1;
        if (j > 0)val_end = j + 1;
        if (val_end > lim_pos)val_end = lim_pos;
        m.emplace(s.substr(key_pos, key_end - key_pos), s.substr(val_pos, val_end - val_pos));
        key_pos = val_end;
        while ((key_pos < s.size() && s[key_pos] <= 32 || s[key_pos] == ';'))
            ++key_pos;
        if (val_end == 0)break;
    }
    return m;
}
int main() {
    std::string s ="
File="c:\dir\ocean\nCCS_test.txt"n
iEcho=10000; iHrShift=0 rho_Co2 = 1.15d0;n
Liner=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
  auto m = mappify(s);
    for (auto const& p : m)
      std::cout << '{' << p.first << " :=> " << p.second << '}' << 'n';
    return 0;
}