【问题标题】:How do I convert multiple JSON files with unidentical structure to a single pandas dataframe?如何将具有不同结构的多个 JSON 文件转换为单个 pandas 数据框?
【发布时间】:2022-11-27 04:39:21
【问题描述】:

输入是许多结构不同的 JSON 文件,所需的输出是单个数据帧。

输入说明:

每个JSON 文件可能有 1 个或多个攻击者和 1 个受害者。 attackers 键指向字典列表。每个字典都是 1 个攻击者,具有 character_idcorporation_idalliance_id 等键。victim 键指向具有相似键的字典。这里需要注意的重要一点是,相同 JSON 之间的键可能不同。例如,一个 JSON 文件可能有 attackers 键,如下所示:

{
    "attackers": [
        {
            "alliance_id": 99005678,
            "character_id": 94336577,
            "corporation_id": 98224639,
            "damage_done": 3141,
            "faction_id": 500003,
            "final_blow": true,
            "security_status": -9.4,
            "ship_type_id": 73796,
            "weapon_type_id": 3178
        },
        {
            "damage_done": 1614,
            "faction_id": 500003,
            "final_blow": false,
            "security_status": 0,
            "ship_type_id": 32963
        }
    ],
...

这里的 JSON 文件有 2 个攻击者。但只有第一个攻击者拥有上述密钥。同样,victim 可能如下所示:

...
"victim": {
        "character_id": 2119076173,
        "corporation_id": 98725195,
        "damage_taken": 4755,
        "faction_id": 500002,
        "items": [...
...

输出说明:

作为输出,我想从存储在同一目录中的许多(大约 400,000 个)此类 JSON 文件创建一个数据框。结果数据帧的每一行都应该有 1 名攻击者和 1 名受害者。具有多个攻击者的 JSON 应拆分为相同数量的行,其中攻击者的属性不同,但受害者属性相同。例如,如果有 3 名攻击者,则为 3 行;如果某个攻击者没有键值对,则为 NaN 值。因此,上述示例数据框中第二个攻击者的 character_id 应该是 NaN

当前方法:

为此,我首先创建一个空列表。然后遍历所有文件,打开它们,将它们加载为 JSON 对象,转换为数据帧,然后将数据帧附加到列表中。请注意pd.DataFrame([json.load(fi)])pd.json_normalize(json.load(fi)) 具有相同的输出。

mainframe = []

for file in tqdm(os.listdir("D:/Master/killmails_jul"), ncols=100, ascii='          >'):
    with open("%s/%s" % ("D:/Master/killmails_jul", file),'r') as fi:
        mainframe.append(pd.DataFrame([json.load(fi)]))

在这个循环之后,我留下了一个数据帧列表,我使用 pd.concat() 将它们连接起来。

mainframe = pd.concat(mainframe)

到目前为止,无论attackers 的数量如何,数据框每个 JSON 只有 1 行。为了解决这个问题,我在下一步中使用pd.explode()

mainframe = mainframe.explode('attackers')
mainframe.reset_index(drop=True, inplace=True)

现在每个攻击者都有单独的行,但是 attackersvictim 键仍然隐藏在各自的列中。为了解决这个问题,我通过 pd.apply(pd.Series) 水平“分解”了两列,并应用前缀以便于识别,如下所示:

intframe = mainframe["attackers"].apply(pd.Series).add_prefix("attackers_").join(mainframe["victim"].apply(pd.Series).add_prefix("victim_"))

在下一步中,我将这个中间框架与大型机结合起来以保留 killmail_idkillmail_hash 列。然后删除 attackersvictim 列,因为我现在已经展开了它们。

mainframe = intframe.join(mainframe)
mainframe.fillna(0, inplace=True)
mainframe.drop(['attackers','victim'], axis=1, inplace=True)

这为我提供了包含以下 24 列的所需输出:

['attackers_character_id', 'attackers_corporation_id', 'attackers_damage_done', 'attackers_final_blow', 'attackers_security_status', 'attackers_ship_type_id', 'attackers_weapon_type_id', 'attackers_faction_id', 'attackers_alliance_id', 'victim_character_id', 'victim_corporation_agen_id', victim_items', 'victim_position', 'victim_ship_type_id', 'victim_alliance_id', 'victim_faction_id', 'killmail_id', 'killmail_time', 'solar_system_id', 'killmail_hash', 'http_last_modified', 'war_id', 'moon_id']

问题:

有没有比我现在做的更好的方法?我尝试使用发电机,但无法让它们工作。我得到一个AttributeError: 'str' object has no attribute 'read'

all_files_paths = glob(os.path.join('D:\\Master\\kmrest', '*.json'))

def gen_df(files):
    for file in files:
        with open(file, 'r'):
            data = json.load(file)
        data = pd.DataFrame([data])
        yield data

mainframe = pd.concat(gen_df(all_files_paths), ignore_index=True)

pd.concat() 函数与生成器一起使用会导致二次复制吗? 另外,我担心打开和关闭许多文件会减慢计算速度。也许最好先从所有 JSON 创建一个 JSONL 文件,然后为每一行创建一个数据框。

如果你想获得这些文件,我正在尝试与你合作,可以单击here。让我知道是否需要更多信息。

【问题讨论】:

  • items 每个受害者的预期待遇是什么?

标签: python json pandas dataframe


【解决方案1】:

您可以使用 pd.json_normalize() 来帮助完成繁重的工作:

首先,加载您的数据:

import json
import requests
import tarfile
from tqdm.notebook import tqdm

url = 'https://data.everef.net/killmails/2022/killmails-2022-11-22.tar.bz2'
with requests.get(url, stream=True) as r:
    fobj = io.BytesIO(r.raw.read())
    with tarfile.open(fileobj=fobj, mode='r:bz2') as tar:
        json_files = [it for it in tar if it.name.endswith('.json')]
        data = [json.load(tar.extractfile(it)) for it in tqdm(json_files)]

对你的文件做同样的事情:

import json
from glob import glob

def json_load(filename):
    with open(filename) as f:
        return json.load(f)

topdir = '...'  # the dir containing all your json files
data = [json_load(fn) for fn in tqdm(glob(f'{topdir}/*.json'))]

一旦你在data 中有了一个字典列表:

others = ['killmail_id', 'killmail_hash']
a = pd.json_normalize(data, 'attackers', others, record_prefix='attackers.')
v = pd.json_normalize(data).drop('attackers', axis=1)
df = a.merge(v, on=others)

一些快速检查:

>>> df.shape
(44903, 26)

# check:
>>> sum([len(d['attackers']) for d in data])
44903

>>> df.columns
Index(['attackers.alliance_id', 'attackers.character_id',
       'attackers.corporation_id', 'attackers.damage_done',
       'attackers.final_blow', 'attackers.security_status',
       'attackers.ship_type_id', 'attackers.weapon_type_id',
       'attackers.faction_id', 'killmail_id', 'killmail_hash', 'killmail_time',
       'solar_system_id', 'http_last_modified', 'victim.alliance_id',
       'victim.character_id', 'victim.corporation_id', 'victim.damage_taken',
       'victim.items', 'victim.position.x', 'victim.position.y',
       'victim.position.z', 'victim.ship_type_id', 'victim.faction_id',
       'war_id', 'moon_id'],
      dtype='object')

>>> df.iloc[:5, :5]
   attackers.alliance_id  attackers.character_id  attackers.corporation_id  attackers.damage_done  attackers.final_blow
0  99007887.0             1.450608e+09            2.932806e+08              1426                   False               
1  99010931.0             1.628193e+09            5.668252e+08              1053                   False               
2  99007887.0             1.841341e+09            1.552312e+09              1048                   False               
3  99007887.0             2.118406e+09            9.872458e+07               662                   False               
4  99005839.0             9.573650e+07            9.947834e+08               630                   False               

>>> df.iloc[-5:, -5:]
       victim.position.z  victim.ship_type_id  victim.faction_id  war_id  moon_id
44898  1.558110e+11       670                 NaN                NaN     NaN     
44899 -7.678686e+10       670                 NaN                NaN     NaN     
44900 -7.678686e+10       670                 NaN                NaN     NaN     
44901 -7.678686e+10       670                 NaN                NaN     NaN     
44902 -7.678686e+10       670                 NaN                NaN     NaN     

另请注意,根据需要,攻击者丢失的密钥是 NaN

>>> df.iloc[15:20, :2]
    attackers.alliance_id  attackers.character_id
15  99007887.0             2.117497e+09          
16  99011893.0             1.593514e+09          
17         NaN             9.175132e+07          
18         NaN             2.119191e+09          
19  99011258.0             1.258332e+09          

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-21
    • 2015-05-16
    • 2021-08-26
    • 2020-02-29
    • 1970-01-01
    • 2019-08-02
    相关资源
    最近更新 更多