简单的个人足迹图

简单的个人足迹图

很久以前用Streamlit和高德平台的接口做了个个人足迹图,这两天又拿出来翻修一下,把整个过程遇到的问题整理记录一下。

image-20230806181851811

基本逻辑

基本的逻辑是调用高德的地理编码接口,传进去去过的城市名称,返回这个城市的经纬度,然后传给st.map,在世界地图上显示这个城市的位置。

1
2
3
4
5
6
7
8
9
10
11
def getLoc(address):
par = {'address': address, 'key': 'your key'}
url = 'http://restapi.amap.com/v3/geocode/geo'
res = requests.get(url, par)
json_data = json.loads(res.text)
with open('m.json', 'w') as f:
json.dump(json_data, f)
geo = json_data['geocodes'][0]['location']
return (geo.split(',')[0], geo.split(',')[1])

getLoc('上海')

可以把这个streamlit进程配置成守护进程。守护进程在后台持续运行,独立于用户会话或终端,并在系统启动时自动启动。这样这个streamlit页面就可以一直安全稳定地存在了。如果是用宝塔面板搭建的网站的话,直接用面板的进程守护管理器就可以简单地完成这个过程。

缓存

补充了些暑假新去的城市,发现每次调整多选框的时候都会非常慢,而且调了一段时间收到了高德平台的提示,提示地理编码接口的调用量已达配额80%。于是才反应过来,正常情况下在Streamlit中每次调整多选框、进行交互操作、或者更改代码时,整个Python脚本都会从上到下重新执行一遍。如果有些比较耗时的部分,比如读取数据库,或者向上面这样发送HTML请求,就会占用很多资源。

为了解决这个问题,最新版本的Streamlit提供了两个缓存装饰器st.cache_datast.cache_resource。当使用缓存装饰器装饰一个函数时,Streamlit会检查三件事情:

  1. 函数的名称
  2. 组成函数主体的实际代码
  3. 调用函数时传递的输入参数

如果这三个组合第一次出现,Streamlit会执行一遍这个函数并将结果存储在缓存中,依据装饰器persist参数的值,缓存可能保存在内存中也可能写到磁盘里。下次调用该函数时,如果这三个值没有改变,Streamlit 就知道可以完全跳过执行函数,而从本地缓存中读取输出并将其传递给调用者。

s two caching decorators and their use cases. Use st.cache_data for anything you

st.cache_data 是推荐用于缓存返回数据的计算结果的装饰器,例如从 CSV 文件加载 DataFrame,对 NumPy 数组进行转换,查询 API 或任何返回可序列化数据对象(如 str、int、float、DataFrame、array、list 等)的函数。每次函数被调用时,它都会创建数据的新副本,确保数据在缓存中安全免受修改和竞态条件的影响。大多数情况下,st.cache_data 的行为是我们想要的,因此如果不确定该使用哪个装饰器,可以先尝试使用 st.cache_data 看看是否满足需求。

1
2
3
4
5
connection = database.connect()

@st.cache_data
def query():
return pd.read_sql_query("SELECT * from table", connection)

st.cache_resource 是推荐用于缓存全局资源的装饰器,例如机器学习模型或数据库连接等不可序列化的对象。使用 st.cache_resource,你可以在应用程序的所有运行和会话中共享这些资源,而无需复制或重复加载。值得注意的是,对缓存的返回值进行的任何修改都直接影响缓存中的对象,因此在使用 st.cache_resource 时,务必确保资源是不可变的,或者适当地处理修改操作。

1
2
3
4
5
6
7
8
9
@st.cache_resource
def init_connection():
host = "hh-pgsql-public.ebi.ac.uk"
database = "pfmegrnargs"
user = "reader"
password = "NWDMCE5xdipIjRrp"
return psycopg2.connect(host=host, database=database, user=user, password=password)

conn = init_connection()

总的来说,st.cache_data适合缓存函数会返回一个具体结果的情况,st.cache_resource适合缓存全局资源加载的情况。所以对于上面返回经纬度的函数,加上一个@st.cache_data就可以了。

子域名DNS解析

DNS解析是由专门的DNS服务器提供的服务。DNS(Domain Name System)是一个分布式的系统,用于将人类可读的域名(例如:example.com)转换为计算机可理解的IP地址(例如:192.0.2.1)。实际上我们手动建立自己的个人网站,一共会跟两个供应商打交道,一个是服务器提供商,比如购买一个一核50G的小服务器,服务器提供商也会分配一个唯一的IP地址给你的服务器。另一个是域名注册商,卖给我们一个域名,比如miaomiao.com。那么IP和域名是怎么绑定到一起的呢?就是通过DNS解析。

当我们在浏览器中输入一个网址,比如 www.example.com,计算机首先会向本地的DNS解析器(通常是你的网络服务提供商或本地路由器)发起DNS解析请求。本地的DNS解析器会检查自己的缓存中是否有对应的域名解析结果,如果有并且没有过期,它会直接返回解析结果。如果没有或者已过期,本地DNS解析器会向根域名服务器发起请求。根域名服务器会指导本地DNS解析器去询问顶级域名服务器,比如 .com 域的顶级域名服务器。然后顶级域名服务器又会指导本地DNS解析器去询问下一级域名服务器,直到最终找到对应的域名解析结果。一旦找到了对应的IP地址,本地DNS解析器会将解析结果缓存起来,并返回给计算机。

