FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54

0 环境

  • 使用的系统:mac
  • 使用的框架:FastSoyAdmin
  • 使用的数据库:PostgreSQL
  • 技术:fastapi、vue3

1 导出功能描述

导出流程就是点击导出按钮,然后把我想要的前端展示的表格数据下载到本地,或者说在导出时,还有其他的数据处理诉求等,但本质上就是将这些数据洗洗干净放到excel中供用户下载到本地使用。知道干啥了,那么这个导出的活该谁做呢。

2 后端导出报错

后端导出其实前后端都要做处理,后端要从数据库里拿到相应的data,然后生成表格(使用openpyxl或者xlsxwriter库),然后在用StreamingResponse方式将excel返回给你前端,前端使用blob方式下载(封装好了就调用就好了)。我遇到的坑是明明都默认是utf-8,导出流时报错。

1 后端导出处理思路复盘

这里我就遇到一个坑,当我用openpyxl 生成excel后,都没问题,直到return StreamingResponse xxx发给前端时,会报错 'latin-1' codec can't encode characters in position 41-54: ordinal not in range(256)。排查如下:

1 mac语言环境排查

一开始我怀疑是不是mac默认语言环境的问题,查看了 echo $LANGlocale ,也是有utf-8的。重设了语言环境,

bash 复制代码
echo 'export LANG="en_US.UTF-8"' >> ~/.zshrc
source ~/.zshrc
locale charmap

2 ide排查

默认环境是utf-8,这里我又重新设置了一遍了。

3 数据库排除

打开cli客户端,找到对应数据库,右键编辑数据库,找到字符分类,也是utf-8 ,没有问题呀。

为了以防万一,我又用命令查了一遍。复制如下代码:

sql 复制代码
-- 验证数据库编码
SELECT datname, pg_encoding_to_char(encoding) 
FROM pg_database WHERE datname = current_database();

-- 验证服务器编码
SHOW server_encoding;

-- 验证客户端编码
SHOW client_encoding;

-- 验证当前会话编码
SELECT pg_client_encoding();

可以看到也是utf-8格式的,没有问题的。其实这里最保险起见还是换个mysql、SQLite这类的都试下。

4 连接postgresql库排查

有人说是连接库的有问题,url里强制加入utf-8,在测还是报错,我直接utf-8 格式先写入到本地,打开发现也没问题,说明连接这个库也是么有问题的。StreamingResponse 也设置过了utf-8

3 解决替代方案

最后换到云服务器测,就没问题。虽然没找到真正原因,还是准备了替代方案。

1 前端导出

假如数据量不大的话,直接问ai,前端xlsx和file-saver导出excel文件,别忘了让后端先判断有导出这个权限后,然后再给你数据。但是哪天保不齐某个用户不单单只要几条,ta突发奇想我全都要导出,数据量还可以的情况,前后端谁做都是可以的,但一定要从后端拿到全部data数据,然后前端做处理生成excel。注意啦,假如导出数据量不大的话,python + tortoise orm完全可以的,可一旦数据量多了 + 有判断处理的要求的时候,请用go rust这类的编程语言作为底层帮你专门处理数据,当然也可以在数据库层面想办法,比如写存储过程这类的也可以的,不然直接用python的orm,会卡的要死。

1 xlsx和file-saver导出excel

安装xlsx和file-saver依赖 pnpm add xlsx file-saver @types/xlsx @types/file-saver

exportToExcel需要的参数:table数据, 文件名,表头。

js 复制代码
import { saveAs } from 'file-saver';
import * as XLSX from 'xlsx';

/**
 * 导出数据到Excel文件
 * @param data 要导出的数据数组
 * @param filename 导出的文件名(不含扩展名)
 * @param sheetName 工作表名称
 */
