繁体   English   中英

化学式解析器C ++

[英]Chemical formula parser C++

我目前正在开发一个程序,可以解析化学式并返回分子量和百分比组成。 以下代码适用于H 2 O,LiOH,CaCO 3甚至C 12 H 22 O 11等化合物。 然而,它不能理解具有位于括号内的多原子离子的化合物,例如(NH 42 SO 4

我不是在寻找一个必须为我编写程序的人,而只是给我一些关于如何完成这项任务的技巧。

目前,程序遍历输入的字符串raw_molecule ,首先查找每个元素的原子序数,以存储在向量中(我使用map<string, int>来存储名称和原子#)。 然后它找到每个元素的数量。

bool Compound::parseString() {
map<string,int>::const_iterator search;
string s_temp;
int i_temp;

for (int i=0; i<=raw_molecule.length(); i++) {
    if ((isupper(raw_molecule[i]))&&(i==0))
        s_temp=raw_molecule[i];
    else if(isupper(raw_molecule[i])&&(i!=0)) {
        // New element- so, convert s_temp to atomic # then store in v_Elements
        search=ATOMIC_NUMBER.find (s_temp);
        if (search==ATOMIC_NUMBER.end()) 
            return false;// There is a problem
        else
            v_Elements.push_back(search->second); // Add atomic number into vector

        s_temp=raw_molecule[i]; // Replace temp with the new element

    }
    else if(islower(raw_molecule[i]))
        s_temp+=raw_molecule[i]; // E.g. N+=a which means temp=="Na"
    else
        continue; // It is a number/parentheses or something
}
// Whatever's in temp must be converted to atomic number and stored in vector
search=ATOMIC_NUMBER.find (s_temp);
if (search==ATOMIC_NUMBER.end()) 
    return false;// There is a problem
else
    v_Elements.push_back(search->second); // Add atomic number into vector

// --- Find quantities next --- // 
for (int i=0; i<=raw_molecule.length(); i++) {
    if (isdigit(raw_molecule[i])) {
        if (toInt(raw_molecule[i])==0)
            return false;
        else if (isdigit(raw_molecule[i+1])) {
            if (isdigit(raw_molecule[i+2])) {
                i_temp=(toInt(raw_molecule[i])*100)+(toInt(raw_molecule[i+1])*10)+toInt(raw_molecule[i+2]);
                v_Quantities.push_back(i_temp);
            }
            else {
                i_temp=(toInt(raw_molecule[i])*10)+toInt(raw_molecule[i+1]);
                v_Quantities.push_back(i_temp);
            }

        }
        else if(!isdigit(raw_molecule[i-1])) { // Look back to make sure the digit is not part of a larger number
            v_Quantities.push_back(toInt(raw_molecule[i])); // This will not work for polyatomic ions
        }
    }
    else if(i<(raw_molecule.length()-1)) {
        if (isupper(raw_molecule[i+1])) {
            v_Quantities.push_back(1);
        }
    }
    // If there is no number, there is only 1 atom. Between O and N for example: O is upper, N is upper, O has 1.
    else if(i==(raw_molecule.length()-1)) {
        if (isalpha(raw_molecule[i]))
            v_Quantities.push_back(1);
    }
}

return true;
}

这是我的第一篇文章,所以如果我收录的信息太少(或者说太多),请原谅我。

虽然你可以做一个类似ad-hoc扫描器的事情,可以处理一个级别的parens,但用于这类事情的规范技术是编写一个真正的解析器。

有两种常见的方法可以做到这一点......

  1. 递归下降
  2. 基于语法规范文件的机器生成的自下而上解析器。

(从技术上讲,还有第三类,PEG,机器生成自上而下。)

无论如何,对于情况1,当您看到(然后从该递归级别返回)令牌时,您需要编写对解析器的递归调用

通常会创建一个树状的内部表示; 这被称为语法树 ,但在您的情况下,您可以跳过它,只返回递归调用的原子权重,添加到您将从第一个实例返回的级别。

对于案例2,您需要使用yacc之类的工具将语法转换为解析器。

您的解析器了解某些事情。 它知道当它看到N时,这意味着“氮原子类型”。 当它看到O ,它意味着“氧气类型原子”。

这与C ++中的标识符概念非常相似。 当编译器看到int someNumber = 5; 它说,“存在一个名为someNumberint类型的变量,其中存储了数字5 ”。 如果以后使用的名称someNumber ,它知道你在谈论的是 someNumber (只要你在正确的范围是)。

回到你的原子解析器。 当你的解析器看到一个后跟一个数字的原子时,它知道将该数字应用于那个原子。 所以O2意思是“2种氧原子”。 N2表示“2种氮原子”。

这对你的解析器意味着什么。 这意味着看到一个原子是不够的 这是一个良好的开端,但仅知道分子中存在多少原子是不够的。 它需要阅读下一件事。 因此,如果它看到O后跟N ,则它知道O表示“1氧原子类型”。 如果它看到O后面没有任何东西(输入结束),那么它再次表示“1氧气类型原子”。

这就是你目前所拥有的。 但这是错的 因为数字并不总是修改原子; 有时,它们会修改原子 (NH4)2SO4

所以现在,您需要更改解析器的工作方式 当它看到O ,它需要知道这不是“氧气类型原子”。 它是“含氧 ”。 O2是“含氧的2个基团”。

一个组可以包含一个或多个原子。 所以当你看到(你知道你正在创建一个 。因此,当你看到(...)3 ,你会看到“3个包含...的组”。

那么, (NH4)2什么? 它是“含有[1个含氮的基团,然后含有4个含氢基团]的2个基团”。

这样做的关键是理解我刚写的内容。 组可以包含其他组 有小组嵌套。 你如何实现嵌套?

好吧,你的解析器目前看起来像这样:

NumericAtom ParseAtom(input)
{
  Atom = ReadAtom(input); //Gets the atom and removes it from the current input.
  if(IsNumber(input)) //Returns true if the input is looking at a number.
  {
    int Count = ReadNumber(input); //Gets the number and removes it from the current input.
    return NumericAtom(Atom, Count);
  }

  return NumericAtom(Atom, 1);
}

vector<NumericAtom> Parse(input)
{
  vector<NumericAtom> molecule;
  while(IsAtom(input))
    molecule.push_back(ParseAtom(input));
  return molecule;
}

您的代码调用ParseAtom()直到输入运行干,将每个atom + count存储在一个数组中。 显然你在那里有一些错误检查,但是现在让我们忽略它。

你需要做的是停止解析原子。 您需要解析 ,这些可以是单个原子,也可以是由()对表示的一组原子。

Group ParseGroup(input)
{
    Group myGroup; //Empty group

    if(IsLeftParen(input)) //Are we looking at a `(` character?
    {
        EatLeftParen(input); //Removes the `(` from the input.

        myGroup.SetSequence(ParseGroupSequence(input)); //RECURSIVE CALL!!!

        if(!IsRightParen(input)) //Groups started by `(` must end with `)`
            throw ParseError("Inner groups must end with `)`.");
        else
            EatRightParen(input); //Remove the `)` from the input.
    }
    else if(IsAtom(input))
    {
        myGroup.SetAtom(ReadAtom(input)); //Group contains one atom.
    }
    else
        throw ParseError("Unexpected input."); //error

    //Read the number.
    if(IsNumber(input))
        myGroup.SetCount(ReadNumber(input));
    else
        myGroup.SetCount(1);

    return myGroup;
}

vector<Group> ParseGroupSequence(input)
{
    vector<Group> groups;

    //Groups continue until the end of input or `)` is reached.
    while(!IsRightParen(input) and !IsEndOfInput(input)) 
        groups.push_back(ParseGroup(input));

    return groups;
}

这里最大的区别是ParseGroup (与ParseAtom函数ParseAtom )将调用ParseGroupSequence 这将调用ParseGroup 哪个可以调用ParseGroupSequence 等等。一个Group可以包含原子或一Group S(如NH4 ),存储为vector<Group>

当函数可以自己调用(直接或间接)时,它被称为递归 哪个好,只要不能无限递归。 并且没有机会,因为它只会在每次看到时递归(

那么这是如何工作的呢? 好吧,让我们考虑一些可能的输入:

NH3

  1. ParseGroupSequence 它不在输入或)的末尾,所以它调用ParseGroup
    1. ParseGroup看到一个N ,它是一个原子。 它将此原子添加到Group 然后它看到一个H ,这不是一个数字。 因此,它将Group的计数设置为1,然后返回Group
  2. 回到ParseGroupSeqeunce ,我们将返回的组存储在序列中,然后在循环中迭代。 我们没有看到输入结束或) ,所以它调用ParseGroup
    1. ParseGroup看到一个H ,它是一个原子。 它将此原子添加到Group 然后它看到一个3 ,这是一个数字。 因此,它读取此数字,将其设置为Group的计数,并返回该Group
  3. 回到ParseGroupSeqeunce ,我们将返回的Group存储在序列中,然后在循环中迭代。 我们没有看到) ,但我们确实看到了输入的结束。 所以我们返回当前vector<Group>

(NH 3)2

  1. ParseGroupSequence 它不在输入或)的末尾,所以它调用ParseGroup
    1. ParseGroup看到一个(它是一个Group的开头。它吃掉这个字符(从输入中删除它)并在Group上调用ParseGroupSequence
      1. ParseGroupSequence不在输入或)的末尾,因此它调用ParseGroup
        1. ParseGroup看到一个N ,它是一个原子。 它将此原子添加到Group 然后它看到一个H ,这不是一个数字。 因此,它将组的计数设置为1,然后返回Group
      2. 回到ParseGroupSeqeunce ,我们将返回的组存储在序列中,然后在循环中迭代。 我们没有看到输入结束或) ,所以它调用ParseGroup
        1. ParseGroup看到一个H ,它是一个原子。 它将此原子添加到Group 然后它看到一个3 ,这是一个数字。 因此,它读取此数字,将其设置为Group的计数,并返回该Group
      3. 回到ParseGroupSeqeunce ,我们将返回的组存储在序列中,然后在循环中迭代。 我们看不到输入的结束,但我们确实看到了) 所以我们返回当前vector<Group>
    2. 回到第一次调用 ParseGroup ,我们得到vector<Group> 我们将它作为序列粘贴到我们当前的Group中。 我们检查,看看下一个字符是) ,吃它,然后继续。 我们看到一个2 ,这是一个数字。 因此,它读取此数字,将其设置为Group的计数,并返回该Group
  2. 现在,方式, 途径回到原来的ParseGroupSequence电话,我们存储返回Group序列中,然后遍历在我们的循环。 我们没有看到) ,但我们确实看到了输入的结束。 所以我们返回当前vector<Group>