当注册一个域名时,域名注册商通常会配套提供DNS解析服务。他们会在他们的DNS服务器上设置域名的DNS记录,将域名映射到相应的IP地址。假设我们所拥有的域名叫作example.com,我们现在想要再设置一些子域名,比如map.example.com或者mail.example.com,这时候要去域名提供商处配置DNS解析,否则这个子域名无法正确地映射到对应的IP地址。(理论上,主域名-IP的绑定和子域名-IP的绑定是同样简单的,然而如果你域名指向的是国内的服务器IP,根据法律法规必须进行ICP备案,否则是无法进行DNS解析的。当然,如果购买的是国外的服务器就没有这个问题了..)

反向代理

image-20230806213403036

正常情况下,当启动Streamlit服务时,这个服务会启动在本地的某个端口上(默认是8501),如果想要访问这个Streamlit页面,需要采用IP+端口号的形式。现在我们想用某个域名访问这个页面,比如我们想把这个Streamlit服务挂在map.miaomiao.com上,这个时候需要进行反向代理。

反向代理(Reverse Proxy)是一种网络服务架构,它充当位于客户端和实际提供服务的后端服务器之间的中间层。当客户端发起请求时,请求首先被发送到反向代理服务器,然后反向代理服务器将请求转发到后端服务器,获取响应后再将响应返回给客户端。反向代理的一个主要功能是提供负载均衡,比如每天有几千万人同时向www.baidu.com发起请求,不可能靠一个服务器支撑起这么大的请求。实际上我们向www.baidu.com发起的请求,被反向代理转发到后面大量的服务器上,由这些服务器实际处理请求,再将响应经反向代理服务器返回给用户。

将互联网与内网连接起来的代理伺服器。

在Streamlit这个需求里,Streamlit服务所在的http://server_Ip:port就相当于是后端服务器,我们的子域名map.miaomiao.com就相当于是反向代理服务器。有很多软件可以实现反向代理功能,最常用的是开源的nginx,占用内存小,并发能力强,包括百度、京东、淘宝、新浪、网页等等都采用了niginx.. nginx本身已经做好了所有的事情,在下载了nginx并启动了nginx服务后,我们在大部分情况下所需要做的事情就是写配置文件,告诉nginx应该怎么去做反向代理就可以了。

nginx的配置文件主要由以下几个块组成,首先是全局配置,比如worker_processes设定启动nginx时使用的worker进程数量。nginx 是多进程模型的服务器,在运行时会创建多个 worker 进程来处理客户端请求。events块配置 Nginx 的事件模型,比如worker_connections设定了每个worker进程的最大连接数,这里一般也不需要怎么调整。

http块用于配置 HTTP 服务器相关的设置,包括服务器监听端口、访问日志、缓存、负载均衡、反向代理等,这是我们最常需要配置的部分。http块上面是一些http的全局配置,下面包含了若干个server块,每个server块对应一个域名和端口,比如下面的两个server块分别监听example.com的80端口和subdomain.example.com的8080端口。server块下面的location块决定了这个域名下具体目录的代理规则。比如在server_nameexample.com的块下,/就指代当前域名的根目录,也就是example.com/location ^~ /就指代当前域名下的所有目录。如果匹配到了包含 proxy_passlocation 块,那么 nginx 就会将请求转发给 proxy_pass 指定的后端服务器。后端服务器接收到请求后进行处理,并将处理结果返回给 nginx。nginx 收到后端服务器的响应后,将响应内容返回给客户端,完成了反向代理的过程。

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

# 全局配置

worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

# events 块
events {
worker_connections 1024;
}

# http 块
http {
include mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;

# server 块
server {
listen 80;
server_name example.com;
# location 块
location ^~ / {
proxy_pass http://backend-server;
}
}

server {
listen 8080;
server_name subdomain.example.com;

location ^~ / {
proxy_pass http://backend-server;
}
}
}

对于当前需求,大体上的结构是这样的,

1
2
3
4
5
6
7
8
server {
listen 80;
server_name map.example.com

location ^~ / {
proxy_pass http://server_IP:8501/;
}
}

当然还需要些具体的其他配置,官网上可以找到相关的设定,我直接采用了宝塔面板的图形界面.. 另外在使用反向代理的Streamlit服务的时候,在启动的时候需要指定

1
streamlit run map.py --server.headless=true

否则会一直卡死到Please wait.. 页面。

页面背景

在Steamlit可以通过手动定义css的方式来更改页面的背景,首先在Streamlit的Python文件目录下面新建一个.streamlit/config.toml文件,在文件里写入

1
2
[server]
enableStaticServing = true

以启动静态文件配置,之后把想要设置为背景的图片放到根目录下的static目录下,键入如下代码就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
image = './app/static/image.webp'

css = f'''
<style>
.stApp {{
background-image: url({image});
background-size: cover;

}}
.stApp > header {{
background-color: transparent;
}}
</style>
'''
st.markdown(css, unsafe_allow_html=True)