☰
结构化文本转DataFrame:pandas解析CSV、JSON与固定宽度文件实战
2026/10/16 7:15:21 网站建设 项目流程

经常有人拿一些“半死不活”的文本文件给我:有的是老系统导出的定宽文本,一列一列全靠空格硬怼;有的是日志文件每行一段 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 Lines

JSON 严格来说不是“表格”文本,但它的结构是键值对,算半结构化数据。尤其是日志文件、接口返回结果、埋点数据,经常是一行一个 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(..., lines=True)读取,或者先用 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 不止能读 CSV

read_csv实际上是一个非常通用且强大的文本解析器,不只是处理“逗号分隔”的文件。通过sep参数,它可以读取几乎所有字符分隔的文本。我经常用sep='\t'处理 TSV,用sep=';'处理分号文件,甚至用sep=r'[;|]'兼容多种分隔符同时并存的情况。正则表达式在sep中是被支持的,这招在脏数据场景里特别管用。

常用参数里,有几个我觉得价值最高:

  • header=None:文件没有表头时,避免第一行被当成列名。
  • 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,每行一个对象,需要指定lines=True。如果 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], header=None)

这里的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", encoding="utf-8-sig", skiprows=1, sep=",", quotechar='"', skipinitialspace=True )

skiprows=1跳过第一行无用的活动标题;skipinitialspace=True自动去掉字段值左侧多余空格;quotechar='"'让 pandas 正确识别被引号包围的逗号属于同一个字段。这样读进来的 DataFrame 干净很多。如果还有个别行字段数不对,可以配合on_bad_lines='warn'先观察坏行,而不是让程序直接崩掉。

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", encoding="utf-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(name="count")

这里并不直接使用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], header=None, names=["日期", "交易号", "金额", "渠道"] ) df["金额"] = df["金额"].astype(float)

这里的header=None是因为文件没有表头,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 里要用encoding='utf-8-sig',不带 BOM 就用'utf-8'。GBK 文件则用encoding='gbk'。

如果连具体编码都判断不出来,还可以用第三方库chardet自动检测。虽然不是 100% 准确,但至少能给出一个候选方案。遇到不确定的文件,先探测再读取,比反复试错要快很多。

4.2 ParserError:行内分隔符太多

pandas 报ParserError: Error tokenizing data最常见的原因是:某一行的字段数量比其他行多,通常是因为某些字段含有分隔符但没有被引号包围。比如一个用户地址字段“北京市,朝阳区”,在 CSV 里如果没加引号,解析器会以为这一行被切成了更多列,导致列数不一致。

解决办法有几种:

  • 如果分隔符本身就是用户数据的一部分,那必须在导出时用引号包起来,比如"北京市,朝阳区",读取时设置quotechar='"'。
  • 如果无法从源头修正,可以在读取时用sep指定一个数据里绝对不会出现的字符,比如sep='|',然后自己在文本层面先替换。
  • 使用on_bad_lines='warn'让 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"], errors="coerce")会把无法解析的值变成NaN,这比直接astype更安全。

4.4 超大文本读取性能与内存问题

文本文件大起来没有上限,几百万行日志、几个 GB 的 CSV 都很常见。如果用默认参数直接读,很容易把内存吃满。我处理大文件的经验是:能少读就少读,能分块就不全量。

最直接的方法是usecols只保留需要的列。有些文件可能有几十列,但分析只需要 3 列,读入时裁剪能大幅减少内存。第二是分块读取:

chunk_iter = pd.read_csv("huge.csv", chunksize=100000) result = [] for chunk in chunk_iter: result.append(chunk.groupby("category")["amount"].sum()) final = pd.concat(result).groupby(level=0).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), sep=sep, encoding=encoding, skiprows=skiprows, header=header, **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), widths=widths, header=header, **kwargs ) else: raise ValueError(f"Unsupported format_type: {format_type}")

这个模板的核心是先统一输入为字符串,再通过StringIO传入不同读取函数。实际使用时可以根据业务需求继续扩展,比如增加对“伪表格”的正则归一化。模板本身不复杂,但能让你在处理新文本时少敲很多重复代码。

5.4 最后再分享一个小技巧

我在实际项目里踩过最深的一个坑,是处理金额字段时想当然地用了astype(float),结果数据里藏了几个带逗号的千分位字符串,比如"1,234.50",直接转换报错。后来我养成了一个习惯:所有文本解析后的数值列先做一次“清洗再转换”,比如去掉千分位逗号、币种符号、多余空格,然后再统一转类型。这个动作看起来微不足道,但能避免掉很多脏数据带来的连环报错。

还有一个小技巧:如果某列数据中缺失值是用空字符串或特殊字符比如"-"表示,常规read_csv只会把空字符串变成NaN,但"-"会被当成字符串。我一般会在读取后做一次统一替换:df.replace("-", pd.NA),这样所有缺失表示都能归一化,后续统计才靠谱。

解析结构化文本转 pandas 表这件事,说难不难,说简单也不简单。真正难的不是某个函数怎么用,而是面对真实世界里那些不规矩的数据时,能不能先冷静观察、再选择合适的工具、最后留好排查余地。希望这篇实战笔记能让你在处理类似任务时少踩几个坑,多留几分耐心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询