import json import re from bs4 import BeautifulSoup from typing import Dict, List, Any, Optional from hypy_utils.tqdm_utils import tmap from pathlib import Path import requests r_html_birthday = re.compile(r'data-pid="(\d+)".*?(.+)?.*?_blank">(.+)?') r_html_history = re.compile(r'(\d+)年.*?>(.+)?[\n <]') r_html_people = re.compile(r'(\d+)年 (.+)?((.+)?)') def parse_birthday_page(html_string: str) -> Dict[str, Any]: soup = BeautifulSoup(html_string, 'html.parser') form = soup.find('form') # day note: second h2 h2s = form.find_all('h2') for br in h2s[1].find_all("br"): br.replace_with("\n") day_note = '\n'.join([line.strip() for line in h2s[1].get_text().splitlines()]) # birthdays, first h6 h6s = form.find_all('h6') html = h6s[0].decode_contents() matches = r_html_birthday.findall(html) birthdays = [[int(pid), name.strip(), anime.strip()] for pid, name, anime in matches] # 何の日・行事, second h6, font tags events = [tag.get_text(strip=True) for tag in h6s[1].find_all('font')] # 歴史・出来事, third h6 html = h6s[2].decode_contents() matches = r_html_history.findall(html) history = [[int(year), event.strip()] for year, event in matches] # 有名人及び声優, fourth h6 html = h6s[3].decode_contents() matches = r_html_people.findall(html) people = [[int(year), name.strip(), profession.strip()] for year, name, profession in matches] return { 'note': day_note, 'birthdays': birthdays, 'events': events, 'history': history, 'people': people } def crawl_page(month: int, day: int) -> str: cache = Path(__file__).parent / "cache" / f"{month:02d}-{day:02d}.html" if not cache.exists(): url = f"https://bd.fan-web.jp/sayhappy_sp.cgi?month={month}&day={day}" req = requests.get(url) if req.status_code != 200: raise Exception(f"Failed to fetch page: {req.status_code}") cache.parent.mkdir(parents=True, exist_ok=True) cache.write_text(req.text.encode('latin-1').decode('utf-8'), encoding='utf-8') return cache.read_text(encoding='utf-8') def crawl_page_util(t: tuple[int, int]) -> dict: month, day = t return parse_birthday_page(crawl_page(month, day)) def crawl_all_pages() -> list: cache = (Path(__file__).parent / 'bdfan.json') if cache.exists(): print(f"Loading cached data from {cache}") return json.loads(cache.read_text(encoding='utf-8')) inputs = [(month, day) for month in range(1, 13) for day in range(1, 32)] results = tmap(crawl_page_util, inputs, desc="Crawling BDFan Pages", max_workers=8) results = [{'month': month, 'day': day, **data} for (month, day), data in zip(inputs, results)] cache.write_text(json.dumps(results, ensure_ascii=False), encoding='utf-8') return results def crawl_all_pages_with_hearts() -> list: results = crawl_all_pages() # Add hearts data pids = [pid for page in results for pid, _, _ in page['birthdays']] print(f"Found {len(pids)} unique PIDs for hearts data.") hearts = get_hearts(pids) (Path(__file__).parent / 'bdfan_hearts.json').write_text(json.dumps(hearts), encoding='utf-8') for page in results: for birthday in page['birthdays']: pid = birthday[0] birthday.append(hearts.get(pid, 0)) # Save the results with hearts (Path(__file__).parent / 'bdfan_with_hearts.json').write_text(json.dumps(results, ensure_ascii=False), encoding='utf-8') return results def get_hearts_raw(pids: List[int]) -> Dict[int, int]: url = 'https://bd.fan-web.jp/iine/cn/server.php' headers = { 'accept': 'application/json, text/javascript, */*; q=0.01', 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', } data = {'requ': json.dumps({ "cmd": "init", "cnt_load": 1, "b_css_reset": 1, "pollx": [{"wid": i + 1, "pid": str(pid), "tid": "tpl-sb-heart-s"} for i, pid in enumerate(pids)], })} resp = requests.post(url, headers=headers, data=data) resp.raise_for_status() return {int(pid): details['b1'] for pid, details in resp.json().get('vcntsx', {}).items()} def get_hearts(pids: List[int]) -> Dict[int, int]: pids = list(set(pids)) # Remove duplicates # Paginate requests page_size = 5000 results = {} for i in range(0, len(pids), page_size): batch = pids[i:i + page_size] print(f"Fetching hearts for PIDs {i + 1} to {i + len(batch)}...") batch_results = get_hearts_raw(batch) results.update(batch_results) return results if __name__ == '__main__': crawl_all_pages_with_hearts()