结构化文本转DataFrame:pandas解析CSV、JSON与固定宽度文件实战

📅 发布时间:2026/10/16 7:15:30
结构化文本转DataFrame:pandas解析CSV、JSON与固定宽度文件实战
经常有人拿一些“半死不活”的文本文件给我有的是老系统导出的定宽文本一列一列全靠空格硬怼有的是日志文件每行一段 JSON字段嵌套了三层有的干脆是 CSV 但分隔符一会儿是逗号一会儿是分号。最后诉求都一样帮我弄成表最好能用 pandas 直接处理。做数据分析这几年“把乱七八糟的文本变成规整 DataFrame”这件事我干过太多回踩过的坑基本都能编成一本小册子了。这篇分享就是围绕“解析结构化文本转换成 pandas 数据表”这个主题把我常用的思路、工具和参数选择一次讲清楚适合刚接触 pandas 的读者也适合正在被脏文本折磨的“老打工人”。我会从常见格式分类讲起再到核心参数最后给几个完整可跑的案例和排查套路。1. 先搞清楚“结构化文本”到底有哪些形态解析文本之前最重要的事不是写代码而是先搞清楚你手上的文本属于哪一类。很多人一上来就pd.read_csv()结果报错然后骂 pandas 不靠谱。其实很多问题都是因为文本本身并不是标准 CSV或者根本不是“表格形态”的文本。我习惯先把结构化文本分成四种常见情况再选对应的处理方式。1.1 最常见分隔符文件分隔符文件是大家遇到最多的包括 CSV逗号分隔、TSV制表符分隔以及各种自定义分隔符文件比如竖线|、分号;、^之类。这类文件的基本结构是一行一条记录字段之间用分隔符切分第一行可能是字段名也可能直接就是数据。看起来很简单但实际项目里经常遇到列里面有同样的分隔符、字段名带空格、甚至某一行少了一个字段的情况。比如某次处理一个活动报名表导出时用户填写的备注里带了逗号又没有做引号包围直接read_csv就崩了。处理这类文件的核心思路是先确认分隔符、表头、引号规则再决定read_csv的参数。千万不要盲猜最好用文本编辑器或者命令行工具先看前几行。只要能确认这三件事90% 的分隔符问题都能解决。1.2 半结构化JSON 与 JSON LinesJSON 严格来说不是“表格”文本但它的结构是键值对算半结构化数据。尤其是日志文件、接口返回结果、埋点数据经常是一行一个 JSON 对象也就是 JSON Lines 格式。这种数据转换成 pandas 表有一个特别常见的需求把嵌套在多层字典里的字段拍平变成列。举个例子一个用户行为日志文件每行可能长这样{timestamp: 2024-01-15 10:23:01, user_id: 1001, event: click, context: {page: home, device: mobile}}如果用pd.read_json直接读context这一列会变成 Python 字典对象没法直接分析。需要把里面的page、device提到顶层。这里的关键工具是json_normalize新版 pandas 里是pd.json_normalize它能自动把嵌套字段展开成独立列。JSON Lines 数据用read_json(..., linesTrue)读取或者先用 Python 的json模块逐行处理。1.3 固定宽度文本固定宽度文本是很多人没接触过的“老古董”但确实存在于银行对账单、保险公司保单、旧版 ERP 系统导出的报告中。这类文件没有任何分隔符靠的是列在每一行中的固定位置。比如前 8 位是日期第 9 到第 20 位是流水号第 21 到 30 位是金额。看起来像用空格补位实际上可能是各种字符补齐看着是整齐的矩形。处理固定宽度文本传统方法是按照切割位置手动切片但 pandas 提供了read_fwf函数专门干这个事。它有两种方式一种是给widths也就是每列的宽度列表另一种是给colspecs也就是每列的起止区间。如果文件够规整你甚至可以只指定infer_nrows让 pandas 自动推断列宽。但这类文件坑很多比如中文的宽度计算、全角空格、末尾多余列后面我会具体讲。1.4 网页复制出来的“伪表格”还有一种类型被我归为“其他”你从网页、PDF、钉钉聊天记录、Excel 单元格复制出来的文本。这些文本看起来有对齐但实际是空格和制表符混用甚至不同字段之间的空格数量都不一样。如果把文本粘贴到编辑器里可能会看到类似这样的东西订单号 日期 金额 A001 2024-01-01 12.50 A002 2024-01-02 23.00看着像固定宽度但分隔符不稳定。这类数据不能直接当成 CSV 处理也不能直接用read_fwf因为空格数量不恒定。我的做法是先用正则做归一化把连续空白替换成单一制表符再转成 TSV 交给 pandas。或者利用split()方法配合列表推导构造 DataFrame反而比硬用read_csv稳得多。2. 核心解析工具与参数拆解搞清楚文本类型之后接下来就是选工具。pandas 里用于文本解析的接口不少很多人只知道read_csv但其实read_json、read_fwf、read_table以及StringIO的组合才是把各类文本转成 DataFrame 的完整工具箱。下面我把每个工具的关键参数和适用场景拆开讲。2.1 read_csv 不止能读 CSVread_csv实际上是一个非常通用且强大的文本解析器不只是处理“逗号分隔”的文件。通过sep参数它可以读取几乎所有字符分隔的文本。我经常用sep\t处理 TSV用sep;处理分号文件甚至用sepr[;|]兼容多种分隔符同时并存的情况。正则表达式在sep中是被支持的这招在脏数据场景里特别管用。常用参数里有几个我觉得价值最高headerNone文件没有表头时避免第一行被当成列名。skiprows跳过文件开头若干行比如说明文字、单位行、杂注。comment指定注释字符遇到该字符开头的行直接忽略。encoding解决乱码后面再细说。usecols只读需要的列能明显减少内存占用和解析时间。dtype手动指定列的数据类型避免自动推断出错。parse_dates把指定列解析成日期时间对象。on_bad_lines新版本参数用于处理解析失败的坏行老版本是error_bad_lines。举个例子一个文本文件前几行是# 这是某活动的每日销售数据 # 生成时间2024-01-01 日期,商品,销量 2024-01-01,A,10 2024-01-02,B,20直接read_csv会因为注释行而解析失败正确做法是import pandas as pd df pd.read_csv( sales.txt, comment#, parse_dates[日期] )comment#会跳过所有以#开头的行。parse_dates把日期列转成时间类型后面做时序统计才有意义。2.2 read_json 和 json_normalize 的正确用法read_json可以直接把 JSON 文件读成 DataFrame但它的行为有很多细节。如果文件是标准的 JSON 数组比如[{a: 1}, {a: 2}]它会自动转成 DataFrame。如果是 JSON Lines每行一个对象需要指定linesTrue。如果 JSON 最外层是一个嵌套对象字段值又是数组read_json的推断经常不符合预期。实际做数据清洗时我更推荐先用 Python 的json模块加载文本然后用pd.json_normalize处理嵌套结构。举个日志例子import json import pandas as pd raw_line {timestamp: 2024-01-15 10:23:01, user_id: 1001, event: click, context: {page: home, device: mobile}} obj json.loads(raw_line) df pd.json_normalize(obj)json_normalize会把context.page和context.device自动展开成顶层列名。如果输入的是多个对象组成的列表也能直接拍平成一个 DataFrame。对于 JSON 字段名里带点号的情况可以给json_normalize传sep_比如context.page变成context_page避免和真正的层级冲突。2.3 read_fwf 处理固定宽度文本read_fwf是官方提供的固定宽度文件读取函数。最直接的用法是手动指定每列宽度df pd.read_fwf(fixed.txt, widths[8, 13, 10], headerNone)这里的widths表示第 1 列 8 个字符第 2 列 13 个字符第 3 列 10 个字符。pandas 会按照字符位置切割每一行。另一种方式是colspecs[(0, 8), (8, 21), (21, 31)]起止区间更精确。自动推断列宽虽然省事但前提是文本必须有明显且稳定的空白分隔。如果文件是用空格手动对齐的自动推断有成功的时候但一旦某些字段长度到达阈值列宽就会错乱。我的习惯是能用widths就用手动指定只有确认文件足够规整才用infer_nrows。固定宽度文件常常伴随表头和一些页脚说明读取时还得配合skiprows和skip_footer。2.4 用 StringIO 让文本变成“文件”还有一个经常被忽略的工具是io.StringIO。当数据不是存储在文件里而是以字符串形式存在于内存中时很多 pandas 函数可以直接读取这种“类文件对象”。比如从数据库返回一个多行字符串或者从一个爬虫结果里截取一段表格文本都可以直接用StringIO。from io import StringIO import pandas as pd text 日期,商品,销量 2024-01-01,A,10 2024-01-02,B,20 df pd.read_csv(StringIO(text))这样做的好处是省去了临时写文件的步骤也让解析函数可以接收任意来源的文本。我写的一些通用解析模板核心就是先接收一个字符串再用StringIO套进不同的 pandas 读取函数。这也让“解析结构化文本转换成 pandas 数据表”这件事变得异常干净。3. 实操过程从原始文本到 DataFrame 的完整案例理论再多不如直接看案例。这一节我准备了四个典型场景每个都给出原始文本、解题思路和完整代码这些都是我在项目里处理过的高频情况。案例里的数据都是仿真的但结构完全复刻真实工作中的脏数据。3.1 案例一乱到没脾气的 CSV某运营同学发来一个文件前几行是这样的活动数据-2024年1月 日期, 商品, 销量(件), 备注 2024-01-01, A, 10, 卖得挺好 2024-01-01, B, 20, 库存不足,可能要补货 2024-01-02, C, 15,这个文件有三个问题第一行是标题需要跳过第二行是表头但字段名里有空格第四行备注里含中文逗号但在引号内其实是合法的但第三行最后一个字段为空字符串。如果直接pd.read_csv第一行会被当成数据空字段也会导致缺失值。处理方案import pandas as pd df pd.read_csv( messy.csv, encodingutf-8-sig, skiprows1, sep,, quotechar, skipinitialspaceTrue )skiprows1跳过第一行无用的活动标题skipinitialspaceTrue自动去掉字段值左侧多余空格quotechar让 pandas 正确识别被引号包围的逗号属于同一个字段。这样读进来的 DataFrame 干净很多。如果还有个别行字段数不对可以配合on_bad_lineswarn先观察坏行而不是让程序直接崩掉。3.2 案例二JSON Lines 日志转统计表某服务每天生成一份 JSON Lines 日志每行代表一次用户访问结构包含基本信息和嵌套的上下文。原始日志摘录{timestamp: 2024-01-15 10:23:01, user_id: 1001, event: click, context: {page: home, device: mobile}} {timestamp: 2024-01-15 10:25:40, user_id: 1002, event: view, context: {page: detail, device: desktop}} {timestamp: 2024-01-15 10:28:12, user_id: 1001, event: click, context: {page: cart, device: mobile}}目标是把嵌套字段拍平并统计每天的点击量。完整代码import json import pandas as pd from io import StringIO records [] with open(user_actions.jsonl, r, encodingutf-8) as f: for line in f: line line.strip() if line: records.append(json.loads(line)) df pd.json_normalize(records) df[timestamp] pd.to_datetime(df[timestamp]) df[date] df[timestamp].dt.date daily_stats df.groupby([date, event]).size().reset_index(namecount)这里并不直接使用read_json而是逐行json.loads好处是能处理某一行 JSON 格式错误时单独记录不影响整体数据。json_normalize自动展开context.page和context.device。最后利用dt.date提取日期再分组统计。3.3 案例三固定宽度文本的字段切分某老系统导出的对账文件没有分隔符每一行的宽度固定。我抽样看的文件内容如下20240101A00100000012345 20240101A00200000006789 20240102A00300000023456经确认列定义是前 8 位日期第 9 到第 12 位交易编号第 13 到第 22 位是金额最后 3 位是渠道编码。虽然有规律但如果直接看很难想象列边界。用read_fwf处理import pandas as pd df pd.read_fwf( bills.txt, widths[8, 4, 10, 3], headerNone, names[日期, 交易号, 金额, 渠道] ) df[金额] df[金额].astype(float)这里的headerNone是因为文件没有表头names手动指定列名。读取后金额是整数或字符串必须转成float才能计算。实际场景里金额字段可能带有补零比如00000012345代表 123.45那还需要再除以 100这类业务规则要提前问清楚。3.4 案例四网页复制出来的制表符文本从某个后台页面复制数据到 Excel再复制到文本时经常会得到这种“伪表格”订单号 日期 金额 A001 2024-01-01 12.50 A002 2024-01-02 23.00表面看是多个空格但复制出来可能是制表符和空格混合。直接用read_fwf不一定稳因为列宽不固定。我的处理套路是先读原始字符串用正则把连续空白替换成制表符再用read_csv处理。import re import pandas as pd from io import StringIO raw_text 订单号 日期 金额 A001 2024-01-01 12.50 A002 2024-01-02 23.00 # 将连续两个及以上空白替换成单个制表符 normalized re.sub(r[ \t], \t, raw_text) df pd.read_csv(StringIO(normalized), sep\t)正则替换是这里的关键它把所有类型的空白统一成制表符让 pandas 能一次性切分干净。如果表头和数据行都有干净的空格这样做比手动切片快得多。4. 常见问题与排查技巧实录解析文本经常会碰到各种奇怪报错下面的问题是我在实际操作中反复遇到的每个都附带排查思路和解决办法。看完这套速查至少能少掉一半头发。4.1 编码问题乱码的锅该谁背文本读取最常见的问题是乱码。常见原因是文件不是 UTF-8 编码而是 GBK、GB2312、Latin-1 等。直接用read_csv读 UTF-8 会报UnicodeDecodeError或者读出来全是“锟斤拷”之类的乱码。这事第一反应不应该是怪 pandas而是先确认文件真实编码。我常用的确认方法是用二进制模式打开文件看前几个字节with open(data.csv, rb) as f: raw f.read(10) print(raw)如果开头是b\xff\xfe这是 UTF-16 LE 的 BOM如果开头是b\xef\xbb\xbf这是带 BOM 的 UTF-8如果看起来像普通的 ASCII 文本那大概率是 GBK 或 UTF-8。带 BOM 的 UTF-8 文件在 pandas 里要用encodingutf-8-sig不带 BOM 就用utf-8。GBK 文件则用encodinggbk。如果连具体编码都判断不出来还可以用第三方库chardet自动检测。虽然不是 100% 准确但至少能给出一个候选方案。遇到不确定的文件先探测再读取比反复试错要快很多。4.2 ParserError行内分隔符太多pandas 报ParserError: Error tokenizing data最常见的原因是某一行的字段数量比其他行多通常是因为某些字段含有分隔符但没有被引号包围。比如一个用户地址字段“北京市,朝阳区”在 CSV 里如果没加引号解析器会以为这一行被切成了更多列导致列数不一致。解决办法有几种如果分隔符本身就是用户数据的一部分那必须在导出时用引号包起来比如北京市,朝阳区读取时设置quotechar。如果无法从源头修正可以在读取时用sep指定一个数据里绝对不会出现的字符比如sep|然后自己在文本层面先替换。使用on_bad_lineswarn让 pandas 跳过坏行并输出警告先观察哪些行出了问题再决定是清洗还是修补。新版本 pandas 里on_bad_lines有三个常用值error默认直接报错、warn跳过并警告、skip静默跳过。如果脏行不多用warn模式先把数据读进来分析完再单独处理被跳过的行效率会高很多。4.3 dtype 被自动推断成 object 之后怎么办pandas 读取文本时会自动推断每列数据类型但文本文件往往有前几行是字符串后面是数字的情况导致整列被推断成object。另外有些列本身是混合类型比如金额列里有空值或者用户 ID 里有“--”表示缺失自动推断也会失败。遇到这种问题最好的办法是在读取时手动指定dtype。例如df pd.read_csv(data.csv, dtype{user_id: str, amount: float})指定为字符串的列哪怕里面有001这种数据也不会被转换成整数 1。指定为 float 的列如果有个别无法转换的值pandas 会将其设为NaN但不会影响整列类型。如果读取完成后才发现列类型不对也可以用astype强制转换但要先确认没有脏值。比如pd.to_numeric(df[amount], errorscoerce)会把无法解析的值变成NaN这比直接astype更安全。4.4 超大文本读取性能与内存问题文本文件大起来没有上限几百万行日志、几个 GB 的 CSV 都很常见。如果用默认参数直接读很容易把内存吃满。我处理大文件的经验是能少读就少读能分块就不全量。最直接的方法是usecols只保留需要的列。有些文件可能有几十列但分析只需要 3 列读入时裁剪能大幅减少内存。第二是分块读取chunk_iter pd.read_csv(huge.csv, chunksize100000) result [] for chunk in chunk_iter: result.append(chunk.groupby(category)[amount].sum()) final pd.concat(result).groupby(level0).sum()chunksize让 pandas 每次只读 10 万行处理完再读取下一批内存占用小很多。对于真的大到单机跑不动的文件还可以考虑用 DuckDB 先做聚合再把结果读入 pandas但那就是另一个话题了。至少在文本解析层面chunksize已经是处理大文件最直接的方案。5. 一些实战心得与工具组合拳最后这部分是我平时处理“结构化文本转 pandas 表”时的一些经验总结包括工具链的选择和可复用的函数模板。有了这些思路遇到没见过的文本格式会更有底气。5.1 先“看”再“解析”永远不要盲读我犯过最多错误的根源就是拿到文件不观察直接read_csv结果被各种奇怪格式打脸。现在我给自己定了一个规矩不管文件看起来多正常先看前十行。在 Jupyter Notebook 里可以用!head -n 10 data.csv也可以用 Python 打开文件读取前几行。观察的目的是搞清楚三件事有没有 BOM 或编码问题表头在第几行分隔符到底是什么不同行的分隔符是否一致。尤其是那种第一行是表格标题第二行才是列名的文件如果不先观察很容易把多余的标题当成第一条数据。观察成本很低但能省下后面无数的排查时间。记住解析器的第一参数不是函数名而是你对数据形态的判断。5.2 手头没有 pandas 时先用内置模块做清洗有时候文本实在太乱直接交给 pandas 会让报错信息变得很难懂。这种情况我会先用 Python 内置的csv模块或正则把文本清洗成相对标准的形态再用StringIO喂给 pandas。有人觉得多此一举但实际效果很好。比如某文件每一行开头都有一个行号像这样1, 2024-01-01, A, 10 2, 2024-01-02, B, 20虽然看起来是 CSV但行号列会影响聚合。直接用read_csv读取后还得删除行号列而用 Python 先读每行去掉,前面的序号再交给StringIO整个过程可控可调试。对于更复杂的情况内置csv.reader支持自定义 dialect遇到奇怪的引号或转义符也更好处理。5.3 一个可复用的解析函数模板为了减少重复劳动我整理了一个解析文本到 DataFrame 的函数模板基本覆盖了常见情况。它接收原始文本字符串和解析配置返回 DataFrame。代码结构如下import re import json import pandas as pd from io import StringIO from typing import Optional, Union def text_to_dataframe( text: str, format_type: str csv, sep: Optional[str] None, encoding: str utf-8, skiprows: Optional[int] None, header: Union[int, None] infer, **kwargs ) - pd.DataFrame: if format_type csv: if sep is None: sep , return pd.read_csv( StringIO(text), sepsep, encodingencoding, skiprowsskiprows, headerheader, **kwargs ) elif format_type jsonlines: records [] for line in text.splitlines(): line line.strip() if line: records.append(json.loads(line)) return pd.json_normalize(records) elif format_type fixed: widths kwargs.pop(widths, None) return pd.read_fwf( StringIO(text), widthswidths, headerheader, **kwargs ) else: raise ValueError(fUnsupported format_type: {format_type})这个模板的核心是先统一输入为字符串再通过StringIO传入不同读取函数。实际使用时可以根据业务需求继续扩展比如增加对“伪表格”的正则归一化。模板本身不复杂但能让你在处理新文本时少敲很多重复代码。5.4 最后再分享一个小技巧我在实际项目里踩过最深的一个坑是处理金额字段时想当然地用了astype(float)结果数据里藏了几个带逗号的千分位字符串比如1,234.50直接转换报错。后来我养成了一个习惯所有文本解析后的数值列先做一次“清洗再转换”比如去掉千分位逗号、币种符号、多余空格然后再统一转类型。这个动作看起来微不足道但能避免掉很多脏数据带来的连环报错。还有一个小技巧如果某列数据中缺失值是用空字符串或特殊字符比如-表示常规read_csv只会把空字符串变成NaN但-会被当成字符串。我一般会在读取后做一次统一替换df.replace(-, pd.NA)这样所有缺失表示都能归一化后续统计才靠谱。解析结构化文本转 pandas 表这件事说难不难说简单也不简单。真正难的不是某个函数怎么用而是面对真实世界里那些不规矩的数据时能不能先冷静观察、再选择合适的工具、最后留好排查余地。希望这篇实战笔记能让你在处理类似任务时少踩几个坑多留几分耐心。