使用多个 API 调用填充可变嵌套字典

Populating a variably nested dictionary with multiple API calls

我正在 www.gpcontract.co.uk 使用 public API 来填充代表英国卫生组织层次结构的大型可变嵌套字典。

一些背景资料

最高级别是英国的四个国家(英格兰、苏格兰、威尔士和北爱尔兰),然后是区域组织,一直到各个诊所。每个国家的层次结构深度不同,并且会根据年份而变化。每个组织都有一个名称、组织代码和列出其子组织的字典。

不幸的是,API 不提供完整的嵌套层次结构,但对 http://www.gpcontract.co.uk/api/children/[organisation code]/[year] 的调用将 return 任何其他组织的直接子组织。

为了可以在我的应用程序中轻松导航层次结构,我想生成一个完整层次结构的离线字典(每年),它将使用 pickle 保存并与应用程序捆绑在一起.

得到这个意味着很多 API 调用,我在将 returned JSON 转换成我需要的字典对象时遇到了问题。

这里是层次结构的一小部分的示例(我只显示了一个子组织作为示例)。

JSON 层次结构示例

{
  "eng": {
    "name": "England",
    "orgcode": "eng",
    "children": {}
  },
  "sco": {
    "name": "Scotland",
    "orgcode": "sco",
    "children": {}
  },
  "wal": {
    "name": "Wales",
    "orgcode": "wal",
    "children": {}
  },
  "nir": {
    "name": "Northern Ireland",
    "orgcode": "nir",
    "children": {
      "blcg": {
        "name": "Belfast Local Commissioning Group",
        "orgcode": "blcg",
        "children": {
          "abc": {
            "name": "Random Clinic",
            "orgcode": "abc",
            "children": {}
          }
        }
      }
    }
  }
}

这是我用来进行 API 调用和填充字典的脚本:

我的脚本

import json, pickle, urllib.request, urllib.error, urllib.parse

# Organisation hierarchy may vary between years. Set the year here.
year = 2017

# This function returns a list containing a dictionary for each child organisation with keys for name and orgcode
def get_child_orgs(orgcode, year):
    orgcode = str(orgcode)
    year = str(year)

    # Correct 4-digit year to 2-digit
    if len(year) > 2:
        year = year[2:]

    try:
        child_data = json.loads(urllib.request.urlopen('http://www.gpcontract.co.uk/api/children/' + str(orgcode) + '/' + year).read())

        output = []

        if child_data != []:
            for item in child_data['children']:
                output.append({'name' : item['name'], 'orgcode' : str(item['orgcode']).lower(), 'children' : {}})
        return output
    except urllib.error.HTTPError:
        print('HTTP error!')
    except:
        print('Other error!')

# I start with a template of the top level of the hierarchy and then populate it
hierarchy = {'eng' : {'name' : 'England', 'orgcode' : 'eng', 'children' : {}}, 'nir' : {'name' : 'Northern Ireland', 'orgcode' : 'nir', 'children' : {}}, 'sco' : {'name' : 'Scotland', 'orgcode' : 'sco', 'children' : {}}, 'wal' : {'name' : 'Wales', 'orgcode' : 'wal', 'children' : {}}}

print('Loading data...\n')

# Here I use nested for loops to make API calls and populate the dictionary down the levels of the hierarchy. The bottom level contains the most items.
for country in ('eng', 'nir', 'sco', 'wal'): 

    for item1 in get_child_orgs(country, year):
        hierarchy[country]['children'][item1['orgcode']] = item1

        for item2 in get_child_orgs(item1['orgcode'], year):
            hierarchy[country]['children'][item1['orgcode']]['children'][item2['orgcode']] = item2

            # Only England and Wales hierarchies go deeper than this
            if country in ('eng', 'wal'):

                level3 = get_child_orgs(item2['orgcode'], year)
                # Check not empty array
                if level3 != []:
                    for item3 in level3:
                        hierarchy[country]['children'][item1['orgcode']]['children'][item2['orgcode']]['children'][item3['orgcode']] = item3

                        level4 = get_child_orgs(item3['orgcode'], year)
                        # Check not empty array
                        if level4 != []:
                            for item4 in level4:
                                hierarchy[country]['children'][item1['orgcode']]['children'][item2['orgcode']]['children'][item3['orgcode']]['children'][item4['orgcode']] = item4

