使用 pandas 解析漂亮的表格数据
Parse prettyprinted tabular data with pandas
复制包含不同分隔符、列名称中的空格等的 table 的最佳方法是什么。函数 pd.read_clipboard()
无法自行管理此任务。
示例 1:
| Age Category | A | B | C | D |
|--------------|---|----|----|---|
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
预期结果:
Age Category A B C D
21-26 2 2 4 1
26-31 7 11 12 5
31-36 3 5 5 2
36-41 2 4 1 7
41-46 0 1 3 2
46-51 0 0 2 3
编辑:
示例 2:
+---+---------+--------+
| id|firstName|lastName|
+---+---------+--------+
| 1| Mark| Brown|
| 2| Tom|Anderson|
| 3| Joshua|Peterson|
+---+---------+--------+
预期结果:
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson
我正在寻找一种可以应用于最常见 table 类型的通用方法。
对于这种类型的 table,您可以简单地使用:
df = pd.read_clipboard(sep='|')
然后需要进行最少的清理:
df = df.drop(0)
df = df.drop(['Unnamed: 0','Unnamed: 6'], axis=1)
至于 "writing such a spreadsheet" 问题...我看不出有什么比简单的演示更方便的了,但是鉴于上面的清理 df
,这里的代码很糟糕:
df1 = df.append(pd.DataFrame({i:['-'*len(i)] for i in df.columns})).sort_index() #adding the separator to column titles
df2 = pd.DataFrame({str(i)+'|':['|']*len(df1) for i in range(len(df1.columns))})
df3 = df1.join(df2)
col_order = [j for i in [[df1.columns[x], df2.columns[x]] for x in range(len(df1.columns))] for j in i]
df3.index = ['|']*len(df3.index)
然后:
df3[col_order]
Age Category 0| A 1| B 2| C 3| D 4|
| -------------- | --- | ---- | ---- | --- |
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
(已编辑)
之所以如此复杂,是因为这些类型的 ASCII 表在设计时并没有真正考虑到数据传输。它们的真正功能是以视觉上令人愉悦的方式描绘数据。
这并不意味着不能用它转入pandas!让我们从 .read_clipboard()
:
开始
df = pd.read_clipboard(sep='|').iloc[1:,1:-1]
我们定义 |
作为分隔符,而不是使用逗号作为(默认)分隔符。
.iloc[1:,1:-1]
去掉了第一行(-----------
)和第一列和最后一列:因为每行开头和结尾的尾随|
pandas
在那里看到 'empty' 列。
现在剩下的就是从列名和值中去除空格:
stripped_columns = []
for column_name in df.columns:
df[column_name] = df[column_name].str.strip()
stripped_columns.append(column_name.strip())
df.columns = stripped_columns
如果您希望 Age Category
成为您的索引:
df.set_index('Age Category', inplace=True)
我要做的最后一步是确保您所有的列现在实际上都包含数字而不是字符串:
df = df.astype('int')
导致:
<class 'pandas.core.frame.DataFrame'>
Index: 6 entries, 21-26 to 46-51
Data columns (total 4 columns):
A 6 non-null int64
B 6 non-null int64
C 6 non-null int64
D 6 non-null int64
dtypes: int64(4)
memory usage: 400.0+ bytes
我不确定你从剪贴板读取它的原因是什么。更优雅的解决方案可能是将其粘贴到 .csv
文件中并使用 .read_csv()
必须提供的更高级的功能。然而,必要的转换将保持不变。
一种选择是硬着头皮对数据进行预处理。这并不是那么糟糕,只有这么多情况 pd.read_csv
可以在其参数中处理,如果您想详尽处理您处理的情况,您最终将转向正则表达式。
为了处理漂亮打印表的大多数常见情况,我只编写了一个循环来过滤行中的 out/replace 个字符,然后使用相对简单的 read_csv
调用读取输出。
import os
def load(filename):
with open(filename) as fin, open('temp.txt', 'w') as fout:
for line in fin:
if not line.strip()[:2] in {'|-', '+-'}: # filter step
fout.write(line.strip().strip('|').replace('|', ',')+'\n')
df = pd.read_csv('temp.txt', sep=r'\s*,\s*', engine='python')
os.unlink('temp.txt') # cleanup
return df
df1 = load('data1.txt')
df2 = load('data2.txt')
df1
Age Category A B C
0 21-26 2 2 4
1 26-31 7 11 12
2 31-36 3 5 5
3 36-41 2 4 1
4 41-46 0 1 3
5 46-51 0 0 2
df2
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson
这是另一个使用 re.sub
and io.StringIO
的潜在解决方案:
from io import StringIO
import re
text1 = """
| Age Category | A | B | C | D |
|--------------|---|----|----|---|
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
"""
text2= """
+---+---------+--------+
| id|firstName|lastName|
+---+---------+--------+
| 1| Mark| Brown|
| 2| Tom|Anderson|
| 3| Joshua|Peterson|
+---+---------+--------+
"""
df1 = pd.read_csv(StringIO(re.sub(r'[|+]|-{2,}', ' ', text1)), sep='\s{2,}', engine='python')
df2 = pd.read_csv(StringIO(re.sub(r'[|+]|-{2,}', ' ', text2)), sep='\s{2,}', engine='python')
[出局]
df1
Age Category A B C D
0 21-26 2 2 4 1
1 26-31 7 11 12 5
2 31-36 3 5 5 2
3 36-41 2 4 1 7
4 41-46 0 1 3 2
5 46-51 0 0 2 3
df2
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson
复制包含不同分隔符、列名称中的空格等的 table 的最佳方法是什么。函数 pd.read_clipboard()
无法自行管理此任务。
示例 1:
| Age Category | A | B | C | D |
|--------------|---|----|----|---|
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
预期结果:
Age Category A B C D
21-26 2 2 4 1
26-31 7 11 12 5
31-36 3 5 5 2
36-41 2 4 1 7
41-46 0 1 3 2
46-51 0 0 2 3
编辑:
示例 2:
+---+---------+--------+
| id|firstName|lastName|
+---+---------+--------+
| 1| Mark| Brown|
| 2| Tom|Anderson|
| 3| Joshua|Peterson|
+---+---------+--------+
预期结果:
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson
我正在寻找一种可以应用于最常见 table 类型的通用方法。
对于这种类型的 table,您可以简单地使用:
df = pd.read_clipboard(sep='|')
然后需要进行最少的清理:
df = df.drop(0)
df = df.drop(['Unnamed: 0','Unnamed: 6'], axis=1)
至于 "writing such a spreadsheet" 问题...我看不出有什么比简单的演示更方便的了,但是鉴于上面的清理 df
,这里的代码很糟糕:
df1 = df.append(pd.DataFrame({i:['-'*len(i)] for i in df.columns})).sort_index() #adding the separator to column titles
df2 = pd.DataFrame({str(i)+'|':['|']*len(df1) for i in range(len(df1.columns))})
df3 = df1.join(df2)
col_order = [j for i in [[df1.columns[x], df2.columns[x]] for x in range(len(df1.columns))] for j in i]
df3.index = ['|']*len(df3.index)
然后:
df3[col_order]
Age Category 0| A 1| B 2| C 3| D 4|
| -------------- | --- | ---- | ---- | --- |
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
(已编辑)
之所以如此复杂,是因为这些类型的 ASCII 表在设计时并没有真正考虑到数据传输。它们的真正功能是以视觉上令人愉悦的方式描绘数据。
这并不意味着不能用它转入pandas!让我们从 .read_clipboard()
:
df = pd.read_clipboard(sep='|').iloc[1:,1:-1]
我们定义 |
作为分隔符,而不是使用逗号作为(默认)分隔符。
.iloc[1:,1:-1]
去掉了第一行(-----------
)和第一列和最后一列:因为每行开头和结尾的尾随|
pandas
在那里看到 'empty' 列。
现在剩下的就是从列名和值中去除空格:
stripped_columns = []
for column_name in df.columns:
df[column_name] = df[column_name].str.strip()
stripped_columns.append(column_name.strip())
df.columns = stripped_columns
如果您希望 Age Category
成为您的索引:
df.set_index('Age Category', inplace=True)
我要做的最后一步是确保您所有的列现在实际上都包含数字而不是字符串:
df = df.astype('int')
导致:
<class 'pandas.core.frame.DataFrame'>
Index: 6 entries, 21-26 to 46-51
Data columns (total 4 columns):
A 6 non-null int64
B 6 non-null int64
C 6 non-null int64
D 6 non-null int64
dtypes: int64(4)
memory usage: 400.0+ bytes
我不确定你从剪贴板读取它的原因是什么。更优雅的解决方案可能是将其粘贴到 .csv
文件中并使用 .read_csv()
必须提供的更高级的功能。然而,必要的转换将保持不变。
一种选择是硬着头皮对数据进行预处理。这并不是那么糟糕,只有这么多情况 pd.read_csv
可以在其参数中处理,如果您想详尽处理您处理的情况,您最终将转向正则表达式。
为了处理漂亮打印表的大多数常见情况,我只编写了一个循环来过滤行中的 out/replace 个字符,然后使用相对简单的 read_csv
调用读取输出。
import os
def load(filename):
with open(filename) as fin, open('temp.txt', 'w') as fout:
for line in fin:
if not line.strip()[:2] in {'|-', '+-'}: # filter step
fout.write(line.strip().strip('|').replace('|', ',')+'\n')
df = pd.read_csv('temp.txt', sep=r'\s*,\s*', engine='python')
os.unlink('temp.txt') # cleanup
return df
df1 = load('data1.txt')
df2 = load('data2.txt')
df1
Age Category A B C
0 21-26 2 2 4
1 26-31 7 11 12
2 31-36 3 5 5
3 36-41 2 4 1
4 41-46 0 1 3
5 46-51 0 0 2
df2
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson
这是另一个使用 re.sub
and io.StringIO
的潜在解决方案:
from io import StringIO
import re
text1 = """
| Age Category | A | B | C | D |
|--------------|---|----|----|---|
| 21-26 | 2 | 2 | 4 | 1 |
| 26-31 | 7 | 11 | 12 | 5 |
| 31-36 | 3 | 5 | 5 | 2 |
| 36-41 | 2 | 4 | 1 | 7 |
| 41-46 | 0 | 1 | 3 | 2 |
| 46-51 | 0 | 0 | 2 | 3 |
"""
text2= """
+---+---------+--------+
| id|firstName|lastName|
+---+---------+--------+
| 1| Mark| Brown|
| 2| Tom|Anderson|
| 3| Joshua|Peterson|
+---+---------+--------+
"""
df1 = pd.read_csv(StringIO(re.sub(r'[|+]|-{2,}', ' ', text1)), sep='\s{2,}', engine='python')
df2 = pd.read_csv(StringIO(re.sub(r'[|+]|-{2,}', ' ', text2)), sep='\s{2,}', engine='python')
[出局]
df1
Age Category A B C D
0 21-26 2 2 4 1
1 26-31 7 11 12 5
2 31-36 3 5 5 2
3 36-41 2 4 1 7
4 41-46 0 1 3 2
5 46-51 0 0 2 3
df2
id firstName lastName
0 1 Mark Brown
1 2 Tom Anderson
2 3 Joshua Peterson