目前在做一个问答类的服务,通过互联网、线下咨询等多种渠道获取大量非结构化的数据。这些数据在原始状态下往往杂乱无章,难以直接利用。为了更好地分析和应用这些数据,我们需要将其整理成结构化的格式。本文将介绍如何将如图所示的非关系型数据整理成对齐的 JSON 格式。
数据结构描述
如图所示,我们的原始数据包含了一个层级结构(没错就是思维导图)。
从顶层的 "Idea" 开始,逐级细化为 "A" 和 "B",再进一步细化为 "A1"、"A2"、"B1" 和 "B2",最终到达 "A1a"、"A1b" 和 "A2a" 等具体条目。为了便于处理,我们将这种层级结构的数据导出成了一个表格形式,其中每一行代表一个完整的层级路径。
例如:
- "Idea -> A -> A1 -> A1a" 对应表格中的一行,其中每一列分别存储层级中的一个节点。
经过裁剪,真实数据大概是这个样子
| 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|
| 学龄前(0-3周) | 饮食营养 | 0-3岁儿童需要吃些什么样的食物才能获得足够的营养? | ||
| 学龄前(0-3周) | 饮食营养 | 哪些食物是0-3岁儿童的健康成长必不可少的? | ||
| 学龄前(0-3周) | 饮食营养 | 儿童在什么年龄段需要添加辅食? | ||
| 学龄前(0-3周) | 饮食营养 | 添加辅食的时候应该注意些什么? | ||
| 学龄前(0-3周) | 饮食营养 | 有哪些食物会导致婴幼儿过敏? | ||
| 学龄前(0-3周) | 饮食营养 | 食品中的铁对于0-3岁儿童来说为何如此重要? |
-
列1固定是学龄段
-
如果列5不为空一定是问题。
-
学龄段和问题之间的一定是tag,区别只不过是tag有多少的问题(1-3个)
这有何难?按行转json不就行了吗
要将一个 CSV 文件按照行转换为 JSON 格式,可以使用 pandas 库中的 to_json 方法。我很很快快快搞出来这个:
import pandas as pd
# 读取 CSV 文件
csv_ = pd.read_csv('家长问题收集.csv')
# 将 DataFrame 按行转换为 JSON 格式
json_result = csv_.to_json(orient='records', force_ascii=False)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)
在这个代码中:
pd.read_csv('家长问题收集.csv')读取 CSV 文件并将其存储在 DataFrame 中。to_json(orient='records', force_ascii=False)将 DataFrame 按行(记录)转换为 JSON 格式。orient='records'表示每一行是一个 JSON 对象,force_ascii=False确保非 ASCII 字符能够正确显示。- 最后,JSON 数据被打印出来并保存到文件 '家长问题收集.json' 中。
python run.py
# 得到的结果
[ {
"1": "学龄前(0-3周)",
"2": "饮食营养",
"3": "0-3岁儿童需要吃些什么样的食物才能获得足够的营养?",
"4": null,
"5": null
},
。。。。。。
{
"1": "学龄前(0-3周)",
"2": "特殊情况下的护理",
"3": null,
"4": null,
"5": "如何与专业人士合作处理严重的行为问题?"
},
]
从结果看呢,还是不错的。但是当tag数量大于1的时候问题就来了,《如何与专业人士合作处理严重的行为问题?》这里中间丢了两个tag。看了一下原文,大致明白了。
这里的合并过的单元格在处理的时候除了第一行之外都会按照null处理。但是我们期望是234都是标签可以放在一个str[]中
向前填充
对于这种合并单元格的情况,我又想到了先将合并的单元格数据进行填充,然后再进行转换。可以使用 pandas 的 ffill(向前填充)方法来补全合并单元格的数据。以下是一个示例代码,展示了如何完成这个任务:
import pandas as pd
# 读取 CSV 文件
csv_ = pd.read_csv('家长问题收集.csv')
# 使用 ffill 方法填充合并单元格的数据
csv_filled = csv_.fillna(method='ffill')
# 将 DataFrame 按行转换为 JSON 格式
json_result = csv_filled.to_json(orient='records', force_ascii=False)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)
在这个代码中:
pd.read_csv('家长问题收集.csv')读取 CSV 文件并将其存储在 DataFrame 中。fillna(method='ffill')方法用于向前填充合并单元格的数据(即用前一行的值填充当前行的空值)。to_json(orient='records', force_ascii=False)将 DataFrame 按行(记录)转换为 JSON 格式。orient='records'表示每一行是一个 JSON 对象,force_ascii=False确保非 ASCII 字符能够正确显示。- 最后,JSON 数据被打印出来并保存到文件 '家长问题收集.json' 中。
这样我们就可以先填充合并单元格的数据,然后将其逐行转换为 JSON 格式了。
又又不行了
运行之后又出现了一个奇怪的问题
[
。。。。。。
{
"1": "大学",
"2": "社会责任与志愿服务",
"3": "孩子在志愿服务和社会责任上有疑问时,家长应该如何引导?",
"4": "孩子在社会责任和法律义务上有疑问时,家长应该如何解释?",
"5": "财务记录过程中应关注哪些细节和方法?"
},
{
"1": "幼儿园",
"2": "入园准备",
"3": "选择合适的幼儿园",
"4": "如何选择适合宝宝的幼儿园?",
"5": "如何与专业人士合作处理严重的行为问题?"
},
{
"1": "幼儿园",
"2": "入园准备",
"3": "选择合适的幼儿园",
"4": "幼儿园的教育理念和方法应考虑哪些因素?",
"5": "如何与专业人士合作处理严重的行为问题?"
},
]
新出现的问题也很经典,这是把不足3个tag的也补满了。一个问题没有在应该结束的地方结束,这显然也不符合我们的预期。
那我们试试自己遍历每一行
遍历表格
从需求上看列12345中必有三个不是null,或者说是列123不可能是空的,从3开始找到第一个非空值(真正的问题)向上填充,才能得到一个完整的问题。至于的null就可以略过了。
也就是说我们的需求要满足需要确保从列3到列5中找到第一个非空值并将其向上填充,同时避免覆盖其他已经存在的值
import pandas as pd
# 读取 CSV 文件
csv_ = pd.read_csv('家长问题收集.csv')
# 填充列1和列2的空值
csv_filled = csv_.copy()
columns_to_fill = ['1', '2']
for column in columns_to_fill:
csv_filled[column] = csv_filled[column].fillna(method='ffill')
# 向上填充列3到列5的空值
for col in ['3', '4', '5']:
for i in range(len(csv_filled) - 1, -1, -1):
if pd.isna(csv_filled.at[i, col]):
for j in range(i + 1, len(csv_filled)):
if pd.notna(csv_filled.at[j, col]):
csv_filled.at[i, col] = csv_filled.at[j, col]
break
# 将 DataFrame 按行转换为 JSON 格式
json_result = csv_filled.to_json(orient='records', force_ascii=False)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)
在这个代码中:
- 我们读取 CSV 文件并创建一个副本。
- 填充列1和列2的空值。
- 从最后一行开始向上遍历 DataFrame,对于每一列(列3到列5),如果当前单元格为空,则向下查找第一个非空值并将其填充到当前单元格。
- 将填充后的 DataFrame 按行转换为 JSON 格式并保存到文件中。
这样可以确保列3到列5的空值向上填充,但不会覆盖已有的值。
又又又不行了了
python run.py
{
"1": "学龄前(0-3周)",
"2": "特殊情况下的护理",
"3": "选择合适的幼儿园",
"4": "如何选择适合宝宝的幼儿园?",
"5": "如何通过积极的教育方法引导宝宝的行为?"
},
{
"1": "学龄前(0-3周)",
"2": "特殊情况下的护理",
"3": "选择合适的幼儿园",
"4": "如何选择适合宝宝的幼儿园?",
"5": "如何与专业人士合作处理严重的行为问题?"
},
{
"1": "幼儿园",
"2": "入园准备",
"3": "选择合适的幼儿园",
"4": "如何选择适合宝宝的幼儿园?",
"5": "如何确保孩子在幼儿园吃到均衡的营养?"
},
让我大跌眼镜,12列木有问题。3到列5顺序乱麻了,尤其是到了下班点脑袋更是浆糊一片。这思路到底是哪里不行呢?
过程解决不了,那结果呢?
中间无论是用pandas还是自己遍历都搞得乱七八糟,我都要放弃然后自己去手工调整了。突然眼前一亮:我能手动改结果,py就能帮我改啊。
回到向前填充的时候,我直接去掉有问题的数据不就行了吗?这些数据可以自己下一个定义——问题不可能重复也不会出现在tag中。那我们可以在生成后遍历一下把异常值去掉。
至于这个定义对不对,等内容部门的人找来再说吧。反正俺要下班
处理过程中,可以创建一个问题集合。如果集合中问题存在或单元格内容是NaN基本上可以断定这个问题是无效的(可能就是多填充的)那就指针前挪一位把这个列中的值当做异常值pass掉。
上代码
import pandas as pd
import json
# 读取 CSV 文件
csv_ = pd.read_csv('家长问题收集.csv')
# 使用 ffill 方法填充合并单元格的数据
csv_filled = csv_.fillna(method='ffill')
# 创建新的数组格式
new_array = []
existing_titles = set()
for _, row in csv_filled.iterrows():
level = row['1']
# 找到最后一个非空列作为 title,并调整指针位置
title = None
for col in reversed(['5', '4', '3']):
if pd.notna(row[col]) and row[col] not in existing_titles:
title = row[col]
break
# 如果 title 是 NaN 或已经存在于 existing_titles 中,则跳过处理
if title is None or title in existing_titles:
continue
# 将 title 添加到 existing_titles 中
existing_titles.add(title)
# tags 是 level 和 title 之间的列
tags = []
for col in ['2', '3', '4']:
if pd.notna(row[col]) and row[col] != title:
tags.append(row[col])
elif row[col] == title:
break
new_array.append({
'level': level,
'tag': tags,
'title': title
})
# 将新数组转换为 JSON 格式
json_result = json.dumps(new_array, ensure_ascii=False, indent=4)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)
运行一下 python run.py
[
......
{
"level": "学龄前(0-3周)",
"tag": ["特殊情况下的护理", "社交与行为问题", "行为问题"],
"title": "如何通过积极的教育方法引导宝宝的行为?"
},
{
"level": "学龄前(0-3周)",
"tag": ["特殊情况下的护理", "社交与行为问题", "行为问题"],
"title": "如何与专业人士合作处理严重的行为问题?"
}
]
完美解决,溜之~
代码回顾
import pandas as pd
import json
# 读取 CSV 文件
csv_ = pd.read_csv('家长问题收集.csv')
# 使用 ffill 方法填充合并单元格的数据
csv_filled = csv_.fillna(method='ffill')
这两行代码的作用是读取 CSV 文件,并使用 ffill 方法填充合并单元格的数据。ffill 方法会将前一行的值填充到当前行的空值中。
# 创建新的数组格式
new_array = []
existing_titles = set()
这里我们创建了一个空数组 new_array 来存储最终的 JSON 数据,并创建了一个集合 existing_titles 来跟踪已经处理过的标题,确保不会重复处理。
for _, row in csv_filled.iterrows():
level = row['1']
# 找到最后一个非空列作为 title
title = None
for col in ['5', '4', '3']:
if pd.notna(row[col]):
title = row[col]
break
这段代码遍历每一行,并从最后一列开始向前查找第一个非空值,将其作为标题(title)。这样可以确保我们找到的是每行的实际问题,而不是标签。
# 如果 title 是 NaN 或已经存在于 existing_titles 中,则跳过处理
if title is None or title in existing_titles:
continue
# 将 title 添加到 existing_titles 中
existing_titles.add(title)
这里检查 title 是否为空或已经存在于 existing_titles 集合中。如果是,则跳过该行的处理。这可以防止重复处理相同的问题。
# tags 是 level 和 title 之间的列
tags = []
for col in ['2', '3', '4']:
if pd.notna(row[col]) and row[col] != title:
tags.append(row[col])
else:
break
这段代码从第二列到第四列(跳过第一列和最后一列)提取标签。如果列值非空并且不等于 title,则将其添加到 tags 列表中。一旦遇到空值或 title,则停止添加标签。
new_array.append({
'level': level,
'tag': tags,
'title': title
})
将处理后的数据(包括 level、tags 和 title)添加到 new_array 中。
# 将新数组转换为 JSON 格式
json_result = json.dumps(new_array, ensure_ascii=False, indent=4)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)
最后,将 new_array 转换为 JSON 格式并打印以及保存到文件中。
通过上述步骤,我们解决了以下问题:
- 填充合并单元格的数据:使用
ffill方法填充空值。 - 确保每行数据的完整性:从最后一列开始查找第一个非空值作为问题标题。
- 避免重复处理:使用集合跟踪已经处理过的标题。
- 提取标签:从第二列到第四列提取标签,确保标签不包含问题标题。
附件——函数封装
import pandas as pd
import json
def csv_to_json(csv_file, first_key, last_key):
"""
将 CSV 文件转换为 JSON 格式,其中第一个和最后一个列作为固定键,中间列作为标签。
参数:
csv_file (str): CSV 文件路径。
first_key (str): JSON 中第一个列的键名。
last_key (str): JSON 中最后一个列的键名。
返回:
str: 转换后的 JSON 字符串。
"""
# 读取 CSV 文件
csv_ = pd.read_csv(csv_file)
# 使用 ffill 方法填充合并单元格的数据
csv_filled = csv_.fillna(method='ffill')
# 创建新的数组格式
new_array = []
existing_titles = set()
for _, row in csv_filled.iterrows():
level = row['1']
# 找到有效的 title
title = None
for col in reversed(row.index):
if pd.notna(row[col]) and row[col] not in existing_titles:
title = row[col]
break
# 如果 title 仍然是 None,说明没有找到有效的 title,跳过处理
if title is None:
continue
# 将 title 添加到 existing_titles 中
existing_titles.add(title)
# tags 是 level 和 title 之间的列
tags = []
for col in row.index[1:]:
if pd.notna(row[col]) and row[col] != title:
tags.append(row[col])
elif row[col] == title:
break
new_array.append({
first_key: level,
'tag': tags,
last_key: title
})
# 将新数组转换为 JSON 格式
json_result = json.dumps(new_array, ensure_ascii=False, indent=4)
return json_result
# 示例调用
csv_file = '家长问题收集.csv'
first_key = 'level'
last_key = 'title'
json_result = csv_to_json(csv_file, first_key, last_key)
# 打印或保存 JSON 结果
print(json_result)
# 如果需要保存到文件
with open('家长问题收集.json', 'w', encoding='utf-8') as f:
f.write(json_result)