此解析器使用递归“下降”到每个组中。 因此,这种解析器被称为“递归下降解析器”(对于这种事物有一个正式的定义,但这是对该概念的良好的理解)。

写下您想要阅读和识别的字符串的语法规则通常很有帮助。 语法只是一堆规则,它们说明什么样的字符序列是可以接受的,并且暗示是不可接受的。 它有助于在编写程序之前和编写程序时使用语法,并且可能会被提供给解析器生成器(如DigitalRoss所述)

例如,没有多原子离子的简单化合物的规则如下:

Compound:  Component { Component };
Component: Atom [Quantity] 
Atom: 'H' | 'He' | 'Li' | 'Be' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'
  • [...]被视为可选项,并且将成为程序中的if测试(无论是存在还是缺失)
  • | 是替代品,所以if .. else if .. else或switch'test',它表示输入必须匹配其中一个
  • { ... }被读作0或更多的重复,并且将是程序中的while循环
  • 引号之间的字符是字符串中的字符。 所有其他的单词都是规则的名称,对于递归下降解析器,最终是被调用的函数的名称,并处理输入。

例如,实现“数量”规则的函数只需读取一个或多个数字字符,并将它们转换为整数。 实现Atom规则的函数读取足够的字符以确定它是哪个原子,并将其存储起来。

