0%

使用Sqlite3+Express.js+React实现在线答题

将word格式的题库转为txt格式,导入至sqlite3中,使用Express.js做服务端提供json格式数据,使用React做前端获取服务端数据。本文为第一部分,实现导入数据和服务端提供数据API接口。

数据导入

源格式

源题库为word格式,题型分别为单选、多选和判断题。

单选题

多选题

判断题

转为txt格式

为便于读取,word格式另存为txt格式,文件名为questions.txt

建立数据库

我使用的IPython来建立数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sqlite3
conn=sqlite3.connect('mydb.db')
c=conn.cursor()
c.execute('''create table (
id int primary key, // 主键
description text, // 题目
answer text, // 答案
A text, //选择项A。判断题时,A为正确。
B text, //选择项B。判断题时,B为错误。
C text, //选择项C
D text, //选择项D
E text, //选择项E。默认最多5个选择项。
)''')
c.commit()

从txt文件中提取试题信息

txt题库中,每道题都以阿拉伯数字+.开始,形如1.,选择题题干中正确答案在全角括号中,形如(ABCD),判断题题干行中会有×符合,据此提取数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import re // 导入正则表达式模块
p1=re.compile("\d+.") // 判断是否为新的题干的正则表达式
p2=re.compile("((.+))") // 提取正确答案的正则表达式
f=open('questions.txt','r') // 打开文本文件
lines=f.readlines() // 读取全部文本
questions = [] // 建立空题库,在遍历文本中追加
description='' // 初始化题目
answer='' // 初始化答案
A='' // 初始化选项
B=''
C=''
D=''
E=''
for line in lines:
if p1.match(s): // 该行以数字+.开始,遇到一道新题
if description != '': // 确定题目非空
questions.append([description,answer,answerA, // 新题追加到题库中,
answerB,answerC,answerD,answerE,remark])
question='' // 然后清空各字段
answer=''
answerA=''
answerB=''
answerC=''
answerD=''
answerE=''
remark=''
if '×' in s or '√' in line: // 这是一道判断题吗
description=s[:s.find('(')] // 提取题目表述
answerA='√'
answerB='×'
remark='2' // 为便于排序,备注中判断题标记为2
if '×' in s: // 答案为×
answer='B'
elif '√' in s:
answer='A'
continue // 判断题没有选择项,所以直接跳到下一个循环
else: // 不是判断,那就是选择题了
description=s[:s.find('(')+1]+s[s.find(')'):] // 提取题目描述
answer=p2.search(s).group(1).strip()
if len(answer)>1:
remark='1' // 多选题标记为1
else:
remark='0' // 单选题标记为0
else: // 该行不是以数字+.开始,是选择项
answers = s.split() // 选择项之间以空格分开
for an in answers:
if an.startswith('A'): // 选项A
answerA=an
elif an.startswith('B'): // 选项B
answerB=an
elif an.startswith('C'): // 选项C
answerC=an
elif an.startswith('D'): // 选项D
answerD=an
elif an.startswith('E'): // 选项E
answerE=an

试题信息导入数据库

现在,所有题目都在questions数组中,可以插入到数据库中了:

1
2
3
4
5
6
7
8
conn = sqlite3.connect('mydb.db') // 连接数据库
c=conn.cursor() // 获取游标
i=1 // 计数器,做ID赋值用
for q in questions:
// 执行插入
c.execute("insert into question(id,description,answer,A,B,C,D,E,remark) values(%d,'%s','%s','%s','%s','%s','%s','%s','%s')"%(i,q[0],q[1],q[2],q[3],q[4],q[5],q[6],q[7]))
i=i+1
conn.commit() // 提交

使用express.js建立服务端

新建目录express-sqlite3:

1
$ mkdir express-sqlite3 ; cd express-sqlite3

建立新文件package.json,输入以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "express-sqlite3",
"version": "1.0.0",
"description": "",
"main": "restapi.js",
"dependencies": {
"express": "^4.13.1",
"sqlite3": "https://github.com/mapbox/node-sqlite3/tarball/master"
},
"devDependencies": {},
"scripts": {
"start": "node restapi.js"
},
"author": "",
"license": "ISC"
}

