跳到主要内容 Python 实现中秋月相计算、月饼切分与可视化 | 极客日志
Python AI 算法
Python 实现中秋月相计算、月饼切分与可视化 通过 Python 代码精确计算 2025 年中秋月相,得出月球被照亮程度约为 94.91%。利用 turtle 库绘制月相图直观展示。探讨月饼公平切分算法,包括经典圆心辐射切割及不过圆心的平行弦切割方案。使用马尔可夫链生成中秋诗词。基于模拟数据构建月球表面地形 3D 可视化,并制作一个月内月相变化动画,验证“十五的月亮十六圆”现象。
苹果系统 发布于 2026/2/4 更新于 2026/4/18 8.1K 浏览一、月相计算:今晚的月亮到底有多圆
今天是中秋节,作为 Python 开发者,可以用代码来精确计算月相,看看今年中秋的月亮到底有多圆。
月相的计算涉及到朔望月的概念。思路其实挺简单的,找一个已知的新月时间点作为基准,然后根据朔望月周期(29.53 天)往后推算就行。
from datetime import datetime, timedelta
import math
def calculate_moon_phase (date ):
"""计算指定日期的月相(0-1,0 为新月,0.5 为满月)"""
known_new_moon = datetime(2000 , 1 , 6 , 18 , 14 )
synodic_month = 29.53058867
days_diff = (date - known_new_moon).total_seconds() / 86400
phase = (days_diff % synodic_month) / synodic_month
return phase
mid_autumn = datetime(2025 , 10 , 6 , 12 , 0 )
phase = calculate_moon_phase(mid_autumn)
illumination = (1 - abs (phase - 0.5 ) * 2 ) * 100
print (f"2025 年中秋月相值:{phase:.4 f} " )
print (f"月球被照亮程度:{illumination:.2 f} %" )
运行结果显示,2025 年中秋的月相值为 0.4745,接近满月的 0.5,月球被照亮的程度约为 94.91%。也就是说,中秋夜的月亮已经非常圆了,肉眼几乎看不出和满月的差别。
1. 月相可视化
光有数字还不够直观,我们可以用 turtle 库画出当前的月相。虽然 turtle 通常被认为是初学者的玩具,但用来绘制简单的天文图形却恰到好处。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
import turtle
import math
def draw_moon_phase (phase, radius=100 ):
"""绘制月相图"""
screen = turtle.Screen()
screen.bgcolor("black" )
moon = turtle.Turtle()
moon.speed(0 )
moon.color("white" )
moon.penup()
moon.goto(0 , -radius)
moon.pendown()
moon.circle(radius)
if phase < 0.5 :
offset = radius * (1 - phase * 4 )
for angle in range (180 ):
rad = math.radians(angle)
x = offset * math.cos(rad)
y = radius * math.sin(rad)
moon.goto(x, y - radius)
else :
offset = radius * ((phase - 0.5 ) * 4 )
for angle in range (180 ):
rad = math.radians(angle)
x = -offset * math.cos(rad)
y = radius * math.sin(rad)
moon.goto(x, y - radius)
turtle.done()
用 turtle 画了个中秋月亮:白色圆代表月球轮廓,根据刚才算出的月相值,再画一个椭圆阴影,就能直观看到它接近满月的样子。当月相从 0 向 0.5 过渡时,阴影逐渐消失;从 0.5 向 1 过渡时,阴影又逐渐增加。
二、月饼切分算法:公平分配的艺术 中秋吃月饼是传统,但如何公平地分月饼却是个数学问题。假设有一家人围坐在一起,如何用最少的刀数把圆形月饼切成等份?
1. 经典切分策略 最直观的方法是从圆心出发,向外辐射切割。如果要分给 n 个人,我们需要 n 刀,每刀之间的角度是 360/n 度。但这要求第一刀的位置必须精确定位圆心,实际操作中并不容易。
import numpy as np
import matplotlib.pyplot as plt
def fair_mooncake_division (n, radius=1 ):
"""计算 n 等分月饼的切割路径"""
plt.figure(figsize=(8 , 8 ))
ax = plt.subplot(111 , projection='polar' )
theta = np.linspace(0 , 2 *np.pi, 100 )
ax.plot(theta, [radius]*100 , 'brown' , linewidth=3 )
ax.fill(theta, [radius]*100 , 'wheat' , alpha=0.5 )
angles = [2 *np.pi*i/n for i in range (n)]
for angle in angles:
ax.plot([angle, angle], [0 , radius], 'r--' , linewidth=2 )
for i, angle in enumerate (angles):
mid_angle = angle + np.pi/n
ax.text(mid_angle, radius*0.6 , f'{i+1 } ' , fontsize=14 , ha='center' )
ax.set_ylim(0 , radius*1.2 )
plt.title(f'月饼{n} 等分方案' , pad=20 )
plt.show()
return angles
用极坐标画了个圆形月饼,然后按等角度切成 n 等份,每份角度是 360/n。图中红色虚线就是切刀位置,简单直观。这其实就是最少刀数 问题的经典解法:n 个人用 n 刀,从圆心放射状切。
这种方法是最直观的等分方式,通过从圆心出发的放射状切割,确保了每份的面积完全相等。
2. 进阶问题:不过圆心的切分 更有趣的是这样一个问题:如果切割时不经过圆心,能否仍然保证每份面积相等,这就需要用到更复杂的几何计算了。
def calculate_chord_position (n, piece_index, radius=1 ):
""" 计算平行弦切割的位置
n: 总份数
piece_index: 当前是第几份(从 0 开始)
"""
target_area = np.pi * radius**2 / n
cumulative_area = target_area * (piece_index + 1 )
def area_to_left (h ):
"""计算距圆心高度为 h 的弦左侧的面积"""
if abs (h) >= radius:
return 0 if h > 0 else np.pi * radius**2
angle = 2 * np.arccos(h / radius)
sector = 0.5 * radius**2 * angle
triangle = h * np.sqrt(radius**2 - h**2 )
return sector - triangle + np.pi * radius**2 / 2
from scipy.optimize import brentq
h = brentq(lambda x: area_to_left(x) - cumulative_area, -radius, radius)
return h
其实通俗一点讲就是不用从圆心下刀,也能把月饼等面积切成 n 份。思路是用一系列平行的直线(弦)来切,关键是算出每条弦该放在哪里。结果如下所示:
跟上面的第一种做法完全是一样的结果,先算出每份应有的面积对第 i 份,求一条弦,使得它左侧的面积正好等于 i 份的总面积,最后用数值方法解方程,找到弦的位置 h。
三、诗词生成:中秋凑诗 既然是中秋佳节,怎能少了诗词助兴?不用复杂的大模型,一个简单的'马尔可夫链'就够——说穿了,就是让代码先记几句经典中秋诗,再照着'前两个字啥样,就接啥字'的规矩,自己拼出两句来。
先说说这个'凑诗逻辑':它记东西很'短视',下一个字选什么,只看前面一两个字。比如学过'举头望明月',下次碰到'举头',就大概率会接'望';碰到'望明',就可能接'月'。就像学说话的小孩,先背熟几个词组,再瞎组合,偶尔能蒙对味儿。
import random
from collections import defaultdict
class PoemGenerator :
"""基于马尔可夫链的诗词生成器"""
def __init__ (self, order=2 ):
self .order = order
self .chain = defaultdict(list )
def train (self, poems ):
"""训练模型"""
for poem in poems:
words = ['<START>' ] * self .order + list (poem) + ['<END>' ]
for i in range (len (words) - self .order):
state = tuple (words[i:i+self .order])
next_word = words[i+self .order]
self .chain[state].append(next_word)
def generate (self, length=28 ):
"""生成诗句"""
state = ('<START>' ,) * self .order
result = []
while len (result) < length:
if state not in self .chain:
break
next_word = random.choice(self .chain[state])
if next_word == '<END>' :
break
result.append(next_word)
state = state[1 :] + (next_word,)
return '' .join(result)
training_poems = [
"明月几时有把酒问青天" ,
"但愿人长久千里共婵娟" ,
"海上生明月天涯共此时" ,
"露从今夜白月是故乡明" ,
"举头望明月低头思故乡"
]
generator = PoemGenerator(order=2 )
generator.train(training_poems)
for i in range (5 ):
poem = generator.generate(length=28 )
lines = [poem[i:i+7 ] for i in range (0 , 28 , 7 )]
print ('\n' .join(lines))
print ()
能看出来,它没什么'逻辑',比如第一首里'问青天'接'但愿人长'有点跳,但'明月''天涯''婵娟'这些中秋关键词都在,偶尔还能拼出'海上生明月天涯共'这种像模像样的句子。
要是把 order 改成 1(只记前 1 个字),就会更'放飞',比如可能凑出'明月天涯共此时望',虽然乱,但说不定有意外的意境;改成 3 的话,就几乎是抄原诗的片段了。总之,不算真的'写诗',但中秋凑个热闹,看代码瞎编几句带月亮的话,还挺好玩儿的。
四、月球数据可视化:用数据看月亮 NASA 和各国航天机构提供了大量的月球观测数据。我们可以用这些数据来创建月球的三维可视化,或者分析月球表面的地形特征。
1. 先画月球表面:模拟环形山地形 月球表面坑坑洼洼全是环形山,咱们不用真的下载 NASA 数据,用代码'造'一份模拟地形,再用 3D 图显出来。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
def generate_moon_terrain (size=200 ):
"""造一份模拟月球地形数据:主要模拟环形山和月海"""
x = np.linspace(-1 , 1 , size)
y = np.linspace(-1 , 1 , size)
X, Y = np.meshgrid(x, y)
Z = np.zeros_like(X) - 0.2
def add_crater (X, Y, Z, center_x, center_y, radius, depth ):
"""给地形加一个环形山:center 是中心,radius 是半径,depth 是深度"""
distance = np.sqrt((X - center_x)**2 + (Y - center_y)**2 )
crater = -depth * np.exp(-(distance**2 )/(2 *radius**2 ))
crater += 0.1 * np.exp(-((distance - radius)**2 )/(2 *(0.02 )**2 ))
Z += crater
return Z
for _ in range (15 ):
cx = np.random.uniform(-0.8 , 0.8 )
cy = np.random.uniform(-0.8 , 0.8 )
r = np.random.uniform(0.05 , 0.15 )
d = np.random.uniform(0.3 , 0.8 )
Z = add_crater(X, Y, Z, cx, cy, r, d)
Z += np.random.randn(size, size) * 0.02
return X, Y, Z
def plot_moon_terrain ():
"""画月球地形的 3D 图"""
X, Y, Z = generate_moon_terrain(size=200 )
fig = plt.figure(figsize=(10 , 8 ))
ax = fig.add_subplot(111 , projection='3d' )
surf = ax.plot_surface(
X, Y, Z, cmap=cm.gist_gray,
linewidth=0 , antialiased=True , alpha=0.8
)
ax.set_xlabel('经度(简化)' , fontsize=12 )
ax.set_ylabel('纬度(简化)' , fontsize=12 )
ax.set_zlabel('高程(km,相对值)' , fontsize=12 )
ax.set_title('中秋观月:月球表面地形模拟(环形山清晰可见)' , fontsize=14 , pad=20 )
fig.colorbar(surf, shrink=0.5 , aspect=10 , label='高程(相对值)' )
ax.view_init(elev=30 , azim=45 )
plt.tight_layout()
plt.show()
if __name__ == "__main__" :
plot_moon_terrain()
月球正面(咱们中秋看到的那面)有大片'月海'(平坦的暗色区域),背面全是环形山,运行结果如下:
代码里虽然没分正反面,但能直观看到:月球不是'光滑的球',而是被撞得坑坑洼洼的,这些坑是几十亿年前小行星撞的,记录了太阳系早期的历史。
2. 再做月相动画:看一个月月亮怎么变 我们还可以模拟一个月内月相的变化过程,生成一个类似延时摄影的效果。中秋只看一天的满月不过瘾,咱们用代码做个'延时摄影',把一个月的月相变化动画放出来,还能标上第几天。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from matplotlib.animation import FuncAnimation
def create_moon_phase_anim (save_gif=True ):
"""生成月相变化动画(Windows 适配版)"""
fig, ax = plt.subplots(figsize=(8 , 8 ))
ax.set_xlim(-1.5 , 1.5 )
ax.set_ylim(-1.5 , 1.5 )
ax.set_aspect('equal' )
ax.axis('off' )
fig.patch.set_facecolor('black' )
ax.set_facecolor('black' )
def update_frame (frame ):
ax.clear()
ax.set_xlim(-1.5 , 1.5 )
ax.set_ylim(-1.5 , 1.5 )
ax.set_aspect('equal' )
ax.axis('off' )
ax.set_facecolor('black' )
phase = frame / 100
day = int (phase * 29.53 ) + 1
moon = plt.Circle((0 , 0 ), radius=1 , color='white' , fill=True )
ax.add_patch(moon)
if phase < 0.5 :
shadow_width = 2 * (0.5 - phase)
shadow = Ellipse((shadow_width/2 , 0 ), width=shadow_width, height=2 , color='black' , fill=True )
ax.add_patch(shadow)
else :
shadow_width = 2 * (phase - 0.5 )
shadow = Ellipse((-shadow_width/2 , 0 ), width=shadow_width, height=2 , color='black' , fill=True )
ax.add_patch(shadow)
phase_names = {0 :'新月' , 0.25 :'上弦月' , 0.5 :'满月' , 0.75 :'下弦月' }
closest_phase = min (phase_names.keys(), key=lambda x: abs (x - phase))
phase_name = phase_names[closest_phase]
ax.text(0 , -1.3 , f'朔望月第{day} 天 | {phase_name} ' , color='white' , ha='center' , fontsize=14 , weight='bold' )
anim = FuncAnimation(fig, update_frame, frames=100 , interval=100 , repeat=True , blit=False )
if save_gif:
anim.save('中秋月相变化.gif' , writer='pillow' , fps=10 , dpi=100 )
print ('GIF 已保存到当前文件夹:中秋月相变化.gif' )
else :
plt.show()
if __name__ == "__main__" :
create_moon_phase_anim(save_gif=True )
这个动画展示了一个完整朔望月的月相变化,结果如下所示:
动画里有个细节:满月不一定在第 15 天,可能在第 16 天——这就是'十五的月亮十六圆'的原因。因为朔望月是 29.53 天,不是整数,新月出现的时间每天会延后一点,满月自然也可能延后到第 16 天。比如 2025 年的中秋(10 月 6 日),满月就可能在 10 月 7 日凌晨,正好对应了我们之前算过照亮程度是 94.91%。
五、总结 代码就分享到这里。这些项目大部分我自己跑过,效果还不错。
月相计算那个我刚才又跑了一遍,2025 年中秋(10 月 6 日)的月相值是 0.4745,照亮度 94.91%。虽然不是 100% 的满月,但 94.91% 已经很圆了,肉眼基本看不出来。这也验证了'十五的月亮不一定十五圆'这个说法!
今年虽然不是完美满月,但 94.91% 的圆度已经足够亮了。晚上记得出去看看,天气好的话应该挺漂亮的。