关于递归下降解析器的一个好处是错误消息可能非常有用,并且形式为“期望一个Atom名称,但得到%c”或“期待一个”)但是到达了字符串的结尾“。 在发生错误后恢复有点复杂,因此您可能希望在第一个错误时抛出异常。

那么多原子离子只是括号的一个层次吗? 如果是这样,语法可能是:

Compound: Component { Component }  
Component: Atom [Quantity] | '(' Component { Component } ')' [Quantity];
Atom: 'H' | 'He' | 'Li' ...
Quantity: Digit { Digit }
Digit: '0' | '1' | ... '9'

或者它更复杂,并且符号必须允许嵌套括号。 一旦清楚,您就可以找到解析的方法。

我不知道你的问题的整个范围,但递归下降解析器编写相对简单,并且看起来足够你的问题。

考虑将程序重构为简单的递归下降解析器

首先,您需要更改parseString函数以获取要解析的string ,以及通过引用传递的开始解析的当前位置。

通过这种方式,您可以构建代码,以便在看到a时( 在下一个位置调用相同的函数,然后返回Composite ,并使用结束) 当你看到a )本身,你返回而不消耗它。 这可以让你使用无限嵌套()公式,虽然我不确定是否有必要(自从我上次看到化学式时已超过20年)。

这样,您只需编写一次解析复合的代码,并根据需要多次重复使用它。 很容易补充你的读者使用破折号等公式,因为你的解析器只需要处理基本的构建块。

也许你可以在解析之前摆脱括号。 你需要找到多少“括号中的括号”(对不起我的英语),然后重写它就像从“最深”开始的那样:

  1. (NH 4 (Na 2 H 43 Zn) 2 SO 4 (这个公式并不意味着任何,实际......)

  2. (NH 4 Na 6 H 12 Zn) 2 SO 4

  3. NH 8 Na 12 H 24 Zn 2 SO 4

  4. 没有括号,让我们用NH 8 Na 12 H 24 Zn 2 SO 4运行您的代码

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM