Заполнение словаря с переменной вложенностью несколькими вызовами API
Я использую публичный API на www.gpcontract.co.uk
заполнить большой переменно-вложенный словарь, представляющий иерархию организаций здравоохранения Великобритании.
Некоторая справочная информация
Верхним уровнем иерархии являются четыре страны Великобритании (Англия, Шотландия, Уэльс и Северная Ирландия), а затем региональные организации вплоть до отдельных клиник. Глубина иерархии различна для каждой из стран и может меняться в зависимости от года. У каждой организации есть название, orgcode и словарь, в котором перечислены ее дочерние организации.
К сожалению, полная вложенная иерархия недоступна из API, но вызывает http://www.gpcontract.co.uk/api/children/[organisation code]/[year]
вернет непосредственные детские организации любого другого.
Чтобы в моем приложении можно было легко перемещаться по иерархии, я хочу создать автономный словарь этой полной иерархии (на годовой основе), который будет сохранен с помощью pickle
и в комплекте с приложением.
Получение этого означает много вызовов API, и у меня возникают проблемы с преобразованием возвращаемого 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!')
Эта проблема
Кажется, что это работает большую часть времени, но это кажется хакерским и иногда дает сбой, когда вложенный цикл возвращает "NoneType - это не повторяемая ошибка". Я понимаю, что это делает много вызовов API и занимает несколько минут, но я не могу найти способ обойти это, так как я хочу, чтобы завершенная иерархия была доступна в автономном режиме, чтобы пользователь мог быстро выполнять поиск данных. Затем я буду использовать API немного по-другому, чтобы получить фактические данные о здравоохранении для выбранной организации.
Мой вопрос
Существует ли более понятный и гибкий способ сделать это, который бы соответствовал вложенности переменных в иерархии организации?
Есть ли способ сделать это значительно быстрее?
Я относительно неопытен с JSON, поэтому любая помощь будет оценена.
1 ответ
Я думаю, что этот вопрос лучше подходит для Exchange Code Stack Exchange, но, как вы упоминаете, ваш код иногда дает сбой и возвращается NoneType
ошибки я отдам в пользу сомнения.
Глядя на ваше описание, это то, что выделяется для меня
У каждой организации есть название, orgcode и словарь, в котором перечислены ее дочерние организации. [Вызовы API] вернут непосредственные дочерние организации любого другого.
Итак, что мне подсказывает (и как это выглядит в ваших выборочных данных), что все ваши данные в точности эквивалентны; иерархия существует только из-за вложенности данных и не поддерживается форматом какого-либо конкретного узла.
Следовательно, это означает, что вы должны иметь один фрагмент кода, который обрабатывает вложение бесконечного (или произвольно, если вы предпочитаете) глубокого дерева. Очевидно, вы делаете это для самого вызова 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)
Стоит отметить (так как вы проверяли пустые списки перед повторением в исходном коде), что for x in y
по-прежнему работает правильно, если y
это пустой список, поэтому вам не нужно проверять.
NoneType
Вероятно, ошибка возникает, потому что вы ловите ошибку в get_child_orgs
а затем неявно вернуть None
, Поэтому, например, level3 = get_child_orgs[etc...]
результаты в level3 = None
; это ведет к if None != []:
в следующей строке значение True, а затем вы пытаетесь перебрать None
с for item3 in None:
что поднимет ошибку. Как отмечено в коде выше, именно поэтому я проверяю правдивость children
,
Что касается того, может ли это быть сделано быстрее, вы можете попробовать работать с threading/multiprocessing
модули. Я просто не знаю, насколько это будет выгодно по трем причинам:
- Я не пробовал API, поэтому я не знаю, сколько времени вы выиграете от реализации нескольких потоков / процессов
- Я видел API, которые запрашивают тайм-аут от IP-адресов, когда вы делаете запросы слишком быстро / слишком часто (что делает реализацию бессмысленной)
- Вы говорите, что запускаете этот процесс только один раз в год, поэтому время выполнения в перспективе полного года кажется довольно незначительным (очевидно, если текущие вызовы API не занимают буквальных дней)
Наконец, я бы просто спросил, pickle
является подходящим способом хранения информации, или если вы не будете просто лучше использовать json.dump/load
(для записи, json
модуль не заботится, если вы измените расширение на .dat
если вы неравнодушны к этому имени расширения).