您现在的位置是:首页 >技术交流 >UniApp + Flask 的天文学科普小程序 教程网站首页技术交流

UniApp + Flask 的天文学科普小程序 教程

LIVEJREEY 2025-03-21 00:01:03
简介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文档如下:

API概览_API 文档_对象存储 - 七牛开发者中心

因为经常要切换localhost和实际URL,我编写了config.js全局配置请求的URL

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。