diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc index b656a39..7605a0c 100644 Binary files a/__pycache__/app.cpython-313.pyc and b/__pycache__/app.cpython-313.pyc differ diff --git a/app.py b/app.py index 809b844..7a32b77 100644 --- a/app.py +++ b/app.py @@ -2,6 +2,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError import re +from html import unescape TARGET_URL = "https://m.slrclub.com/l/free" @@ -26,21 +27,93 @@ class ProxyHandler(BaseHTTPRequestHandler): charset = response.headers.get_content_charset(failobj="utf-8") raw_html = response.read().decode(charset, errors="replace") - # Rewrite relative links to use this local proxy. - if "]*>)", - rf"\1", - raw_html, - count=1, - flags=re.IGNORECASE, - ) + # Extract up to 10 post entries (제목 + 링크) and render a simple list. + items = [] + + # First try: find anchors inside list items (common for post lists) + li_blocks = re.findall(r"]*>(.*?)", raw_html, flags=re.DOTALL | re.IGNORECASE) + for block in li_blocks: + m = re.search(r"]+href=[\"']([^\"']+)[\"'][^>]*>(.*?)", block, flags=re.DOTALL | re.IGNORECASE) + if m: + href = m.group(1).strip() + anchor_html = m.group(0) + inner_html = m.group(2) + txt = re.sub(r"<[^>]+>", "", inner_html).strip() + txt = unescape(txt) + + # If inner text is empty, try anchor title or image alt attributes + if not txt: + t = re.search(r'title=["\']([^"\']+)["\']', anchor_html, flags=re.IGNORECASE) + if t: + txt = unescape(t.group(1).strip()) + + if not txt: + img_alt = re.search(r']+alt=["\']([^"\']+)["\']', anchor_html, flags=re.IGNORECASE) + if img_alt: + txt = unescape(img_alt.group(1).strip()) + + # Only append if we have a non-empty title + if txt: + items.append((href, txt)) + + # Fallback: collect anchors from whole page if no list-items found + if not items: + anchors = re.findall(r"]+href=[\"']([^\"']+)[\"'][^>]*>(.*?)", raw_html, flags=re.DOTALL | re.IGNORECASE) + for href, txt_html in anchors: + href = href.strip() + txt = re.sub(r"<[^>]+>", "", txt_html).strip() + txt = unescape(txt) + if txt and not href.lower().startswith("javascript:"): + items.append((href, txt)) + + # Deduplicate and filter out obvious navigation/header links + seen = set() + filtered = [] + for href, txt in items: + key = (href, txt) + if key in seen: + continue + seen.add(key) + lowtxt = (txt or "").strip().lower() + if href in ("#", "/", "", "/l/free"): + continue + if lowtxt in ("홈", "로그인", "검색", "공지", "more", "더보기"): + continue + filtered.append((href, txt)) + + # Skip the top 5 posts and show the next 10 + posts = filtered[5:5+10] + + # Build simple HTML list for posts only + host = self.headers.get("Host", f"localhost:{self.server.server_port}") + out_lines = [ + '', + 'SLRClub - 게시글 목록', + '

게시글 (최대 10개)

', + '
    ' + ] + for href, txt in posts: + # Build absolute URL to original site so clicks open the real page + abs_url = href + if href.startswith("/"): + abs_url = "https://m.slrclub.com" + href + elif href.startswith("//"): + abs_url = "https:" + href + elif not href.startswith("http"): + # relative path without leading slash + abs_url = "https://m.slrclub.com/" + href.lstrip("/") + + # Escape minimal characters in title + safe_txt = txt.replace("<", "<").replace(">", ">") + out_lines.append(f'
  • {safe_txt}
  • ') + + out_lines.extend(["
", ""]) + out = "\n".join(out_lines) self.send_response(200) - self.send_header("Content-Type", f"{content_type}; charset={charset}") + self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() - self.wfile.write(raw_html.encode(charset, errors="replace")) + self.wfile.write(out.encode("utf-8", errors="replace")) except HTTPError as e: self.send_response(e.code)