Я работал над тем, где я хочу улучшить свои знания, чтобы сделать безопасный парсинг JSON. Таким образом, я мог бы в значительной степени сделать уродливый обходной путь и превратить его в попытку, но это не так. Я хотел бы знать, где что-то пошло не так и как я могу улучшить код, с которым работал:
import json
from json import JSONDecodeError
import requests
payload = {
"name": None,
"price": None,
"sizes": None
}
headers = {'origin': 'https://www.aplace.com', 'Content-Type': 'application/json'}
with requests.get("http://api.aplace.com/sv/dam/aaltoili-iso-mehu-gren-red-sand", headers=headers) as response:
if not response.ok:
print(f"Requests NOT ok -> {payload}")
try:
# They added extra comma to fail the JSON. Fixed it by removing duplicate commas
doc = json.loads(response.text.replace(",,", ",")).get("productData", {})
except (JSONDecodeError, KeyError, ValueError) as err:
print(f"Not able to parse json -> {err} -> Payload: {payload}")
raise
if doc:
product_name = doc.get('name', {})
product_brand = doc.get('brandName', {})
product_price = doc.get('markets', {}).get('6', {}).get('pricesByPricelist', {}).get('3', {}).get('price', {})
# Added exception incase the [0] is not able to get any value from 'media'
try:
product_image = doc.get('media', {})[0].get('sources', {}).get('full', {}).get('url', {})
except KeyError:
product_image = None
product_size = doc.get('items', {})
try:
if product_name and product_brand:
payload['name'] = f"{product_brand} {product_name}"
if product_price:
payload['price'] = product_price.replace('xa0', '')
if product_image:
payload['image'] = product_image
if product_size:
count = 0
sizes = []
sizesWithStock = []
for value in product_size.values():
if value.get('stockByMarket', {}).get('6', {}):
count += value['stockByMarket']['6']
sizes.append(value['name'])
sizesWithStock.append(f"{value['name']} - ({value['stockByMarket']['6']})")
payload['sizes'] = sizes
payload['sizesWithStock'] = sizesWithStock
payload['stock'] = count
except Exception as err:
print("Create notification if we hit here later on")
raise
print(f"Finished payload -> {payload}")
Моя цель здесь — охватить все значения, и если мы не находим его внутри json, мы используем операторы if, чтобы увидеть, поймали ли мы значение или нет.
Что меня раздражает:
- Длинные вложенные диктовки, но я не уверен, есть ли другой способ, есть ли лучший способ?
- try-except for product_image, потому что это список, и я просто хочу взять первое в списке, можно ли пропустить try-except?
- Если произойдет непредвиденная ошибка, распечатайте исключение, но правильно ли я это делаю?
Надеюсь, отсюда я смогу получить новые знания 🙂
2 ответа
- Вам нужно избавиться от своего антипаттерна «полезной нагрузки». Создаваемый вами словарь не является полезной нагрузкой; это просто мешок с нетипизированными, неорганизованными данными. Если не ООП, то хотя бы поместите это в обычные переменные; но избегайте словаря, если вы на самом деле не делаете что-то, что требует словаря — например, сохранение в файл JSON или отправка во вторичный веб-сервис.
- Напишите универсальную функцию, которая принимает название продукта, а не просто кучу глобального кода.
- Рассмотрите возможность добавления подсказок типа PEP484
- Использовать
response.raise_for_status
вместо проверкиok
себя, особенно потому, что вы заботитесь о правильной обработке исключений - Ваша глобальная замена запятой небезопасна. Я предполагаю, что вместо Они добавили дополнительную запятую, чтобы не выполнить JSON, они просто ошиблись — никогда не приписывайте злобе то, что вы можете приписать невежеству. Попытка заменить это более узко.
- Вы, кажется, хотите использовать
.get('x', {})
как волшебную пулю, не понимая полностью, что она делает, потому что вы использовали ее в некоторых местах, где она вообще не подходит, например.get('price', {})
. Ты сделаешь нет хотите установить цену по умолчанию для словаря. - Подумайте о том, чтобы обернуть исключения в свой собственный тип и перехватить это на верхнем уровне
- По возможности используйте магические константы, например
6
в константе или избегайте этого, выполнив поиск по запросу «то, что вы действительно хотите»; например, строка с конкретной валютой.
Предложенный
import json
from decimal import Decimal
from json import JSONDecodeError
from pprint import pformat
from typing import Dict, Any, Optional
import requests
MARKET_ID = '6'
CURRENCY = 'SEK'
class APlaceError(Exception):
pass
def get_api(product: str) -> Dict[str, Any]:
headers = {
'Origin': 'https://www.aplace.com',
'Accept': 'application/json',
}
try:
with requests.get(f'http://api.aplace.com/sv/dam/{product}', headers=headers) as response:
response.raise_for_status()
payload = response.text.replace(',,"cached"', ',"cached"')
doc = json.loads(payload)
except IOError as err:
raise APlaceError('Unable to get response from APlace server') from err
except JSONDecodeError as err:
raise APlaceError('Unable to parse JSON') from err
if doc.get('content', {}).get('is404'):
raise APlaceError(f'Product "{product}" not found')
return doc
class Product:
def __init__(self, doc: Dict[str, Any]):
try:
data = doc['productData']
except KeyError as e:
raise APlaceError('Empty response') from e
product = data.get('product')
if product is None:
self.id: Optional[int] = None
else:
self.id = int(product)
self.uri: Optional[str] = data.get('uri')
self.name: Optional[str] = data.get('name')
self.brand_name: Optional[str] = data.get('brandName')
self.sku: Optional[str] = data.get('sku')
# etc.
try:
prices = next(
price
for price in data.get('markets', {})
.get(MARKET_ID, {})
.get('pricesByPricelist', {})
.values()
if price.get('price', '').endswith(CURRENCY)
)
price = prices.get('priceAsNumber')
if price is None:
self.price: Optional[float] = None
else:
self.price = Decimal(price)
self.price_with_currency: Optional[str] = prices.get('price')
except StopIteration:
self.price = None
self.price_with_currency = None
# Added exception incase the [0] is not able to get any value from 'media'
try:
self.image_url: Optional[str] = next(
medium.get('sources', {}).get('full', {}).get('url')
for medium in data.get('media', [])
)
except StopIteration:
self.image_url = None
self.stock_by_size = {
item.get('name'): item.get('stockByMarket').get(MARKET_ID, 0)
for item in data.get('items', {}).values()
}
def __str__(self):
return (
f'Brand: {self.brand_name}n'
f'Name: {self.name}n'
f'Price: {self.price_with_currency}n'
f'Image URL: {self.image_url}n'
f'Stock for each size: {pformat(self.stock_by_size)}n'
)
def main():
try:
prod = Product(get_api('aaltoili-iso-mehu-gren-red-sand'))
print(prod)
except APlaceError as e:
print(f'Failed to query product: {e}')
if __name__ == '__main__':
main()
Вам нужны функции.
Функции помогают разделить ваш код и упростить понимание того, как все сочетается друг с другом.У вас должна быть функция для:
Получение страницы.
Разбор JSON с вашим полезным сообщением об ошибке.
Получение полезной нагрузки.
А
main
функция для вызова всех остальных функций.Мы должны вызывать основную функцию только в том случае, если скрипт является точкой входа. Мы можем сделать это с помощью следующего кода:
if __name__ == "__main__": main()
Вы должны добавить функцию пешеходных дорожек.
product_price = doc.get('markets', {}).get('6', {}).get('pricesByPricelist', {}).get('3', {}).get('price', {})
# Added exception incase the [0] is not able to get any value from 'media' try: product_image = doc.get('media', {})[0].get('sources', {}).get('full', {}).get('url', {}) except KeyError: product_image = None
Все было бы намного проще, если бы мы могли просто сделать:
price = get(data, "markets", "6", "pricesByPricelist", "3", "price") image = get(data, "media", 0, "sources", "full", "url")
Построить Гет несложно.
- Берем объект, путь и значение по умолчанию.
- Для каждого сегмента пути мы индексируем объект.
- Если у объекта нет значения (
__getitem__
поднимаетLookupError
) возвращаем значение по умолчанию. - Значение по умолчанию должно вызывать ошибку.
Мы можем гарантировать, что все, что пользователь передаст функции, будет возвращено. Создав новый экземпляр объекта, мы можем затем проверить, есть ли по умолчаниюis
экземпляр. - Для удобства мы можем построить
get
функция, которая по умолчаниюNone
.
_SENTINEL = object() def walk(obj, path, default=_SENTINEL): try: for segment in path: obj = obj[segment] return obj except LookupError: if default is _SENTINEL: raise LookupError(f"couldn't walk path; {path}") from None else: return default def get(obj, *path): return walk(obj, path, default=None)
Я настоятельно рекомендую использовать исключения для потока управления в Python.
Мы можем создать подкласс
BaseException
чтобы сделать исключение, которое выводится на печать пользователю, а затем выходит из системы. Обернувmain()
позвонить вtry
мы можем проверить, возникает ли наша настраиваемая ошибка print, а затем выйти со статусом 1.try: main() except PrettyExitException as e: print(e) raise SystemExit(1) from None
import json
from json import JSONDecodeError
import requests
_SENTINEL = object()
class PrettyExitException(BaseException):
pass
def get_page():
headers = {'origin': 'https://www.aplace.com', 'Content-Type': 'application/json'}
with requests.get("http://api.aplace.com/sv/dam/aaltoili-iso-mehu-gren-red-sand", headers=headers) as response:
if not response.ok:
raise PrettyExitException(f"Requests NOT ok -> {payload}")
return response.text
def parse_json(raw_json):
try:
return json.loads(raw_json)
except (JSONDecodeError, KeyError, ValueError) as err:
raise PrettyExitException(f"Not able to parse JSON -> {err} -> Payload: {payload}") from None
def walk(obj, path, default=_SENTINEL):
try:
for segment in path:
obj = obj[segment]
return obj
except LookupError:
if default is _SENTINEL:
raise LookupError(f"couldn't walk path; {path}") from None
else:
return default
def get(obj, *path):
return walk(obj, path, default=None)
def build_payload(data):
name = get(data, "name")
brand = get(data, "brandName")
price = get(data, "markets", "6", "pricesByPricelist", "3", "price")
image = get(data, "media", 0, "sources", "full", "url")
size = get(data, "items")
count = 0
sizes = []
sizes_with_stock = []
if name is not None and brand is not None:
name = f"{brand} {name}"
else:
name = None
if price is not None:
price = price.replace("xa0", "")
if size is not None:
for value in size.values():
stock = get(value, "stockByMarket", "6")
if stock is not None:
_name = get(value, "name")
count += stock
sizes.append(_name)
sizes_with_stock.append(f"{_name} - ({stock})")
return {
"name": name,
"price": price,
"image": image,
"sizes": sizes,
"sizesWithStock": sizes_with_stock,
"stock": count,
}
def main():
raw_data = get_page()
data = parse_json(raw_data.replace(",,", ","))
payload = build_payload(data.get("productData", {}))
print(f"Finished payload -> {payload}")
if __name__ == "__main__":
try:
main()
except PrettyExitException as e:
print(e)
raise SystemExit(1) from None
Привет! Спасибо за отличный обзор! В настоящее время я тестировал ваш код, и похоже, что все не возвращает None, а также
UnboundLocalError: local variable 'stock' referenced before assignment
при запуске: ‘(— Транспортир
- 1
@ProtractorNewbie Извините, я допустил несколько опечаток! Теперь код должен быть в порядке.
— Пейлонрайз
Без проблем! Есть еще одна небольшая опечатка в
name = get(value, "name")
поскольку вы уже используете его наверху 🙂 Но другой вопрос, было бы хорошо просто использоватьif name and brand
так какname
не имеет значения, то он не будет выполнять оператор if без необходимости добавленияis None
?— Транспортир
- 1
@ProtractorNewbie Черт возьми, ты снова прав! Простите. Да, вы можете использовать
if name and brand
. Я не уверен, как выглядят ваши данные, поэтому решил перестраховаться и предположить, что любая допустимая строка является допустимым именем / брендом. Да, если вы считаете, что API, возвращающий пустое имя, следует рассматривать как не имеющий значения, измените строку на то, что лучше всего подходит для вас.— Пейлонрайз
Я думаю, так как, если мы не сможем получить имя от вашего
get
функция, то в любом случае она вернет None, вот о чем я думал. Так что либо он его найдет, либо вернет None, и мы можем легкоif name...
в этом случае, если я не упускаю что-то среднее между вашей функцией ходьбы 🙂 Но мне очень нравится эта функция, она выглядит намного лучше!— Транспортир
Привет еще раз! Вы были великолепны, и я искренне хотел, чтобы у вас было пожертвование, которое я тоже мог бы сделать! Вы были действительно круты для меня, и я многому у вас научился! Честно!
— Транспортир
Причина, по которой я использую полезную нагрузку, заключается в том, что я хочу вернуть ее как dict, но я также могу ее изменить. План состоял в том, чтобы иметь префикс дикта, в котором я мог бы добавлять что-то в полезную нагрузку, а затем заменять, используя
payload["name"]
как и я, но я мог бы сделать что-то вроде{"name": self.name or _prefix.name_
вроде, как бы, что-то вроде. Я думаю, как вы говорите, я должен убрать полезную нагрузку. Но причина также в том, что когда мы вызываем ошибку или код состояния ответа не в порядке, я хочу вернуть хотя бы некоторую информацию в полезную нагрузку с префиксом, если это имеет смысл?— Транспортир
Рад, что вы оценили обзор 🙂 Насчет вашей «хоть какой-то информации» желание хорошее, но исполнение нет. Вполне возможно заполнить класс только некоторыми членами; следовательно
Optional
обозначение, которое принимаетNone
если значение отсутствует.— Райндериен
Действительно! Я просто подумал, например, о том, что у нас есть страница, которая найдена, но API не обновлен полностью, что означает, например, что значение
data = doc['productData']
еще не помещен в API. Итак, я подумал, что в этом исключении нужно вернуть полезную нагрузку, которую я мог бы, например, разделить и разделить URL-адрес, чтобы получить конец URL-адреса, который обычно является названием продукта. тогда я мог бы знать, по крайней мере, что есть продукт с названием какlittle information to know...
— В этом есть какой-то смысл? : D— Транспортир
Что-то вроде? Если все, что у вас есть, это URI продукта, а все остальное не работает, вы можете просто распечатать URI продукта. Нет необходимости в зачистке и раскалывании.
— Райндериен
Показывать 1 больше комментариев