#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ TopHub网站数据抓取脚本 负责从tophub.today网站抓取数据,根据指定规则过滤并保存 """ import requests from lxml import html import json import time import os import re import subprocess import sys from datetime import datetime from loguru import logger # 配置日志 logger.add("tophub_scraper.log", rotation="10 MB", level="INFO") class TopHubScraper: """TopHub网站数据抓取器""" def __init__(self): """ 初始化抓取器 """ self.base_url = "https://tophub.today/" self.ban_list_file = "tophub_ban_column.txt" self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }) self.ban_list = self.load_ban_list() def load_ban_list(self): """ 加载需要过滤的栏目列表 Returns: set: 需要过滤的栏目集合 """ ban_list = set() try: if os.path.exists(self.ban_list_file): with open(self.ban_list_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line: ban_list.add(line) logger.info(f"已加载 {len(ban_list)} 个需要过滤的栏目") else: logger.warning(f"过滤文件 {self.ban_list_file} 不存在,将不过滤任何栏目") except Exception as e: logger.error(f"加载过滤文件失败: {e}") return ban_list def fetch_webpage(self): """ 获取网页内容 Returns: str: 网页HTML内容 """ logger.info(f"正在获取网页内容: {self.base_url}") try: response = self.session.get(self.base_url, timeout=10) response.raise_for_status() logger.info("网页内容获取成功") return response.text except requests.RequestException as e: logger.error(f"获取网页内容失败: {e}") raise def delete_date_txt_files(self): """ 删除本地目录下所有以日期格式开头的txt文件 匹配格式: YYYY年MM月DD日HHMMSS.txt """ logger.info("开始删除日期格式的txt文件") deleted_count = 0 # 定义日期格式的正则表达式模式 date_pattern = r'^\d{4}年\d{1,2}月\d{1,2}日\d{6}\.txt$' try: # 获取当前目录下的所有txt文件 for filename in os.listdir('.'): if filename.endswith('.txt') and re.match(date_pattern, filename): try: os.remove(filename) logger.info(f"已删除文件: {filename}") deleted_count += 1 except Exception as e: logger.error(f"删除文件 {filename} 失败: {e}") logger.info(f"删除完成,共删除 {deleted_count} 个日期格式的txt文件") except Exception as e: logger.error(f"删除文件时出错: {e}") def scrape_by_node_ids(self): """ 根据节点ID范围抓取数据 Returns: list: 包含已抓取数据的列表 """ try: # 运行逻辑前,先删除所有日期格式的txt文件 self.delete_date_txt_files() # 1. 获取网页内容 html_content = self.fetch_webpage() tree = html.fromstring(html_content) # 2. 创建输出文件名(基于当前日期时间) now = datetime.now() output_file = f"{now.year}年{now.month}月{now.day}日{now.hour}{now.minute}{now.second}.txt" scraped_data = [] # 3. 遍历节点ID范围 for node_id in range(1, 1000): # 从1到999 xpath = f'//*[@id="node-{node_id}"]' logger.info(f"正在查找节点: {xpath}") # 查找节点 nodes = tree.xpath(xpath) if not nodes: continue # 没有找到节点,跳过下一个数字 node = nodes[0] # 查找span标签 spans = node.xpath('.//span') if not spans: logger.info(f"节点 {node_id} 中未找到span标签,跳过") continue # 获取第一个span的文本内容 span_text = spans[0].text_content().strip() if not span_text: logger.info(f"节点 {node_id} 的span标签为空,跳过") continue # 检查是否在过滤列表中(部分匹配) should_skip = False for ban_word in self.ban_list: if ban_word in span_text: logger.info(f"节点 {node_id} 的内容 '{span_text}' 包含过滤词 '{ban_word}',跳过") should_skip = True break if should_skip: continue logger.info(f"节点 {node_id} 的内容 '{span_text}' 通过过滤,继续处理") # 查找a元素 links = node.xpath('.//a') if not links: logger.info(f"节点 {node_id} 中未找到a元素,跳过") continue # 提取所有链接和文本 for link in links: link_text = link.text_content().strip() href = link.get('href', '') if link_text and href: # 补全相对链接 if not href.startswith('http'): href = f"https://tophub.today{href}" # 当category和text的值相同时,跳过当前循环 if span_text == link_text: logger.info(f"节点 {node_id} 的分类和标题相同 ({span_text}),跳过") continue scraped_data.append({ 'node_id': node_id, 'category': span_text, 'text': link_text, 'link': href }) # 4. 保存数据到文件 if scraped_data: self.save_to_file(scraped_data, output_file) logger.info(f"成功抓取 {len(scraped_data)} 条数据,保存到 {output_file}") else: logger.warning("未抓取到任何数据") return scraped_data except Exception as e: logger.error(f"抓取数据时出错: {e}") raise def save_to_file(self, data, filename): """ 将数据保存到文件 Args: data (list): 要保存的数据 filename (str): 文件名 """ try: with open(filename, 'w', encoding='utf-8') as f: for item in data: f.write(f"节点ID: {item['node_id']}\n") f.write(f"分类: {item['category']}\n") # 使用正则表达式清洗标题,去除数字序号和多余空白 title_text = item['text'] # 处理多行标题,提取实际内容 lines = title_text.strip().split('\n') if len(lines) >= 2: # 第二行通常是实际标题内容 cleaned_title = lines[1].strip() else: # 如果只有一行,尝试使用正则表达式 match = re.match(r'^\d+\s+(.+)$', title_text.strip(), re.DOTALL) if match: cleaned_title = match.group(1).strip() else: cleaned_title = title_text.strip() f.write(f"标题: {cleaned_title}\n") f.write(f"链接: {item['link']}\n") f.write("-" * 50 + "\n") logger.info(f"数据已保存到 {filename}") except Exception as e: logger.error(f"保存文件失败: {e}") raise def call_add_data_script(self): """ 调用本地的tophub_add_data_to_db.py脚本 """ logger.info("准备调用tophub_add_data_to_db.py脚本") try: # 检查tophub_add_data_to_db.py是否存在 if not os.path.exists("tophub_add_data_to_db.py"): logger.error("tophub_add_data_to_db.py文件不存在,无法调用") return # 调用tophub_add_data_to_db.py脚本 logger.info("正在调用tophub_add_data_to_db.py...") # 使用Popen方式处理可能的编码问题 process = subprocess.Popen([sys.executable, "tophub_add_data_to_db.py"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace') # 使用replace模式处理无法解码的字符 # 实时读取输出以避免编码问题 try: stdout, stderr = process.communicate(timeout=300) # 5分钟超时 except subprocess.TimeoutExpired: process.kill() logger.error("tophub_add_data_to_db.py执行超时") return if process.returncode == 0: logger.info("tophub_add_data_to_db.py调用成功") if stdout: logger.info(f"脚本输出: {stdout}") else: logger.error(f"tophub_add_data_to_db.py调用失败,返回码: {process.returncode}") if stderr: logger.error(f"错误信息: {stderr}") if stdout: logger.info(f"脚本输出: {stdout}") except UnicodeDecodeError as e: logger.error(f"编码解码错误: {e}") logger.info("可能是脚本输出包含非UTF-8编码字符,已尝试使用replace模式处理") except Exception as e: logger.error(f"调用tophub_add_data_to_db.py时出错: {e}") if __name__ == "__main__": scraper = TopHubScraper() try: # 抓取数据 scraped_data = scraper.scrape_by_node_ids() # 抓取完成后调用tophub_add_data_to_db.py脚本 if scraped_data: scraper.call_add_data_script() else: logger.warning("未抓取到数据,跳过调用tophub_add_data_to_db.py脚本") except Exception as e: logger.error(f"程序执行出错: {e}") raise