wangzhibo
2025-07-30 25087cbe79c8c4992551477d55d9db8bbea2202e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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())