1import asyncio
2import json
3import random
4import re
5import sys
6from datetime import datetime, timezone
7from urllib.parse import quote, urljoin
8
9import httpx
10from bs4 import BeautifulSoup
11
12try:
13 from apify import Actor
14except ImportError:
15 Actor = None
16
17BASE_URL = "https://www.iosys.co.jp"
18
19
20def parse_item(li):
21
22 hidden = {}
23 for inp in li.find_all("input", type="hidden"):
24 name = inp.get("name")
25 value = inp.get("value", "").strip()
26 if name in ("gn", "name", "rank", "spec"):
27 hidden[name] = value
28
29 product_id = hidden.get("gn", "")
30
31
32 title = hidden.get("name", "")
33 if not title:
34 p_name = li.select_one("p.name")
35 if p_name:
36 title = p_name.get_text(" ", strip=True)
37 if not title:
38 a = li.select_one("a[href]")
39 if a:
40 title = a.get_text(" ", strip=True)
41
42
43 brand = ""
44 maker_el = li.select_one("p.maker")
45 if maker_el:
46 maker_text = maker_el.get_text(" ", strip=True)
47 m = re.search(r"[::]\s*(.+)", maker_text)
48 brand = m.group(1).strip() if m else maker_text.replace("メーカー", "").strip(" ::")
49
50
51 price = 0
52 price_el = li.select_one("div.price p")
53 if price_el:
54 price_text = price_el.get_text(" ", strip=True)
55 price_match = re.search(r"([\d,]+)\s*円", price_text)
56 if price_match:
57 price = int(price_match.group(1).replace(",", ""))
58
59
60 stock = 0
61 stock_el = li.select_one("p.stock")
62 if stock_el:
63 stock_text = stock_el.get_text(" ", strip=True)
64 stock_match = re.search(r"(\d+)", stock_text)
65 if stock_match:
66 stock = int(stock_match.group(1))
67
68
69 release = ""
70 release_el = li.select_one("p.release")
71 if release_el:
72 release_text = release_el.get_text(" ", strip=True)
73 release_match = re.search(r"(\d{4}/\d{1,2})", release_text)
74 release = release_match.group(1) if release_match else release_text
75
76
77 condition = ""
78 cond_el = li.select_one("div.rank p.condition")
79 if cond_el:
80 condition = cond_el.get_text(" ", strip=True)
81 if not condition:
82 condition = hidden.get("rank", "")
83
84 rank = hidden.get("rank", "") or condition
85 spec = hidden.get("spec", "")
86
87
88 product_url = ""
89 image_url = ""
90 a_el = li.select_one("a[href]")
91 if a_el:
92 href = a_el.get("href")
93 if href:
94 product_url = urljoin(BASE_URL, href)
95
96 img_el = li.select_one("img")
97 if img_el:
98
99 src = (
100 img_el.get("data-src")
101 or img_el.get("data-original")
102 or img_el.get("src")
103 )
104 if src and "dummy.gif" not in src:
105 image_url = urljoin(BASE_URL, src)
106
107 return {
108 "productId": product_id,
109 "title": title,
110 "brand": brand,
111 "price": price,
112 "rank": rank,
113 "stock": stock,
114 "release": release,
115 "condition": condition,
116 "spec": spec,
117 "imageUrl": image_url,
118 "productUrl": product_url,
119 "scrapedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
120 }
121
122
123async def fetch_page(client, url):
124 for attempt in range(3):
125 try:
126 resp = await client.get(url)
127 if resp.status_code == 200:
128 resp.encoding = "utf-8"
129 return resp.text
130 if resp.status_code in (429, 500, 502, 503, 504):
131 raise httpx.HTTPStatusError(
132 f"HTTP {resp.status_code}",
133 request=resp.request,
134 response=resp,
135 )
136
137 return None
138 except Exception:
139 if attempt == 2:
140 raise
141 wait = 2 ** attempt
142 await asyncio.sleep(wait + random.uniform(0.5, 1.5))
143 return None
144
145
146async def main():
147 if Actor is not None:
148 await Actor.init()
149
150 try:
151 actor_input = await Actor.get_input() if Actor is not None else None
152 if not actor_input:
153 raw_input = sys.stdin.read()
154 actor_input = json.loads(raw_input) if raw_input.strip() else {}
155 keyword = actor_input.get("keyword", "iPhone")
156 max_items = int(actor_input.get("maxItems", 100))
157 if max_items <= 0:
158 max_items = 100
159
160 proxy_url = None
161 proxy_config = actor_input.get("proxyConfiguration")
162 if proxy_config and Actor is not None:
163 proxy = await Actor.create_proxy_configuration(
164 actor_proxy_input=proxy_config
165 )
166 if proxy:
167 proxy_url = await proxy.new_url()
168
169 local_items = []
170
171 async with httpx.AsyncClient(
172 timeout=30.0,
173 proxy=proxy_url,
174 follow_redirects=True,
175 ) as client:
176 collected = 0
177 page = 1
178
179 while collected < max_items:
180 url = f"{BASE_URL}/items?search={quote(keyword)}&page={page}"
181 html = await fetch_page(client, url)
182 if not html:
183 break
184
185 soup = BeautifulSoup(html, "html.parser")
186 items = soup.select("li.item")
187 if not items:
188 break
189
190 for li in items:
191 if collected >= max_items:
192 break
193
194 item = parse_item(li)
195 if item and item.get("productId"):
196 if Actor is not None:
197 await Actor.push_data(item)
198 else:
199 local_items.append(item)
200 collected += 1
201
202
203 if len(items) < 24:
204 break
205
206 page += 1
207 await asyncio.sleep(random.uniform(1, 3))
208
209
210 if Actor is None:
211 print(json.dumps(local_items, ensure_ascii=False, indent=2))
212 finally:
213 if Actor is not None:
214 await Actor.exit()
215
216
217if __name__ == "__main__":
218 asyncio.run(main())