export function exportToExcel<T extends Record<string, any>>(
  data: T[],
  filename: string = '导出数据',
  sheetName: string = 'Sheet1'
): void {
  try {
    // 创建工作簿
    const workbook = XLSX.utils.book_new();
    
    // 将数据转换为工作表
    const worksheet = XLSX.utils.json_to_sheet(data);
    
    // 将工作表添加到工作簿
    XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
    
    // 生成Excel文件二进制数据
    const excelBuffer = XLSX.write(workbook, {
      bookType: 'xlsx',
      type: 'array'
    });
    
    // 创建Blob并保存文件
    const blob = new Blob([excelBuffer], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    });
    
    saveAs(blob, `${filename}.xlsx`);
  } catch (error) {
    console.error('导出Excel失败:', error);
    throw new Error('导出Excel文件时出错');
  }
}

假如没有导出权限的要求时,先判断有没有数据,在调用exportToExcel(最好这里的数据是从后端拿的好),若是后端你也要写的话,也请别忘了在路由中加入dependencies=[DependPermission],在这个导出路由里,根据前端请求获取数据库的数据,整成个数组发给前端,前端exportToExcel(后端发来的data, {表格名,表头})。

js 复制代码
<script setup lang="ts">
import { ref } from 'vue';
import { exportToExcel } from '@/utils/excelExporter';

const isLoading = ref(false);
const tableData = ref<Array<Record<string, any>>>([]);

// 模拟获取数据
const fetchData = async () => {
  // 实际项目中替换为API调用
  return [
    { id: 1, product: '商品A', price: 100, stock: 50 },
    { id: 2, product: '商品B', price: 200, stock: 30 }
  ];
};

const handleExport = async () => {
  try {
    isLoading.value = true;
    
    // 如果没有数据,先获取数据
    if (tableData.value.length === 0) {
      tableData.value = await fetchData();
    }
    
    exportToExcel(tableData.value, {
      filename: '商品列表',
      headers: {
        id: 'ID',
        product: '商品名称',
        price: '价格',
        stock: '库存'
      }
    });
  } catch (error) {
    console.error('导出失败:', error);
  } finally {
    isLoading.value = false;
  }
};
</script>

<template>
  <button @click="handleExport" :disabled="isLoading">
    {{ isLoading ? '导出中...' : '导出Excel' }}
  </button>
</template>

2 后端生成静态文件,前端访问对应url

其实就是后端挂载的static那个目录,我们将写入的excel文件最后放在这个目录里即可,最后前端根据url(ip:端口/static//xxx.xlsx),访问这个目录即可,比如get请求时带入responseType: 'blob',然后用file-saver里的saveAs下载文件即可,saveAs保存的两个入参,一个是:填入blob那个请求,拿到返回的data,第二个就是:定义文件名,目前测试效果下也很不错。你要是不想用第三库,就用原生的方式,拼个a标签,然后将url和文件名塞入进去也可以的,当然要求高的话,还是使用专业的oss存储,我是打算有机会用rustfs**存储excel。

4 小结

根据后台显示的报错,排除所有可能的情况,挨个排查一遍后,还是无果,还可以试着换个window或者打包放到linux环境跑下,说不定你和你的代码都没问题呢。当然为了保险起见,还是要留些替代方案,比如前端导出的方案,或者后端生成Excel(文件名最好加个时间戳哦)在放到静态目录中,然后前端访问静态文件的方式下载等等方案。

相关推荐
回家路上绕了弯17 分钟前
深度理解 volatile 与 synchronized:并发编程的两把钥匙
java·后端
程序员清风18 分钟前
ThreadLocal在什么情况下会导OOM?
java·后端·面试
毕了业就退休20 分钟前
websocket 的心跳机制你知道几种
前端·javascript·http
子林super20 分钟前
aiforcast集群单节点CPU使用率100%问题
前端
CF14年老兵22 分钟前
为什么 position: absolute 在 Flexbox 里会失效?
前端·css·trae
Juchecar23 分钟前
TypeScript 与 JavaScript 的关系及学习建议
javascript
就是帅我不改24 分钟前
基于领域事件驱动的微服务架构设计与实践
后端·面试·架构
JohnYan26 分钟前
Bun技术评估 - 25 Utils(实用工具)
javascript·后端·bun
xianxin_26 分钟前
CSS 选择器
前端
徐小夕27 分钟前
花3个月时间,写了一款协同文档编辑器
前端·vue.js·算法