Python用Flask后端解析Excel图表,Vue3+ECharts前端动态还原(附全套代码)
以下是完整的 Flask + Vue 3 前端模板 方案,实现 上传 Excel 文件(不再用链接),后端解析 chart1.xml,返回结构化数据,前端用 ECharts 渲染图表。

项目结构
project/ ├── app.py ├── templates/ │ └── index.html 1. 后端:app.py
import xml.etree.ElementTree as ET import io from zipfile import ZipFile, BadZipFile from flask import Flask, render_template, request, jsonify from flask_cors import CORS # 可选,如果有跨域需求 app = Flask(__name__) CORS(app)# 允许跨域(同域其实不需要,但留着保险)defparse_chart_from_bytes(file_data): res ={} result ={}try: archive = ZipFile(io.BytesIO(file_data)) chart_data = archive.read('xl/charts/chart1.xml') res['code']=200 res['msg']="获取图表信息成功" tree = ET.parse(io.BytesIO(chart_data)) root = tree.getroot() ns ={'c':'http://schemas.openxmlformats.org/drawingml/2006/chart','a':'http://schemas.openxmlformats.org/drawingml/2006/main'}# 图表类型 type_mapping =[('pieChart','饼图','pie'),('lineChart','折线图','line'),('barChart','柱形图','bar'),('areaChart','面积图','line'),('scatterChart','散点图','scatter'),('radarChart','雷达图','radar'),] chart_type_cn ='其它' echarts_type ='line' is_area =Falsefor tag, cn, en in type_mapping:if root.find(f'.//c:{tag}', ns)isnotNone: chart_type_cn = cn echarts_type = en if cn =='面积图': is_area =Truebreak result['chart_type_cn']= chart_type_cn result['echarts_type']= echarts_type result['is_area']= is_area # 标题 title_text ="" title_elem = root.find('.//c:title/c:tx/c:rich', ns)if title_elem isnotNone:for t in title_elem.iterfind('.//a:t', ns):if t.text: title_text += t.text result['title']= title_text.strip()or"无标题"# 系列数据 series_list =[] ser_elements = root.findall('.//c:ser', ns)for idx, ser inenumerate(ser_elements):# 系列名称 name_elem = ser.find('c:tx/c:v', ns) name = name_elem.text if name_elem isnotNoneelsef"系列{idx +1}"# 类别(X轴) cat_vs = ser.findall('c:cat//c:pt/c:v', ns) categories =[v.text for v in cat_vs if v.text isnotNone]# 数值(Y轴) val_vs = ser.findall('c:val//c:pt/c:v', ns) values =[]for v in val_vs:if v.text:try: values.append(float(v.text))except ValueError: values.append(v.text) series_list.append({"name": name,"categories": categories,"data": values })# 共享类别轴(如果每个系列都没有独立类别)if series_list andall(len(s['categories'])==0for s in series_list): shared_vs = root.findall('.//c:cat//c:pt/c:v', ns) shared_cat =[v.text for v in shared_vs if v.text isnotNone]if shared_cat:for s in series_list: s['categories']= shared_cat # 饼图特殊处理if chart_type_cn =="饼图"and series_list:for s in series_list: pie_data =[] cats = s['categories']or[f"项{i+1}"for i inrange(len(s['data']))]for i, val inenumerate(s['data']): name = cats[i]if i <len(cats)elsef"项{i+1}" value = val ifisinstance(val,(int,float))else0 pie_data.append({"name": name,"value": value}) s['data']= pie_data # 最终类别轴(非饼图)if chart_type_cn !="饼图"and series_list and series_list[0]['categories']: result['categories']= series_list[0]['categories']else: result['categories']=[]# 系列(只保留 name 和 data) result['series']=[{"name": s["name"],"data": s["data"]}for s in series_list] res['data']= result except BadZipFile: res['code']=404 res['msg']="无效的Excel文件"except KeyError: res['code']=404 res['msg']="未找到图表信息(无chart1.xml)"except Exception as e: res['code']=500 res['msg']=f"解析失败: {str(e)}"return res @app.route('/')defindex():return render_template('index.html')@app.route('/api/upload_chart', methods=['POST'])defupload_chart():if'file'notin request.files:return jsonify({"code":400,"msg":"没有上传文件"})file= request.files['file']iffile.filename ==''ornotfile.filename.lower().endswith('.xlsx'):return jsonify({"code":400,"msg":"请上传 .xlsx 文件"}) file_data =file.read()return jsonify(parse_chart_from_bytes(file_data))if __name__ =='__main__': app.run(host='127.0.0.1', port=5000, debug=True)2. 前端模板:templates/index.html
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>上传 Excel → ECharts 渲染</title><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js"></script><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"></script><style>body{font-family: Arial, sans-serif;padding: 20px;background: #f5f5f5;}.container{max-width: 800px;margin: auto;background: white;padding: 30px;border-radius: 8px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}input[type="file"]{padding: 10px;margin-bottom: 15px;}button{padding: 12px 24px;font-size: 16px;background: #409eff;color: white;border: none;border-radius: 4px;cursor: pointer;}button:hover{background: #66b1ff;}button:disabled{background: #a0cfff;cursor: not-allowed;}#chart{width: 100%;height: 600px;margin-top: 30px;}.msg{margin: 15px 0;font-weight: bold;color: #e6a23c;}</style></head><body><divid="app"class="container"><h2>上传 Excel 文件 → ECharts 渲染图表</h2><inputtype="file"accept=".xlsx"@change="onFileChange"/><button@click="uploadAndRender":disabled="!selectedFile">上传并渲染图表</button><divclass="msg">{{ message }}</div><divref="chartRef"id="chart"></div></div><script>const{ createApp, ref, onMounted, nextTick }= Vue;createApp({setup(){const selectedFile =ref(null);const message =ref('请选择一个包含图表的 .xlsx 文件');const chartRef =ref(null);let myChart =null;onMounted(()=>{ myChart = echarts.init(chartRef.value);});constonFileChange=(e)=>{const file = e.target.files[0];if(file && file.name.endsWith('.xlsx')){ selectedFile.value = file; message.value =`已选择文件:${file.name}`;}else{ selectedFile.value =null; message.value ='请上传 .xlsx 文件';}};constuploadAndRender=async()=>{if(!selectedFile.value)return; message.value ='正在上传并解析...';const formData =newFormData(); formData.append('file', selectedFile.value);try{const response =awaitfetch('/api/upload_chart',{method:'POST',body: formData });const json =await response.json();if(json.code !==200){ message.value =`错误:${json.msg}`;return;}const data = json.data;const option ={title:{text: data.title,left:'center',textStyle:{fontSize:18}},tooltip:{trigger: data.echarts_type ==='pie'?'item':'axis'},legend:{data: data.series.map(s=> s.name),top:30}};if(data.echarts_type ==='pie'){ option.series = data.series.map(s=>({type:'pie',name: s.name,radius:'55%',center:['50%','60%'],data: s.data,emphasis:{itemStyle:{shadowBlur:10,shadowOffsetX:0,shadowColor:'rgba(0, 0, 0, 0.5)'}}}));}elseif(data.echarts_type ==='radar'){const maxVal = Math.max(...data.series.flatMap(s=> s.data.filter(v=>typeof v ==='number')),100); option.radar ={indicator: data.categories.map(name=>({ name,max: maxVal *1.2}))}; option.series =[{type:'radar',data: data.series.map(s=>({name: s.name,value: s.data }))}];}else{ option.xAxis ={type:'category',data: data.categories }; option.yAxis ={type:'value'}; option.series = data.series.map(s=>({type: data.echarts_type,name: s.name,data: s.data,areaStyle: data.is_area ?{opacity:0.3}:undefined,smooth: data.echarts_type ==='line'?true:false}));}awaitnextTick(); myChart.setOption(option,true); message.value =`渲染成功!图表类型:${data.chart_type_cn}(${data.series.length} 个系列)`;}catch(err){ message.value ='上传或解析失败:'+ err.message; console.error(err);}};return{ selectedFile, message, chartRef, onFileChange, uploadAndRender };}}).mount('#app');</script></body></html>使用方式
- 把上面的文件按结构放好。
- 浏览器打开
http://127.0.0.1:5000 - 选择一个包含图表(chart1)的
.xlsx文件 → 点击“上传并渲染图表”
运行:
python app.py 安装依赖:
pip install flask flask-cors 功能特点
- 支持上传本地 Excel 文件解析(安全,只读内存)。
- 支持单/多系列图表。
- 饼图、折线、柱形、面积、散点、雷达图基本还原。
- 前端美化了些样式和交互。
相关文章
Python 使用 openpyxl 从 URL 读取 Excel 并获取 Sheet 及单元格样式信息
Python 解析 Excel 图表(Chart)信息实战:从 xlsx 中提取标题、字体和数据