""" 台湾彩券资讯采集模块 - 列表接口: https://api.taiwanlottery.com/TLCAPIWeB/News/List - 详情页面: https://www.taiwanlottery.com/news/news/{newsId} 写入分类: - la_article_cate.id = 21 (彩票资讯) """ import html import json import re import time from datetime import datetime from urllib.parse import urljoin import pymysql import requests from article_publish_helper import has_publishable_article_content, resolve_crawled_article_is_show ARTICLE_CID = 21 LIST_API = "https://api.taiwanlottery.com/TLCAPIWeB/News/List?keyword&from=2025-06&to=2026-06&type=2&webtag=1&pageSize=20&pageNo={page}" DETAIL_URL = "https://www.taiwanlottery.com/news/news/{news_id}" DETAIL_API = "https://api.taiwanlottery.com/TLCAPIWeB/News/Detail/{news_id}" SOURCE_NAME = "台湾彩券" MAX_PAGES = 3 def clean_text(text: str) -> str: return re.sub(r"\s+", " ", html.unescape(text or "").replace("\u3000", " ")).strip() def parse_announce_date(value: str) -> tuple[int, str]: if not value: now = int(time.time()) return now, datetime.fromtimestamp(now).strftime("%Y-%m-%d %H:%M:%S") try: dt = datetime.fromisoformat(value.replace("Z", "+00:00")) ts = int(dt.timestamp()) return ts, dt.strftime("%Y-%m-%d %H:%M:%S") except Exception: now = int(time.time()) return now, datetime.fromtimestamp(now).strftime("%Y-%m-%d %H:%M:%S") def fetch_list(session: requests.Session, page_no: int) -> list[dict]: resp = session.get(LIST_API.format(page=page_no), timeout=30) resp.raise_for_status() data = resp.json() if data.get("rtCode") != 0: raise RuntimeError(f"台湾彩券列表接口异常: {data.get('rtMsg')}") return data.get("content", {}).get("newsListRes", []) or [] def strip_wrapper_tags(html_text: str) -> str: content = html_text or "" content = re.sub(r"^\ufeff", "", content) content = re.sub(r"]*>", "", content, flags=re.I) content = re.sub(r"", "", content, flags=re.I) content = re.sub(r"", "", content, flags=re.I) content = re.sub(r"]*>", "", content, flags=re.I) return content.strip() def parse_detail(session: requests.Session, news_id: str) -> dict: api_url = DETAIL_API.format(news_id=news_id) resp = session.get(api_url, timeout=30) resp.raise_for_status() data = resp.json() if data.get("rtCode") != 0: raise RuntimeError(f"台湾彩券详情接口异常: {data.get('rtMsg')}") detail = data.get("content") or {} url = DETAIL_URL.format(news_id=news_id) title = clean_text(detail.get("newsTitle", "")) published_ts, published_at = parse_announce_date(detail.get("announceDate", "")) raw_html = strip_wrapper_tags(detail.get("content", "")) inner_doc = raw_html image_urls = [] for src in re.findall(r']+src="([^"]+)"', inner_doc, re.I): full = urljoin(url, src) if full not in image_urls: image_urls.append(full) paragraphs = [] for raw in re.findall(r"]*>([\s\S]*?)

", inner_doc, re.I): text = clean_text(re.sub(r"<[^>]+>", " ", raw)) if len(text) > 8: paragraphs.append(text) published_text = "" if paragraphs and re.match(r"^\d{2}/\d{2}/\d{4}", paragraphs[0]): published_text = paragraphs.pop(0) content_parts = [f"

{text}

" for text in paragraphs] for image in image_urls[:3]: content_parts.append(f'') desc = paragraphs[0][:255] if paragraphs else title[:255] return { "title": title[:255], "desc": desc, "content": "".join(content_parts), "image": image_urls[0] if image_urls else "", "published_text": published_text[:64], "published_ts": published_ts, "published_at": published_at, "source_url": url, } def dedupe_by_source(items: list[dict]) -> list[dict]: seen = set() result = [] for item in items: key = item.get("source_url", "") if key and key not in seen: seen.add(key) result.append(item) return result def sync_to_db(conn, articles: list[dict]) -> tuple[int, int, int]: if not articles: return 0, 0, 0 urls = [a["source_url"] for a in articles if a.get("source_url")] with conn.cursor() as cur: placeholders = ",".join(["%s"] * len(urls)) cur.execute( f"SELECT id, source_url, content, is_show FROM la_article WHERE source_url IN ({placeholders}) AND cid = %s", urls + [ARTICLE_CID] ) existing = {row["source_url"]: row for row in cur.fetchall()} titles = [a["title"][:255] for a in articles if a.get("title")] existing_titles = set() if titles: with conn.cursor() as cur: t_placeholders = ",".join(["%s"] * len(titles)) cur.execute( f"SELECT title FROM la_article WHERE title IN ({t_placeholders}) AND cid = %s", titles + [ARTICLE_CID] ) existing_titles = {row["title"] for row in cur.fetchall()} new_count = 0 update_count = 0 now = int(time.time()) for article in articles: source_url = article.get("source_url", "") title = article.get("title", "") if not source_url or not title: continue ext_json = json.dumps({ "source": "taiwan_lottery", "news_id": article.get("news_id", ""), }, ensure_ascii=False) if source_url in existing: existing_row = existing[source_url] next_is_show = existing_row["is_show"] if has_publishable_article_content(existing_row.get("content", "")) else 0 with conn.cursor() as cur: cur.execute( """UPDATE la_article SET title=%s, `desc`=%s, abstract=%s, image=%s, author=%s, published_at=%s, category=%s, content=%s, ext=%s, is_show=%s, update_time=%s WHERE id=%s""", ( title[:255], (article.get("desc") or "")[:255], (article.get("desc") or "")[:500], article.get("image", "")[:128], SOURCE_NAME, article.get("published_at", "")[:64], "lottery,taiwan", article.get("content") or "", ext_json, next_is_show, now, existing_row["id"], ) ) update_count += 1 else: if title[:255] in existing_titles: continue with conn.cursor() as cur: cur.execute( """INSERT INTO la_article (cid, title, `desc`, abstract, image, author, published_at, category, source_url, content, ext, is_show, sort, click_virtual, click_actual, create_time, update_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", ( ARTICLE_CID, title[:255], (article.get("desc") or "")[:255], (article.get("desc") or "")[:500], article.get("image", "")[:128], SOURCE_NAME, article.get("published_at", "")[:64], "lottery,taiwan", source_url[:500], article.get("content") or "", ext_json, resolve_crawled_article_is_show(article.get("content") or ""), 0, 0, 0, article.get("published_ts") or now, now, ) ) existing[source_url] = {"id": cur.lastrowid, "content": article.get("content") or "", "is_show": resolve_crawled_article_is_show(article.get("content") or "")} new_count += 1 existing_titles.add(title[:255]) return len(articles), new_count, update_count def run(db_config: dict): session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (compatible; SportEra/1.0)", "Accept": "application/json,text/html,*/*", }) all_articles = [] for page_no in range(1, MAX_PAGES + 1): print(f"=== 台湾彩券资讯 第{page_no}页 ===") list_items = fetch_list(session, page_no) print(f" 列表返回 {len(list_items)} 条") if not list_items: break for item in list_items: news_id = item.get("newsId", "") if not news_id: continue detail = parse_detail(session, news_id) all_articles.append({ "news_id": news_id, "title": detail.get("title") or clean_text(item.get("newsTitle", "")), "desc": detail.get("desc", ""), "content": detail.get("content", ""), "image": detail.get("image", ""), "published_ts": detail.get("published_ts"), "published_at": detail.get("published_at"), "source_url": detail.get("source_url"), }) all_articles = dedupe_by_source(all_articles) print(f"去重后共 {len(all_articles)} 条") if not all_articles: print("无可写入数据,跳过") return {"success": True, "candidate_count": 0, "saved_count": 0, "summary_text": "无可写入数据,跳过"} conn = pymysql.connect(**db_config, cursorclass=pymysql.cursors.DictCursor) try: total, new_count, update_count = sync_to_db(conn, all_articles) conn.commit() summary = f"共 {total} 条, 新增 {new_count}, 更新 {update_count}" print(summary) return {"success": True, "candidate_count": total, "saved_count": new_count + update_count, "summary_text": summary} finally: conn.close()