from pathlib import Path
|
import random
|
import os
|
import re
|
import time
|
from datetime import datetime
|
import requests
|
import asyncio
|
from bs4 import BeautifulSoup
|
from docx import Document
|
from docx.shared import Inches
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
from PIL import Image
|
from io import BytesIO
|
from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError
|
from auto_media_publisher.config.conf_base import PLAYWRIGHT_STEALTH_JS_PATH,DOCX_DIR_DOWNLOADED,WEIXIN_GZH_WATERMARK_HEIGHT,DAJIALA_URL_PREFIX,DAJIALA_CATEGORY_DICT,DEFAULT_USER_AGENT,DAJIALA_MAX_CONCURRENT_PAGE
|
from auto_media_publisher.utils.utils_database import dajiala_get_pending_downloads,dajiala_update_download_status
|
from auto_media_publisher.utils.logger import get_logger
|
from auto_media_publisher.utils.utils_network import get_free_valid_proxies
|
|
|
class DajialaDownloader():
|
def __init__(self):
|
super().__init__()
|
self.base_url = DAJIALA_URL_PREFIX
|
self.category_dict = DAJIALA_CATEGORY_DICT
|
self.logger = get_logger(self.__class__.__name__,"admin")
|
|
self.valid_proxies = []
|
self.proxy_index = 0
|
self.failed_proxies = set()
|
|
async def refresh_proxies(self):
|
self.valid_proxies = await get_free_valid_proxies()
|
if self.valid_proxies:
|
self.logger.info(f"[Proxy] 获取并验证有效代理 {len(self.valid_proxies)} 个")
|
|
def get_next_proxy(self):
|
if self.valid_proxies:
|
while self.proxy_index < len(self.valid_proxies):
|
proxy = self.valid_proxies[self.proxy_index]
|
self.proxy_index += 1
|
if proxy not in self.failed_proxies:
|
return proxy
|
return None
|
|
async def init_browser_with_proxy(self, playwright, proxy_raw):
|
try:
|
proxy = {"server": f"http://{proxy_raw}"}
|
browser = await playwright.chromium.launch(headless=False, channel= "chrome",args=["--blink-settings=imagesEnabled=false"])
|
context = await browser.new_context(proxy=proxy,user_agent=DEFAULT_USER_AGENT,viewport={"width": 1920, "height": 1080})
|
if os.path.exists(PLAYWRIGHT_STEALTH_JS_PATH):
|
await context.add_init_script(path=Path(PLAYWRIGHT_STEALTH_JS_PATH))
|
page = await context.new_page()
|
return browser, page
|
except Exception as e:
|
self.logger.error(f"[Worker] 浏览器初始化失败 proxy={proxy_raw}:{e}")
|
|
async def worker(self, playwright, proxy_raw, url_queue: asyncio.Queue):
|
try:
|
browser, page = await self.init_browser_with_proxy(playwright, proxy_raw)
|
self.logger.info(f"[Worker] 浏览器启动完成,代理: {proxy_raw}")
|
except Exception as e:
|
self.logger.error(f"[Worker] 浏览器初始化失败 proxy={proxy_raw}:{e}")
|
return
|
while True:
|
try:
|
row = await url_queue.get()
|
except asyncio.CancelledError:
|
break
|
if row is None:
|
# 队列结束信号
|
break
|
|
url = row["url"]
|
self.logger.info(f"[Worker] 开始处理 URL: {url}")
|
docx_path_name = self.get_docx_path_name(row)
|
|
try:
|
success = await self.download_single_url(page, url, docx_path_name)
|
if success:
|
dajiala_update_download_status(url, "done")
|
self.logger.info(f"[Worker] 下载成功: {url}")
|
else:
|
dajiala_update_download_status(url, "fail")
|
self.failed_proxies.add(proxy_raw)
|
self.logger.warning(f"[Worker] 下载失败: {url}")
|
await asyncio.sleep(random.uniform(15, 30)) # 秒
|
except Exception as e:
|
self.failed_proxies.add(proxy_raw)
|
self.logger.error(f"[Worker] 异常下载: {url} - {e}")
|
|
url_queue.task_done()
|
|
await browser.close()
|
self.logger.info(f"[Worker] 浏览器关闭,代理: {proxy_raw}")
|
|
def get_docx_path_name(self, row):
|
try:
|
datepart = row["updatetime"][:10]
|
title_part = re.sub(r'[\\/:*?"<>|]', '', row["title"])
|
return os.path.join(DOCX_DIR_DOWNLOADED, str(row["type"]) + "_" + row["type_name"], f"{datepart}_{title_part}.docx")
|
except:
|
return os.path.join(DOCX_DIR_DOWNLOADED, "0_未知", f"{datetime.now().isoformat()}.docx")
|
|
|
async def download_single_url(self,page, url,docx_path_name):
|
try:
|
await page.goto(url, timeout=30000)
|
await page.wait_for_timeout(30000) # 等待页面加载完成
|
|
# 检查是否有验证框
|
if await page.query_selector("#js_verify"):
|
self.logger.info(f"[download] 检测到验证框,尝试点击验证: {url}")
|
try:
|
await page.click("#js_verify")
|
await page.wait_for_timeout(5000) # 再等 5 秒
|
except Exception as ve:
|
self.logger.warning(f"[download] 点击验证失败: {ve}")
|
|
html_content = await page.content()
|
soup = BeautifulSoup(html_content, "html.parser")
|
div = soup.find("div", id="img-content")
|
|
if not div:
|
self.logger.warning(f"[download] 没找到 #img-content: {url}")
|
return False
|
|
doc = Document()
|
|
for element in div.find_all(["h1", "p", "img", "audio", "video"]):
|
if element.name in ["h1", "p"]:
|
paragraph = doc.add_paragraph()
|
run = paragraph.add_run(element.get_text())
|
|
# 字体颜色处理
|
if "style" in element.attrs:
|
style = element.attrs["style"].lower()
|
if "color" in style:
|
color = style.split("color:")[1].split(";")[0].strip()
|
rgb = self.get_rgb_color(color)
|
if rgb:
|
run.font.color.rgb = rgb
|
|
elif element.name == "img":
|
img_url = element.get("data-src") or element.get("src")
|
if img_url:
|
self.insert_image(doc, img_url)
|
|
elif element.name in ["audio", "video"]:
|
media_url = element.get("src")
|
if media_url:
|
doc.add_paragraph(f"媒体资源: {media_url}")
|
|
os.makedirs(os.path.dirname(docx_path_name), exist_ok=True)
|
doc.save(docx_path_name)
|
|
self.logger.info(f"[download] 保存完成: {docx_path_name}")
|
return True
|
|
except Exception as e:
|
self.logger.error(f"[download] 下载失败 {url}: {e}")
|
return False
|
|
def insert_image(self, doc, img_url):
|
try:
|
time.sleep(1)
|
headers = {"User-Agent": DEFAULT_USER_AGENT}
|
response = requests.get(img_url, timeout=50,headers=headers)
|
if response.status_code == 200:
|
image = Image.open(BytesIO(response.content))
|
width, height = image.size
|
cropped = image.crop((0, 0, width, height - WEIXIN_GZH_WATERMARK_HEIGHT))
|
|
buffer = BytesIO()
|
cropped.save(buffer, format=image.format or "PNG")
|
buffer.seek(0)
|
|
paragraph = doc.add_paragraph()
|
run = paragraph.add_run()
|
run.add_picture(buffer, width=Inches(6))
|
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
except Exception as e:
|
self.logger.warning(f"[image] 下载或插图失败 {img_url}: {e}")
|
|
|
def get_rgb_color(self, color_str):
|
""" 解析 HTML 颜色字符串并转换为 RGB """
|
from docx.shared import RGBColor
|
import re
|
match = re.search(r'#([0-9a-fA-F]{6})', color_str)
|
if match:
|
hex_color = match.group(1)
|
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
|
return RGBColor(r, g, b)
|
return None
|
|
async def _async_run(self):
|
await self.refresh_proxies()
|
urls = dajiala_get_pending_downloads()
|
self.logger.info(f"[DownloaderDajiala] 待下载 URL 数量: {len(urls)}")
|
|
url_queue = asyncio.Queue()
|
for row in urls:
|
await url_queue.put(row)
|
|
if not self.valid_proxies:
|
return
|
|
async with async_playwright() as playwright:
|
workers = []
|
for proxy_raw in self.valid_proxies[:DAJIALA_MAX_CONCURRENT_PAGE]:
|
workers.append(asyncio.create_task(self.worker(playwright, proxy_raw, url_queue)))
|
|
# 等待队列处理完成
|
await url_queue.join()
|
|
# 结束所有 worker
|
for _ in workers:
|
await url_queue.put(None) # 结束信号
|
|
await asyncio.gather(*workers)
|
|
|
def run(self):
|
asyncio.run(self._async_run())
|