运维实战|零基础也能上手:用AI+Python一键统计阿里云+腾讯云费用(附完整代码)

一、这篇教程能帮你解决什么?

你是否有这样的烦恼?

每月月底,财务同事准时来催:“这个月的云服务费用统计好了吗?”

然后你打开阿里云控制台、腾讯云控制台,一个一个产品翻账单,手动加起来,填到Excel里,再写邮件发过去。每次至少花半小时,而且怕算错、怕漏掉产品。

学完这篇教程,你能做什么?

  1. 自动拉取阿里云和腾讯云的账单数据
  2. 自动生成汇总报表
  3. 自动对比上月数据,发现异常自动提醒
  4. 自动发送邮件给财务/领导
  5. 设定时任务,每月1号自动跑,从此不用手动操作

你需要什么基础?

几乎不需要! 只要你会:

  • ✅ 复制粘贴文字(会用Ctrl+C/V就行)
  • ✅ 双击运行一个文件
  • ✅ 看懂简单的表格

我不需要你会Python,不需要你懂API,不需要你学过编程。


二、准备工作(5分钟搞定)

2.1 检查Python是否装好

Python就是运行脚本的工具。先看看你的电脑有没有装:

Windows用户

  1. Win + R,输入 cmd,回车
  2. 在黑色窗口里输入:python --version
  3. 如果显示类似 Python 3.x.x,说明装好了 ✅
  4. 如果提示"不是内部命令",说明没装 → 去 python.org 下载安装(安装时勾选"Add Python to PATH")

Mac用户

  1. 打开"终端"(在应用程序→实用工具里)
  2. 输入:python3 --version
  3. Mac一般自带Python,直接能用 ✅

2.2 准备AccessKey(云账号的"门禁卡")

什么是AccessKey?

把云平台想象成一栋大楼:

  • 你的云账号 = 你的身份证(证明你是谁)
  • AccessKey = 你的门禁卡(用来刷卡开门、进入各个房间)
  • API = 大楼里的自动服务台(你刷门禁卡,服务台帮你查东西)

AI脚本需要通过AccessKey来"刷卡"进入云平台,帮你查询账单数据。所以你需要先从云平台办一张门禁卡。

