构建开源问卷
问卷调查是社会科学中最常用的研究方法之一,可以帮助研究者快速且有效地了解人群的观点、态度。同时,问卷还可以用于实验研究中被试信息的收集,例如使用问卷题项测量被试在实验前后的观念变化。在现有的计算社会科学研究技术条件下,基于在线问卷还可以实现在线实验。
研究中最常使用的在线问卷一般是基于商业化平台的,例如Qualtrics、Credamo、问卷星(Sojump)等。这些平台虽然能提供简单高效的问卷开发工具,但是在内容自由度上做了一定限制。如果希望在线实验中包含较复杂的任务,这些平台就很难满足开发需求。或者开发成本会变得很高昂,例如Qualtrics开启Javascript脚本需要购买300磅一个月的高级权限。
为了实现更多样的研究目的,也为了降低研究成本,基于开源工具搭建问卷就成了一个非常现实的解决方案。
通常,开发一个网站需要开发者具备实现前端界面设计、后端逻辑控制以及硬件服务器部署的能力。全面掌握这些能力绝对不是一页笔记能解决的,有没有什么上手简单、功能全面、部署容易的实现方法呢?
有的兄弟,有的,这么好的方案当然是不止一个。本文介绍基于Python中的Streamlit库搭建开源问卷。(部分材料来自于Streamlit文档,文档永远是学习开发的最好材料~)
使用以下命令安装本文所需的库。
pip install streamlit
Streamlit的原理及常用控件
Streamlit是一个基于Python的轻量web开发工具,它的优点是简单快捷,适合于小团队内部工具的开发或者交付客户的系统demo。
调查问卷具有并发访问数少,总访问次数有限的特点,因此Streamlit能够满足一个开源问卷的搭建。
Streamlit的运行原理可以总结为顺序执行+按需进行缓存。具体来说:
- Streamlit app 本质是一个按从上到下顺序执行的 Python 脚本。
- 每当用户通过浏览器选项卡访问应用程序时,系统都会启动一个新会话(session),并从头开始执行脚本。
- 在脚本执行过程中,Streamlit 会实时将输出内容渲染到浏览器界面。
- 当用户与界面中的任何交互部件(widget)进行操作时,都会触发脚本的重新执行,同时 Streamlit 会相应地更新浏览器中的渲染内容。
- 通过缓存机制,应用程序可以避免重复计算开销较大的函数,从而确保状态更新保持高效。
- 会话状态(session state)功能支持在脚本重新运行期间(例如组件被点击时)保存关键信息。
- Streamlit 支持多页面设计,这些页面既可以作为独立脚本存放在
pages
文件夹中,也可以在主脚本中以函数形式进行定义。
Streamlit app 遵循Server-Client的架构,Server是Python后端,通过streamlit run APP.py
启动,Client是浏览器前端。
因此,这意味着部署app的服务器承担全部数据的计算和存储任务,需要谨慎考虑并发请求数量;app也无法主动读取用户设备上的文件,需要依赖st.file_uploader
来实现上传;这也意味着app无法在用户的设备上打开/跳转指定的程序。但是我们可以在服务器上部署数据库存储用户答卷,或者在app中嵌入远程数据库连接的功能。
Streamlit app 使用会话状态来存储变量,只要本次会话还未结束,那么这些变量就不会丢失,这是我们能用Streamlit开发开源问卷的基础条件。
在本项目中,我们会用到以下几个组件:
st.button
,按钮,可以用于实现翻页以及提交功能。st.radio
,单选,用于实现选择题。st.selectbox
,选项框,在选项数量多或字数多时可以用该组件代替st.radio
,默认是折叠状态,界面更整洁。st.slider
,滑动条,用于输入数值,有变体st.select_slider
。st.text_input
,单行的文本输入框。st.text_area
,多行的文本输入区域。
控件组合实现简单问卷
页面基本设置
导入Streamlit并做一些页面参数的设置:
import streamlit as st
# 页面基本信息,page_title对应浏览器上标签页的标题,page_icon则是logo,可以用:emoji:或单个emoji字符
st.set_page_config(page_title="Survey", page_icon=":happy:")
我们想实现一个可以翻页的问卷,可以考虑Streamlit原生支持的多页app。但这有一点不好,页面切换时url会发生变化,这不利于我们控制作答流程。
因此,可以使用一个略显鲁莽的方式,我们在页面状态上定义页数,在app主体上使用if语句来决定在指定页上出现的内容。
# 初始化页面时,若是page_num不存在,则进行创建
if "page_num" not in st.session_state:
st.session_state.page_num = 0
if st.session_state.page_num == 0:
# 这里是页面呈现信息
pass
if st.session_state.page_num == 1:
# 这里是页面呈现信息
pass
# ...
加入控件
下面开始加入实际的题项来完善此问卷。Streamlit中的控件可以很好地支持我们实现单选、多选、填空等题型。以单选为例,可以使用pills
,radio
,selectbox
以及select_slider
等控件实现。
choice = st.selectbox("请选择一个选项", ["选项A", "选项B"])
此函数有两个必须参数,分别为说明语以及选项。说明语部分是markdown文本,因此可以在其中嵌入加粗、斜体、颜色徽章等样式。
在这一段代码中,我们使用了变量choice
来承接函数的返回值。而在触发交互后,页面会重新运行,这意味着变量很可能会丢失!(假如当前页面没有这道题)因此,可以将choice的值记录在会话状态中,以保证在页面重新运行时依然能够保留已选项。
# At the begin of program
if "choice" not in st.session_state:
st.session_state.choice = None
# ...
choice = st.selectbox("请选择一个选项", ["选项A", "选项B"])
st.session_state["choice"] = choice
如果你对streamlit的工作方式更熟悉了,可以尝试使用控件的key
参数。当指定key
时,streamlit会为控件在会话状态中对应key
的位置记录其当前状态。使用key
参数时需要注意值不能重复,它必须是唯一的。
其他控件的使用方法与之相似,可在streamlit的文档中具体查看~
翻页以及完整性检查
接下来就是实现翻页的按钮。我们在之前已经定义了表示页数的会话状态参数,并且知道了可以将变量值传入指定会话状态参数中。那么翻页按钮只需要能“触发”会话状态修改即可。
在streamlit中,控件会有一个on_click
或on_change
参数,它接受可调用的函数。这个参数表示当控件被点击或者内容改变时会发生什么。
于是我们先定义一个按钮被点击的事件,然后将其填入on_click
。
def goToNextPage():
st.session_state["page_num"] += 1
st.button("下一页", on_click=goToNextPage)
这时当被试点击该按钮,就会触发goToNextPage
,会话状态中的页数加1,接着页面会通过if语句定义的逻辑切换到新的内容。
如果我们想设置某些题是不可跳过的,被试必须完成作答才能翻页,该如何实现呢?非常简单,只要一行if。假设这个页面上有q1,q2,q3三道题。
if q1 and q2 and q3:
st.session_state["q1"] = q1
st.session_state["q2"] = q2
st.session_state["q3"] = q3
st.button("下一页", on_click=goToNextPage)
当q1,q2,q3都有答案时(非空),首先保存它们的值,接着显示翻页按钮。反之,只要缺失一个,翻页按钮都不会出现。这样就实现了一个较简单的完整性检查。
复杂的格式验证可以通过额外引入Pydantic
库实现,本文不详细介绍。
测试...
几行代码几次点击部署为web应用
更深入的开发...
- 复杂实验交互
- 远程数据库连接
- 自定义CSS样式
- 复杂控制逻辑