# Save the completed hierarchy with pickle
file_name = 'hierarchy_' + str(year) + '.dat'
with open(file_name, 'wb') as out_file:
    pickle.dump(hierarchy, out_file)

print('Success!')

问题

这似乎大部分时间都有效,但感觉很笨拙,有时当嵌套的 for 循环 return 变成 "NoneType is not iterable error" 时会崩溃。我意识到这会进行大量 API 调用并且需要几分钟才能完成 运行,但我看不出解决这个问题的方法,因为我希望完整的层次结构可离线供用户使用以使数据可搜索迅速地。然后,我将以稍微不同的方式使用 API 来获取所选组织的实际医疗保健数据。

我的问题

是否有更简洁、更灵活的方式来执行此操作以适应组织层次结构的可变嵌套?

有没有办法更快地做到这一点?

我对 JSON 比较缺乏经验,所以任何帮助将不胜感激。

我认为这个问题可能更适合在 Code Review Stack Exchange 上提出,但正如您提到的您的代码有时会崩溃和 returns NoneType 错误我会给它带来好处的疑问。

看了你的描述,这就是让我印象深刻的地方

Each organisation has a name, orgcode and dictionary listing its child organisations. [API calls] will return the immediate child organisations of any other.

所以,这对我来说(以及它在您的样本数据中的样子)表明您的所有数据都完全相同;层次结构仅由于数据嵌套而存在,不受任何特定节点格式的强制。

因此,这意味着您应该能够使用一段代码来处理无限(或任意,如果您愿意)深树的嵌套。显然,您为 API 调用自身 (get_child_orgs()) 执行此操作,因此只需复制它来构建树。

def populate_hierarchy(organization,year):
    """ Recursively Populate the Organization Hierarchy

        organization should be a dict with an "orgcode" key with a string value
        and "children" key with a dict value.

        year should be a 2-4 character string representing a year.
    """
    orgcode = organization['orgcode']

    ## get_child_orgs returns a list of organizations
    children = get_child_orgs(orgcode,year)

    ## get_child_orgs returns None on Errors
    if children:
        for child in children:

            ## Add child to the current organization's children, using
            ## orgcode as its key
            organization['children'][child['orgcode']] = child

            ## Recursively populate the child's sub-hierarchy
            populate_hierarchy(child,year)

    ## Technically, the way this is written, returning organization is
    ## pointless because we're modifying organization in place, but I'm
    ## doing it anyway to explicitly denote the end of the function
    return organization

 for country in hierarchy.values():
     populate_hierarchy(country,year)

值得注意的是(因为您在迭代原始代码之前检查了空列表)如果 y 是空列表,for x in y 仍然可以正常运行,因此您不需要检查。

可能会出现 NoneType 错误,因为您捕获了 get_child_orgs 中的错误,然后隐含地 return None。因此,例如 level3 = get_child_orgs[etc...] 结果是 level3 = None;这导致下一行中的 if None != []: 为 True,然后您尝试使用 for item3 in None: 迭代 None,这会引发错误。如上面的代码所述,这就是我检查 children.

真实性的原因

至于是否可以更快地完成,您可以尝试使用 threading/multiprocessing 模块。我只是不知道其中任何一个的利润有多大,原因有以下三个:

  1. 我还没有尝试过 API,所以我不知道你需要多少时间来实现多个 threads/processes
  2. 我经常 quickly/too 看到 API 来自 IP 地址的超时请求(这会使实施毫无意义)
  3. 你说你每年只 运行 这个过程一次,所以从一整年的角度来看,运行时间似乎微不足道(显然,除非当前的 API 调用确实需要几天时间才能完成完成)

最后,我只想问一下 pickle 是否是存储信息的合适方法,或者你是否最好使用 json.dump/load(为了记录,json 模块不关心是否将扩展名更改为 .dat 如果您偏爱该扩展名)。