商飞实习
商飞实习
暑假在上海浦东的商飞试飞中心实习了六周,还有三天结束。实习主要的工作是面向试飞测试数据,建立一套数据组织流程,并依据流程开发一套Web工具,最终基于所开发的工具,根据专业、试飞科目等信息对典型测试数据进行整理。我负责的部分主要是用Streamlit搭一个前端,这篇Blog主要的目的也是把学习和工作过程中遇到的一些问题记录整理一下。(感谢冯部长 主任和科室同事的照顾..
Web开发
一般情况下,当我们想开发一个Web应用时,比如搭建一个网页实现一些功能,整个开发可以划分成后端和前端两个部分。前端主要负责网页界面的开发,后端则负责实现具体的逻辑。比如当用户点击前端界面的某个按钮时,触发后端的某个函数,实现数据拉取等功能,将结果返回后,再由前端展示给用户。
就像Python有很多库一样,当我们想用Python训练一个神经网络的时候,不需要从具体的某个节点和反向传播算法写起,TensorFlow或者Pytorch库已经把这些都写好了,我们只需要导入这些库,并按照他们提供的接口,就可以直接快速搭建起来一个神经网络了。
Web开发也是一样,前后端都有相应的框架去帮助我们构建一个Web应用,当我们想做一个比如滑动拼图验证码的时候,不需要手动实现每一个细节,只需要按照框架的接口建立一个验证码对象就可以了。后段框架可以提供包括数据库连接、身份验证等各种功能的接口,常用的后端框架比如基于Python的Django和Flask。前端主要实现的功能是界面展示与交互,要实现这个功能,整个前端可以拆分成三个要素:
- HTML:搭建网页结构
- CSS:定义网页样式
- JavaScript:实现网页交互
因此,当我们谈前端的框架时,其实包含了两部分,一部属于CSS框架,预定义好了一些好看的界面和组件样式,比如Semantic UI、Bootstrap;另一部分主要功能是提供Js框架,实现交互的功能,比如典型的前端三大框架,angular、react、vue。
但即使我们想用这些框架搭建一个网页,也不可避免地需要学习基本的Web知识,比如路由、前后端信息传输、HTML+CSS+Js... 于是人们建立了又一层的封装,比如基于Python的Dash和Streamlit。开发者基本不需要任何Web的知识,不需要手动实现后端和前端之间的信息交互,一切流程都被封装起来了,用户只需要简单的定义些网页元素,然后后端直接写点Python函数,就可以建立一个简单的Web应用了。
Streamlit
Streamlit很好上手,也有很多基础教程,这里主要记录些我遇到的一些特殊需求。
树状目录
一个典型的树状目录结构可以用嵌套的 JSON 对象和数组来表示,比如对于这样的一个树状结构,
1 | - Root |
用一个JSON可以表示成
1 | { |
Streamlit中现在有两个插件可以实现树状目录,一个是streamlit_tree_select
,一个是st_ant_tree
,这两个插件都是接收一个嵌套的JSON对象(在Python里就是一个嵌套的字典,在Python中可以很简单地用标准库json
实现字典和json数据的转换,json.loads
接收一个json字符串,返回一个字典;json.dumps
接收一个字典,返回一个json字符串。),来实现树状目录。具体的接口和样式略有不同,st_ant_tree
长成下面这样,可以折叠、定制长度,适合树中目录文件比较多的情况。
streamlit_tree_select
长成下面这样,看起来更加强壮,没法控制展示的长度,适合目录文件比较少的情况。
静态元素加载
Streamlit网页端没法直接通过HTML方式加载当前目录下的文件,就像一个网站只能通过相对路径访问自己站点目录下的页面,没法用相对路径访问同一计算机非站点目录下的文件一样。streamlit提供了static文件夹的方式,以实现在HTML元素中实现静态元素的加载,比如在某个div
元素中加载某些图片资源,像下面这样:
1 | <div class="image"> |
我们需要在streamlit的.py文件下面新建一个 static
目录,当将静态文件比如图片资源放置在 static
文件夹下时,streamlit 会将这些文件复制到应用程序的根目录中,并通过 streamlit 服务器对外提供访问。这使得客户端可以通过相对路径或者指定正确的 URL 来访问这些静态资源。streamlit设计的相对目录为/app/static
,也就是说假如我在static
目录下面放置了一个叫作wakuwaku.png
的图片,当我们启动streamlit
服务后,在浏览器中输入
1 | http://localhost:8501/app/static/wakuwaku.jpg |
是可以找到这张图片资源的(假如端口是8501)。但是,如果将静态文件放置在其他位置(不在 static
文件夹下),客户端是无法直接找到和访问它们的。这是因为 streamlit 并不会对位于其他位置的文件进行特殊处理,也不会提供对这些文件的访问能力。
同时,为了启用这个功能,我们需要在streamlit的.py文件下面新建一个 .streamlit
目录,在该目录下新建一个config.toml
文件,在文件里设置
1 | [server] |
但如果我们不是在HTML的src或者href来通过相对路径访问元素,而是通过Python代码,这个就无所谓了,因为Python本质上就是在当前主机的后端执行操作,而不涉及Web客户端的内容。
CSS
在 HTML 中,网页的内容被组织为一个个元素的集合。每个 HTML 元素由一个起始标签(opening tag)和一个结束标签(closing tag)组成,标签用来定义元素的类型和属性。标签的组合形成了嵌套的层级结构,从而构成了网页的整体结构。HTML 是一种标记语言,它有一组预定义的元素,这些元素用于创建网页的结构和内容。我们不能定义新的元素,但可以使用自定义的数据属性(data attributes)来扩展现有的 HTML 元素,以存储自定义数据或信息。自定义数据属性以 data-
为前缀,并可以添加在任何 HTML 元素上,例如:
1 | <div data-custom-attribute="some value">This is a div with a custom data attribute.</div> |
CSS美化了网页的样式,本质上就是定义了这些元素们的样式属性。所以CSS最重要的一部分,就是选择器,也就是选择要应用样式的HTML元素。一般常用的有以下几个选择器(大部分摘自RUNOOB.COM):
元素选择器:通过元素名称选择HTML元素。
如下代码,
p
选择器将选择所有<p>
元素:1
2
3p {
color: blue;
}类选择器:通过类别名称选择具有特定类别的HTML元素。
类选择器以
.
开头,后面跟着类别名称。如下代码,
.highlight
选择器将选择所有具有类别为highlight
的元素。1
2
3.highlight {
background-color: yellow;
}元素标签里的
class
属性包括highlight
的,就都会得到这样的属性,1
<p class="highlight">This is a paragraph element with class "highlight"</p>
我们也可以指定同时属于两个类的元素的属性
1
2
3.highlight.strengthen {
font-size: 2rem;
}这时只有同时指定了两个类的元素才会获得这个属性:
1
<div class="highlight strengthen">Content</div>
ID 选择器:通过元素的唯一标识符(ID)选择 HTML 元素。
ID 选择器以
#
开头,后面跟着 ID 名称。如下代码,
#runoob
选择器将选择具有 ID 为 "runoob" 的元素。1
2
3#runoob {
width: 200px;
}在 HTML 文档中,每个元素的 ID 属性都应该是唯一的,即不同元素不能拥有相同的 ID。
1
<div id="header">This is the header</div>
属性选择器:通过元素的属性选择 HTML 元素。属性选择器可以根据属性名和属性值进行选择。
如下代码,
input[type="text"]
选择器将选择所有type
属性为 "text" 的<input>
元素。1
2
3input[type="text"] {
border: 1px solid gray;
}后代选择器:通过指定元素的后代关系选择 HTML 元素。
后代选择器使用空格分隔元素名称。
如下代码,
div p
选择器将选择所有在<div>
元素内的<p>
元素。1
2
3div p {
font-weight: bold;
}后代选择器也可以连续使用,表示后好几代的元素,比如
1
2
3div div div {
color: #333;
}表示
div
元素下嵌套的第二层div
元素。伪元素选择器:伪元素通常用于在选中元素的首部、尾部或者内部的某一行添加一些特殊样式、内容或图标。
伪元素选择器使用双冒号
::
(早期的规范使用单冒号:
来表示伪元素)表示伪元素。如下代码表示在
<footer>
元素后面插入虚拟的内容。在这个例子中,虚拟的内容是 "祝我们的大飞机越来越好✈️!❤️ from Tsinghua University",并通过 CSS 样式来定义这个虚拟元素的样式。1
2
3
4
5
6
7
8footer::after {
content:'祝我们的大飞机越来越好✈️!❤️ from Tsinghua University';
visibility: visible;
display: block;
position: relative;
padding: 5px;
top: 2px;
}这些选择器也是可以进行组合的,比如在更改streamlit多选框的样式的时候,
1
2
3.stMultiSelect div div div div div::nth-of-type(2) {
visibility: hidden;
}这里就同时采用了三个元素选择器,
.stMultiSelect
: 这是一个类选择器,它选择所有具有class="stMultiSelect"
属性的元素。div div div div div
: 这是四个连续的后代选择器,表示选择位于.stMultiSelect
元素内的第四层嵌套<div>
元素。换句话说,这是一个表示深度为 5 的嵌套结构。:nth-of-type(2)
: 这是伪类选择器,用于选择一组元素中的第二个元素。
1
2
3
4
5
6
7
8
9
10
11
12
13<!-- 在父元素中选择第二个 <div> 元素 -->
<div>
<div>First Div</div>
<div>Second Div</div> <!-- 这个会被选择 -->
<div>Third Div</div>
</div>
<!-- 在父元素中选择偶数位置的 <p> 元素 -->
<div>
<p>Paragraph 1</p>
<p>Paragraph 2</p> <!-- 这个会被选择 -->
<p>Paragraph 3</p>
</div>综合起来,这个复杂选择器会选择在
.stMultiSelect
元素内第四层嵌套结构中的所有<div>
元素,并将其中的第二个<div>
元素的visibility
设置为hidden
,即将其隐藏起来。
css样式可以直接写到元素的style
属性中,
1 | <div style="color: red; font-size: 16px;">This is some text.</div> |
也可以将样式写在 <style>
标签内,放置在 HTML 文档的 <head>
部分。这样的样式表将适用于整个 HTML 文档。
1 | <!DOCTYPE html> |
也可以写进css文件中,在 HTML 文档中使用 <link>
标签将其链接到页面上。
1 |
|
在stremlit中,理论上虽然可以把css文件放到static文件夹中用<link>
标签加载,但是不知道为什么一直报错.. 不过我们还可以通过read
文件的方式手动写进streamlit页面中:
1 | # streamlit.py |
通过这种方式在streamlit中写HTML时,必须要保证st.markdown
中是一个完整的HTML元素,否则是无法正确渲染的。所以考虑以下这个需求,左侧是卡片目录树,我们需要在一个页面上显示用户勾选的数据卡片,同时还需要些按钮,来控制这些卡片的行为,在streamlit里我们应该怎么做呢?一个可选的方案是维护一个巨大的HTML字符串,假如用户勾选了5张卡片,那么这时就可以向这个字符串写进去5个HTML元素。如果要控制卡片的显示风格,也是通过写进<style>
标签来实现的。
多页面及参数传递
streamlit官方提供了多页面的接口,但是效果并不好。为了在streamlit里实现多页面,我们可以借助st_pages
库提供的接口。
1 | st_pages.show_pages( |
在这里有两点需要注意,一是页面的地址名称不再是它们的.py文件名了,而是后面指定的名字,比如家和卡片配置;二是所有的Python路径都默认是对应streamlit客户端页面的路径,比如启动的streamlit客户端是main_config.py,那么page/data_config.py
中Python的默认路径还是main_config.py所在的路径。
现在我们已经建立了多个页面,那么如何在多页面间实现参数传递呢?这时候可以借助URL中的查询字符串。
1 | url?name1=value1&name2=value2 |
url
就是我们要访问的页面的链接,?
后面的就是查询字符串,当我们访问这个地址的时候,访问的就是url
指向的地址,但同时会向这个地址传递两个参数,分别是name1
和name2
,这两个参数的值分别是value1
和value2
。
在streamlit中,我们可以通过一个<a>
元素来实现查询字符串的配置,
1 | <a class="right-corner" href="卡片配置?TABLE_NAME={row['TABLE_NAME']}&TABLE_CATALOG={row['TABLE_CATALOG']}&TABLE_ID={row['TABLE_ID']}&TABLE_OWNER={row['TABLE_OWNER']}&TABLE_TYPE={row['TABLE_TYPE']}" target="_blank" style="color: black;"> |
然后在目标页面中,通过streamlit提供的方法收获这些参数,
1 | query_params = st.experimental_get_query_params() |
其实搜索引擎搜索本质也是这样,我们向搜索引擎所在的服务器发送一个GET
请求,后面的字符串会被解析为查询字符串,搜索引擎服务器收到这个 GET 请求后,会根据查询字符串中的关键词进行搜索,并返回相应的搜索结果页面。搜索结果会包含与搜索关键词相关的网页、图片、视频等内容。
Git协作
对于一个简单的、安全性需求不太高的工作,也不需要folk、branch这些操作,大家都在主分支上轮流提交就完事了。这个时候,在新建了一个Git项目之后,大家先分别Clone到本地,A修改代码,Commit + Push之后,远程主分支更新,这时B通过Pull拉取合并远程主分支的代码,B再把新修改的部分继续Commit+Push...
我们可以在Git项目目录下面建立一些比如.ipynb的调试文件,这时候可以在项目目录下面新建一个.gitignore
文件,.gitignore
中包含的文件会被排除掉当前Git项目。