化学式解析器C ++

时间:2012-03-31 17:07:35

标签: c++

我目前正在开发一个程序,可以解析化学式并返回分子量和百分比组成。以下代码适用于H 2 O,LiOH,CaCO 3 等化合物,甚至C 12 H 22 < /子> 0 <子> 11 。但是,它无法理解括号内的多原子离子,如(NH 4 2 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;
}

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

5 个答案:

答案 0 :(得分:6)

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

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

  1. Recursive descent
  2. 基于语法规范文件的机器生成的自下而上解析器。
  3. (从技术上讲,还有第三类,PEG,机器生成自上而下。)

    无论如何,对于案例1,当您看到(时,需要对您的解析器进行递归调用,然后在)上从此递归级别返回令牌。

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

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

答案 1 :(得分:4)

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

这与C ++中的标识符概念非常相似。当编译器看到int someNumber = 5;时,它会说,&#34;存在名为someNumber int类型的变量,其中存储了数字5&#34;。如果您以后使用名称someNumber,则表示您已经在谈论 someNumber(只要您处于正确的范围内)。

返回原子解析器。当你的解析器看到一个后跟一个数字的原子时,它知道将该数字应用于那个原子。所以O2表示&#34; 2氧原子类型&#34;。 N2表示&#34; 2原子氮类型。&#34;

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

你现在拥有的是什么。但它错误。因为数字不总是修改原子;有时,他们会修改原子的。与(NH4)2SO4中一样。

现在,您需要更改解析器的工作方式。当它看到O时,它需要知道这不是&#34;氧气类型的原子&#34;。它是含有氧气的&#34; &#34;。 O2是&#34; 2个包含氧气的组&#34;。

一个组可以包含一个或多个原子。因此,当您看到(时,您就知道自己正在创建。因此,当您看到(...)3时,您会看到&#34; 3个包含...&#34;的组。

那么,(NH4)2是什么?它是含有[1个含有氮的基团,然后含有4个含氢基团的基团]&#34;

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

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

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(),直到输入干涸,将每个原子+计数存储在一个数组中。显然你在那里有一些错误检查,但是现在让我们忽略它。

您需要做的是停止解析原子。您需要解析 groups ,它们是单个原子,或由()对表示的一组原子。

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函数类似)会调用ParseGroupSequence。这将调用ParseGroup。哪个可以拨打ParseGroupSequence。等等Group可以包含一个原子或一系列Group s(例如NH4),存储为vector<Group>

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

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

NH 3

  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>
  4. (NH 3)2

    1. ParseGroupSequence被调用。它不在输入的末尾或),因此它会调用ParseGroup
      1. ParseGroup会看到(,这是Group的开头。它会吃掉这个字符(从输入中删除它)并在ParseGroupSequence上调用Group
        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>
    3. 此解析器使用递归到#34;下降&#34;进入每个小组。因此,这种解析器被称为&#34;递归下降解析器&#34; (这是对这类事情的正式定义,但这是对这一概念的良好理解)。

答案 2 :(得分:3)

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

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

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

答案 3 :(得分:1)

考虑将您的计划重新构建为简单的Recursive Descent Parser

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

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

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

答案 4 :(得分:0)

也许你可以在解析之前摆脱括号。你需要找到有多少“括号中的括号”(对不起我的英语)并将其重写为“最深”的开头:

  1. (NH <子> 4 (钠<子> 2 ħ<子> 4 )<子> 3 Zn)的<子> 2 SO 4 (这个公式并不意味着任何,实际上......)

  2. (NH <子> 4 娜<子> 6 ħ<子> 12 Zn)的<子> 2 SO <子> 4 < /子>

  3. NH <子> 8 娜<子> 12 ħ<子> 24 锌<子> 2 SO <子> 4

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