前言

最近遇到一个需求,有几十个Excel,每个的字段都不一样,然后都差不多是第一行是表头,后面几千上万的数据,需要把这些Excel中的数据全都加入某个已经上线的Django项目

这就需要每个Excel建个表,然后一个个导入了

这样的效率太低,不能忍

所以我造了个自动生成 Model 和导入脚本的轮子

思路

首先拿出 pandas,它的 DataFrame 用来处理数据很方便

pandas 加载 Excel 之后,提取表头,我们要通过表头来生成数据表的字段。有些 Excel 的表头是中文的,需要先做个转换。

一开始我是想用翻译API,全都翻译成英文,不过发现免费的很慢有限额,微软、DeepL都要申请,很麻烦。索性用个拼音转换库,全都转换成拼音得了~

然后字段的长度也要确定,或者全部用不限制长度的 TextField

权衡一下,我还是做一下字段长度判定的逻辑,遍历整个表,找出各个字段最长的数据,然后再加一个偏移量,作为最大长度。

接着生成 Model 类,这里我用 jinja2 模板语言,先把大概的模板写好,然后根据提取出来的字段名啥的生成。

最后生成 admin 配置和导入脚本,同理,也是用 jinja2 模板。

实现

简单介绍下思路,现在开始上代码。

就几行而已,Python很省代码~

模型

首先定义俩模型

字段模型

class Field(object):
def __init__(self, name: str, verbose_name: str, max_length: int = 128):
self.name = name
self.verbose_name = verbose_name
self.max_length = max_length def __str__(self):
return f'<Field>{self.name}:{self.verbose_name}' def __repr__(self):
return self.__str__()

Model模型

为了符合Python关于变量的命名规范,snake_name 属性是用正则表达式实现驼峰命名转蛇形命名

class Model(object):
def __init__(self, name: str, verbose_name: str, id_field: Field, fields: List[Field]):
self.name = name
self.verbose_name = verbose_name
self.id_field = id_field
self.fields: List[Field] = fields @property
def snake_name(self):
import re
pattern = re.compile(r'(?<!^)(?=[A-Z])')
name = pattern.sub('_', self.name).lower()
return name def __str__(self):
return f'<Model>{self.name}:{self.verbose_name}' def __repr__(self):
return self.__str__()

代码模板

使用 jinja2 实现。

本身 jinja2 是 Flask、Django 之类的框架用来渲染网页的。

不过单独使用的效果也不错,我的 DjangoStarter 框架也是用这个 jinja2 来自动生成 CRUD 代码~

Model模板

# -*- coding:utf-8 -*-
from django.db import models class {{ model.name }}(models.Model):
"""{{ model.verbose_name }}"""
{% for field in model.fields -%}
{{ field.name }} = models.CharField('{{ field.verbose_name }}', default='', null=True, blank=True, max_length={{ field.max_length }})
{% endfor %}
class Meta:
db_table = '{{ model.snake_name }}'
verbose_name = '{{ model.verbose_name }}'
verbose_name_plural = verbose_name

Admin配置模板

@admin.register({{ model.name }})
class {{ model.name }}Admin(admin.ModelAdmin):
list_display = [{% for field in model.fields %}'{{ field.name }}', {% endfor %}]
list_display_links = None def has_add_permission(self, request):
return False def has_delete_permission(self, request, obj=None):
return False def has_view_permission(self, request, obj=None):
return False

数据导入脚本

这里做了几件事:

  • 使用 pandas 处理空值,填充空字符串
  • 已有数据进行批量更新
  • 新数据批量插入

更新逻辑麻烦一点,因为数据库一般都有每次最大更新数量的限制,所以我做了分批处理,通过 update_data_once_max_lines 控制每次最多同时更新多少条数据。

def import_{{ model.snake_name }}():
file_path = path_proc(r'{{ excel_filepath }}') logger.info(f'读取文件: {file_path}')
xlsx = pd.ExcelFile(file_path)
df = pd.read_excel(xlsx, 0, header={{ excel_header }})
df.fillna('', inplace=True) logger.info('开始处理数据') id_field_list = {{ model.name }}.objects.values_list('{{ model.id_field.name }}', flat=True)
item_list = list({{ model.name }}.objects.all()) def get_item(id_value):
for i in item_list:
if i.shen_qing_ren_zheng_jian_hao_ma == id_value:
return i
return None insert_data = []
update_data_once_max_lines = 100
update_data_sub_set_index = 0
update_data = [[]]
update_fields = set() for index, row in df.iterrows():
if '{{ model.id_field.verbose_name }}' not in row:
logger.error('id_field {} is not existed'.format('{{ model.id_field.verbose_name }}'))
continue if row['{{ model.id_field.verbose_name }}'] in id_field_list:
item = get_item(row['{{ model.id_field.verbose_name }}'])
{% for field in model.fields -%}
if '{{ field.verbose_name }}' in row:
if item.{{ field.name }} != row['{{ field.verbose_name }}']:
item.{{ field.name }} = row['{{ field.verbose_name }}']
update_fields.add('{{ field.name }}')
{% endfor %}
if len(update_data[update_data_sub_set_index]) >= update_data_once_max_lines:
update_data_sub_set_index += 1
update_data.append([])
update_data[update_data_sub_set_index].append(item)
else:
# {% for field in model.fields -%}{{ field.verbose_name }},{%- endfor %}
model_obj = {{ model.name }}()
{% for field in model.fields -%}
if '{{ field.verbose_name }}' in row:
model_obj.{{ field.name }} = row['{{ field.verbose_name }}']
{% endfor %}
insert_data.append(model_obj) logger.info('开始批量导入')
{{ model.name }}.objects.bulk_create(insert_data)
logger.info('导入完成') if len(update_data[update_data_sub_set_index]) > 0:
logger.info('开始批量更新')
for index, update_sub in enumerate(update_data):
logger.info(f'正在更新 {index * update_data_once_max_lines}-{(index + 1) * update_data_once_max_lines} 条数据')
{{ model.name }}.objects.bulk_update(update_sub, list(update_fields))
logger.info('更新完成')

