Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import requests
- import json
- import os
- from bs4 import BeautifulSoup
- from typing import Dict, Any, Optional, List
- import re
- class KoneBaseClient:
- """Kone API의 기본 클라이언트"""
- def __init__(self, secure_neko_cookie: str):
- """
- Args:
- secure_neko_cookie: __Secure_Neko 쿠키 값
- """
- self.headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0'
- }
- self.cookies = {
- '__Secure_Neko': secure_neko_cookie
- }
- def _request(self, method: str, url: str, **kwargs) -> requests.Response:
- """API 요청을 보내는 기본 메서드
- Args:
- method: HTTP 메서드 (GET, POST, OPTIONS 등)
- url: 요청 URL
- **kwargs: requests 라이브러리에 전달할 추가 인수
- Returns:
- Response 객체
- """
- # 헤더와 쿠키 설정
- if 'headers' not in kwargs:
- kwargs['headers'] = {}
- if 'cookies' not in kwargs:
- kwargs['cookies'] = {}
- # 기본 헤더와 쿠키 병합
- kwargs['headers'].update(self.headers)
- kwargs['cookies'].update(self.cookies)
- # 요청 실행
- response = requests.request(method, url, **kwargs)
- return response
- class KoneAPI(KoneBaseClient):
- """API 서버 (api.kone.gg)에 대한 클라이언트
- 참고: 모든 이미지는 WEBP 포맷입니다.
- """
- def __init__(self, secure_neko_cookie: str, sub_name: str = "trickcalrevive"):
- """
- Args:
- secure_neko_cookie: __Secure_Neko 쿠키 값
- sub_name: 서브 이름 (기본값: trickcalrevive)
- """
- super().__init__(secure_neko_cookie)
- self.base_url = "https://api.kone.gg"
- self.sub_name = sub_name
- # 배너 관련 API
- def get_sub_banner(self) -> requests.Response:
- """서브 배너 가져오기 (WEBP 포맷)"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/banner"
- return self._request("GET", url)
- # 아이콘 관련 API
- def get_sub_icon(self) -> requests.Response:
- """서브 아이콘 가져오기 (WEBP 포맷)"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/icon"
- return self._request("GET", url)
- def get_profile_icon(self, profile_id: str) -> requests.Response:
- """프로필 아이콘 가져오기 (WEBP 포맷)
- Args:
- profile_id: 프로필 ID
- """
- url = f"{self.base_url}/v0/profile/{profile_id}/icon"
- return self._request("GET", url)
- # 운영자 관련 API
- def get_sub_moderators(self) -> requests.Response:
- """서브 운영자 목록 가져오기"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/moderators"
- return self._request("GET", url)
- def options_sub_moderators(self) -> requests.Response:
- """서브 운영자 OPTIONS 요청"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/moderators"
- return self._request("OPTIONS", url)
- # 공지사항 관련 API
- def get_sub_notices(self) -> requests.Response:
- """서브 공지사항 목록 가져오기"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/notices"
- return self._request("GET", url)
- def options_sub_notices(self) -> requests.Response:
- """서브 공지사항 OPTIONS 요청"""
- url = f"{self.base_url}/v0/sub/{self.sub_name}/notices"
- return self._request("OPTIONS", url)
- # 게시글 관련 API
- def post_sub_article(self, title: str, content: str, text_type: str = "text") -> requests.Response:
- """서브 게시글 작성
- Args:
- title: 게시글 제목
- content: 게시글 내용 (HTML 형식)
- text_type: 게시글 카테고리 (선택 사항, 기본값: text)
- """
- url = f"{self.base_url}/v0/article/publish/{self.sub_name}"
- data = {
- "title": title,
- "content": content,
- "type": text_type
- }
- return self._request("POST", url, json=data)
- def edit_sub_article(self, article_id: str,title: str, content: str) -> requests.Response:
- """서브 게시글 수정
- Args:
- article_id: 수정할 게시글 제목
- title: 게시글 제목
- content: 게시글 내용 (HTML 형식)
- """
- url = f"{self.base_url}/v0/article/{article_id}"
- data = {
- "title": title,
- "content": content,
- "category": None
- }
- return self._request("PATCH", url, json=data)
- def options_sub_article(self) -> requests.Response:
- """서브 게시글 OPTIONS 요청"""
- url = f"{self.base_url}/v0/article/publish/{self.sub_name}"
- return self._request("OPTIONS", url)
- # 댓글 관련 API
- def post_comment(self, article_id: str, content: str, comment_type: str = "text") -> requests.Response:
- """댓글 작성
- Args:
- article_id: 게시글 ID
- content: 댓글 내용
- comment_type: 댓글 유형 (기본값: text)
- """
- url = f"{self.base_url}/v0/comment/write"
- data = {
- "article_id": article_id,
- "content": content,
- "type": comment_type
- }
- return self._request("POST", url, json=data)
- def edit_comment(self, comment_id: str, content: str) -> requests.Response:
- """댓글 수정
- Args:
- comment_id: 댓글 ID
- content: 수정할 댓글 내용
- """
- url = f"{self.base_url}/v0/comment/{comment_id}"
- data = {
- "content": content
- }
- return self._request("PATCH", url, json=data)
- def options_comment(self) -> requests.Response:
- """댓글 OPTIONS 요청"""
- url = f"{self.base_url}/v0/comment/write"
- return self._request("OPTIONS", url)
- def get_comment(self, comment_id: str) -> requests.Response:
- """댓글 정보 가져오기
- Args:
- comment_id: 댓글 ID
- """
- url = f"{self.base_url}/v0/comment/{comment_id}"
- return self._request("GET", url)
- def options_comment_by_id(self, comment_id: str) -> requests.Response:
- """특정 댓글 OPTIONS 요청
- Args:
- comment_id: 댓글 ID
- """
- url = f"{self.base_url}/v0/comment/{comment_id}"
- return self._request("OPTIONS", url)
- class KoneWebAPI(KoneBaseClient):
- """웹 서버 (kone.gg)에 대한 클라이언트
- 참고: web 서브 웹 페이지는 모두 HTML 포맷입니다.
- """
- def __init__(self, secure_neko_cookie: str, sub_name: str = "trickcalrevive"):
- """
- Args:
- secure_neko_cookie: __Secure_Neko 쿠키 값
- sub_name: 서브 이름 (기본값: trickcalrevive)
- """
- super().__init__(secure_neko_cookie)
- self.base_url = "https://kone.gg"
- self.sub_name = sub_name
- def get_sub_list(self, rsc: Optional[str] = None) -> requests.Response:
- """서브 목록 가져오기
- Args:
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- """
- url = f"{self.base_url}/sub-list"
- params = {}
- if rsc:
- params["_rsc"] = rsc
- return self._request("GET", url, params=params)
- def get_sub(self, rsc: Optional[str] = None) -> requests.Response:
- """서브 메인 페이지(HTML) 가져오기
- Args:
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- """
- url = f"{self.base_url}/s/{self.sub_name}"
- params = {}
- if rsc:
- params["_rsc"] = rsc
- return self._request("GET", url, params=params)
- def get_sub_posts(self, rsc: Optional[str] = None) -> List[Dict[str, Any]]:
- """서브 메인 페이지에서 게시글 목록을 파싱하여 반환(DICT)
- Args:
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- Returns:
- 파싱된 게시글 정보 목록 (딕셔너리 리스트)
- """
- # 서브 메인 페이지 가져오기
- response = self.get_sub(rsc)
- if response.status_code != 200:
- raise Exception(f"서브 페이지 가져오기 실패: {response.status_code}")
- # HTML 파싱
- soup = BeautifulSoup(response.text, 'html.parser')
- posts = []
- # 게시글 링크 요소 찾기
- article_links = soup.select('a.group.overflow-hidden')
- for link in article_links:
- post = {}
- # 게시글 ID와 URL 추출
- href = link.get('href', '')
- if href:
- post['article_id'] = href.split('/')[-1].split('#')[0]
- post['url'] = href
- # 제목 추출
- title_span = link.select_one('span.overflow-hidden.text-nowrap.text-ellipsis')
- if title_span:
- post['title'] = title_span.text.strip()
- # PC 버전과 모바일 버전 레이아웃 처리
- # PC 버전 파싱 시도
- md_contents = link.select_one('.hidden.md\\:contents')
- if md_contents:
- # 작성자
- author_div = md_contents.select_one('.col-span-2.overflow-hidden')
- if author_div:
- post['author'] = author_div.text.strip()
- # 작성일
- date_div = md_contents.select_one('.col-span-2.text-center.text-zinc-700')
- if date_div:
- post['date'] = date_div.text.strip()
- # 조회수
- views_div = md_contents.select_one('.col-span-1.text-center.text-zinc-700')
- if views_div:
- try:
- post['views'] = int(views_div.text.strip())
- except ValueError:
- post['views'] = 0
- # 추천수
- likes_elements = md_contents.select('.col-span-1.text-center')
- if len(likes_elements) > 1: # 일반적으로 마지막 요소가 추천수
- likes_div = likes_elements[-1]
- try:
- post['likes'] = int(likes_div.text.strip())
- except ValueError:
- post['likes'] = 0
- else:
- # 모바일 버전 파싱
- mobile_contents = link.select_one('.contents.md\\:hidden')
- if mobile_contents:
- # 작성자
- author_div = mobile_contents.select_one('.flex.items-center.justify-center.gap-1.overflow-hidden')
- if author_div:
- post['author'] = author_div.text.strip()
- # 아이콘으로 구분된 값들 (작성일, 조회수, 추천수)
- stat_elements = mobile_contents.select('.flex.gap-1')
- if len(stat_elements) >= 3:
- # 작성일
- post['date'] = stat_elements[0].text.strip()
- # 조회수
- views_text = stat_elements[1].text.strip()
- try:
- post['views'] = int(views_text)
- except ValueError:
- post['views'] = 0
- # 추천수
- likes_text = stat_elements[2].text.strip()
- try:
- post['likes'] = int(likes_text)
- except ValueError:
- post['likes'] = 0
- posts.append(post)
- return posts
- def get_sub_write(self, rsc: Optional[str] = None) -> requests.Response:
- """서브 글쓰기 페이지 가져오기
- Args:
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- """
- url = f"{self.base_url}/s/{self.sub_name}/write"
- params = {}
- if rsc:
- params["_rsc"] = rsc
- return self._request("GET", url, params=params)
- def get_article(self, article_id: str, rsc: Optional[str] = None) -> requests.Response:
- """특정 게시글 페이지(HTML) 가져오기
- Args:
- article_id: 게시글 ID
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- """
- url = f"{self.base_url}/s/{self.sub_name}/{article_id}"
- params = {}
- if rsc:
- params["_rsc"] = rsc
- return self._request("GET", url, params=params)
- def _parse_comments_html(self, html_content):
- """
- 게시글 HTML 내용에서 댓글을 파싱하여 리스트 형태로 반환합니다.
- Args:
- html_content: 게시글 HTML 내용
- Returns:
- list: 댓글 정보 리스트 (댓글 ID, 작성자, 내용, 작성 시간, 추천 수 등)
- """
- import re
- soup = BeautifulSoup(html_content, 'html.parser')
- comments = []
- # 댓글 컨테이너 찾기
- comment_containers = soup.select('div.relative.px-4.py-2')
- for container in comment_containers:
- # 댓글 ID 추출
- comment_id_span = container.select_one('span[id^="c_"]')
- if not comment_id_span:
- continue
- comment_id = comment_id_span.get('id').replace('c_', '')
- # 댓글 작성자 정보
- author_link = container.select_one('a.flex.items-center.gap-1.text-sm.font-medium.hover\\:underline')
- author = author_link.text.strip() if author_link else None
- author_handle = None
- if author_link and author_link.get('href'):
- author_handle = author_link.get('href').replace('/u/', '')
- # 댓글 작성 시간
- time_span = container.select_one('div.flex.gap-1.text-xs.text-zinc-500 span')
- created_at = time_span.text.strip() if time_span else None
- # 편집 시간 (있는 경우)
- edited_at = None
- time_spans = container.select('div.flex.gap-1.text-xs.text-zinc-500 span')
- if len(time_spans) > 1 and '(' in time_spans[1].text:
- edited_at = time_spans[1].text.strip().replace('(', '').replace(')', '')
- # 댓글 내용
- content_p = container.select_one('p.text-sm.max-w-xl.whitespace-pre-wrap')
- content = content_p.text.strip() if content_p else None
- # 삭제된 댓글 확인
- is_deleted = False
- deleted_p = container.select_one('p.text-sm.opacity-50')
- if deleted_p and "삭제된 댓글입니다" in deleted_p.text:
- is_deleted = True
- content = None
- # 추천 수 (투표수)
- votes = 0
- # 댓글 투표수는 HTML에서 명시적으로 표시되지 않아 버튼 요소를 찾아야 함
- # 부모 댓글 ID
- parent_id = None
- # 중첩된 댓글인지 확인
- if 'ml-4' in container.get('class', []):
- # 부모 댓글 찾기
- parent_container = container.find_previous_sibling('div', class_='relative')
- if parent_container:
- parent_id_span = parent_container.select_one('span[id^="c_"]')
- if parent_id_span:
- parent_id = parent_id_span.get('id').replace('c_', '')
- comment = {
- 'id': comment_id,
- 'author': author,
- 'author_handle': author_handle,
- 'content': content,
- 'created_at': created_at,
- 'edited_at': edited_at,
- 'votes': votes,
- 'is_deleted': is_deleted,
- 'parent_id': parent_id
- }
- comments.append(comment)
- return comments
- def _parse_article_html(self, html_content):
- """
- 게시글 HTML 내용을 파싱하여 게시글 정보를 dictionary 형태로 반환합니다.
- Args:
- html_content: 게시글 HTML 내용
- Returns:
- dict: 게시글 정보 (제목, 작성자, 조회수, 좋아요 수)
- """
- import re
- soup = BeautifulSoup(html_content, 'html.parser')
- # 제목 추출
- title_element = soup.select_one('h1.text-2xl.font-bold')
- title = title_element.text.strip() if title_element else None
- # 작성자 추출
- author_element = soup.select_one('a.text-sm.font-medium[href^="/u/"]')
- author = author_element.text.strip() if author_element else None
- # 조회수와 좋아요 수 추출
- stats_element = soup.select_one('div.text-xs.text-zinc-400')
- views = None
- likes = None
- if stats_element:
- stats_text = stats_element.text.strip()
- views_match = re.search(r'조회\s+(\d+)', stats_text)
- likes_match = re.search(r'좋아요\s+(\d+)', stats_text)
- if views_match:
- views = int(views_match.group(1))
- if likes_match:
- likes = int(likes_match.group(1))
- # 게시글 내용 추출 및 HTML 태그 제거
- content_element = soup.select_one('div.prose.prose-invert.max-w-none')
- content = None
- if content_element:
- # 줄바꿈 태그를 먼저 \n으로 대체
- for br in content_element.find_all('br'):
- br.replace_with('\n')
- # h1 태그는 제목으로 강조 처리
- for h1 in content_element.find_all('h1'):
- # 제목 앞뒤로 개행 추가하고 강조
- h1_text = h1.get_text(strip=True)
- h1.replace_with(f'\n\n{h1_text}\n')
- # 텍스트만 추출
- content = content_element.get_text()
- # 여러 개의 연속된 줄바꿈을 최대 2개로 제한
- content = re.sub(r'\n{3,}', '\n\n', content)
- # 앞뒤 공백 제거
- content = content.strip()
- return {
- 'title': title,
- 'author': author,
- 'views': views,
- 'likes': likes,
- 'content': content
- }
- def get_article_info(self, article_id, rsc=None, parse_comments=True):
- """
- 게시글 정보와 댓글을 가져옵니다.
- Args:
- article_id: 게시글 ID
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- parse_comments: 댓글 파싱 여부 (기본값: True)
- Returns:
- dict: 게시글 정보 (제목, 작성자, 조회수, 좋아요 수, 내용, 댓글 목록)
- """
- response = self.get_article(article_id, rsc)
- if response.status_code != 200:
- raise Exception(f"게시글 가져오기 실패: {response.status_code}")
- article_info = self._parse_article_html(response.text)
- # 댓글 파싱 옵션이 활성화된 경우 댓글 정보도 추가
- if parse_comments:
- comments = self._parse_comments_html(response.text)
- article_info['comments'] = comments
- return article_info
- def get_user_profile(self, handle: str, rsc: Optional[str] = None) -> requests.Response:
- """사용자 프로필 페이지 가져오기
- Args:
- handle: 사용자 핸들(아이디)
- rsc: _rsc 쿼리 매개변수 (선택 사항)
- """
- url = f"{self.base_url}/u/{handle}"
- params = {}
- if rsc:
- params["_rsc"] = rsc
- return self._request("GET", url, params=params)
- class KoneClient:
- """Kone API의 통합 클라이언트"""
- def __init__(self, secure_neko_cookie: str, sub_name: str = "kone"):
- """
- Args:
- secure_neko_cookie: __Secure_Neko 쿠키 값
- sub_name: 서브 이름 (기본값: trickcalrevive)
- """
- self.api = KoneAPI(secure_neko_cookie, sub_name)
- self.web = KoneWebAPI(secure_neko_cookie, sub_name)
Advertisement
Add Comment
Please, Sign In to add comment