簡體   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