# -*- coding: UTF-8 -*- ''' @Project :domainScanDemo @File :juziseo.py @IDE :PyCharm @Author :梦伴 @Date :2026/4/10 15:00 @explain : 桔子SEO API封装类 - 提供登录、域名查询等功能 ''' # 导入标准库 import os # 操作系统接口 import re from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 # 导入第三方库 import requests # HTTP请求库 from loguru import logger # 日志记录 from requests.cookies import RequestsCookieJar # Cookie管理 from detect.geetest2 import Geetest2 from detect.locked_pickle import load_pickle_locked, save_pickle_atomic # AES-CBC 加密 def aes_cbc_encrypt(data, key=b'pvjxzjmzwawfscft', iv=b'qvibva1wg0uxwjeu'): cipher = AES.new(key, AES.MODE_CBC, iv) padded_data = pad(data, AES.block_size) # PKCS7填充 encrypted = cipher.encrypt(padded_data) return base64.b64encode(encrypted).decode() class Juziseo: """ 桔子SEO API封装类 """ cookie: RequestsCookieJar = {} # 桔子SEO Cookie # 通用请求头配置 headers = { 'accept': 'application/json, text/javascript, */*; q=0.01', # 接受的内容类型 'accept-language': 'zh-CN,zh;q=0.9', # 接受的语言 'content-type': 'application/x-www-form-urlencoded', # 内容类型 'origin': 'https://seo.juziseo.com', # 请求源 'priority': 'u=1, i', # 请求优先级 'referer': 'https://seo.juziseo.com/login', # 来源页面 'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"', # 浏览器标识 'sec-ch-ua-mobile': '?0', # 是否移动端 'sec-ch-ua-platform': '"Windows"', # 操作系统平台 'sec-fetch-dest': 'empty', # 请求目标 'sec-fetch-mode': 'cors', # 请求模式 'sec-fetch-site': 'same-origin', # 请求站点 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36', # 用户代理 'x-requested-with': 'XMLHttpRequest', # AJAX请求标识 } def __init__(self, proxies: dict = None): """ 初始化桔子SEO类 :param proxies: 代理配置 """ self.validate = None self.challenge = None self.gt = None self.session = requests.Session() # 创建会话对象 self.session.proxies = proxies # 设置代理 self.request_timeout = max( 0.5, float(os.getenv("DOMAINCHECK_JUZISEO_TIMEOUT", "10") or 10), ) self.base_url = "https://seo.juziseo.com" # 设置基础URL def _request(self, method: str, url: str, **kwargs): kwargs.setdefault("timeout", self.request_timeout) return getattr(self.session, method)(url, **kwargs) def save_cookies(self, filepath="juziseo_cookies.pkl"): """ 保存Cookie到文件和Redis :param filepath: 文件路径 """ save_pickle_atomic(filepath, self.cookie) logger.info(f"已保存桔子SEO Cookie到 {filepath}") # 保存到Redis try: from app.utils.redis_client import get_redis_client redis_client = get_redis_client(role="standard") # 将cookie转换为字典 cookie_dict = {} for cookie in self.cookie: cookie_dict[cookie.name] = cookie.value redis_client.set('domain_tool:juziseo_cookies', str(cookie_dict)) except Exception as e: pass def load_cookies(self, filepath="juziseo_cookies.pkl"): """ 从文件加载Cookie :param filepath: 文件路径 """ try: # 尝试加载 self.cookie = load_pickle_locked( filepath, default_factory=requests.cookies.RequestsCookieJar, ) # logger.info(f"已从 {filepath} 加载桔子SEO Cookie") except: # 加载失败 self.cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar logger.warning(f"加载桔子SEO Cookie失败,创建空Cookie") def start_msg_captcha_servlet(self): url = f"{self.base_url}/class/gtcode/msg/StartMsgCaptchaServlet.php" response = self._request("get", url, headers=self.headers, cookies=self.cookie).json() logger.info(response) self.gt = response['gt'] self.challenge = response['challenge'] return response['success'] == 1 def get_captcha(self): GETT2 = Geetest2() code = GETT2.get_tp(gt=self.gt, challenge=self.challenge, type_='auto') logger.info(code) if code['result'] == 'success': self.challenge = code['challenge'] self.gt = code['gt'] self.validate = code['validate'] return True else: logger.error(f"获取桔子SEO验证码失败: {code.get('msg', '未知错误')}") return False def login(self, username, password): """ 登录桔子SEO :param username: 账号 :param password: 密码 :return: tuple - (是否成功, 消息) """ try: for _ in range(3): if self.start_msg_captcha_servlet(): if self.get_captcha(): break data = { 'return_url': '/', 'user_name': aes_cbc_encrypt(bytes(username, 'utf-8')), 'password': aes_cbc_encrypt(bytes(password, 'utf-8')), 'geetest_challenge': self.challenge, 'geetest_validate': self.validate, 'geetest_seccode': self.validate + '|jordan', '_post_type': 'ajax', } logger.debug(data) # 发送登录请求 url = f"{self.base_url}/account/ajax/login_process/" response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie) logger.info(response.text) # 解析响应 result = response.json() if result.get('errno') == 1: # 登录成功,保存Cookie self.cookie = response.cookies self.save_cookies() logger.info("桔子SEO登录成功") return True, "登录成功" else: logger.error(f"桔子SEO登录失败: {result.get('msg', '未知错误')}") return False, result.get('msg', '登录失败') except Exception as e: logger.error(f"桔子SEO登录异常: {e}") return False, f"登录失败: {str(e)}" def check_history(self, domain: str, sensitive_words: list = None): # 检测域名历史是否存在敏感词 if sensitive_words is None: sensitive_words = [] data = { 'qrtypeindex': '1', 'domains': domain, '_post_type': 'ajax', } url = f"{self.base_url}/snapshot/save/" response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() # logger.info(response) if response['errno'] == 1: url = response['rsm']['url'] response_html = self._request("get", url, headers=self.headers, cookies=self.cookie).content.decode('utf-8') title_sensitive_words_match = re.search(r'标题敏感词\D*(\d+)', response_html, re.S) title_suspected_sensitive_words_match = re.search(r'标题有疑似敏感词\D*(\d+)', response_html, re.S) content_sensitive_words_match = re.search(r'内容敏感词\D*(\d+)', response_html, re.S) baidu_sensitive_words_match = re.search(r'百度历史收录敏感\D*(\d+)', response_html, re.S) if bool(title_sensitive_words_match or title_suspected_sensitive_words_match or content_sensitive_words_match or baidu_sensitive_words_match): return False, "存在敏感词" subdomain_match = re.search(r'子域名:\D*(\d+)', response_html, re.S) if subdomain_match: return False, f"存在子域名: {subdomain_match.group(1)}" for sensitive_word in sensitive_words: if sensitive_word in response_html: return False, f"存在敏感词: {sensitive_word}" return True, 'success' return False, str(response.get('err', '请求失败')) def check_external_link(self, domain: str, sensitive_words: list = None): # 外链查询域名是否存在敏感词 if sensitive_words is None: sensitive_words = [] data = { 'qrtypeindex': '1', 'domains': domain, '_post_type': 'ajax', } url = f"{self.base_url}/domain_rank/save_domain/" response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() logger.info(response) if response['errno'] == 1: url = response['rsm']['url'] response_html = self._request("get", url, headers=self.headers, cookies=self.cookie).content.decode('utf-8') subdomain_match = re.search(r'子域名:\D*(\d+)', response_html, re.S) if subdomain_match: return False, f"存在子域名: {subdomain_match.group(1)}" for sensitive_word in sensitive_words: if sensitive_word in response_html: return False, f"存在敏感词: {sensitive_word}" return True, 'success' return False, str(response.get('err', '请求失败'))