主体代码

剩下的全是核心代码了

引用依赖

先把用到的库导入

import os
import re
from typing import List, Optional from pypinyin import pinyin, lazy_pinyin, Style
from jinja2 import Environment, PackageLoader, FileSystemLoader

或者后面直接去我的完整代码里面拿也行~

老规矩,我封装了一个类。

构造方法需要指定 Excel 文件地址,还有表头的行索引。

class ExcelToModel(object):
def __init__(self, filepath, header_index=0):
self.filepath = filepath
self.header_index = header_index
self.columns = []
self.fields: List[Field] = [] self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.template_path = os.path.join(self.base_dir, 'templates')
self.jinja2_env = Environment(loader=FileSystemLoader(self.template_path)) self.load_file()

这里面有个 self.load_file() 后面再贴。

字段名中文转拼音

用了 pypinyin 这个库,感觉还不错。

转换后用正则表达式,去除符号,只保留英文和数字。

代码如下,也是放在 ExcelToModel 类里边。

@staticmethod
def to_pinyin(text: str) -> str:
pattern = r'~`!#$%^&*()_+-=|\';"":/.,?><~·!@#¥%……&*()——+-=“:’;、。,?》{《}】【\n\]\[ '
text = re.sub(r"[%s]+" % pattern, "", text)
return '_'.join(lazy_pinyin(text, style=Style.NORMAL))

加载文件

拿出万能的 pandas,按照前面说的思路,提取表头转换成字段,并且遍历数据确定每个字段的最大长度,我这里偏移值是32,即在当前数据最大长度基础上加上32个字符。

def load_file(self):
import pandas as pd
xlsx = pd.ExcelFile(self.filepath)
df = pd.read_excel(xlsx, 0, header=self.header_index)
df.fillna('', inplace=True)
self.columns = list(df.columns)
for col in self.columns:
field = Field(self.to_pinyin(col), col)
self.fields.append(field)
for index, row in df.iterrows():
item_len = len(str(row[col]))
if item_len > field.max_length:
field.max_length = item_len + 32 print(field.verbose_name, field.name, field.max_length)

如果觉得这样生成表太慢,可以把确定最大长度的这块代码去掉,就下面这块代码

for index, row in df.iterrows():
item_len = len(str(row[col]))
if item_len > field.max_length:
field.max_length = item_len + 32

手动指定最大长度或者换成不限制长度的 TextField 就行。

生成文件

先构造个 context 然后直接用 jinja2 的 render 功能生成代码。

为了在导入时判断数据存不存在,生成代码时要指定 id_field_verbose_name,即Excel文件中类似“证件号码”、“编号”之类的列名,注意是Excel中的表头列名。

def find_field_by_verbose_name(self, verbose_name) -> Optional[Field]:
for field in self.fields:
if field.verbose_name == verbose_name:
return field
return None def generate_file(self, model_name: str, verbose_name: str, id_field_verbose_name: str, output_filepath: str):
template = self.jinja2_env.get_template('output.jinja2')
context = {
'model': Model(
model_name, verbose_name,
self.find_field_by_verbose_name(id_field_verbose_name),
self.fields
),
'excel_filepath': self.filepath,
'excel_header': self.header_index,
}
with open(output_filepath, 'w+', encoding='utf-8') as f:
render_result = template.render(context)
f.write(render_result)

使用

看代码。

tool = ExcelToModel('file.xlsx')
tool.generate_file('CitizenFertility', '房价与居民生育率', '证件号码', 'output/citizen_fertility.py')

生成出来的代码都在一个文件里,请根据实际情况放到项目的各个位置。

完整代码

发布到Github了

地址: https://github.com/Deali-Axy/excel_to_model

小结

目前看来完美契合需求,极大节省工作量~

实际跑起来,不得不吐槽 Python 羸弱的性能,占内存还大… 凑合着用吧。也许后面有时间会优化一下~

