webmazta commited on
Commit ·
cb096d4
1
Parent(s): eea5613
fix(article): improve image extraction for lazy-loaded and JSON-LD images
Browse files- article_rewriter.py +79 -19
article_rewriter.py
CHANGED
|
@@ -519,33 +519,93 @@ def scrape_article(url: str) -> dict:
|
|
| 519 |
|
| 520 |
# --- Extract images ---
|
| 521 |
images = []
|
| 522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
og_image = soup.find("meta", property="og:image")
|
| 524 |
if og_image and og_image.get("content"):
|
| 525 |
img_url = og_image["content"]
|
| 526 |
if not img_url.startswith("http"):
|
| 527 |
img_url = urljoin(url, img_url)
|
| 528 |
-
|
|
|
|
|
|
|
| 529 |
|
| 530 |
-
# Get images from article body
|
| 531 |
if content_elem:
|
| 532 |
for img in content_elem.find_all("img"):
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
|
| 550 |
# --- Extract embedded media (videos, social posts, audio) ---
|
| 551 |
embeds = []
|
|
|
|
| 519 |
|
| 520 |
# --- Extract images ---
|
| 521 |
images = []
|
| 522 |
+
|
| 523 |
+
# 1. Try JSON-LD schema thumbnailUrl (most reliable for WordPress/Yoast SEO)
|
| 524 |
+
for script in soup.find_all("script", type="application/ld+json"):
|
| 525 |
+
if script.string:
|
| 526 |
+
try:
|
| 527 |
+
data = json.loads(script.string)
|
| 528 |
+
# Handle both single object and @graph array
|
| 529 |
+
if isinstance(data, dict):
|
| 530 |
+
if "thumbnailUrl" in data:
|
| 531 |
+
img_url = data["thumbnailUrl"]
|
| 532 |
+
if not img_url.startswith("http"):
|
| 533 |
+
img_url = urljoin(url, img_url)
|
| 534 |
+
images.append(img_url)
|
| 535 |
+
print(f" 🖼️ Found image in JSON-LD thumbnailUrl: {img_url}")
|
| 536 |
+
# Check image object
|
| 537 |
+
if "image" in data and isinstance(data["image"], dict) and "url" in data["image"]:
|
| 538 |
+
img_url = data["image"]["url"]
|
| 539 |
+
if not img_url.startswith("http"):
|
| 540 |
+
img_url = urljoin(url, img_url)
|
| 541 |
+
if img_url not in images:
|
| 542 |
+
images.append(img_url)
|
| 543 |
+
print(f" 🖼️ Found image in JSON-LD image.url: {img_url}")
|
| 544 |
+
elif isinstance(data, list):
|
| 545 |
+
for item in data:
|
| 546 |
+
if isinstance(item, dict):
|
| 547 |
+
if "thumbnailUrl" in item:
|
| 548 |
+
img_url = item["thumbnailUrl"]
|
| 549 |
+
if not img_url.startswith("http"):
|
| 550 |
+
img_url = urljoin(url, img_url)
|
| 551 |
+
if img_url not in images:
|
| 552 |
+
images.append(img_url)
|
| 553 |
+
print(f" 🖼️ Found image in JSON-LD graph thumbnailUrl")
|
| 554 |
+
if "image" in item and isinstance(item["image"], dict) and "url" in item["image"]:
|
| 555 |
+
img_url = item["image"]["url"]
|
| 556 |
+
if not img_url.startswith("http"):
|
| 557 |
+
img_url = urljoin(url, img_url)
|
| 558 |
+
if img_url not in images:
|
| 559 |
+
images.append(img_url)
|
| 560 |
+
print(f" 🖼️ Found image in JSON-LD graph image.url")
|
| 561 |
+
except (json.JSONDecodeError, KeyError, TypeError):
|
| 562 |
+
pass
|
| 563 |
+
|
| 564 |
+
# 2. Try og:image
|
| 565 |
og_image = soup.find("meta", property="og:image")
|
| 566 |
if og_image and og_image.get("content"):
|
| 567 |
img_url = og_image["content"]
|
| 568 |
if not img_url.startswith("http"):
|
| 569 |
img_url = urljoin(url, img_url)
|
| 570 |
+
if img_url not in images:
|
| 571 |
+
images.append(img_url)
|
| 572 |
+
print(f" 🖼️ Found og:image: {img_url}")
|
| 573 |
|
| 574 |
+
# 3. Get images from article body (handle lazy-loading attributes)
|
| 575 |
if content_elem:
|
| 576 |
for img in content_elem.find_all("img"):
|
| 577 |
+
# Check multiple lazy-load attribute patterns (order matters: real src first, then lazy attrs)
|
| 578 |
+
src = None
|
| 579 |
+
for attr in ["src", "data-src", "data-lazy-src", "data-original", "data-srcset"]:
|
| 580 |
+
val = img.get(attr, "")
|
| 581 |
+
if attr == "data-srcset":
|
| 582 |
+
# Take first URL from srcset (format: "url 1024w, url2 300w")
|
| 583 |
+
if val:
|
| 584 |
+
src = val.split()[0] if val.split() else None
|
| 585 |
+
elif val and not val.startswith("data:image"):
|
| 586 |
+
src = val
|
| 587 |
+
break
|
| 588 |
+
|
| 589 |
+
if not src:
|
| 590 |
+
continue
|
| 591 |
+
|
| 592 |
+
if not src.startswith("http"):
|
| 593 |
+
src = urljoin(url, src)
|
| 594 |
+
|
| 595 |
+
# Skip tiny icons, avatars, tracking pixels
|
| 596 |
+
width = img.get("width", "")
|
| 597 |
+
height = img.get("height", "")
|
| 598 |
+
try:
|
| 599 |
+
if width and int(width) < 300:
|
| 600 |
+
continue
|
| 601 |
+
if height and int(height) < 200:
|
| 602 |
+
continue
|
| 603 |
+
except ValueError:
|
| 604 |
+
pass
|
| 605 |
+
|
| 606 |
+
if src not in images:
|
| 607 |
+
images.append(src)
|
| 608 |
+
print(f" 🖼️ Found image in article body: {src[:80]}...")
|
| 609 |
|
| 610 |
# --- Extract embedded media (videos, social posts, audio) ---
|
| 611 |
embeds = []
|