| from src.datacollection.design_object_model import DesignObject |
| from typing import List |
| import csv |
| from pathlib import Path |
| import re |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| from curl_cffi import requests |
| from curl_cffi.requests import BrowserType |
| import json, ssl, time, math, warnings, os, re |
| from urllib.parse import unquote, urljoin |
| from pyquery import PyQuery as pq |
| import pandas as pd |
| import inspect |
|
|
|
|
| ssl._create_default_https_context = ssl._create_unverified_context |
| warnings.filterwarnings("ignore") |
|
|
|
|
| class DzSpider(object): |
| def __init__(self): |
| self.all_objects = [] |
| self.data = {} |
| self.folder = fr'{os.getcwd()}' |
| self.start_page = 1 |
| self.end_page = 50 |
| self.spider_num = 1 |
| self.page_size = 40 |
| self.has_finish = False |
| self.reset_end_page = True |
| self.session = None |
| self.headers = { |
| 'accept': '*/*', |
| 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', |
| 'cache-control': 'no-cache', |
| 'content-type': 'text/plain', |
| 'pragma': 'no-cache', |
| 'referer': 'https://www.moma.org/collection/', |
| 'sec-ch-ua': '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"', |
| 'sec-ch-ua-mobile': '?0', |
| 'sec-ch-ua-platform': '"Windows"', |
| 'sec-fetch-dest': 'empty', |
| 'sec-fetch-mode': 'cors', |
| 'sec-fetch-site': 'same-origin', |
| 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', |
| 'x-requested-with': 'XMLHttpRequest' |
| } |
|
|
| def init_session(self): |
| """Initialize session with proper configuration""" |
| |
| browser_types = [ |
| 'chrome120', |
| 'chrome110', |
| 'chrome101', |
| 'chrome99', |
| 'safari15_5', |
| 'edge99' |
| ] |
|
|
| for browser_type in browser_types: |
| try: |
| if hasattr(BrowserType, browser_type): |
| self.session = requests.Session( |
| impersonate=getattr(BrowserType, browser_type), |
| timeout=30 |
| ) |
| print(f"Session initialized successfully with {browser_type}") |
| return True |
| except Exception as e: |
| print(f"Failed to initialize with {browser_type}: {e}") |
| continue |
|
|
| |
| try: |
| self.session = requests.Session(timeout=30) |
| print("Session initialized successfully (fallback mode)") |
| return True |
| except Exception as e: |
| print(f"Failed to initialize session completely: {e}") |
| return False |
|
|
| def run_task(self): |
| if not self.init_session(): |
| print("Failed to initialize session, exiting...") |
| return |
|
|
| page_index = self.start_page |
| self.reset_end_page = True |
| self.has_finish = False |
|
|
| print(f"Starting scraper from page {self.start_page} to {self.end_page}") |
|
|
| while page_index <= self.end_page: |
| try: |
| print(f"\n{'=' * 50}") |
| print(f"Processing page {page_index}") |
| print(f"{'=' * 50}") |
|
|
| objects = self.get_one_page(page_index) |
| if not objects: |
| print("No more objects found. Stopping.") |
| break |
|
|
| self.all_objects.extend(objects) |
| print(f"✅ Scraped {len(objects)} items on page {page_index}") |
| page_index += 1 |
| time.sleep(1) |
|
|
| except KeyboardInterrupt: |
| print("\nScraping interrupted by user") |
| break |
| except Exception as e: |
| print(f"Error processing page {page_index}: {e}") |
| break |
|
|
| print(f"\nScraping completed! Total items processed: {self.spider_num - 1}") |
|
|
|
|
| def get_one_page(self, page_index: int) -> List[DesignObject]: |
| req_url = "https://www.moma.org/collection/" |
| params = { |
| "classifications": "37", |
| "date_begin": "1960", |
| "date_end": "2010", |
| "include_uncataloged_works": "false", |
| "on_view": "false", |
| "recent_acquisitions": "false", |
| "with_images": "true", |
| "page": page_index, |
| } |
|
|
| print(f"Fetching page {page_index} …") |
| html = self.session.get(req_url, headers=self.headers, params=params).text |
| doc = pq(html) |
| items = doc("li a") |
|
|
| if items.length == 0: |
| print(f"⚠️ Page {page_index} returned no items.") |
| return [] |
|
|
| objects: list[DesignObject] = [] |
|
|
| for i, a in enumerate(items.items(), 1): |
| href = urljoin(req_url, a.attr("href") or "") |
| makers = a("div>div:eq(1) p:eq(0)").text() |
| title = a("div>div:eq(1) p:eq(1)").text() |
| year = a("div>div:eq(1) p:eq(2)").text() |
|
|
| detail = self.get_detail(href) |
|
|
| obj = DesignObject( |
| name=title, |
| year=year, |
| classification=detail["classification"], |
| dimension=detail["dimension"], |
| makers=[makers], |
| image_urls=detail["image_urls"], |
| country="USA", |
| source="https://www.moma.org/", |
| ) |
| objects.append(obj) |
| |
|
|
| return objects |
|
|
| def get_detail(self, href: str) -> dict: |
| detail = { |
| "image_urls": [], |
| "dimension": "N/A", |
| "classification": "N/A", |
| } |
|
|
| try: |
| resp = self.session.get(href, headers=self.headers, timeout=30) |
| resp.raise_for_status() |
| doc = pq(resp.text) |
|
|
| |
| main = doc("#main") or doc |
| title = main.find("h1").eq(0) |
|
|
| seen = set() |
| for node in main.find("*"): |
| if node is title[0]: |
| break |
| for img in pq(node).find("img").items(): |
| src = img.attr("src") |
| if src and src not in seen: |
| seen.add(src) |
| detail["image_urls"].append(urljoin(href, src)) |
|
|
| |
| dim_dd = ( |
| doc('#caption dt:contains("Dimensions")').next("dd") |
| or doc('dt:contains("Dimensions")').next("dd") |
| ) |
| if dim_dd: |
| detail["dimension"] = dim_dd.text().strip() |
|
|
| |
| dept_dd = ( |
| doc('#caption dt:contains("Department")').next("dd") |
| or doc('dt:contains("Department")').next("dd") |
| ) |
| if dept_dd: |
| detail["classification"] = dept_dd.text().strip() |
|
|
| except Exception as exc: |
| print(f"Error scraping {href}: {exc}") |
|
|
| return detail |
|
|
|
|
| def save_design_objects_to_xlsx(objects: List[DesignObject], delimiter: str = "|||"): |
| |
| data_dir = Path(__file__).resolve().parent.parent.parent / "data" / "metadata" |
| data_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| script_name = Path(inspect.stack()[-1].filename).stem |
| filename = f"{script_name}.xlsx" |
| output_path = data_dir / filename |
|
|
| |
| rows = [] |
| for obj in objects: |
| rows.append({ |
| "name": obj.name, |
| "year": obj.year, |
| "classification": obj.classification, |
| "dimension": obj.dimension, |
| "makers": delimiter.join(obj.makers), |
| "image_urls": delimiter.join(obj.image_urls), |
| "country": obj.country, |
| "price": obj.price or "", |
| "popularity": obj.popularity or "", |
| "source": obj.source or "", |
| }) |
|
|
| pd.DataFrame(rows).to_excel(output_path, index=False) |
| print(f"✅ Saved {len(rows)} records to: {output_path}") |
|
|
|
|
| if __name__ == '__main__': |
| print("Starting MoMA Collection Scraper") |
|
|
| spider = DzSpider() |
| spider.run_task() |
| save_design_objects_to_xlsx(spider.all_objects) |
|
|
| print("\nScraper finished!") |