1. 程式人生 > 實用技巧 >批量新增題目功能(正則表示式的使用案例)

批量新增題目功能(正則表示式的使用案例)

問題描述

根據業務需求,需要有一個批量新增題目的功能。

如上圖所示,左邊是純文字輸入的題目,右邊需要解析成一個個對應的題目。

右邊的題目就是一個元件,根據不同的型別進行顯示,這個元件這裡不做談論。這裡要做的就是根據左邊的文字內容,解析成一個個物件的形式,傳入右邊的元件進行渲染。

這篇文章,就是如何把左邊的純文字,解析成一個個的物件,這就考驗文字的拆分能力了。

指定拆分規則

第一步當然是指定規則,不然沒法知道以什麼的方式進行解析。

這裡為了方便處理,對每個題目型別的格式加以限定,每個題目之間也用空行進行分隔。

1、每個題目之間必須以空行分割,題幹中間不得換行。
2、所有題型必須含有 “答案:”欄位,且不能為空。
3、【選擇題】最多支援10個選項A,B,C,D,E,F,G,H,I,J,且必須按順序輸入。
4、【選擇題】選項號A-H與內容之間必須用 ”、“ 或 ”.“ 分隔開。
5、【選擇題】答案中不能加空格。
6、【判斷題】答案僅支援 “正確”,“錯誤” 或者 “對”,“錯”。
7、【填空題】每個空使用至少三個短下劃線 “___” 作為空的位置。
8、【填空題】多個填空的答案用 “|” 分割。每個填空有多個答案的話用 “&&” 分隔。單個答案不用新增。
9、【填空題】答案如果多於題目中需要填空的個數,多於的答案將被忽略。
10、【問答題】的答案可以為空。
11、【檔案上傳題】的答案必須為 “[檔案]” 。

拆分題目

下一步就是拆分每個題目。

以為核心就是一句正則表示式 /\n\s*\n\s*/g,所以直接上程式碼:

// 將批量題目分成一個個題目字串
function getEachSub(whole) {
    let me = this;
    me.previewSubjects = [];

    // 主要就是這句話
    let eachSourceSubs = whole.trim().split(/\n\s*\n\s*/g);
    if(eachSourceSubs.length) {
        eachSourceSubs.forEach(item=>{
            // assembleSub就是把每個題目的字串轉換成題目物件,儲存到previewSubjects數組裡
            me.previewSubjects.push(me.assembleSub(item.trim()));
        });
    }
}

題目字串轉為物件

題目的型別總共分為:

  • 單選題(radio)
  • 多選題(chk)
  • 判斷題(judge)
  • 填空題(fillin)
  • 問答題(textarea)
  • 檔案題(file)

每個題目的物件為:

{
    inpttype:'', // 型別
    subject:'', // 題幹
    answer: '', // 正確答案
    items:[], // 題目選項(單選,多選,填空使用)
    err:'', // 題目解析有問題時的報錯資訊
}

解析的思路:

  • 由於每題都必須含有“答案”欄位,所以通過答案進行拆分可以得到題乾和正確答案內容。
    • 如果匹配到多個“答案”,則報錯,因為每個題目只能有一個答案
  • 答案對了,判斷題型
    • 如果題目中有A-Z開頭的,那麼就是單選題或者多選題
      • 如果是單選題或者多選題
        • 把從“答案”開始到末尾所有字串取到,然後去掉“答案”本身就是這道題目的正確答案。 (如果正確答案長度>1,就是多選題,否則為單選題)
        • 剩下的部分為題乾和選項。再通過選項前面的A-Z進行拆分後,陣列的第一個就是題幹,剩下的就是選項了。
        • 最後,判斷答案與選項是否匹配。
      • 否則
        • 通過“答案”拆分後,陣列的第一個就是題幹
          • 如果題幹中包含“___”就是填空題,它的答案選項就是在陣列的第二個裡面。
          • 如果陣列的第二個是“對”“錯”“正確”“錯誤”,它就是判斷題,那麼陣列第一個就是題幹了。
          • 如果陣列的第二個是“[檔案]”,它就是檔案題了。
          • 剩下的就是問答題。

具體的程式碼如下:

