You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Pydantic/SQLModel解析CSV导出的多对多关系字符串

解决SQLAdmin导出CSV后多对多关系字段的解析问题

问题描述

从FastAPI/SQLModel/SQLAdmin后端导出CSV时,多对多关系字段assessed_in被导出为Period模型实例的字符串形式,示例如下:

"[Period(id=1, name='1. quarter 2022', period_id='q1_2022', status=<PeriodStatusEnum.in_assessment: 'in_assessment'>), Period(id=2, name='2. Quarter 2022', period_id='q2_2022', status=<PeriodStatusEnum.in_assessment: 'in_assessment'>)]"

读取CSV后,其他字段可正常解析,但assessed_in因格式问题无法通过Dimension.model_validate完成解析。数据库模型定义:

# Database Models

class PeriodStatusEnum(str, enum.Enum):
    in_assessment = "in_assessment"
    readonly = "readonly"
    hidden = "hidden"

class Period(BaseClass, table=True):
    period_id: str = Field(index=True, unique=True)
    name: str
    status: PeriodStatusEnum = Field(sa_column=Column(Enum(PeriodStatusEnum)))

class DimensionPeriodLink(SQLModel, table=True):
    dimension_id: int | None = Field(default=None, foreign_key="dimension.id", primary_key=True)
    period_id: int | None = Field(default=None, foreign_key="period.id", primary_key=True)
    
class Dimension(BaseClass, table=True):
    dimension_id: str = Field(index=True, unique=True)
    name_EN: str
    name_DE: str
    order_nr: int
    assessed_in: list[Period] = Relationship(link_model=DimensionPeriodLink)

读取CSV得到的字典示例:

dimension_dict = {'assessed_in': "[Period(status=<PeriodStatusEnum.in_assessment: 'in_assessment'>, id=1, name='1. Quartal 2022', period_id='q1_2022'), Period(status=<PeriodStatusEnum.in_assessment: 'in_assessment'>, id=2, name='2. Quartal 2022', period_id='q2_2022')]", 'level_definitions': '[]', 'questions': '[]', 'id': '1', 'dimension_id': 'DIM10', 'name_EN': 'Organizational structure and roles', 'name_DE': 'Organisationsstruktur und Rollen', 'order_nr': '1'}

解决方案

方法1:自定义字符串解析函数,转换为可验证的Period列表

针对导出的特殊字符串格式,编写解析函数提取Period属性,转成字典后创建实例:

import re
from enum import Enum
from sqlmodel import SQLModel, Field, Column, Enum as SQLModelEnum

# 确保PeriodStatusEnum和Period模型已定义

def parse_assessed_in_str(assessed_in_str: str) -> list[Period]:
    cleaned_str = assessed_in_str.strip('"[]')
    if not cleaned_str:
        return []
    
    # 分割多个Period实例字符串
    period_strings = re.split(r'),\s*Period\(', cleaned_str)
    period_strings[0] = period_strings[0].replace('Period(', '')
    period_strings[-1] = period_strings[-1].rstrip(')')
    
    periods = []
    for p_str in period_strings:
        attrs = {}
        # 匹配key=value格式,处理字符串和枚举值
        matches = re.findall(r'(\w+)=([^\s,]+|\'.*?\'|<.*?>)', p_str)
        for key, value in matches:
            if key == 'status':
                # 提取枚举的实际值
                enum_value = re.search(r"'(\w+)'", value).group(1)
                attrs[key] = PeriodStatusEnum(enum_value)
            elif value.startswith("'") and value.endswith("'"):
                attrs[key] = value.strip("'")
            elif value.isdigit():
                attrs[key] = int(value)
            else:
                attrs[key] = value
        periods.append(Period(**attrs))
    return periods

# 使用示例
dimension_dict['assessed_in'] = parse_assessed_in_str(dimension_dict['assessed_in'])
new_record = Dimension.model_validate(dimension_dict)

方法2:修改SQLAdmin导出逻辑,输出结构化JSON

如果能修改SQLAdmin配置,自定义assessed_in字段的导出格式,直接输出JSON数组:

from sqladmin import ModelView
from sqlmodel import Session
import json

class DimensionAdmin(ModelView, model=Dimension):
    def format_assessed_in(self, obj: Dimension, session: Session) -> str:
        # 将Period列表转为JSON字符串
        periods = [{"id": p.id, "period_id": p.period_id, "name": p.name, "status": p.status.value} for p in obj.assessed_in]
        return json.dumps(periods)
    
    # 覆盖导出字段的格式化规则
    column_formatters_export = {
        "assessed_in": format_assessed_in,
    }

导出后读取CSV时,直接解析JSON即可:

import json

dimension_dict['assessed_in'] = [Period(**p) for p in json.loads(dimension_dict['assessed_in'])]
new_record = Dimension.model_validate(dimension_dict)

方法3:自定义Pydantic字段类型自动解析

创建自定义字段类型,让Pydantic自动处理特殊字符串格式:

from pydantic import GetJsonSchemaHandler
from pydantic_core import core_schema
from sqlmodel import SQLModel

# 复用方法1的解析函数
def parse_assessed_in_str(assessed_in_str: str) -> list[Period]:
    # 实现同方法1
    pass

class PeriodListField(list[Period]):
    @classmethod
    def __get_pydantic_core_schema__(cls, source_type, handler: GetJsonSchemaHandler) -> core_schema.CoreSchema:
        def parse_str(value: str) -> list[Period]:
            return parse_assessed_in_str(value)
        
        return core_schema.no_info_wrap_validator_function(
            parse_str,
            core_schema.list_schema(handler(Period)),
            serialization=core_schema.plain_serializer_function_ser(lambda x: str(x)),
        )

# 创建专门用于CSV解析的验证模型
class DimensionCSVCreate(SQLModel):
    dimension_id: str
    name_EN: str
    name_DE: str
    order_nr: int
    assessed_in: PeriodListField
    # 其他字段...

# 使用示例
new_record = DimensionCSVCreate.model_validate(dimension_dict)
db_record = Dimension.from_orm(new_record)

内容的提问来源于stack exchange,提问作者Simon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 02:25:35