您现在的位置是:首页 >技术交流 >UniApp + Flask 的天文学科普小程序 教程网站首页技术交流
UniApp + Flask 的天文学科普小程序 教程
一、前言
接入API:阿里云机器翻译、讯飞星火语言模型(LITE版本免费调用)、DeepSeek语言模型、七牛云图片存储(免费域名只有一个月,每隔一个月就要申请新的域名)
选用七牛云图片存储是因为微信小程序代码大小有限制,之前图片放到本地超出大小,后面移动到七牛云解决问题。
技术:爬虫
前端UniApp 和微信小程序开发者工具调试,后端使用Python Flask和POSTMAN测试,用内网穿刺工具来实现前后端的连接(用的花生壳,需要10块开通HTTPS服务)
功能展示:
二、功能对应实现的思路
下方导航栏图标微信小程序貌似只支持本地路径的图标,我使用hbuilderx来写代码的,只需要在manifest.json配置微信小程序的AppID,package.json编写页面路径配置,新建页面它自动会注册到package.json里面。
天体图鉴(文章展示)
直接编写的VUE静态页面,因为有几个子页面的样式一样,所以写了一个通用VUE组件
文章的内容是使用数组来存储,每个子页面只有数组内容不同
sections: [
// 将每个段落的内容整理成对象数组
{
title: '行星:宇宙中的游走者',
content: '“行星”这个词来自古希腊语,意思是“游走者”。古代的天文学家发现有些星星在夜空中的位置不断变化,与那些看似固定的恒星不同。这些神秘的“游走者”就是我们现在所说的行星!太阳系中有八大行星,从离太阳最近的水星到最远的海王星。',
image: 'http://sr5go4oo1.hn-bkt.clouddn.com/staitc/planet/earth/bada.jpeg'
},
{
title: '行星的定义',
content: '2006年,国际天文学联合会给行星下了一个新定义:1. 必须绕着一颗恒星运转。2. 要有足够的质量使它成为近似球形。3. 已经清除了轨道附近的其他物体。按照这个定义,冥王星被重新分类为“矮行星”,因为它没有完全清除其轨道上的邻居。'
}
]
对应的temple渲染代码
<view v-if="block.title" class="section-title">{
{ block.title }}</view>
<view v-for="(contentItem, contentIndex) in block.contentList" :key="contentIndex"
class="section-content" v-
html="processContent(contentItem.content)"></view>
<!-- 图片 -->
<image mode="widthFix" v-if="block.image" :src="block.image" class="section-image" alt="相关图片"
@click="previewImage(block.image)" />
在初始化页面时处理数组文章内容
processSections() {
const processedSections = [];
let currentBlock = null;
this.sections.forEach(section => {
if (section.title) {
if (currentBlock) {
processedSections.push(currentBlock);
}
currentBlock = {
title: section.title,
contentList: [],
image: null
};
}
if (section.content) {
currentBlock.contentList.push(section);
}
if (section.image) {
currentBlock.image = section.image;
}
});
if (currentBlock) {
processedSections.push(currentBlock);
}
this.processedSections = processedSections;
},
mounted() {
this.processSections();
}
样式是使用了背景图片覆盖(微信小程序不支持本地背景图片),以及磨砂效果,配色是网上选的
.container {
background: url('https://bpic.588ku.com/illus_water_img/21/01/20/6e9d51c69be8b2edb5d0fd64aafeac69.jpg!/fw/750/quality/99/unsharp/true/compress/true') no-repeat center center fixed;
background-size: cover;
height: 100vh;
/* 确保容器高度覆盖整个视窗 */
box-sizing: border-box;
/* 确保padding和border包含在元素的总大小内 */
overflow-y: auto;
/* 允许垂直滚动 */
}
.section-block {
background-color: rgba(36, 36, 68, 0.4);
/* 设置内容块的背景颜色,带有一定的透明度 */
backdrop-filter: blur(10px);
/* 添加磨砂效果 */
border-radius: 10px;
/* 圆角 */
margin-bottom: 20px;
/* 每个内容块之间的间距 */
padding: 15px;
/* 内容块内的内边距 */
}
星空小助手(AI问答)
讯飞星火官方API文档星火认知大模型Web API文档 | 讯飞开放平台文档中心
DeepSeek API响应速度有些慢,我后面用讯飞星火,效果差一点也勉强能用。
DeepSeek 有免费额度,而讯飞星火选择最低参数版的免费调用。
后端(讯飞)代码,在每次询问前加入了提示词
@app.route('/chat', methods=['POST'])
def chat():
"""接收前端发送的聊天消息,使用讯飞星火认知大模型生成回复"""
data = request.get_json()
user_message = data.get('message')
if not user_message:
return jsonify({"error": "Message is required"}), 400
# 添加提示语句
system_message = "我是青少年,请用易懂有趣味的语言"
messages = [
ChatMessage(
role="system",
content=system_message
),
ChatMessage(
role="user",
content=user_message
)
]
handler = ChunkPrintHandler()
response = spark.generate([messages], callbacks=[handler])
# 提取生成的文本
generated_text = response.generations[0][0].text
return jsonify({"response": generated_text})
网站科普(爬虫)
爬虫实现,但Python爬取速度慢,我解决方案是先保存到本地,然后定时每天重新爬取最新的,更新本地文件,后端只需要根据页码发送本地文件JSON数据即可。也可以用Redis等其他缓存技术,没试过。
爬取过程十分简单,以天文学最新消息 | 天文现象 | 天文百科知识 | 天文学视频 | Star Walk为例子
打开开发者工具选择 (检查元素按钮)选取图片或者标题
就可以看到对应的IMG H2属于o2bz183这个类的,正好下一条也是这个类,直接爬取就行。
for item in soup.select('div.o2bzl80'):
# 获取标题
title_tag = item.select_one('h2')
if not title_tag:
continue
title = title_tag.get_text(strip=True)
# 获取正文
summary_tag = item.select_one('p')
summary = summary_tag.get_text(strip=True) if summary_tag else None
# 获取时间
time_tag = item.select_one('time')
datetime_str = time_tag['datetime'] if time_tag and 'datetime' in time_tag.attrs else None
datetime_obj = datetime.fromisoformat(datetime_str) if datetime_str else None
# 获取缩略图
thumbnail_tag = item.select_one('img')
thumbnail = thumbnail_tag['src'] if thumbnail_tag else None
# 获取超链接
link_tag = item.select_one('a')
link = link_tag['href'] if link_tag and 'href' in link_tag.attrs else None
news_item = {
'title': title,
'summary': summary,
'datetime': datetime_obj,
'thumbnail': thumbnail,
'link': link # 添加超链接
}
news_items.append(news_item)
return news_items
遇到过几个问题:第一个网站需要点击更多才能加载后面的内容,另一个是有页码,但具体内容需要进子页面爬取(速度更慢)
第一个网站我解决方案是一直点击更多直到获取足够的信息,然后下载到本地(这样就有了全部的资讯),但要注意本地HTML的图片img属性路径也是本地的,需要爬取img的其他属性。
第二个我是获取子页面的超链接,如果用户需要具体子页面信息,再进行另一个爬取子页面即可。
遇到的问题是微信小程序不能直接展示HTML,好像可以用 webview组件,我用的是爬取后重新获取h1标题a标签,发送给前端重新进行渲染。
后端的代码(爬取子页面的img h1 a标签)
def fetch_content_from_url(url):
"""从给定的URL中抓取特定class的内容,并提取img、p、h2和a标签的内容"""
try:
# 检查URL是否以http或https开头,如果没有则添加https://starwalk.space/
if not url.startswith(('http://', 'https://')):
url = f"https://starwalk.space/{url}"
response = requests.get(url)
response.raise_for_status() # 如果响应状态码不是200,抛出异常
soup = BeautifulSoup(response.text, 'html.parser')
# 查找所有class为x6h6yq4的div元素
elements = soup.find_all('div', class_='x6h6yq4')
content_list = []
for element in elements:
item = []
# 提取所有子元素
for child in element.descendants:
if child.name == 'img':
item.append({'tag': 'img', 'src': child.get('src')})
elif child.name == 'p':
item.append({'tag': 'p', 'text': child.get_text(strip=True)})
elif child.name == 'h2':
item.append({'tag': 'h2', 'text': child.get_text(strip=True)})
elif child.name == 'a':
item.append({'tag': 'a', 'href': child.get('href'), 'text': child.get_text(strip=True)})
elif child.name == 'h3':
item.append({'tag': 'h3', 'text': child.get_text(strip=True)})
elif child.name == 'ul':
# 提取ul中的li标签内容
li_items = []
for li in child.find_all('li'):
li_items.append({'tag': 'li', 'text': li.get_text(strip=True)})
item.append({'tag': 'ul', 'items': li_items})
content_list.append(item)
return content_list
except requests.RequestException as e:
print(f"Error fetching URL: {e}")
return []
前端渲染代码
<view v-for="(item, index) in section" :key="index">
<image v-if="item.tag === 'img'" :src="item.src" mode="widthFix" class="image"></image>
<h2 v-else-if="item.tag === 'h2'" class="h2">{
{ item.text }}</h2>
<p v-else-if="item.tag === 'p'" class="p">{
{ item.text }}</p>
<a v-else-if="item.tag === 'a'" class="a" @click.prevent="handleClick(item.href)">{
{ item.text }}</a>
<ul v-else-if="item.tag === 'ul'">
<li v-for="(liItem, liIndex) in item.items" :key="liIndex" class="li">{
{ liItem.text }}</li>
</ul>
</view>
效果不是很好,但也勉强可以正常阅读
知识竞赛
用户账号管理
没有做数据库,开始是想微信授权登陆拿到微信用户信息,但看了微信文档看不明白,以前可以用uni.getUserProfile(已经弃用,只能返回默认的微信用户和灰色头像),wx.login可以获取到一个openid和session_key(需要后端请求),后面还需要请求其他的就没弄明白。具体的流程可以参考:使用(APP)微信授权登陆获取用户信息的流程:
我采用下面的:
大概意思就是给你图片选择按钮和名称输入框,点击可以提供微信用户的头像和名称。
前端代码
<view class='infobox'>
<!-- 头像选择 -->
<button class="avatar-wrapper" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image class="avatar" :src="avatarUrl"></image>
</button>
<!-- 昵称填写 -->
<input type="text" placeholder-class="input-placeholder" class="nickname-input weui-input"
v-model="nickname" placeholder="请输入昵称" />
<!-- 保存按钮 -->
<button class="save-button" @click="saveUserInfo">保存</button>
</view>
JS代码
<script>
export default {
data() {
return {
avatarUrl: '', // 默认头像URL
nickname: '' // 用户昵称
};
},
onShow() {
// 页面显示时自动加载最新用户信息
this.loadUserInfo();
},
methods: {
onChooseAvatar(e) {
const {
avatarUrl
} = e.detail;
this.avatarUrl = avatarUrl;
},
saveUserInfo() {
uni.setStorage({
key: 'userInfo',
data: {
avatarUrl: this.avatarUrl,
nickname: this.nickname
},
success: () => {
console.log('用户信息已保存');
uni.navigateBack({
delta: 1,
});
},
});
},
loadUserInfo() {
uni.getStorage({
key: 'userInfo',
success: (res) => {
const {
avatarUrl,
nickname
} = res.data;
this.avatarUrl = avatarUrl || '';
this.nickname = nickname || '';
}
});
}
}
};
</script>
我的用户信息只用作排行榜,所以没严格的安全要求,因此十分简单
知识问答实现
我的问题库(AI生成)和用户分数数据都存储在本地JSON文件中,
- 设置一个get_quesion来获取题目信息,因为需要获取没选过的题目还要随机选,
- 添加了一个属性.isFirstRequest(是否是第一题),第一题就重置所有题目.used标志为未选取
- 有倒计时,但只在前端实现,有作弊风险。(后端用线程实现过,但BUG很多,游戏结束了它还在倒计时)
- 分数只在服务器端计算,在前端发送答案submit_question时,后端会发送当前的分数给前端显示
整个是AI写出来的,BUG很多,修到后面虽然没有大BUG,但代码变得很⑩不想动
总结
Python 实现简单的后端处理还是可以的,但一旦功能开始多了起来,可能要用到蓝图来管理,而我是集成到一个代码文件中,导致我现在不想碰代码。
一开始是想搭建服务器来连接前后端的,但成本高,阿里云的服务器一年79¥ + SSL证书 + 域名(还要备案),后面才选用内网穿刺。
七牛云性价比高,官方的API文档如下:
因为经常要切换localhost和实际URL,我编写了config.js全局配置请求的URL