// 將一個個題目字串拆解/組合成題目物件
function assembleSub(eachSub) {
    let me = this;
    let subObj = {
        inpttype:'', // 型別
        subject:'', // 題幹
        answer: '', // 正確答案
        items:[], // 題目選項
        err:'' // 錯誤提示
    };
    let ansArr = eachSub.match(/\n\s*答案[::]/g);

    if(ansArr) {
        if(ansArr.length>1) { // 匹配到多個答案
            subObj.err = '每道題只能有一個答案';
        } else {
            /**
             * 單選題和多選題
             */
            if (eachSub.search(/\n\s*[A-Z][\.、]/ig) > -1) {
                let selReg = /\n\s*答案[::]\s*[A-Z]+/i; // 單選題和多選題

                let selectAns = eachSub.match(selReg);

                if(selectAns) {
                    let ans = selectAns[0].trim().replace(/^答案[::]\s*/g, '').toUpperCase();
                    subObj.answer = ans;
                    // 單選題與多選題
                    subObj.inpttype = ans.length===1 ? 'radio' : 'chk';


                    let sourceTimu = eachSub.replace(selReg,'');
                    // 拆分題干與選項
                    let sourceTimuArr = sourceTimu.split(/[A-Z][、\.]/ig);
                    if(sourceTimuArr.length == 1){
                        subObj.err = '選項不能為空';
                    }else if(sourceTimuArr.length  > 11){
                        subObj.err = '選項數量不能大於10個';
                    }

                    let valArr = [];
                    sourceTimuArr.map((item,i)=>{
                        sourceTimuArr[i] = item.trim().replace(/\s+/g, ' ');

                        if(i===0) {
                            // 題幹
                            subObj.subject = sourceTimuArr[i];
                        } else {
                            // 選項
                            let obj = {
                                pic:'',
                                text: sourceTimuArr[i],
                                val: String.fromCharCode(65+i-1) // ascii轉字母
                            };
                            subObj.items.push(obj);
                            valArr.push(obj.val);
                        }
                    });

                    // 單選題
                    if(subObj.answer.length === 1) {
                        if(!valArr.includes(subObj.answer)) {
                            subObj.err= '答案選項不正確';
                        }
                    } else { // 多選題
                        for (let a of subObj.answer) {
                            if(!valArr.includes(a)) {
                                subObj.err= '答案選項不正確';
                                break;
                            }
                        }
                    }
                } else {
                    subObj.err = '選擇題答案不正確';
                }
            }
            else {
                let reg = /\n\s*答案[::]\s*/g;
                let regArr = eachSub.split(reg);

                subObj.subject = regArr[0];
                /**
                 * 填空題
                 */
                if(regArr[0].includes('___')) {
                    subObj.inpttype = 'fillin';
                    let len = regArr[0].match(/_{3,}/g).length;
                    let fillinAns = regArr[1].split(/\s*\|\s*/g);

                    if(len>0 && len <= fillinAns.length) {
                        for(let i=0; i<len; i++) {
                            if(fillinAns[i].includes('&&')) {
                                subObj.items.push({
                                    matchtype: "equal",
                                    vals: fillinAns[i].split('&&').filter(item=>{return item})
                                });
                            } else {
                                subObj.items.push({
                                    matchtype: "equal",
                                    vals: [fillinAns[i]]
                                });
                            }
                        }
                    } else {
                        subObj.err = '填空題答案個數錯誤';
                    }
                }
                /**
                 * 判斷題
                 */
                else if(regArr[1].trim() === '對'
                    || regArr[1].trim() === '錯'
                    || regArr[1].trim() ==='正確'
                    || regArr[1].trim() ==='錯誤') {
                    subObj.inpttype = 'judge';

                    subObj.answer = (regArr[1].trim() === '對'|| regArr[1].trim() === '正確') ? '1' : '0';
                }
                /**
                 * 檔案上傳題
                 */
                else if(regArr[1].trim() === '[檔案]'){
                    subObj.inpttype = 'file';
                }
                /**
                 * 問答題
                 */
                else {
                    subObj.inpttype = 'textarea';
                    subObj.answer = regArr[1].trim();
                }
            }
        }
    } else { // 未匹配到則為null
        subObj.err = '題目缺少答案'
    }

    return subObj;
}

總結

  • 本文主要考驗正則表示式的使用。
  • 遺留問題:左右聯動還未實現(左邊滾動的時候,右邊可以跟著滾動,類似markdown編輯器的效果)。
  • 正則表示式視覺化測試地址:https://regexr-cn.com/