1import asyncio
2import json
3import random
4import sys
5from datetime import datetime, timezone
6
7import httpx
8
9API_URL = "https://shop.kitamura.jp/ec/api/cache/s/v1/used_sell_search"
10
11HEADERS = {
12 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124 Safari/537.36",
13 "Referer": "https://shop.kitamura.jp/ec/ct/used/list",
14 "X-Requested-With": "XMLHttpRequest",
15 "Accept": "application/json, text/plain, */*",
16}
17
18try:
19 from apify import Actor
20except Exception:
21 Actor = None
22
23
24def parse_item(item: dict) -> dict:
25 product_id = str(item.get("id", ""))
26 category = item.get("category")
27 if isinstance(category, list):
28 category_str = ", ".join(category)
29 else:
30 category_str = category or ""
31 return {
32 "productId": product_id,
33 "title": item.get("title", ""),
34 "price": item.get("price", 0),
35 "maker": item.get("maker", ""),
36 "shop": item.get("shop", ""),
37 "category": category_str,
38 "description": item.get("description", ""),
39 "imageUrl": item.get("image_link", ""),
40 "productUrl": f"https://shop.kitamura.jp/ec/prd/{product_id}",
41 "janCode": item.get("jan_code", ""),
42 "salesStatus": item.get("sales_status", ""),
43 "scrapedAt": datetime.now(timezone.utc).isoformat(),
44 }
45
46
47async def fetch_json(client: httpx.AsyncClient, params: dict) -> dict:
48 for attempt in range(3):
49 try:
50 resp = await client.get(API_URL, params=params, headers=HEADERS)
51 resp.raise_for_status()
52 return resp.json()
53 except Exception:
54 if attempt == 2:
55 raise
56 await asyncio.sleep((2 ** attempt) + random.random())
57
58
59async def run_scraper(input_data: dict, proxy_url: str | None) -> list:
60 max_items = int(input_data.get("maxItems", 100))
61 max_items = max(1, min(max_items, 1000))
62
63 keyword = input_data.get("keyword", "")
64 sort = input_data.get("sort", "default")
65 category_id = input_data.get("categoryId")
66
67 size = min(80, max_items)
68 offset = 1
69 results = []
70
71 async with httpx.AsyncClient(proxy=proxy_url, timeout=30.0, follow_redirects=True) as client:
72 while len(results) < max_items:
73 params = {
74 "query": keyword,
75 "sort": sort,
76 "size": size,
77 "offset": offset,
78 "func": "srch",
79 "ref_id": "used_sell_search",
80 "extra_fields": "sales_status,is_maintenance,sale_start_at,sale_end_at",
81 "site": "ns",
82 "is_logged_in": "0",
83 "aggs": "default",
84 }
85 if category_id:
86 params["categoryId"] = category_id
87
88 data = await fetch_json(client, params)
89 hits = data.get("search", {}).get("hits") or []
90
91 for hit in hits:
92 results.append(parse_item(hit))
93 if len(results) >= max_items:
94 break
95
96 if not hits or len(hits) < size:
97 break
98
99 offset += size
100 await asyncio.sleep(random.uniform(1, 3))
101
102 return results
103
104
105async def run_actor():
106 async with Actor:
107 actor_input = await Actor.get_input() or {}
108
109 proxy_input = actor_input.get("proxyConfiguration")
110 proxy_url = None
111 if proxy_input:
112 proxy_config = await Actor.create_proxy_configuration(
113 actor_proxy_input=proxy_input
114 )
115 proxy_url = await proxy_config.new_url()
116
117 results = await run_scraper(actor_input, proxy_url)
118
119 for item in results:
120 await Actor.push_data(item)
121
122
123async def run_fallback():
124 raw = sys.stdin.read()
125 actor_input = json.loads(raw) if raw else {}
126 proxy_url = None
127 results = await run_scraper(actor_input, proxy_url)
128 print(json.dumps(results, ensure_ascii=False, indent=2))
129
130
131def main():
132 if Actor is not None:
133 asyncio.run(run_actor())
134 else:
135 asyncio.run(run_fallback())