Skip to main content

构建开源问卷

问卷调查是社会科学中最常用的研究方法之一,可以帮助研究者快速且有效地了解人群的观点、态度。同时,问卷还可以用于实验研究中被试信息的收集,例如使用问卷题项测量被试在实验前后的观念变化。在现有的计算社会科学研究技术条件下,基于在线问卷还可以实现在线实验。

研究中最常使用的在线问卷一般是基于商业化平台的,例如Qualtrics、Credamo、问卷星(Sojump)等。这些平台虽然能提供简单高效的问卷开发工具,但是在内容自由度上做了一定限制。如果希望在线实验中包含较复杂的任务,这些平台就很难满足开发需求。或者开发成本会变得很高昂,例如Qualtrics开启Javascript脚本需要购买300磅一个月的高级权限。

为了实现更多样的研究目的,也为了降低研究成本,基于开源工具搭建问卷就成了一个非常现实的解决方案。

通常,开发一个网站需要开发者具备实现前端界面设计、后端逻辑控制以及硬件服务器部署的能力。全面掌握这些能力绝对不是一页笔记能解决的,有没有什么上手简单、功能全面、部署容易的实现方法呢?

有的兄弟,有的,这么好的方案当然是不止一个。本文介绍基于Python中的Streamlit库搭建开源问卷。(部分材料来自于Streamlit文档,文档永远是学习开发的最好材料~)

使用以下命令安装本文所需的库。

pip install streamlit

Streamlit的原理及常用控件

Streamlit是一个基于Python的轻量web开发工具,它的优点是简单快捷,适合于小团队内部工具的开发或者交付客户的系统demo。

调查问卷具有并发访问数少,总访问次数有限的特点,因此Streamlit能够满足一个开源问卷的搭建。

Streamlit

Streamlit的运行原理可以总结为顺序执行+按需要进行缓存。具体来说:

  1. Streamlit app 本质是一个按从上到下顺序执行的 Python 脚本。
  2. 每当用户通过浏览器选项卡访问应用程序时,系统都会启动一个新会话(session),并从头开始执行脚本。
  3. 在脚本执行过程中,Streamlit 会实时将输出内容渲染到浏览器界面。
  4. 当用户与界面中的任何交互部件(widget)进行操作时,都会触发脚本的重新执行,同时 Streamlit 会相应地更新浏览器中的渲染内容。
  5. 通过缓存机制,应用程序可以避免重复计算开销较大的函数,从而确保状态更新保持高效。
  6. 会话状态(session state)功能支持在脚本重新运行期间(例如组件被点击时)保存关键信息。
  7. 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语句来决定在指定页上出现的内容。

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
# ...

加入控件

测试...

几行代码几次点击部署为web应用

更深入的开发...

  1. 复杂实验交互
  2. 远程数据库连接
  3. 自定义CSS样式
  4. 复杂控制逻辑