feat: stabilize multi-region runtime sync and worker orchestration
This commit is contained in:
@@ -12,6 +12,7 @@ import re # 正则表达式模块
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
import requests # HTTP请求库
|
||||
from loguru import logger # 日志记录
|
||||
from requests.adapters import HTTPAdapter
|
||||
@@ -23,7 +24,12 @@ SO_SEARCH_URL = 'https://www.so.com/s' # 360搜索URL
|
||||
SO_REFERER_TEMPLATE = 'https://www.so.com/s?ie=utf-8&q=site%3A{domain}' # Referer模板
|
||||
SEARCH_PATTERN = r'target="_blank">([^<]+)</a></h3>' # 搜索结果匹配模式
|
||||
BLOCKED_CODE = 3 # 拦截状态码
|
||||
_SESSION_LOCAL = threading.local()
|
||||
_DIRECT_HTTP = requests.Session()
|
||||
_DIRECT_HTTP.trust_env = False
|
||||
_DIRECT_HTTP.mount("http://", HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
_DIRECT_HTTP.mount("https://", HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
_PROXY_SESSIONS = OrderedDict()
|
||||
_PROXY_SESSION_LOCK = threading.Lock()
|
||||
SO_TIMEOUT_PROXY = max(
|
||||
1.0,
|
||||
float(os.getenv("DOMAINCHECK_360_TIMEOUT_PROXY", "1.4") or 1.4),
|
||||
@@ -34,6 +40,20 @@ SO_TIMEOUT_DIRECT = max(
|
||||
)
|
||||
|
||||
|
||||
def _proxy_session_cache_limit() -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv("DOMAINCHECK_360_PROXY_SESSION_CACHE_SIZE", "128") or 128))
|
||||
except Exception:
|
||||
return 128
|
||||
|
||||
|
||||
def _close_session_quietly(session) -> None:
|
||||
try:
|
||||
session.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _resolve_360_timeout(proxies: Optional[Dict] = None, budget_seconds: Optional[float] = None) -> float:
|
||||
timeout = float(SO_TIMEOUT_PROXY if proxies else SO_TIMEOUT_DIRECT)
|
||||
if budget_seconds not in (None, "", 0, "0"):
|
||||
@@ -41,15 +61,27 @@ def _resolve_360_timeout(proxies: Optional[Dict] = None, budget_seconds: Optiona
|
||||
return max(0.6, timeout)
|
||||
|
||||
|
||||
def _get_session():
|
||||
session = getattr(_SESSION_LOCAL, "session", None)
|
||||
if session is not None:
|
||||
def _get_session(proxies: Optional[Dict] = None):
|
||||
proxy_url = ""
|
||||
if proxies:
|
||||
proxy_url = str(proxies.get("https") or proxies.get("http") or "").strip()
|
||||
if not proxy_url:
|
||||
return _DIRECT_HTTP
|
||||
|
||||
with _PROXY_SESSION_LOCK:
|
||||
session = _PROXY_SESSIONS.get(proxy_url)
|
||||
if session is not None:
|
||||
_PROXY_SESSIONS.move_to_end(proxy_url)
|
||||
return session
|
||||
session = requests.Session()
|
||||
session.trust_env = False
|
||||
session.mount("http://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
|
||||
session.mount("https://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
|
||||
_PROXY_SESSIONS[proxy_url] = session
|
||||
while len(_PROXY_SESSIONS) > _proxy_session_cache_limit():
|
||||
_, stale_session = _PROXY_SESSIONS.popitem(last=False)
|
||||
_close_session_quietly(stale_session)
|
||||
return session
|
||||
session = requests.Session()
|
||||
session.mount("http://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
|
||||
session.mount("https://", HTTPAdapter(pool_connections=64, pool_maxsize=128, max_retries=0))
|
||||
_SESSION_LOCAL.session = session
|
||||
return session
|
||||
|
||||
|
||||
def check_domain(
|
||||
@@ -95,7 +127,7 @@ def check_domain(
|
||||
try:
|
||||
session_started_at = time.perf_counter()
|
||||
logger.info(f'360阶段: domain={domain} | stage=session_init_start')
|
||||
session = _get_session()
|
||||
session = _get_session(proxies)
|
||||
logger.info(
|
||||
f'360阶段: domain={domain} | stage=session_init_done '
|
||||
f'| elapsed_ms={int((time.perf_counter() - session_started_at) * 1000)}'
|
||||
@@ -103,20 +135,28 @@ def check_domain(
|
||||
# 发送GET请求获取搜索结果
|
||||
request_started_at = time.perf_counter()
|
||||
logger.info(f'360阶段: domain={domain} | stage=request_start | proxy={"yes" if proxies else "no"}')
|
||||
response = session.get(
|
||||
SO_SEARCH_URL,
|
||||
params=params,
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=_resolve_360_timeout(proxies, budget_seconds=budget_seconds),
|
||||
)
|
||||
logger.info(
|
||||
f'360阶段: domain={domain} | stage=request_done | status={response.status_code} '
|
||||
f'| elapsed_ms={int((time.perf_counter() - request_started_at) * 1000)}'
|
||||
)
|
||||
response = None
|
||||
try:
|
||||
response = session.get(
|
||||
SO_SEARCH_URL,
|
||||
params=params,
|
||||
headers=headers,
|
||||
proxies=proxies,
|
||||
timeout=_resolve_360_timeout(proxies, budget_seconds=budget_seconds),
|
||||
)
|
||||
logger.info(
|
||||
f'360阶段: domain={domain} | stage=request_done | status={response.status_code} '
|
||||
f'| elapsed_ms={int((time.perf_counter() - request_started_at) * 1000)}'
|
||||
)
|
||||
|
||||
# 解析响应内容
|
||||
response_html = response.content.decode('utf-8', errors='ignore')
|
||||
# 解析响应内容
|
||||
response_html = response.content.decode('utf-8', errors='ignore')
|
||||
finally:
|
||||
try:
|
||||
if response is not None:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 使用正则表达式提取搜索结果
|
||||
search_results = re.findall(SEARCH_PATTERN, response_html)
|
||||
|
||||
@@ -15,10 +15,23 @@ import subprocess
|
||||
import execjs
|
||||
|
||||
|
||||
def _resolve_geetest_asset_timeout():
|
||||
connect_timeout = max(
|
||||
0.3,
|
||||
float(os.getenv("DOMAINCHECK_GEETEST_ASSET_CONNECT_TIMEOUT", "2.0") or 2.0),
|
||||
)
|
||||
read_timeout = max(
|
||||
0.5,
|
||||
float(os.getenv("DOMAINCHECK_GEETEST_ASSET_READ_TIMEOUT", "4.0") or 4.0),
|
||||
)
|
||||
return (connect_timeout, read_timeout)
|
||||
|
||||
|
||||
class slide():
|
||||
def __init__(self):
|
||||
self.headers = randomUA()
|
||||
self.t = round(time.time() * 1000)
|
||||
self.asset_timeout = _resolve_geetest_asset_timeout()
|
||||
|
||||
def __ease_out_expo(self, sep):
|
||||
if sep == 1:
|
||||
@@ -83,7 +96,7 @@ class slide():
|
||||
for idv, p_url in vdict.items():
|
||||
p_url = 'http://static.geetest.com/' + p_url
|
||||
# print(p_url)
|
||||
vcode = requests.get(p_url, headers=self.headers)
|
||||
vcode = requests.get(p_url, headers=self.headers, timeout=self.asset_timeout)
|
||||
text = vcode.content
|
||||
if idv == 'bg':
|
||||
text = self.tp_huanyuan(text)
|
||||
@@ -131,7 +144,7 @@ class Geetest2():
|
||||
"callback": f"geetest_{self.t}"
|
||||
}
|
||||
try:
|
||||
respe = http.get(url=url, params=params, proxies=proxies).text
|
||||
respe = http.get(url=url, params=params, proxies=proxies, timeout=self.timeout).text
|
||||
except Exception as e:
|
||||
if proxy:
|
||||
if pparmas:
|
||||
|
||||
@@ -13,7 +13,6 @@ import base64 # Base64编码解码
|
||||
import hashlib # 哈希加密
|
||||
import json # JSON数据处理
|
||||
import os # 操作系统接口
|
||||
import pickle # 序列化反序列化
|
||||
import random # 随机数生成
|
||||
import shutil
|
||||
import subprocess # 子进程管理
|
||||
@@ -24,6 +23,7 @@ import requests # HTTP请求库
|
||||
from loguru import logger # 日志记录
|
||||
from requests.cookies import RequestsCookieJar # Cookie管理
|
||||
|
||||
from detect.locked_pickle import load_pickle_locked, save_pickle_atomic
|
||||
|
||||
|
||||
# # 移除自定义子进程类的替换,避免影响其他模块
|
||||
@@ -109,12 +109,19 @@ class JC(object): # 聚查网API封装类
|
||||
|
||||
def __init__(self, proxies: dict = None): # 初始化JC类
|
||||
self.session = requests.Session() # 创建会话对象
|
||||
self.session.timeout = 10 # 设置超时时间
|
||||
self.session.proxies = proxies # 设置代理
|
||||
self.request_timeout = max(
|
||||
0.5,
|
||||
float(os.getenv("DOMAINCHECK_JUCHA_TIMEOUT", "10") or 10),
|
||||
)
|
||||
self.base_url = "https://www.jucha.com" # 设置基础URL
|
||||
self.cookie = requests.cookies.RequestsCookieJar() # 初始化CookieJar
|
||||
self.juming_cookie = requests.cookies.RequestsCookieJar() # 初始化聚名网CookieJar
|
||||
|
||||
def _request(self, method: str, url: str, **kwargs):
|
||||
kwargs.setdefault("timeout", self.request_timeout)
|
||||
return getattr(self.session, method)(url, **kwargs)
|
||||
|
||||
def _get_headers(self, referer: str = None) -> dict: # 获取请求头(可自定义referer)
|
||||
headers = self.headers.copy() # 复制默认请求头
|
||||
if referer: # 如果指定了referer
|
||||
@@ -139,7 +146,7 @@ class JC(object): # 聚查网API封装类
|
||||
return False, f'滑块验证码失败,{retry_times}次内未成功' # 返回失败
|
||||
|
||||
def _check_request(self, url: str, data: dict, headers: dict, cookies=None) -> dict: # 检查请求并处理验证码
|
||||
res = self.session.post(url=url, data=data, headers=headers, cookies=cookies) # 发送POST请求
|
||||
res = self._request("post", url=url, data=data, headers=headers, cookies=cookies) # 发送POST请求
|
||||
try:
|
||||
response = res.json() # 尝试解析JSON
|
||||
except Exception as e: # JSON解析失败
|
||||
@@ -179,7 +186,7 @@ class JC(object): # 聚查网API封装类
|
||||
"seed": calculate_seed(286) # 种子值
|
||||
}
|
||||
url = f"{self.base_url}/captcha/init" # 初始化URL
|
||||
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
|
||||
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
|
||||
if response['code'] == 1: # 初始化成功
|
||||
self.captchaId = response['data']['captchaId'] # 保存验证码ID
|
||||
self.encryptionPublicKey = response['data']['encryptionPublicKey'] # 保存加密公钥
|
||||
@@ -234,8 +241,13 @@ class JC(object): # 聚查网API封装类
|
||||
url = f"{self.base_url}/captcha/verify" # 验证URL
|
||||
verify_headers = self.headers.copy() # 复制请求头
|
||||
verify_headers['Content-Type'] = 'application/json' # 设置内容类型(仅用于验证请求)
|
||||
verify_response = self.session.post(url, headers=verify_headers, data=verify_data,
|
||||
cookies=self.cookie) # 发送验证请求
|
||||
verify_response = self._request(
|
||||
"post",
|
||||
url,
|
||||
headers=verify_headers,
|
||||
data=verify_data,
|
||||
cookies=self.cookie,
|
||||
) # 发送验证请求
|
||||
if verify_response.cookies: # 如果响应中有新的Cookie
|
||||
self.cookie.update(verify_response.cookies) # 合并Cookie(而不是替换)
|
||||
response = verify_response.json() # 解析JSON响应
|
||||
@@ -254,20 +266,12 @@ class JC(object): # 聚查网API封装类
|
||||
return False, "Node.js脚本未输出任何内容" # 返回失败
|
||||
|
||||
def save_cookies(self, filepath="jucha_cookies.pkl"): # 保存Cookie到文件
|
||||
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
|
||||
pickle.dump(self.cookie, f) # 序列化保存Cookie
|
||||
save_pickle_atomic(filepath, self.cookie)
|
||||
logger.info(f"已保存Cookie到文件: {filepath}")
|
||||
# 保存到Redis
|
||||
try:
|
||||
import redis
|
||||
from app.config import config
|
||||
redis_client = redis.Redis(
|
||||
host=config.REDIS_HOST,
|
||||
port=config.REDIS_PORT,
|
||||
password=config.REDIS_PASSWORD,
|
||||
db=config.REDIS_DB,
|
||||
decode_responses=True
|
||||
)
|
||||
from app.utils.redis_client import get_redis_client
|
||||
redis_client = get_redis_client(role="standard")
|
||||
# 将cookie转换为字典
|
||||
cookie_dict = {}
|
||||
# 检查self.cookie的类型
|
||||
@@ -292,25 +296,29 @@ class JC(object): # 聚查网API封装类
|
||||
|
||||
def load_cookies(self, filepath="jucha_cookies.pkl"): # 从文件加载Cookie
|
||||
try: # 尝试加载
|
||||
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
|
||||
loaded_cookie = pickle.load(f) # 反序列化加载Cookie
|
||||
# 检查加载的cookie类型
|
||||
if isinstance(loaded_cookie, dict):
|
||||
# 如果是字典,转换为RequestsCookieJar
|
||||
cookie_jar = requests.cookies.RequestsCookieJar()
|
||||
for name, value in loaded_cookie.items():
|
||||
cookie_jar.set(name, value)
|
||||
self.cookie = cookie_jar
|
||||
else:
|
||||
self.cookie = loaded_cookie
|
||||
loaded_cookie = load_pickle_locked(
|
||||
filepath,
|
||||
default_factory=requests.cookies.RequestsCookieJar,
|
||||
)
|
||||
# 检查加载的cookie类型
|
||||
if isinstance(loaded_cookie, dict):
|
||||
# 如果是字典,转换为RequestsCookieJar
|
||||
cookie_jar = requests.cookies.RequestsCookieJar()
|
||||
for name, value in loaded_cookie.items():
|
||||
cookie_jar.set(name, value)
|
||||
self.cookie = cookie_jar
|
||||
else:
|
||||
self.cookie = loaded_cookie
|
||||
except Exception as e: # 加载失败
|
||||
logger.error(f"加载Cookie失败: {e}")
|
||||
self.cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
|
||||
|
||||
def load_juming_cookies(self, filepath="juming_cookies.pkl"): # 从文件加载聚名网Cookie
|
||||
try: # 尝试加载
|
||||
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
|
||||
self.juming_cookie = pickle.load(f) # 反序列化加载Cookie
|
||||
self.juming_cookie = load_pickle_locked(
|
||||
filepath,
|
||||
default_factory=requests.cookies.RequestsCookieJar,
|
||||
)
|
||||
except: # 加载失败
|
||||
self.juming_cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
|
||||
|
||||
@@ -318,7 +326,8 @@ class JC(object): # 聚查网API封装类
|
||||
params = { # 请求参数
|
||||
'platform': 'juming', # 平台标识
|
||||
}
|
||||
res = self.session.get( # 发送GET请求
|
||||
res = self._request( # 发送GET请求
|
||||
"get",
|
||||
url=f'{self.base_url}/home/login/get_auth_url', # 获取授权URL
|
||||
params=params, # 请求参数
|
||||
headers=self.headers, # 请求头
|
||||
@@ -333,7 +342,8 @@ class JC(object): # 聚查网API封装类
|
||||
combined_cookies = requests.cookies.RequestsCookieJar()
|
||||
combined_cookies.update(self.juming_cookie)
|
||||
combined_cookies.update(self.cookie)
|
||||
response = self.session.get( # 发送GET请求
|
||||
response = self._request( # 发送GET请求
|
||||
"get",
|
||||
url=url, # 跳转URL
|
||||
headers=self.headers, # 请求头
|
||||
cookies=combined_cookies, # 合并后的Cookie
|
||||
@@ -345,7 +355,8 @@ class JC(object): # 聚查网API封装类
|
||||
self.cookie[name] = value # 直接赋值,覆盖同名Cookie
|
||||
if response.status_code == 302: # 重定向状态码
|
||||
url = response.headers['location'] # 获取重定向URL
|
||||
response = self.session.get( # 发送GET请求
|
||||
response = self._request( # 发送GET请求
|
||||
"get",
|
||||
url=url, # 重定向URL
|
||||
headers=self.headers, # 请求头
|
||||
allow_redirects=False # 不自动重定向
|
||||
|
||||
@@ -12,7 +12,6 @@ import base64 # Base64编码模块
|
||||
import hashlib # 哈希算法模块
|
||||
import json # JSON处理模块
|
||||
import os # 操作系统接口模块
|
||||
import pickle # 序列化模块
|
||||
import random # 随机数生成模块
|
||||
import shutil
|
||||
import subprocess # 子进程管理模块
|
||||
@@ -23,6 +22,7 @@ import requests # HTTP请求库
|
||||
from loguru import logger # 日志记录库
|
||||
from requests.cookies import RequestsCookieJar # Cookie处理
|
||||
|
||||
from detect.locked_pickle import load_pickle_locked, save_pickle_atomic
|
||||
|
||||
# 常量定义
|
||||
BASE_URL = "https://www.juming.com" # 聚名网基础URL
|
||||
@@ -170,11 +170,18 @@ class JM(object): # 聚名网API封装类
|
||||
|
||||
def __init__(self, proxies: Optional[Dict] = None): # 初始化方法
|
||||
self.session = requests.Session() # 创建会话对象
|
||||
self.session.timeout = REQUEST_TIMEOUT # 设置超时时间
|
||||
self.session.proxies = proxies # 设置代理
|
||||
self.request_timeout = max(
|
||||
0.5,
|
||||
float(os.getenv("DOMAINCHECK_JUMING_TIMEOUT", str(REQUEST_TIMEOUT)) or REQUEST_TIMEOUT),
|
||||
)
|
||||
self.base_url = BASE_URL # 设置基础URL
|
||||
logger.debug(proxies)
|
||||
|
||||
def _request(self, method: str, url: str, **kwargs):
|
||||
kwargs.setdefault("timeout", self.request_timeout)
|
||||
return getattr(self.session, method)(url, **kwargs)
|
||||
|
||||
def captcha_init(self) -> Tuple[bool, str]: # 初始化滑块验证码
|
||||
"""
|
||||
初始化滑块验证码
|
||||
@@ -194,7 +201,7 @@ class JM(object): # 聚名网API封装类
|
||||
# self.cookie.clear()
|
||||
# self.cookie.update({'acw_sc__v2': '69d948ed49117d89433d4316a79ada6f00d68f26'})
|
||||
# logger.info(self.cookie)
|
||||
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
|
||||
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie).json() # 发送请求
|
||||
logger.info(response)
|
||||
|
||||
if response['code'] == 1: # 初始化成功
|
||||
@@ -271,7 +278,7 @@ class JM(object): # 聚名网API封装类
|
||||
|
||||
url = f"{self.base_url}/captcha/verify" # 验证URL
|
||||
self.headers['Content-Type'] = 'application/json' # 设置内容类型
|
||||
response = self.session.post(url, headers=self.headers, data=verify_data, cookies=self.cookie).json() # 发送验证请求
|
||||
response = self._request("post", url, headers=self.headers, data=verify_data, cookies=self.cookie).json() # 发送验证请求
|
||||
if response['code'] == 1: # 验证成功
|
||||
self.token = response['data']['token'] # 保存token
|
||||
self.session_id = response['data']['session_id'] # 保存会话ID
|
||||
@@ -294,7 +301,7 @@ class JM(object): # 聚名网API封装类
|
||||
"""
|
||||
url = f"{self.base_url}/user_zh/wxdl_ewm" # 二维码URL
|
||||
try: # 异常处理
|
||||
response = self.session.post(url, headers=self.headers).json() # 发送请求
|
||||
response = self._request("post", url, headers=self.headers).json() # 发送请求
|
||||
self.token = response['data']["token"] # 保存token
|
||||
return response['code'] == 1, response['msg'], response['data']['url'] if response['code'] == 1 else None # 返回结果
|
||||
except Exception as e: # 异常处理
|
||||
@@ -338,7 +345,8 @@ class JM(object): # 聚名网API封装类
|
||||
}
|
||||
|
||||
login_url = f"{self.base_url}/user_zh/p_login" # 登录URL
|
||||
response = self.session.post( # 发送登录请求
|
||||
response = self._request( # 发送登录请求
|
||||
"post",
|
||||
url=login_url,
|
||||
headers=self.headers,
|
||||
json=data,
|
||||
@@ -384,7 +392,7 @@ class JM(object): # 聚名网API封装类
|
||||
url = f"{self.base_url}/ykj/get_list" # 列表URL
|
||||
try: # 异常处理
|
||||
self.headers['Content-Type'] = 'application/x-www-form-urlencoded' # 设置内容类型
|
||||
res = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie) # 发送请求
|
||||
res = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie) # 发送请求
|
||||
|
||||
response = res.json() # 解析响应
|
||||
if response['code'] == -401: # 需要验证码
|
||||
@@ -427,7 +435,7 @@ class JM(object): # 聚名网API封装类
|
||||
"""
|
||||
url = f"{self.base_url}/newcha/del_down?scsj={date}" # 下载URL
|
||||
try: # 异常处理
|
||||
response = self.session.get(url, headers=self.headers, cookies=self.cookie, allow_redirects=False) # 发送请求
|
||||
response = self._request("get", url, headers=self.headers, cookies=self.cookie, allow_redirects=False) # 发送请求
|
||||
url = response.headers['Location'] # 获取重定向URL
|
||||
response = self.session.get(url, timeout=DOWNLOAD_TIMEOUT) # 下载文件
|
||||
response_text = response.content.decode('utf-8') # 解码内容
|
||||
@@ -443,20 +451,12 @@ class JM(object): # 聚名网API封装类
|
||||
Args:
|
||||
filepath: 文件路径
|
||||
"""
|
||||
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
|
||||
pickle.dump(self.cookie, f) # 序列化保存
|
||||
save_pickle_atomic(filepath, self.cookie)
|
||||
|
||||
# 保存到Redis
|
||||
try:
|
||||
import redis
|
||||
from app.config import config
|
||||
redis_client = redis.Redis(
|
||||
host=config.REDIS_HOST,
|
||||
port=config.REDIS_PORT,
|
||||
password=config.REDIS_PASSWORD,
|
||||
db=config.REDIS_DB,
|
||||
decode_responses=True
|
||||
)
|
||||
from app.utils.redis_client import get_redis_client
|
||||
redis_client = get_redis_client(role="standard")
|
||||
# 将cookie转换为字典
|
||||
cookie_dict = {}
|
||||
for cookie in self.cookie:
|
||||
@@ -473,8 +473,10 @@ class JM(object): # 聚名网API封装类
|
||||
filepath: 文件路径
|
||||
"""
|
||||
try: # 异常处理
|
||||
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
|
||||
self.cookie = pickle.load(f) # 反序列化加载
|
||||
self.cookie = load_pickle_locked(
|
||||
filepath,
|
||||
default_factory=requests.cookies.RequestsCookieJar,
|
||||
)
|
||||
except: # 异常处理
|
||||
self.cookie = requests.cookies.RequestsCookieJar() # 创建空Cookie
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
|
||||
# 导入标准库
|
||||
import os # 操作系统接口
|
||||
import pickle # 序列化反序列化
|
||||
import re
|
||||
import time # 时间处理
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import pad
|
||||
import base64
|
||||
@@ -22,6 +20,7 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -69,30 +68,29 @@ class Juziseo:
|
||||
self.gt = None
|
||||
self.session = requests.Session() # 创建会话对象
|
||||
self.session.proxies = proxies # 设置代理
|
||||
self.session.timeout = 10 # 设置超时时间10秒
|
||||
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: 文件路径
|
||||
"""
|
||||
with open(filepath, "wb") as f: # 以二进制写入模式打开文件
|
||||
pickle.dump(self.cookie, f) # 序列化保存Cookie
|
||||
save_pickle_atomic(filepath, self.cookie)
|
||||
logger.info(f"已保存桔子SEO Cookie到 {filepath}")
|
||||
|
||||
# 保存到Redis
|
||||
try:
|
||||
import redis
|
||||
from app.config import config
|
||||
redis_client = redis.Redis(
|
||||
host=config.REDIS_HOST,
|
||||
port=config.REDIS_PORT,
|
||||
password=config.REDIS_PASSWORD,
|
||||
db=config.REDIS_DB,
|
||||
decode_responses=True
|
||||
)
|
||||
from app.utils.redis_client import get_redis_client
|
||||
redis_client = get_redis_client(role="standard")
|
||||
# 将cookie转换为字典
|
||||
cookie_dict = {}
|
||||
for cookie in self.cookie:
|
||||
@@ -108,8 +106,10 @@ class Juziseo:
|
||||
:param filepath: 文件路径
|
||||
"""
|
||||
try: # 尝试加载
|
||||
with open(filepath, "rb") as f: # 以二进制读取模式打开文件
|
||||
self.cookie = pickle.load(f) # 反序列化加载Cookie
|
||||
self.cookie = load_pickle_locked(
|
||||
filepath,
|
||||
default_factory=requests.cookies.RequestsCookieJar,
|
||||
)
|
||||
# logger.info(f"已从 {filepath} 加载桔子SEO Cookie")
|
||||
except: # 加载失败
|
||||
self.cookie = requests.cookies.RequestsCookieJar() # 创建空的CookieJar
|
||||
@@ -117,7 +117,7 @@ class Juziseo:
|
||||
|
||||
def start_msg_captcha_servlet(self):
|
||||
url = f"{self.base_url}/class/gtcode/msg/StartMsgCaptchaServlet.php"
|
||||
response = self.session.get(url, headers=self.headers, cookies=self.cookie).json()
|
||||
response = self._request("get", url, headers=self.headers, cookies=self.cookie).json()
|
||||
logger.info(response)
|
||||
self.gt = response['gt']
|
||||
self.challenge = response['challenge']
|
||||
@@ -161,7 +161,7 @@ class Juziseo:
|
||||
logger.debug(data)
|
||||
# 发送登录请求
|
||||
url = f"{self.base_url}/account/ajax/login_process/"
|
||||
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie)
|
||||
response = self._request("post", url, headers=self.headers, data=data, cookies=self.cookie)
|
||||
logger.info(response.text)
|
||||
# 解析响应
|
||||
result = response.json()
|
||||
@@ -190,11 +190,11 @@ class Juziseo:
|
||||
'_post_type': 'ajax',
|
||||
}
|
||||
url = f"{self.base_url}/snapshot/save/"
|
||||
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json()
|
||||
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.session.get(url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
|
||||
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)
|
||||
@@ -221,11 +221,11 @@ class Juziseo:
|
||||
'_post_type': 'ajax',
|
||||
}
|
||||
url = f"{self.base_url}/domain_rank/save_domain/"
|
||||
response = self.session.post(url, headers=self.headers, data=data, cookies=self.cookie).json()
|
||||
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.session.get(url, headers=self.headers, cookies=self.cookie).content.decode('utf-8')
|
||||
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)}"
|
||||
|
||||
91
domainCheck/detect/locked_pickle.py
Normal file
91
domainCheck/detect/locked_pickle.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""Helpers for cross-process safe pickle persistence."""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import tempfile
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
try: # pragma: no cover - platform specific
|
||||
import fcntl
|
||||
except ImportError: # pragma: no cover - platform specific
|
||||
fcntl = None
|
||||
|
||||
try: # pragma: no cover - platform specific
|
||||
import msvcrt
|
||||
except ImportError: # pragma: no cover - platform specific
|
||||
msvcrt = None
|
||||
|
||||
|
||||
def _acquire_platform_lock(lock_file, *, timeout_seconds: float) -> None:
|
||||
deadline = time.monotonic() + max(0.1, float(timeout_seconds or 0.0))
|
||||
while True:
|
||||
try:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
elif msvcrt is not None:
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
return
|
||||
except (BlockingIOError, OSError):
|
||||
if time.monotonic() >= deadline:
|
||||
raise TimeoutError(f"lock acquire timed out for {lock_file.name}")
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def _release_platform_lock(lock_file) -> None:
|
||||
try:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
elif msvcrt is not None:
|
||||
lock_file.seek(0)
|
||||
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _locked_path(path: str, *, timeout_seconds: float = 10.0):
|
||||
normalized_path = os.path.abspath(path)
|
||||
directory = os.path.dirname(normalized_path) or "."
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
lock_path = f"{normalized_path}.lock"
|
||||
with open(lock_path, "a+b") as lock_file:
|
||||
_acquire_platform_lock(lock_file, timeout_seconds=timeout_seconds)
|
||||
try:
|
||||
yield normalized_path
|
||||
finally:
|
||||
_release_platform_lock(lock_file)
|
||||
|
||||
|
||||
def save_pickle_atomic(path: str, value, *, timeout_seconds: float = 10.0) -> None:
|
||||
with _locked_path(path, timeout_seconds=timeout_seconds) as normalized_path:
|
||||
directory = os.path.dirname(normalized_path) or "."
|
||||
fd, temp_path = tempfile.mkstemp(
|
||||
prefix=f".{os.path.basename(normalized_path)}.",
|
||||
suffix=".tmp",
|
||||
dir=directory,
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as temp_file:
|
||||
pickle.dump(value, temp_file)
|
||||
temp_file.flush()
|
||||
os.fsync(temp_file.fileno())
|
||||
os.replace(temp_path, normalized_path)
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def load_pickle_locked(path: str, *, default_factory: Callable[[], T], timeout_seconds: float = 10.0) -> T:
|
||||
with _locked_path(path, timeout_seconds=timeout_seconds) as normalized_path:
|
||||
if not os.path.exists(normalized_path):
|
||||
return default_factory()
|
||||
with open(normalized_path, "rb") as source:
|
||||
return pickle.load(source)
|
||||
@@ -11,6 +11,7 @@
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timezone, timedelta # 日期时间处理,用于时区转换
|
||||
from typing import Optional
|
||||
|
||||
@@ -22,10 +23,24 @@ _DIRECT_HTTP = requests.Session()
|
||||
_DIRECT_HTTP.trust_env = False
|
||||
_DIRECT_HTTP.mount("http://", requests.adapters.HTTPAdapter(pool_connections=256, pool_maxsize=512, max_retries=0))
|
||||
_DIRECT_HTTP.mount("https://", requests.adapters.HTTPAdapter(pool_connections=256, pool_maxsize=512, max_retries=0))
|
||||
_PROXY_MANAGERS = {}
|
||||
_PROXY_MANAGERS = OrderedDict()
|
||||
_PROXY_MANAGER_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _proxy_manager_cache_limit() -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv("DOMAINCHECK_REGISTER_PROXY_SESSION_CACHE_SIZE", "128") or 128))
|
||||
except Exception:
|
||||
return 128
|
||||
|
||||
|
||||
def _close_session_quietly(session) -> None:
|
||||
try:
|
||||
session.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _get_http_manager(proxies: Optional[dict] = None):
|
||||
proxy_url = ""
|
||||
if proxies:
|
||||
@@ -34,12 +49,17 @@ def _get_http_manager(proxies: Optional[dict] = None):
|
||||
return _DIRECT_HTTP
|
||||
with _PROXY_MANAGER_LOCK:
|
||||
manager = _PROXY_MANAGERS.get(proxy_url)
|
||||
if manager is None:
|
||||
manager = requests.Session()
|
||||
manager.trust_env = False
|
||||
manager.mount("http://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
manager.mount("https://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
_PROXY_MANAGERS[proxy_url] = manager
|
||||
if manager is not None:
|
||||
_PROXY_MANAGERS.move_to_end(proxy_url)
|
||||
return manager
|
||||
manager = requests.Session()
|
||||
manager.trust_env = False
|
||||
manager.mount("http://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
manager.mount("https://", requests.adapters.HTTPAdapter(pool_connections=128, pool_maxsize=256, max_retries=0))
|
||||
_PROXY_MANAGERS[proxy_url] = manager
|
||||
while len(_PROXY_MANAGERS) > _proxy_manager_cache_limit():
|
||||
_, stale_manager = _PROXY_MANAGERS.popitem(last=False)
|
||||
_close_session_quietly(stale_manager)
|
||||
return manager
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user