去哪办?(以阿里云为例,腾讯云类似)

  1. 登录阿里云控制台(ram.console.aliyun.com)
  2. 点击右上角头像 → “访问控制”
  3. 左侧菜单 → “用户” → “创建用户”
  4. 勾选"OpenAPI调用访问"
  5. 创建完成后,系统会给你两个东西:
    • AccessKey ID(相当于门禁卡的编号,比如:LTAI5tXXXXXX
    • AccessKey Secret(相当于门禁卡的密码,比如:wE2kXXXXXXXX

⚠️ 重要:AccessKey Secret只在创建时显示一次,马上复制保存!丢了只能重新创建。

  1. 给这个子账号只分配"只读"权限(比如 AliyunBSSReadOnlyAccess),这样就算AccessKey泄露,别人也只能看账单,无法操作你的云资源。

把AccessKey保存到电脑上

在电脑上新建一个文本文件(用记事本就行),内容像这样:

AccessKey ID:LTAI5tXXXXXX
AccessKey Secret:wE2kXXXXXXXX

保存为 aliyun_key.txt,记住保存的位置(比如桌面)。

腾讯云的AccessKey去哪办?

登录腾讯云控制台(console.cloud.tencent.com/cam/capi),步骤类似,创建后保存到 tencent_key.txt


三、阿里云费用统计(跟着做就行)

3.1 创建脚本文件

你不需要写任何代码,只需要复制粘贴

  1. 打开记事本(Windows按Win键搜"记事本")
  2. 把下面这整段代码全部复制,粘贴到记事本里:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
阿里云短信+语音服务费用统计脚本
功能:自动查询阿里云BSS账单,统计短信和语音服务的费用
使用方法:把下面的 "AccessKey文件路径" 和 "要查的月份" 改成你自己的,
         然后双击运行就行了。
"""

import json
import time
import hmac
import hashlib
import base64
import urllib.request
import urllib.parse

# ════════════════════════════════════════════
# 🔧 你只需要改这两个地方!
# ════════════════════════════════════════════

# 第1处:改成你的AccessKey文件路径
# 举例:如果文件在桌面,就是 r"C:\Users\你的用户名\Desktop\aliyun_key.txt"
ACCESSKEY_FILE = r"D:\你的路径\aliyun_key.txt"

# 第2处:改成你要查的月份(格式:年-月)
# 举例:查2026年4月,就是 "2026-04"
BILLING_MONTH = "2026-04"


# ════════════════════════════════════════════
# 下面的代码不需要修改,直接运行就行
# ════════════════════════════════════════════

def read_credentials(file_path):
    """从文件中读取AccessKey"""
    ak_id = ""
    ak_secret = ""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                if 'AccessKey ID' in line:
                    ak_id = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
                elif 'AccessKey Secret' in line:
                    ak_secret = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
        return ak_id, ak_secret
    except Exception as e:
        print(f"❌ 读取AccessKey文件失败: {e}")
        return None, None

def percent_encode(s):
    """阿里云要求的URL编码方式"""
    s = str(s)
    encoded = urllib.parse.quote(s, safe='~')
    return encoded.replace('+', '%20').replace('*', '%2A')

def build_signed_url(ak_id, ak_secret, action, params):
    """
    构造带签名的请求URL
    简单理解:就像在快递单上签名,证明这个请求是你发的
    """
    # 公共参数(每个请求都要带的)
    common = {
        'Format': 'JSON',
        'Version': '2017-12-14',
        'AccessKeyId': ak_id,
        'SignatureMethod': 'HMAC-SHA1',
        'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
        'SignatureVersion': '1.0',
        'SignatureNonce': str(int(time.time() * 1000000)),
        'Action': action,
    }
    
    # 合并所有参数
    all_params = {**common, **params}
    
    # 按字母排序,拼接成字符串
    sorted_keys = sorted(all_params.keys())
    canonical = '&'.join([f"{percent_encode(k)}={percent_encode(all_params[k])}" for k in sorted_keys])
    
    # 生成签名
    string_to_sign = f"GET&{percent_encode('/')}&{percent_encode(canonical)}"
    key = (ak_secret + '&').encode('utf-8')
    signature = base64.b64encode(hmac.new(key, string_to_sign.encode('utf-8'), hashlib.sha1).digest()).decode('utf-8')
    
    # 构造最终URL
    all_params['Signature'] = signature
    final = '&'.join([f"{percent_encode(k)}={percent_encode(all_params[k])}" for k in sorted(all_params.keys())])
    return f"https://business.aliyuncs.com/?{final}"

def call_api(ak_id, ak_secret, action, params):
    """发送请求到阿里云,获取数据"""
    url = build_signed_url(ak_id, ak_secret, action, params)
    req = urllib.request.Request(url)
    req.add_header('Accept', 'application/json')
    try:
        response = urllib.request.urlopen(req, timeout=30)
        return json.loads(response.read().decode('utf-8'))
    except Exception as e:
        print(f"  ❌ 请求失败: {e}")
        return None

def main():
    print("=" * 60)
    print("  阿里云短信+语音服务费用统计")
    print("=" * 60)
    
    # 1. 读取AccessKey
    ak_id, ak_secret = read_credentials(ACCESSKEY_FILE)
    if not ak_id or not ak_secret:
        print("❌ 请检查AccessKey文件路径是否正确")
        return
    
    print(f"✅ AccessKey读取成功")
    print(f"📅 查询月份: {BILLING_MONTH}")
    print()
    
    # 2. 查询所有产品的费用
    print("正在查询阿里云账单...")
    result = call_api(ak_id, ak_secret, 'QueryBillOverview', {
        'BillingCycle': BILLING_MONTH,
    })
    
    if not result or 'Data' not in result:
        print("❌ 未获取到数据,请检查月份格式是否正确")
        return
    
    # 3. 提取短信和语音的费用
    items = result['Data'].get('Items', {}).get('Item', [])
    if isinstance(items, dict):
        items = [items]
    
    # 合并相同产品代码的费用
    merged = {}
    for item in items:
        code = item.get('ProductCode', 'N/A')
        name = item.get('ProductName', 'N/A')
        amount = float(item.get('PretaxAmount', 0))
        if code not in merged:
            merged[code] = {'code': code, 'name': name, 'amount': 0.0}
        merged[code]['amount'] += amount
    
    print(f"\n{'产品代码':<20} {'产品名称':<25} {'费用(元)':>12}")
    print("-" * 60)
    
    sms_total = 0.0
    voice_total = 0.0
    
    for code, info in merged.items():
        name = info['name']
        amount = info['amount']
        
        # 找短信和语音产品
        is_sms = any(kw in name.lower() or kw in code.lower() 
                     for kw in ['短信', 'sms', 'dysms', 'message'])
        is_voice = any(kw in name.lower() or kw in code.lower() 
                       for kw in ['语音', 'voice', 'dyvms', 'vms'])
        
        marker = ''
        if is_sms:
            marker = ' ← 📱短信'
            sms_total += amount
        elif is_voice:
            marker = ' ← 📞语音'
            voice_total += amount
        
        print(f"{code:<20} {name[:23]:<25} {amount:>12.2f}{marker}")
    
    # 4. 输出汇总结果
    print("-" * 60)
    print(f"\n📊 汇总结果:")
    print(f"  短信服务合计: {sms_total:.2f} 元")
    print(f"  语音服务合计: {voice_total:.2f} 元")
    print(f"  {'─' * 40}")
    print(f"  💰 总计(税前): {sms_total + voice_total:.2f} 元")
    print(f"\n✅ 查询完成!")

if __name__ == '__main__':
    main()
  1. 点击记事本的"文件" → “另存为”
  2. 文件名改成:aliyun_cost.py
  3. 保存类型一定要选"所有文件"(不是"文本文件")!
  4. 保存到桌面(方便找到)

3.2 改两个地方就能用

打开 aliyun_cost.py(右键→打开方式→记事本),找到这两行,改成你自己的:

# 改这里 ↓
ACCESSKEY_FILE = r"D:\你的路径\aliyun_key.txt"
# 举例:如果aliyun_key.txt在桌面
# ACCESSKEY_FILE = r"C:\Users\Administrator\Desktop\aliyun_key.txt"

# 改这里 ↓
BILLING_MONTH = "2026-04"
# 改成你要查的月份,比如 "2026-05"

3.3 运行脚本(就两步)

  1. 打开命令行:在脚本所在的文件夹,按住 Shift 键,右键点击空白处,选择"在此处打开PowerShell窗口"

  2. 输入命令,回车

python aliyun_cost.py

3.4 看懂输出结果

运行后,你会看到类似这样的输出:

============================================================
  阿里云短信+语音服务费用统计
============================================================
✅ AccessKey读取成功
📅 查询月份: 2026-04

正在查询阿里云账单...

产品代码               产品名称                     费用(元)
------------------------------------------------------------
dysms                  短信服务                    50,000.00 ← 📱短信
dyvms                  语音服务                    17,935.66 ← 📞语音

📊 汇总结果:
  短信服务合计: 50000.00 元
  语音服务合计: 17935.66 元
  ────────────────────────────────────────
  💰 总计(税前): 67935.66 元

✅ 查询完成!

每一行代表什么?

  • 产品代码:阿里云内部的编号(dysms = 短信服务,dyvms = 语音服务)
  • 产品名称:你能看懂的中文名字
  • 费用(元):这个产品当月花了多少钱(税前)
  • 📱/📞:标记表示这是短信还是语音

⚠️ 常见问题:如果输出里没有看到短信和语音,说明你的阿里云账号里没有这两个产品的费用,或者月份格式不对。


四、腾讯云费用统计(同样的套路)

4.1 复制粘贴第二个脚本

跟刚才一样,新建一个记事本,粘贴下面的代码,保存为 tencent_cost.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
腾讯云人脸核身费用统计脚本
功能:自动查询腾讯云账单,统计人脸核身服务的费用
使用方法:改AccessKey路径和月份,然后运行
"""

import json
import time
import hmac
import hashlib
import urllib.request
import calendar

# ════════════════════════════════════════════
# 🔧 你只需要改这两个地方!
# ════════════════════════════════════════════

# 第1处:改成你的腾讯云AccessKey文件路径
ACCESSKEY_FILE = r"D:\你的路径\tencent_key.txt"

# 第2处:改成你要查的月份
BILLING_MONTH = "2026-04"


# ════════════════════════════════════════════
# 下面的代码不需要修改
# ════════════════════════════════════════════

def read_credentials(file_path):
    """从文件中读取AccessKey"""
    sid = ""
    skey = ""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                line = line.strip()
                if line.startswith('#'):
                    continue
                if 'SecretId' in line or 'AKID' in line:
                    sid = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
                elif 'SecretKey' in line:
                    skey = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
        return sid, skey
    except Exception as e:
        print(f"❌ 读取AccessKey文件失败: {e}")
        return None, None

def sign(key, msg):
    """腾讯云TC3签名算法的一部分"""
    if isinstance(key, str):
        key = key.encode('utf-8')
    if isinstance(msg, str):
        msg = msg.encode('utf-8')
    return hmac.new(key, msg, hashlib.sha256).digest()

def sha256_hex(s):
    """计算SHA256哈希值"""
    if isinstance(s, str):
        s = s.encode('utf-8')
    return hashlib.sha256(s).hexdigest()

def call_api(sid, skey, service, action, region, params):
    """
    调用腾讯云API
    注意:腾讯云的签名方式比阿里云复杂一些,但原理一样——
    都是在你发送的请求上"签名",证明是你发的
    """
    host = f"{service}.tencentcloudapi.com"
    
    # 1. 构造请求内容
    payload = json.dumps(params)
    
    # 2. 构造"规范请求串"(腾讯云要求的格式)
    ct = "application/json; charset=utf-8"
    hashed_payload = sha256_hex(payload)
    canonical = f"POST\n/\n\ncontent-type:{ct}\nhost:{host}\n\ncontent-type;host\n{hashed_payload}"
    
    # 3. 生成签名
    timestamp = str(int(time.time()))
    from datetime import datetime
    date = datetime.utcfromtimestamp(int(timestamp)).strftime("%Y-%m-%d")
    scope = f"{date}/{service}/tc3_request"
    string_to_sign = f"TC3-HMAC-SHA256\n{timestamp}\n{scope}\n{sha256_hex(canonical)}"
    
    # 4. 计算最终签名(分4步)
    secret_date = sign(("TC3" + skey).encode('utf-8'), date)
    secret_service = sign(secret_date, service)
    secret_signing = sign(secret_service, "tc3_request")
    signature = hmac.new(secret_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
    
    # 5. 发送请求
    auth = f"TC3-HMAC-SHA256 Credential={sid}/{scope}, SignedHeaders=content-type;host, Signature={signature}"
    req = urllib.request.Request(f"https://{host}/", data=payload.encode('utf-8'))
    req.add_header('Authorization', auth)
    req.add_header('Content-Type', ct)
    req.add_header('Host', host)
    req.add_header('X-TC-Action', action)
    req.add_header('X-TC-Version', '2018-07-09')
    req.add_header('X-TC-Timestamp', timestamp)
    req.add_header('X-TC-Region', region)
    
    try:
        response = urllib.request.urlopen(req, timeout=30)
        return json.loads(response.read().decode('utf-8'))
    except Exception as e:
        print(f"  ❌ 请求失败: {e}")
        return None

def main():
    print("=" * 60)
    print("  腾讯云人脸核身费用统计")
    print("=" * 60)
    
    # 1. 读取AccessKey
    sid, skey = read_credentials(ACCESSKEY_FILE)
    if not sid or not skey:
        print("❌ 请检查AccessKey文件路径是否正确")
        return
    
    print(f"✅ AccessKey读取成功")
    print(f"📅 查询月份: {BILLING_MONTH}")
    print()
    
    # 2. 构造时间范围(当月1号到当月最后一天)
    year, mon = BILLING_MONTH.split('-')
    begin = f"{year}-{mon}-01 00:00:00"
    last_day = calendar.monthrange(int(year), int(mon))[1]
    end = f"{year}-{mon}-{last_day:02d} 23:59:59"
    
    # 3. 查询所有产品费用
    print("正在查询腾讯云账单...")
    result = call_api(sid, skey, 'billing', 'DescribeBillSummaryByProduct', 'ap-guangzhou', {
        'BeginTime': begin,
        'EndTime': end,
    })
    
    if not result or 'Response' not in result:
        print("❌ 未获取到数据")
        return
    
    resp = result['Response']
    if 'Error' in resp:
        print(f"❌ API错误: {resp['Error'].get('Message', '')}")
        return
    
    # 4. 提取人脸核身的费用
    items = resp.get('SummaryOverview', resp.get('SummarySet', []))
    
    print(f"\n{'产品代码':<30} {'产品名称':<25} {'费用(元)':>12}")
    print("-" * 70)
    
    faceid_total = 0.0
    all_total = 0.0
    
    for item in items:
        code = item.get('BusinessCode', 'N/A')
        name = item.get('BusinessCodeName', 'N/A')
        cost = float(item.get('RealTotalCost', 0))  # 腾讯云金额单位已经是元,不需要换算
        all_total += cost
        
        is_faceid = any(kw in name for kw in ['人脸核身', '云智慧眼', 'faceid', '人脸', '活体'])
        marker = ' ← 🆔人脸核身' if is_faceid else ''
        
        print(f"{code:<30} {name[:23]:<25} {cost:>12.2f}{marker}")
        
        if is_faceid:
            faceid_total += cost
    
    print("-" * 70)
    print(f"\n📊 汇总结果:")
    print(f"  人脸核身服务合计: {faceid_total:.2f} 元")
    print(f"  全部产品合计: {all_total:.2f} 元")
    print(f"\n✅ 查询完成!")

if __name__ == '__main__':
    main()

4.2 和阿里云脚本的区别(通俗解释)

两个脚本做的事情一样:查账单。但"说话方式"不同:

对比项 阿里云 腾讯云
签名方式 HMAC-SHA1(老式门锁,一把钥匙开一把锁) TC3-HMAC-SHA256(指纹锁,更安全但步骤多)
请求方式 GET(跟浏览器打开网页一样) POST(提交表单一样)
金额单位 元(不需要转换)
字段名 PretaxAmount RealTotalCost

💡 打个比方:阿里云和腾讯云就像两家不同的银行。你要查账单,填的表格长得不一样,但目的都一样——查到花了多少钱。

4.3 运行腾讯云脚本

跟阿里云一样:

python tencent_cost.py

五、进阶:两个脚本合体 + 自动发邮件

上面的两个脚本是分开的(一个查阿里云,一个查腾讯云)。现在我们把它合并起来,一次性搞定两边,还能自动发邮件。

5.1 完整的合体脚本

新建 cloud_cost_report.py,粘贴以下代码。这次也是只改配置区域:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
云服务费用统计 + 自动发邮件
功能:同时查阿里云和腾讯云的费用,生成报表,自动发邮件
使用方法:改下面的配置区,然后运行
"""

import json, time, hmac, hashlib, base64, urllib.request, urllib.parse
import smtplib, calendar
from email.mime.text import MIMEText
from email.utils import formataddr
from datetime import datetime

# ════════════════════════════════════════════
# 🔧 配置区(只需要改这里!)
# ════════════════════════════════════════════

# 阿里云AccessKey文件
ALIYUN_KEY_FILE = r"D:\你的路径\aliyun_key.txt"

# 腾讯云AccessKey文件  
TENCENT_KEY_FILE = r"D:\你的路径\tencent_key.txt"

# 查询哪个月份
MONTH = "2026-04"

# 邮箱配置(发件人)
EMAIL_CONFIG = {
    "smtp_server": "smtp.qq.com",        # QQ邮箱就填这个;163邮箱填 smtp.163.com
    "smtp_port": 465,                     # QQ邮箱/163邮箱都用465
    "username": "你的邮箱@qq.com",         # 改成你的邮箱
    "password": "你的邮箱授权码",          # 注意:不是邮箱密码,是"授权码"!
    "sender_name": "系统运维部",           # 发件人显示的名字
}

# 收件人(可以填多个)
RECIPIENTS = ["财务@公司.com", "领导@公司.com"]

# 费用异常阈值:如果本月费用比上月偏差超过20%,自动提醒
DEVIATION_THRESHOLD = 0.20


# ════════════════════════════════════════════
# 阿里云API部分(不需要修改)
# ════════════════════════════════════════════

def read_aliyun_key(file_path):
    """读取阿里云AccessKey"""
    ak_id, ak_secret = "", ""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                if 'AccessKey ID' in line:
                    ak_id = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
                elif 'AccessKey Secret' in line:
                    ak_secret = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
        return ak_id, ak_secret
    except Exception as e:
        print(f"❌ 读取阿里云AccessKey失败: {e}")
        return None, None

def percent_encode(s):
    s = str(s)
    encoded = urllib.parse.quote(s, safe='~')
    return encoded.replace('+', '%20').replace('*', '%2A')

def call_aliyun(ak_id, ak_secret, action, params):
    """调用阿里云API"""
    common = {
        'Format': 'JSON', 'Version': '2017-12-14', 'AccessKeyId': ak_id,
        'SignatureMethod': 'HMAC-SHA1',
        'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
        'SignatureVersion': '1.0',
        'SignatureNonce': str(int(time.time() * 1000000)),
        'Action': action,
    }
    all_p = {**common, **params}
    sorted_k = sorted(all_p.keys())
    canonical = '&'.join([f"{percent_encode(k)}={percent_encode(all_p[k])}" for k in sorted_k])
    sign_str = f"GET&{percent_encode('/')}&{percent_encode(canonical)}"
    key = (ak_secret + '&').encode('utf-8')
    sig = base64.b64encode(hmac.new(key, sign_str.encode('utf-8'), hashlib.sha1).digest()).decode('utf-8')
    all_p['Signature'] = sig
    final = '&'.join([f"{percent_encode(k)}={percent_encode(all_p[k])}" for k in sorted(all_p.keys())])
    url = f"https://business.aliyuncs.com/?{final}"
    
    req = urllib.request.Request(url)
    req.add_header('Accept', 'application/json')
    try:
        resp = urllib.request.urlopen(req, timeout=30)
        return json.loads(resp.read().decode('utf-8'))
    except Exception as e:
        print(f"  ❌ 阿里云请求失败: {e}")
        return None

def fetch_aliyun_cost(month):
    """采集阿里云短信+语音费用"""
    ak_id, ak_secret = read_aliyun_key(ALIYUN_KEY_FILE)
    if not ak_id or not ak_secret:
        return None
    
    result = call_aliyun(ak_id, ak_secret, 'QueryBillOverview', {'BillingCycle': month})
    if not result or 'Data' not in result:
        return None
    
    items = result['Data'].get('Items', {}).get('Item', [])
    if isinstance(items, dict):
        items = [items]
    
    sms_total = 0.0
    voice_total = 0.0
    
    for item in items:
        code = item.get('ProductCode', '')
        name = item.get('ProductName', '')
        amount = float(item.get('PretaxAmount', 0))
        
        if any(kw in name.lower() or kw in code.lower() for kw in ['短信', 'sms', 'dysms', 'message']):
            sms_total += amount
        elif any(kw in name.lower() or kw in code.lower() for kw in ['语音', 'voice', 'dyvms', 'vms']):
            voice_total += amount
    
    return {'sms': sms_total, 'voice': voice_total, 'total': sms_total + voice_total}


# ════════════════════════════════════════════
# 腾讯云API部分(不需要修改)
# ════════════════════════════════════════════

def read_tencent_key(file_path):
    """读取腾讯云AccessKey"""
    sid, skey = "", ""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f.readlines():
                line = line.strip()
                if line.startswith('#'):
                    continue
                if 'SecretId' in line or 'AKID' in line:
                    sid = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
                elif 'SecretKey' in line:
                    skey = line.split(':')[1].strip() if ':' in line else line.split(':')[1].strip()
        return sid, skey
    except Exception as e:
        print(f"❌ 读取腾讯云AccessKey失败: {e}")
        return None, None

def tc3_sign(key, msg):
    if isinstance(key, str): key = key.encode('utf-8')
    if isinstance(msg, str): msg = msg.encode('utf-8')
    return hmac.new(key, msg, hashlib.sha256).digest()

def sha256(s):
    if isinstance(s, str): s = s.encode('utf-8')
    return hashlib.sha256(s).hexdigest()

def call_tencent(sid, skey, service, action, region, params):
    """调用腾讯云API"""
    host = f"{service}.tencentcloudapi.com"
    payload = json.dumps(params)
    
    ct = "application/json; charset=utf-8"
    canonical = f"POST\n/\n\ncontent-type:{ct}\nhost:{host}\n\ncontent-type;host\n{sha256(payload)}"
    
    ts = str(int(time.time()))
    date = datetime.utcfromtimestamp(int(ts)).strftime("%Y-%m-%d")
    scope = f"{date}/{service}/tc3_request"
    sign_str = f"TC3-HMAC-SHA256\n{ts}\n{scope}\n{sha256(canonical)}"
    
    d = tc3_sign(("TC3" + skey).encode('utf-8'), date)
    s = tc3_sign(d, service)
    ss = tc3_sign(s, "tc3_request")
    sig = hmac.new(ss, sign_str.encode('utf-8'), hashlib.sha256).hexdigest()
    
    auth = f"TC3-HMAC-SHA256 Credential={sid}/{scope}, SignedHeaders=content-type;host, Signature={sig}"
    req = urllib.request.Request(f"https://{host}/", data=payload.encode('utf-8'))
    for k, v in [('Authorization', auth), ('Content-Type', ct), ('Host', host),
                  ('X-TC-Action', action), ('X-TC-Version', '2018-07-09'),
                  ('X-TC-Timestamp', ts), ('X-TC-Region', region)]:
        req.add_header(k, v)
    
    try:
        resp = urllib.request.urlopen(req, timeout=30)
        return json.loads(resp.read().decode('utf-8'))
    except Exception as e:
        print(f"  ❌ 腾讯云请求失败: {e}")
        return None

def fetch_tencent_cost(month):
    """采集腾讯云人脸核身费用"""
    sid, skey = read_tencent_key(TENCENT_KEY_FILE)
    if not sid or not skey:
        return None
    
    year, mon = month.split('-')
    begin = f"{year}-{mon}-01 00:00:00"
    last_day = calendar.monthrange(int(year), int(mon))[1]
    end = f"{year}-{mon}-{last_day:02d} 23:59:59"
    
    result = call_tencent(sid, skey, 'billing', 'DescribeBillSummaryByProduct', 'ap-guangzhou', {
        'BeginTime': begin, 'EndTime': end,
    })
    
    if not result or 'Response' not in result:
        return None
    
    resp = result['Response']
    if 'Error' in resp:
        print(f"  ❌ 腾讯云API错误: {resp['Error'].get('Message', '')}")
        return None
    
    items = resp.get('SummaryOverview', resp.get('SummarySet', []))
    faceid_total = 0.0
    
    for item in items:
        name = item.get('BusinessCodeName', '')
        if any(kw in name for kw in ['人脸核身', '云智慧眼', 'faceid', '人脸', '活体']):
            faceid_total += float(item.get('RealTotalCost', 0))
    
    return {'faceid': faceid_total}


# ════════════════════════════════════════════
# 邮件发送部分
# ════════════════════════════════════════════

def send_report(data):
    """生成并发送邮件报表"""
    # 生成邮件内容
    subject = f"{data['month']}月云服务费用(短信+语音+人脸核身)统计报表"
    
    html = f"""
    <html>
    <head><meta charset="utf-8"></head>
    <body style="font-family: Microsoft YaHei, sans-serif; font-size: 14px;">
        <h2>📊 {data['month']}月云服务费用统计报表</h2>
        <p>统计时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
        <p>用途:采购申报</p>
        <hr>
        <table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;">
            <tr style="background:#f5f5f5;">
                <th>云厂商</th><th>产品</th><th>费用(元)</th>
            </tr>
            <tr>
                <td>阿里云</td><td>短信服务</td><td>{data['aliyun_sms']:.2f}</td>
            </tr>
            <tr>
                <td>阿里云</td><td>语音服务</td><td>{data['aliyun_voice']:.2f}</td>
            </tr>
            <tr>
                <td>腾讯云</td><td>人脸核身</td><td>{data['tencent_faceid']:.2f}</td>
            </tr>
            <tr style="font-weight:bold;background:#e8f5e9;">
                <td colspan="2">总计</td><td>{data['total']:.2f}</td>
            </tr>
        </table>
        <br>
        <p style="color:#999;font-size:12px;">数据来源:阿里云BSS OpenAPI / 腾讯云Billing API</p>
    </body>
    </html>
    """
    
    # 发送邮件
    try:
        config = EMAIL_CONFIG
        print(f"📧 正在发送邮件到: {', '.join(RECIPIENTS)}...")
        
        server = smtplib.SMTP_SSL(config['smtp_server'], config['smtp_port'], timeout=30)
        server.login(config['username'], config['password'])
        
        msg = MIMEText(html, 'html', 'utf-8')
        msg['From'] = formataddr((config['sender_name'], config['username']))
        msg['To'] = ', '.join(RECIPIENTS)
        msg['Subject'] = subject
        
        server.send_message(msg)
        server.quit()
        print("✅ 邮件发送成功!")
        return True
    except Exception as e:
        print(f"❌ 邮件发送失败: {e}")
        return False


# ════════════════════════════════════════════
# 主程序
# ════════════════════════════════════════════

def main():
    print("=" * 60)
    print("  云服务费用统计 + 邮件发送")
    print("=" * 60)
    print(f"  📅 查询月份: {MONTH}")
    print()
    
    # 1. 采集阿里云费用
    print("📊 正在采集阿里云费用...")
    aliyun = fetch_aliyun_cost(MONTH)
    if not aliyun:
        print("  ❌ 阿里云数据采集失败,请检查AccessKey和月份")
        return
    print(f"  ✅ 阿里云短信: {aliyun['sms']:.2f} 元")
    print(f"  ✅ 阿里云语音: {aliyun['voice']:.2f} 元")
    
    # 2. 采集腾讯云费用
    print("\n📊 正在采集腾讯云费用...")
    tencent = fetch_tencent_cost(MONTH)
    if not tencent:
        print("  ❌ 腾讯云数据采集失败,请检查AccessKey和月份")
        return
    print(f"  ✅ 腾讯云人脸核身: {tencent['faceid']:.2f} 元")
    
    # 3. 汇总
    data = {
        'month': MONTH,
        'aliyun_sms': aliyun['sms'],
        'aliyun_voice': aliyun['voice'],
        'tencent_faceid': tencent['faceid'],
        'total': aliyun['total'] + tencent['faceid'],
    }
    
    print(f"\n{'─' * 40}")
    print(f"  💰 总计: {data['total']:.2f} 元")
    print(f"{'─' * 40}")
    
    # 4. 发送邮件
    print()
    send_report(data)
    
    print(f"\n✅ 全部完成!")
    print(f"  如需自动定时执行,对AI说:每月1号早上9点自动跑这个脚本")

if __name__ == '__main__':
    main()

5.2 改配置,运行

只需要改最上面的 配置区

# 把文件路径改成你自己的
ALIYUN_KEY_FILE = r"D:\你的路径\aliyun_key.txt"
TENCENT_KEY_FILE = r"D:\你的路径\tencent_key.txt"
MONTH = "2026-04"

# 改成你的邮箱
EMAIL_CONFIG = {
    "smtp_server": "smtp.qq.com",        # 用什么邮箱就填什么
    "username": "你的邮箱@qq.com",
    "password": "你的邮箱授权码",          # QQ邮箱→设置→账户→POP3/SMTP→生成授权码
}

RECIPIENTS = ["收件人1@公司.com", "收件人2@公司.com"]

💡 怎么获取邮箱授权码?

  • QQ邮箱:登录→设置→账户→POP3/IMAP/SMTP服务→开启→生成授权码
  • 163邮箱:登录→设置→POP3/SMTP/IMAP→开启→新增授权码
  • 授权码是一个16位的字母数字组合,不是你的邮箱密码

5.3 运行

python cloud_cost_report.py

输出示例:

============================================================
  云服务费用统计 + 邮件发送
============================================================
  📅 查询月份: 2026-04

📊 正在采集阿里云费用...
  ✅ 阿里云短信: 50000.00 元
  ✅ 阿里云语音: 17935.66 元

📊 正在采集腾讯云费用...
  ✅ 腾讯云人脸核身: 127258.48 元

────────────────────────────────────────
  💰 总计: 195194.14 元
────────────────────────────────────────

📧 正在发送邮件到: 财务@公司.com...
✅ 邮件发送成功!

✅ 全部完成!
  如需自动定时执行,对AI说:每月1号早上9点自动跑这个脚本

六、更进一步:设定时任务,从此不用手动

上面的脚本已经能自动查费用、发邮件了。但每个月还要手动跑一次?太麻烦了。

如果你有WorkBuddy或类似的AI助手,直接说:

“帮我设置一个定时任务:每月1号早上9点自动运行 cloud_cost_report.py,跑完后把结果发邮件给我。”

AI会帮你配好定时任务(Windows用"任务计划程序",Mac/Linux用cron)。从此每个月自动跑,你什么都不用做。


七、常见问题

Q:运行后显示"❌ 读取AccessKey文件失败"怎么办?
A:检查三个地方:

  1. 文件路径对不对(注意Windows路径用 r"D:\..." 这种写法)
  2. 文件内容格式对不对(必须有 AccessKey ID:AccessKey Secret: 这两行)
  3. AccessKey Secret是不是已经失效了(在云平台控制台重新生成一个)

Q:AccessKey放在文件里安全吗?
A:建议做两件事:

  1. 只读权限的子账号AccessKey(只能看账单,不能操作资源)
  2. 把AccessKey文件放在你自己电脑的私密位置,不要上传到GitHub等公开平台

Q:不收短信/语音/人脸核身,能用这个脚本吗?
A:当然能!你只需要改一下脚本里的关键词:

# 把这里改成你要查的产品名称关键词
is_sms = any(kw in name for kw in ['你想查的产品名', '关键词'])

Q:Windows和Mac都能用吗?
A:都能用!Python是跨平台的,Mac用户把路径改成 /Users/你的用户名/... 就行。


八、总结

回顾一下,这篇教程你学会了:

  1. ✅ 准备云平台的AccessKey(子账号+只读权限)
  2. ✅ 用一个Python脚本,自动拉取阿里云和腾讯云的账单
  3. ✅ 自动生成汇总报表,发邮件给财务
  4. ✅ 设定时任务,每月自动跑

从头到尾,你只做了三件事:改路径、改月份、改邮箱。其他都交给脚本。

工具的意义不是让你学技术,而是让你把时间花在更有价值的事情上。


如果这篇文章对你有帮助,欢迎 👍 点赞、⭐ 收藏、💬 评论

有问题在评论区留言,我看到都会回复。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