造个Python轮子,实现根据Excel生成Model和数据导入脚本的更多相关文章

  1. [Python]将Excel文件中的数据导入MySQL

    Github Link 需求 现有2000+文件夹,每个文件夹下有若干excel文件,现在要将这些excel文件中的数据导入mysql. 每个excel文件的第一行是无效数据. 除了excel文件中已 ...

  2. Python xlrd模块读取Excel表中的数据

    1.xlrd库的安装 直接使用pip工具进行安装(当然也可以使用pycharmIDE进行安装,这里就不详述了) pip install xlrd 2.xlrd模块的一些常用命令 ①打开excel文件并 ...

  3. sql server数据库将excel表中的数据导入数据表

    一般有两种方法可以实现,一种是直接写sql语句,另外一种是利用sqlserver的管理工具实现.这里介绍的是后面一种方法. 步骤: 一.准备数据 1.将excel表另存为文本格式,注意文本格式需为ta ...

  4. 将Excel表中的数据导入到数据库

    网上查到的有参考价值的就一家,自己调试发现可行.感谢原创文章:将Excel中数据导入数据库(一) using System; using System.Collections.Generic; usi ...

  5. 怎么把excel表格内的数据导入数据库?

    第一种方法: 思路:想要把excel表格内的数据直接导入数据库不是那么容易,可以把excel表格另存为.csv格式的文档(特点:内容以逗号分割):然后通过一系列的文档操作函数处理成为一个二维数组,然后 ...

  6. excel文档中数据导入sql server注意事项

    进来经常需要对一些基础数据进行更新,而业务方提供的数据源往往都是excel,所以经常需要将excel中数据导入到 数据库临时表,然后再进行处理. 在导入过程中,发现有些数据比如手机号码,如果默认导入, ...

  7. Python全栈 MySQL 数据库 (索引、数据导入、导出)

    ParisGabriel              每天坚持手写  一天一篇  决定坚持几年 为了梦想为了信仰    开局一张图     表字段重命名(change)   alter table 表名 ...

  8. 用python的pandas读取excel文件中的数据

    一.读取Excel文件   使用pandas的read_excel()方法,可通过文件路径直接读取.注意到,在一个excel文件中有多个sheet,因此,对excel文件的读取实际上是读取指定文件.并 ...

  9. python根据已有数据库生成model.py

    有时我们需要根据已存在的数据库进行django开发时,手写model.py是不现实的 先执行下面的语句,在命令行终端会输出所有表的类 python .\manage.py inspectdb 检查无误 ...

  10. Python 用load_workbook 读取excel某个单元格数据、读取excel行数、列数

    from openpyxl import load_workbook path = r'D:\pywork\12' # EXCEL信息所在文件夹 e= load_workbook(path + '/' ...

随机推荐

  1. 关于python文件写入问题

    第一种.用for循环不断打开文件写入关闭 测试代码数据如下: import time begin = time.perf_counter() def a(f, lis): f.write(lis + ...

  2. vim编辑器使用详解

    Linux之vim编辑器使用 vim三种模式:命令模式,插入模式,退出模式 移动光标操作 左移动一个字符: 按 h 键 右移动一个字符:按 l 键 下移动一行:按 j 键 上移动一行:按 k 键 移动 ...

  3. Pytorch: repeat, repeat_interleave, tile的用法

    https://zhuanlan.zhihu.com/p/474153365 torch.repeat 使张量沿着某个维度进行复制, 并且不仅可以复制张量,也可以拓展张量的维度: import tor ...

  4. 在UniApp的H5项目中,生成二维码和扫描二维码的操作处理

    在我们基于UniApp的H5项目中,需要生成一些二维码进行展示,另外也需要让用户可以扫码进行一定的快捷操作,本篇随笔介绍一下二维码的生成处理和基于H5的扫码进行操作.二维码的生成,使用了JS文件wea ...

  5. Pod 使用进阶

    静态 Pod 在 Kubernetes 集群中除了我们经常使用到的普通的 Pod 外,还有一种特殊的 Pod,叫做Static Pod,也就是我们说的静态 Pod,静态 Pod 有什么特殊的地方呢? ...

  6. MySQL8 二进制日志

    启用二进制日志 # cat /etc/my.cnf [mysqld] server_id=100 log_bin=/var/log/mysql/binlogs/server1 # mkdir -p / ...

  7. 常量的定义(const和#define)

    定义常量的方法 //均要在调用前(区别全局变量!!) 1.使用#define预处理器 2.使用const关键字 1.#define #define 常量名 常量值 //定义形式,常量名不可以是数字开头 ...

  8. Linux make编译

    安装问题 linux编译流程 linux开发部分 一般来说著名的linux系统基本上分两大类: RedHat系列:Redhat.Centos.Fedora等 Debian系列:Debian.Ubunt ...

  9. 我的Vue之旅、05 导航栏、登录、注册 (Mobile)

    第一期 · 使用 Vue 3.1 + TypeScript + Router + Tailwind.css 构建手机底部导航栏.仿B站的登录.注册页面. 代码仓库 alicepolice/Vue-05 ...

  10. 洛谷P3376 (最大流模板)

    1 #include<bits/stdc++.h> 2 #define int long long 3 using namespace std; 4 const int maxn=5005 ...