|
10 | 10 | """ |
11 | 11 |
|
12 | 12 | import re |
| 13 | +import time |
13 | 14 | import urllib.parse |
14 | 15 | from html.parser import HTMLParser |
15 | 16 |
|
|
19 | 20 |
|
20 | 21 | _HN_BASE = "https://news.ycombinator.com" |
21 | 22 | _MAX_ITEMS = 30 |
| 23 | +_REQUEST_HEADERS = { |
| 24 | + "User-Agent": ( |
| 25 | + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " |
| 26 | + "AppleWebKit/537.36 (KHTML, like Gecko) " |
| 27 | + "Chrome/120.0.0.0 Safari/537.36" |
| 28 | + ), |
| 29 | + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", |
| 30 | + "Accept-Language": "en-US,en;q=0.5", |
| 31 | + "Accept-Encoding": "gzip, deflate, br", |
| 32 | + "DNT": "1", |
| 33 | + "Connection": "keep-alive", |
| 34 | + "Upgrade-Insecure-Requests": "1", |
| 35 | +} |
22 | 36 |
|
23 | 37 |
|
24 | 38 | # --------------------------------------------------------------------------- |
@@ -105,7 +119,8 @@ def handle_starttag(self, tag: str, attrs: list[tuple]) -> None: |
105 | 119 | if tag == "span" and "age" in classes: |
106 | 120 | date = a.get("title", "") |
107 | 121 | if date: |
108 | | - # Ensure a timezone suffix for RFC 3339 compliance |
| 122 | + # HN title format: "YYYY-MM-DDTHH:MM:SS UNIX_EPOCH" — take only the ISO part |
| 123 | + date = date.split()[0] |
109 | 124 | if "T" in date and not ( |
110 | 125 | date.endswith("Z") or "+" in date[-7:] or "-" in date[-7:] |
111 | 126 | ): |
@@ -164,24 +179,26 @@ def close(self) -> None: |
164 | 179 |
|
165 | 180 | def _fetch_page(url: str, warnings: list[str]) -> tuple[str, str | None]: |
166 | 181 | """ |
167 | | - Fetch one page of the HN favorites listing. |
| 182 | + Fetch one page of the HN favorites listing with simple retry on 429. |
168 | 183 | Returns ``(html_text, next_page_url_or_None)``. |
169 | 184 | """ |
170 | | - try: |
171 | | - resp = requests.get(url, timeout=30, headers={ |
172 | | - "User-Agent": ( |
173 | | - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " |
174 | | - "AppleWebKit/537.36 (KHTML, like Gecko) " |
175 | | - "Chrome/120.0.0.0 Safari/537.36" |
176 | | - ) |
177 | | - }) |
178 | | - resp.raise_for_status() |
179 | | - except requests.RequestException as exc: |
180 | | - msg = f"Warning: HN favorites fetch error ({url}): {exc}" |
181 | | - print(f" {msg}") |
182 | | - warnings.append(msg) |
183 | | - return "", None |
184 | | - return resp.text, None |
| 185 | + for attempt in range(3): |
| 186 | + try: |
| 187 | + resp = requests.get(url, timeout=30, headers=_REQUEST_HEADERS) |
| 188 | + if resp.status_code == 429 and attempt < 2: |
| 189 | + time.sleep(3 * (attempt + 1)) # 3s then 6s |
| 190 | + continue |
| 191 | + resp.raise_for_status() |
| 192 | + except requests.RequestException as exc: |
| 193 | + if attempt < 2: |
| 194 | + time.sleep(3 * (attempt + 1)) |
| 195 | + continue |
| 196 | + msg = f"Warning: HN favorites fetch error ({url}): {exc}" |
| 197 | + print(f" {msg}") |
| 198 | + warnings.append(msg) |
| 199 | + return "", None |
| 200 | + return resp.text, None |
| 201 | + return "", None |
185 | 202 |
|
186 | 203 |
|
187 | 204 | def _scrape_favorites(username: str, max_items: int, warnings: list[str]) -> list[dict]: |
|
0 commit comments