迁移目录
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
台湾彩券资讯采集模块
|
||||
- 列表接口: 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"</?(?:html|body)[^>]*>", "", content, flags=re.I)
|
||||
content = re.sub(r"<head[\s\S]*?</head>", "", content, flags=re.I)
|
||||
content = re.sub(r"<style[\s\S]*?</style>", "", content, flags=re.I)
|
||||
content = re.sub(r"<meta[^>]*>", "", 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'<img[^>]+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"<p[^>]*>([\s\S]*?)</p>", 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"<p>{text}</p>" for text in paragraphs]
|
||||
for image in image_urls[:3]:
|
||||
content_parts.append(f'<img src="{image}" />')
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user