Customer Rating and review changes
This commit is contained in:
@@ -161,42 +161,67 @@ def _reviews(node: dict) -> List[Dict[str, Any]]:
|
||||
return out
|
||||
|
||||
|
||||
def _rating_only_nodes(nodes: List[dict]) -> Dict[Optional[str], dict]:
|
||||
"""Nameless Product nodes that only carry ratings/reviews, keyed by @id.
|
||||
|
||||
Review widgets (Bazaarvoice on samsung.com/in, for one) publish the
|
||||
product's aggregateRating and reviews as a SEPARATE Product node with no
|
||||
name, linked to the real product by the same @id. Without merging, those
|
||||
real reviews would be skipped for lack of a name.
|
||||
"""
|
||||
out: Dict[Optional[str], dict] = {}
|
||||
for node in nodes:
|
||||
if _text(node.get("name")):
|
||||
continue
|
||||
if not (isinstance(node.get("aggregateRating"), dict) or node.get("review") or node.get("reviews")):
|
||||
continue
|
||||
out.setdefault(_text(node.get("@id")), node)
|
||||
return out
|
||||
|
||||
|
||||
def extract_products(html: str) -> List[Dict[str, Any]]:
|
||||
"""All schema.org Product nodes on the page, flattened to plain fields."""
|
||||
products: List[Dict[str, Any]] = []
|
||||
for block in json_ld_blocks(html):
|
||||
for node in _walk(block):
|
||||
if not (_types(node) & _PRODUCT_TYPES):
|
||||
continue
|
||||
name = _text(node.get("name"))
|
||||
if not name:
|
||||
continue
|
||||
offer = _offer(node.get("offers")) if node.get("offers") else {}
|
||||
if not offer and isinstance(node.get("hasVariant"), list):
|
||||
offer = _offer([v.get("offers") for v in node["hasVariant"] if isinstance(v, dict) and v.get("offers")])
|
||||
props = {}
|
||||
for p in node.get("additionalProperty") or []:
|
||||
if isinstance(p, dict) and p.get("name") and p.get("value") not in (None, ""):
|
||||
props[str(p["name"])] = str(p["value"])
|
||||
rating = node.get("aggregateRating") if isinstance(node.get("aggregateRating"), dict) else {}
|
||||
products.append({
|
||||
"name": name,
|
||||
"brand": _text(node.get("brand")),
|
||||
"sku": _text(node.get("sku")) or _text(node.get("productID")),
|
||||
"mpn": _text(node.get("mpn")),
|
||||
"gtin": next((_text(node.get(k)) for k in ("gtin13", "gtin", "gtin12", "gtin14", "gtin8") if node.get(k)), None),
|
||||
"color": _text(node.get("color")),
|
||||
"images": _images(node.get("image")),
|
||||
"description": _text(node.get("description")),
|
||||
"price": offer.get("price"),
|
||||
"currency": offer.get("currency"),
|
||||
"availability": offer.get("availability"),
|
||||
"in_stock": offer.get("in_stock"),
|
||||
# Sites publish 0 for "no ratings yet"; that is not a rating.
|
||||
"rating": (_dec(rating.get("ratingValue")) or None),
|
||||
"review_count": int(_dec(rating.get("reviewCount") or rating.get("ratingCount")) or 0) or None,
|
||||
"reviews": _reviews(node),
|
||||
"properties": props,
|
||||
"evidence": json.dumps({"name": name, "offers": offer.get("raw")}, default=str)[:1500],
|
||||
})
|
||||
nodes = [n for block in json_ld_blocks(html) for n in _walk(block) if _types(n) & _PRODUCT_TYPES]
|
||||
rating_nodes = _rating_only_nodes(nodes)
|
||||
named = [n for n in nodes if _text(n.get("name"))]
|
||||
for node in named:
|
||||
# Attach a ratings-only node: same @id, or - when it has no @id - the
|
||||
# page's only named product (there is then no doubt which it is about).
|
||||
extra = rating_nodes.get(_text(node.get("@id"))) if node.get("@id") else None
|
||||
if extra is None and len(named) == 1:
|
||||
extra = rating_nodes.get(None)
|
||||
if extra is not None:
|
||||
node = {**node,
|
||||
"aggregateRating": node.get("aggregateRating") or extra.get("aggregateRating"),
|
||||
"review": node.get("review") or extra.get("review") or extra.get("reviews")}
|
||||
name = _text(node.get("name"))
|
||||
offer = _offer(node.get("offers")) if node.get("offers") else {}
|
||||
if not offer and isinstance(node.get("hasVariant"), list):
|
||||
offer = _offer([v.get("offers") for v in node["hasVariant"] if isinstance(v, dict) and v.get("offers")])
|
||||
props = {}
|
||||
for p in node.get("additionalProperty") or []:
|
||||
if isinstance(p, dict) and p.get("name") and p.get("value") not in (None, ""):
|
||||
props[str(p["name"])] = str(p["value"])
|
||||
rating = node.get("aggregateRating") if isinstance(node.get("aggregateRating"), dict) else {}
|
||||
products.append({
|
||||
"name": name,
|
||||
"brand": _text(node.get("brand")),
|
||||
"sku": _text(node.get("sku")) or _text(node.get("productID")),
|
||||
"mpn": _text(node.get("mpn")),
|
||||
"gtin": next((_text(node.get(k)) for k in ("gtin13", "gtin", "gtin12", "gtin14", "gtin8") if node.get(k)), None),
|
||||
"color": _text(node.get("color")),
|
||||
"images": _images(node.get("image")),
|
||||
"description": _text(node.get("description")),
|
||||
"price": offer.get("price"),
|
||||
"currency": offer.get("currency"),
|
||||
"availability": offer.get("availability"),
|
||||
"in_stock": offer.get("in_stock"),
|
||||
# Sites publish 0 for "no ratings yet"; that is not a rating.
|
||||
"rating": (_dec(rating.get("ratingValue")) or None),
|
||||
"review_count": int(_dec(rating.get("reviewCount") or rating.get("ratingCount")) or 0) or None,
|
||||
"reviews": _reviews(node),
|
||||
"properties": props,
|
||||
"evidence": json.dumps({"name": name, "offers": offer.get("raw")}, default=str)[:1500],
|
||||
})
|
||||
return products
|
||||
|
||||
Reference in New Issue
Block a user