执行npm install安装包。
新建db目录,将上一步生成的mydb.db文件拷贝至此目录内。
新建restapi.js文件,输入以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database('db/mydb.db');

var express = require('express');
var restapi = express();

restapi.get('/data', function(req, res){
results = [];
db.all("SELECT * from question order by remark,description", function(err, rows){
rows.map((row)=>{
results.push({"id":row.id,
"description":row.description,
"answer":row.answer,
"A":row.A,
"B":row.B,
"C":row.C,
"D":row.D,
"E":row.E,
"remark":row.remark,})
});
res.json(results);
});
});

restapi.listen(3000); // 在3000端口监听

console.log("Submit GET to http://localhost:3000/data");

在命令行执行:
1
2
3
4
npm start
> express-sqlite3@1.0.0 start /home/fanzhh/projects/express-sqlite3
> node restapi.js
Submit GET to http://localhost:3000/data

此时在浏览器输入地址http://localhost:3000/data,页面显示如下:
json数据

建立React项目

首先安装create-react-app,如果你已安装,请略过。

1
$ npm install -g create-react-app

然后新建项目,我们项目的名字为frontend
1
$ create-react-app frontend

安装过程需要几分钟:
2018-01-31 13-49-01屏幕截图.png

从服务器获取json数据我们需要用jquery,安装

1
npm install query

界面设计

我们预想的操作界面是这样的(原谅我粗狂的画风^^):
sketch-1517379201030.png
每道题在一个

中,上面是题目描述部分,下面是选择框。

修改App.js

修改frontend/src/App.js文件。

导入

1
2
3
import React, { Component } from 'react';
import $ from 'jquery';
import './App.css';

题目描述部件

1
2
3
4
5
class DescriptionBar extends Component {
render() {
return <p>{this.props.description}</p>
}
}

答案选择部件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SelectionsBar extends Component {
constructor(props) {
super(props);
this.handleChange=this.handleChange.bind(this); // 为事件绑定this
}
handleChange(event) {
this.props.onChange(event) // 答案选择触发事件传递给props中的onChange
}
render() {
var selection_type = this.props.remark === '1' ? 'checkbox' : 'radio'; // 多选题使用checkbox,单选题使用radio,注意判断题也是单选
var selection_name = this.props.reamrk === '1' ? 'choose_mul' : 'choose_one'
return (
<form>
<fieldset>
<input name={selection_name} type={selection_type} id={this.props.id+'_A'} value='A' onChange={this.handleChange} /><label htmlFor={this.props.id+'_A'}>{this.props.answerA}</label><br /> // 每道题至少两个选择项A和B
<input name={selection_name} type={selection_type} id={this.props.id+'_B'} value='B' onChange={this.handleChange} /><label htmlFor={this.props.id+'_B'}>{this.props.answerB}</label><br /> // 控件ID设为题目的ID+该控件的符号(A?B?C?...)
{this.props.answerC === '' ? '' : (<span><input name={selection_name} type={selection_type} id={this.props.id+'_C'} value='C' onChange={this.handleChange} /><label htmlFor={this.props.id+'_C'}>{this.props.answerC}</label><br /></span>)} // C以下根据内容不为空则显示
{this.props.answerD === '' ? '' : (<span><input name={selection_name} type={selection_type} id={this.props.id+'_D'} value='D' onChange={this.handleChange} /><label htmlFor={this.props.id+'_D'}>{this.props.answerD}</label><br /></span>)}
{this.props.answerE === '' ? '' : (<span><input name={selection_name} type={selection_type} id={this.props.id+'_E'} value='E' onChange={this.handleChange} /><label htmlFor={this.props.id+'_E'}>{this.props.answerE}</label><br /></span>)}
</fieldset>
</form>
)
}
}

提交部件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SubmitBar extends Component {
constructor(props) {
super(props);
this.onClick=this.onClick.bind(this);
}
onClick(event) {
this.props.onClick(event) // 提交事件传递给父部件
}
render() {
return(
<form>
<button type="submit" onClick={this.onClick} >{this.props.answered?'再做一遍错题':'检查'}</button> // 根据父控件状态判断现在是检查之前还是之后,相应改变按钮文字
</form>
)
}
}

