|
|
@@ -773,7 +773,7 @@ class SpiderMonitor(threading.Thread):
|
|
|
|
|
|
|
|
|
class MTScreenshot:
|
|
|
- def __init__(self, d, oss_config, search_key, title_key, scroll_times=1, compress_quality=7, resize_ratio=0.8,device_id=None,
|
|
|
+ def __init__(self, d, oss_config, search_key, title_key, scroll_times=2, compress_quality=7, resize_ratio=0.8,device_id=None,
|
|
|
monitor=None):
|
|
|
self.device_id = device_id
|
|
|
# 接收外部已连接好的u2设备实例
|
|
|
@@ -993,6 +993,10 @@ class MTScreenshot:
|
|
|
|
|
|
|
|
|
class MT:
|
|
|
+ # 封号检测:类变量,跨实例共享,不因重建实例而重置
|
|
|
+ open_page_fail_count = 0
|
|
|
+ open_page_fail_threshold = 6
|
|
|
+
|
|
|
def __init__(
|
|
|
self,
|
|
|
key,
|
|
|
@@ -1079,6 +1083,7 @@ class MT:
|
|
|
self.request_error_threshold = REQUEST_ERROR_STOP_THRESHOLD
|
|
|
self.product_link_missing_count = 0
|
|
|
self.product_link_missing_threshold = PRODUCT_LINK_STOP_THRESHOLD
|
|
|
+ # 封号检测:open_product_list_page 连续失败计数(类变量,跨实例共享,不因重建实例而重置)
|
|
|
# 风控卡死检测:"加载更多"按钮检测
|
|
|
self.load_more_check_rounds = 4
|
|
|
self.load_more_check_interval = 30
|
|
|
@@ -1354,12 +1359,18 @@ class MT:
|
|
|
normalized_title_key = normalize_match_text(self.title_key)
|
|
|
normalized_brand = normalize_match_text(self.brand)
|
|
|
|
|
|
+ # 1. 药品名必须在标题里
|
|
|
if normalized_title_key != "" and normalized_title_key not in normalized_title:
|
|
|
print(f"当前商品名称:{product_title} 不包含{self.title_key}关键字")
|
|
|
return False
|
|
|
- if normalized_brand != "" and normalized_brand not in normalized_title:
|
|
|
- print(f"当前商品名称:{product_title} 不包含{self.brand}品牌")
|
|
|
+
|
|
|
+ # 2. 把药品名从标题里去掉后再检查品牌
|
|
|
+ # (避免品牌是通用名子串的干扰,如品牌"午时" ⊂ 药名"午时茶颗粒")
|
|
|
+ check_target = normalized_title.replace(normalized_title_key, "") if normalized_title_key else normalized_title
|
|
|
+ if normalized_brand != "" and normalized_brand not in check_target:
|
|
|
+ print(f"当前商品名称:{product_title} 去掉药名后不包含{self.brand}品牌")
|
|
|
return False
|
|
|
+
|
|
|
if not self.is_link_spec_useful(product_title):
|
|
|
print(f"当前商品名称:{product_title} 不包含{self.spec_list}品规")
|
|
|
return False
|
|
|
@@ -3617,6 +3628,7 @@ class MT:
|
|
|
return "open_product_list_page"
|
|
|
if step_name == "open_product_list_page":
|
|
|
self.safe_exec(self.open_product_list_page)
|
|
|
+ MT.open_page_fail_count = 0 # 成功则重置封号计数
|
|
|
return "collect_single_product"
|
|
|
if step_name == "collect_single_product":
|
|
|
has_next = self.safe_exec(self.collect_single_product)
|
|
|
@@ -3657,6 +3669,15 @@ class MT:
|
|
|
print(f'{current_step} 执行异常: {e}')
|
|
|
time.sleep(3)
|
|
|
step_failures[current_step] += 1
|
|
|
+ # 封号检测:open_product_list_page 连续失败
|
|
|
+ if current_step == "open_product_list_page":
|
|
|
+ MT.open_page_fail_count += 1
|
|
|
+ print(f"⚠️ open_product_list_page 连续失败 {MT.open_page_fail_count}/{MT.open_page_fail_threshold} 次")
|
|
|
+ if MT.open_page_fail_count >= MT.open_page_fail_threshold:
|
|
|
+ msg = f"疑似账号被封禁:open_product_list_page 连续失败 {self.open_page_fail_threshold} 次"
|
|
|
+ logging.error(msg)
|
|
|
+ self.finish_task_abnormally(self.get_current_page_no(), msg)
|
|
|
+ raise CollectionStopError(msg)
|
|
|
retry_limit = self.workflow_retry_limit.get(current_step)
|
|
|
next_action = self.workflow_error_action.get(current_step)
|
|
|
record_restart_reason(
|
|
|
@@ -3683,8 +3704,6 @@ class MT:
|
|
|
self.monitor.join()
|
|
|
|
|
|
|
|
|
-
|
|
|
-
|
|
|
def fetch_task_from_scheduler(scheduler, device_id):
|
|
|
"""从已有的调度器获取一个任务,转换为 device_list 兼容的格式。没有任务返回 None。"""
|
|
|
task = scheduler.get_task()
|
|
|
@@ -3891,11 +3910,22 @@ def run_device(device_id, scheduler=None):
|
|
|
scheduler.stop()
|
|
|
|
|
|
|
|
|
+class DeviceIdLogFilter(logging.Filter):
|
|
|
+ """给每条日志注入当前设备ID(运行时读取全局 DEVICE_ID,终端传参后自动生效)"""
|
|
|
+ def filter(self, record):
|
|
|
+ record.device_id = DEVICE_ID
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
def main():
|
|
|
logging.basicConfig(
|
|
|
level=logging.INFO,
|
|
|
- format='%(asctime)s [%(threadName)s] %(levelname)s: %(message)s'
|
|
|
+ format='%(asctime)s [%(threadName)s] [%(device_id)s] %(levelname)s: %(message)s'
|
|
|
)
|
|
|
+ # 给所有 root handler 挂上设备ID过滤器
|
|
|
+ _device_filter = DeviceIdLogFilter()
|
|
|
+ for _h in logging.getLogger().handlers:
|
|
|
+ _h.addFilter(_device_filter)
|
|
|
|
|
|
# 终端传入设备 ID:python main.py T4VK4LM7AAUOV8AY
|
|
|
global DEVICE_ID
|
|
|
@@ -3903,6 +3933,13 @@ def main():
|
|
|
DEVICE_ID = sys.argv[1].strip()
|
|
|
logging.info(f"使用终端传入设备: {DEVICE_ID}")
|
|
|
|
|
|
+ # 同步设备ID到 commons.Logger(scheduler 等模块的日志带上设备ID)
|
|
|
+ try:
|
|
|
+ from commons.Logger import set_device_id
|
|
|
+ set_device_id(DEVICE_ID)
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+
|
|
|
# 自动模式:全局只创建一个调度器 + 一个心跳线程,所有任务复用
|
|
|
scheduler = None
|
|
|
if not MANUAL_MODE:
|
|
|
@@ -3983,4 +4020,3 @@ device_list = {
|
|
|
if __name__ == '__main__':
|
|
|
main()
|
|
|
|
|
|
-
|