问题部件

问题部件是题目描述和答案选择的父部件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class QuestionBar extends Component {
render() {
return (
<div>
<DescriptionBar description={this.props.question.description} /> // 题目描述部件
<SelectionsBar // 选择部件
id={this.props.question.id} // 传递属性值
answer={this.props.question.answer}
answerA={this.props.question.A}
answerB={this.props.question.B}
answerC={this.props.question.C}
answerD={this.props.question.D}
answerE={this.props.question.E}
remark={this.props.question.remark}
onChange={this.props.onChange}
/>
{this.props.answered ? (this.props.question.answer===this.props.answer.answer? ('') : (<p style={{"color":"red"}}>正确答案:{this.props.question.answer}</p>) ) : ('')} // 如果当前已经检查,且回答与正确答案不符,则以红色显示正确的答案。
</div>
)
}
}

整体App部件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class App extends Component {
constructor(props) {
super(props);
this.state = {
questions: [], // 初始题目集
current_questions: [], // 当前题目集,加载页面后与初始题目集相同,检查后则只保留错题
answered: false, // 当前答题状态
answers: [], // 答案集
}
this.handleChange = this.handleChange.bind(this);
this.handleCheckClick = this.handleCheckClick.bind(this);
}
componentDidMount() { // 部件加载后获取数据
var that = this;
const url = 'http://localhost:3000/data/';
$(function(){$.ajax({ // 这里很关键不要写错,$(function(){}困扰了我N天,:-<
headers: {
'Content-Type': 'application/json',
},
url: url,
type: "GET",
dataType: "json",
data: {},
success: function(result) {
that.setState({questions:result,current_questions:result,});
var answers = [];
result.forEach((r)=>{
answers.push({'id':r.id,'answer':''})
});
that.setState({answers:answers,});
},
error: function(xhr, status, err) {
console.log(err.Message);
},
})})
}
handleChange(event) { // 选择控件的相应事件
const id = parseInt(event.target.id.split('_')[0]); // 由控件ID获得题目ID和所作选择
const selection = event.target.id.split('_')[1];
const type = event.target.type;
var answers = this.state.answers;
if (type==='radio') { // 单选题直接给答案赋值
answers.find(answer=>answer.id===id).answer = selection;
} else {
if (event.target.checked) { // 多选题,如果勾选
if (!answers.find(answer=>answer.id===id).answer.includes(selection)){
var tmp = answers.find(answer=>answer.id===id).answer + selection;
tmp = tmp.split('').sort().join(''); // 赋值前排序,考虑到用户奇怪的操作方式,想想:ABC===ACB吗?
answers.find(answer=>answer.id===id).answer = tmp;
}
} else { // 如果去掉勾选,答案中也要相应删除
if (answers.find(answer=>answer.id===id).answer.includes(selection)){
answers.find(answer=>answer.id===id).answer = answers.find(answer=>answer.id===id).answer.replace(selection,'')
}
}
}
this.setState({answers:answers,})
}
handleCheckClick(event) { // 检查按钮的相应事件
event.preventDefault();
if (event.target.innerHTML==='检查') {
this.setState({answered:true,});
} else { // 若是再做一遍错题,则需要根据正确与否更新错题库
var current_questions = [];
var answers = [];
this.state.current_questions.forEach((question)=> {
if (this.state.answers.find(answer=>answer.id===question.id).answer!==question.answer) {
current_questions.push(question);
answers.push({'id':question.id,'answer':''})
}
});
this.setState({current_questions:current_questions,answers:answers,answered:false,})
}
}
render() {
var questions = [];
this.state.current_questions.forEach((question)=>{
questions.push(<div className="box effect2"><QuestionBar key={question.id} question={question} answer={this.state.answers.find(answer=>answer.id===question.id)} answered={this.state.answered} onChange={this.handleChange} /></div>)
})
return <div><div>{questions}</div><div className="box effect2"><SubmitBar answered={this.state.answered} onClick={this.handleCheckClick} /></div></div>
}
}

export default App;

OK。

Github

这个项目我放在github上了,地址在这儿

演示地址

点击这儿可以查看heroku上的演示(题库数据量较大,加载大概需要十几秒钟)。