|
|
@@ -10,13 +10,19 @@ from DrissionPage import ChromiumPage, ChromiumOptions
|
|
|
import json
|
|
|
import hashlib
|
|
|
from commons.Logger import get_spider_logger
|
|
|
-from commons.conn_mysql import MySQLPoolOnline
|
|
|
+from commons.conn_mysql import MySQLPoolOn2
|
|
|
from pipelines.drug_pipelines import DrugPipeline
|
|
|
from commons.feishu_webhook import send_text
|
|
|
-from spiders.jd.jd_captcha import handle_jd_slider_captcha
|
|
|
from oss_upload.oss_upload import AliyunOSSUploader
|
|
|
+import threading
|
|
|
+from commons.config import JD_DEVICE_ID
|
|
|
+
|
|
|
+import requests
|
|
|
+
|
|
|
logger = get_spider_logger("jd")
|
|
|
|
|
|
+jd_log = get_spider_logger("jd_log")
|
|
|
+
|
|
|
chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
|
|
|
|
|
|
FETCH_TIMEOUT_FIRST = 5
|
|
|
@@ -35,11 +41,14 @@ return r.bottom > 80 && r.top < h - 40 && r.right > 0 && r.left < w;
|
|
|
"""
|
|
|
|
|
|
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
class JdCrawlerV2:
|
|
|
- def __init__(self, drug_dict=None):
|
|
|
+ def __init__(self, drug_dict=None,scheduler=None):
|
|
|
self.driver = None
|
|
|
self.register_signal_handler()
|
|
|
- self.db = MySQLPoolOnline()
|
|
|
+ self.db = MySQLPoolOn2()
|
|
|
self.ip = None
|
|
|
self.account_name = None
|
|
|
self.login_username = None
|
|
|
@@ -50,11 +59,20 @@ class JdCrawlerV2:
|
|
|
self.ossuploader = AliyunOSSUploader()
|
|
|
self.start_page = 1
|
|
|
self.end_page = 1
|
|
|
+ self.report_data={}
|
|
|
+ self.heartbeat_interval = 30
|
|
|
+
|
|
|
+ self.scheduler = scheduler
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
if self.task_dict:
|
|
|
self.get_product_data()
|
|
|
self.success = True
|
|
|
self.is_no_prodcut = 0
|
|
|
|
|
|
+
|
|
|
def get_product_data(self):
|
|
|
self.task_id = self.task_dict["id"]
|
|
|
self.company_id = self.task_dict["company_id"]
|
|
|
@@ -70,8 +88,10 @@ class JdCrawlerV2:
|
|
|
self.account_id = self.task_dict.get("collect_equipment_account_id", "")
|
|
|
self.collect_region_id = self.task_dict.get("collect_region_id", "")
|
|
|
self.collect_round = self.task_dict.get("collect_round", 1)
|
|
|
- self.start_page = self._parse_page(self.task_dict.get("start_page"), 1)
|
|
|
+ self.start_page = self._parse_page(self.task_dict.get("current_page")+1, 1)
|
|
|
self.end_page = self._parse_page(self.task_dict.get("end_page"), 15)
|
|
|
+ self.username = self.task_dict.get("username",'')
|
|
|
+ self.report_data = {'task_id':self.task_id,'platform':self.platform,'username':self.task_dict.get("username",JD_DEVICE_ID)}
|
|
|
|
|
|
@staticmethod
|
|
|
def _parse_page(value, default=1):
|
|
|
@@ -91,7 +111,7 @@ class JdCrawlerV2:
|
|
|
def init_browser(self):
|
|
|
co = ChromiumOptions().set_browser_path(chrome_path)
|
|
|
debug_port = self._get_free_port()
|
|
|
- co.set_user_data_path(f"./spiders/jd/{self.account_name}")
|
|
|
+ co.set_user_data_path(f"./spiders/jd/test/")
|
|
|
|
|
|
co.set_local_port(debug_port)
|
|
|
co.set_argument(f"--remote-debugging-port={debug_port}")
|
|
|
@@ -100,6 +120,15 @@ class JdCrawlerV2:
|
|
|
co.set_argument("--disable-dev-shm-usage")
|
|
|
co.set_argument("--no-first-run") # 避免首次运行弹窗
|
|
|
co.set_argument("--no-default-browser-check") # 避免默认浏览器检查
|
|
|
+ #co.headless(True)
|
|
|
+ co.set_user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
|
|
|
+
|
|
|
+ # 3. 添加反检测启动参数
|
|
|
+ co.set_argument('--disable-blink-features=AutomationControlled') # 禁用自动化控制标志
|
|
|
+ co.set_argument('--disable-gpu') # 禁用 GPU(无头模式常用)
|
|
|
+ co.set_argument('--no-sandbox') # 禁用沙箱
|
|
|
+ co.set_argument('--start-maximized') # 启动时最大化窗口
|
|
|
+ co.set_argument('--disable-infobars') # 禁用信息栏提示
|
|
|
|
|
|
if self.ip:
|
|
|
proxy = self.ip.strip()
|
|
|
@@ -118,6 +147,8 @@ class JdCrawlerV2:
|
|
|
self._listen_started = True
|
|
|
logger.info("已启动搜索接口监听")
|
|
|
|
|
|
+
|
|
|
+
|
|
|
def register_signal_handler(self):
|
|
|
def handler(signum, frame):
|
|
|
print("\n⚠️ 程序退出")
|
|
|
@@ -312,8 +343,10 @@ class JdCrawlerV2:
|
|
|
ele_xpath = "//div[@id='main_search_conter']//div[contains(@class,'_goodsContainer_')]/div[@data-sku=" + "'" + sku_id + "'" + "]"
|
|
|
ele_screen = self.driver.ele("xpath="+ele_xpath)
|
|
|
upload_key = hashlib.md5(item_url.encode("utf-8")).hexdigest()
|
|
|
- snap_url = self._take_snapshot(upload_key,ele_screen)
|
|
|
|
|
|
+ #后续开启
|
|
|
+ #snap_url = self._take_snapshot(upload_key,ele_screen)
|
|
|
+ snap_url = ''
|
|
|
try:
|
|
|
price = Decimal(str(low_price)).quantize(Decimal("0.00"))
|
|
|
except (InvalidOperation, ValueError):
|
|
|
@@ -526,9 +559,7 @@ class JdCrawlerV2:
|
|
|
|
|
|
time.sleep(random.uniform(3, 5))
|
|
|
|
|
|
- if not handle_jd_slider_captcha(self.driver):
|
|
|
- print("滑块验证码未通过")
|
|
|
- return False
|
|
|
+
|
|
|
|
|
|
return True
|
|
|
|
|
|
@@ -580,8 +611,32 @@ class JdCrawlerV2:
|
|
|
return False
|
|
|
self.sleep(2, 4)
|
|
|
return True
|
|
|
+
|
|
|
+ def _error_report(self,data):
|
|
|
+ self.report_data.update(data)
|
|
|
+ self.scheduler.stop()
|
|
|
+ self.scheduler.post_report(self.report_data)
|
|
|
+
|
|
|
+ def _success_report(self,data):
|
|
|
+ self.report_data.update(data)
|
|
|
+ self.scheduler.post_report(self.report_data)
|
|
|
+
|
|
|
+ def post_report(self,data):
|
|
|
+ url = "http://192.168.2.246:8080/api/collect_task/report"
|
|
|
+ print('传给返回接口的数据',data)
|
|
|
+ headers = {'X-Crawler-Token':'zhijiayun_crawler_2026'}
|
|
|
+ response = requests.post(url, json=data,headers =headers)
|
|
|
+ #记录日志
|
|
|
+ result = response.json()
|
|
|
+
|
|
|
+ if(result.get('code')!='success'):
|
|
|
+ logger.info(f'翻页回传:{result}')
|
|
|
+ self.scheduler.stop()
|
|
|
+ #用守护线程 直接杀死该进程
|
|
|
+ print(f'任务进度上传 {result}')
|
|
|
|
|
|
def crawl(self):
|
|
|
+
|
|
|
total = 0
|
|
|
keyword = self._build_search_keyword()
|
|
|
|
|
|
@@ -590,12 +645,20 @@ class JdCrawlerV2:
|
|
|
|
|
|
if self._is_logged_out():
|
|
|
if not self.login_password or not self.login_username:
|
|
|
+ #这里要报账号异常错误,没登录账号
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':self.start_page,'exception_type':1})
|
|
|
+
|
|
|
return
|
|
|
if not self._ensure_logged_in():
|
|
|
self.disable_account()
|
|
|
send_text(f"京东:{self.account_name}账号登录失败")
|
|
|
+ jd_log.info(f'出现要求登录:::::{self.driver.html}')
|
|
|
self.success = False
|
|
|
+
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':self.start_page,'exception_type':1})
|
|
|
+
|
|
|
return
|
|
|
+
|
|
|
self.driver.get("https://www.jd.com/", timeout=15)
|
|
|
self.sleep(3, 5)
|
|
|
|
|
|
@@ -607,32 +670,46 @@ class JdCrawlerV2:
|
|
|
f"https://search.jd.com/Search?keyword={kw}&enc=utf-8&wq={kw}", timeout=15
|
|
|
)
|
|
|
self.sleep(5, 8)
|
|
|
-
|
|
|
- if self._check_page_blocked():
|
|
|
+ if self._check_page_blocked(): #禁止搜索
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':self.start_page,'exception_type':2})
|
|
|
return
|
|
|
|
|
|
- if not handle_jd_slider_captcha(self.driver, pause_listen=False):
|
|
|
- logger.warning("进入搜索页后滑块验证码处理失败")
|
|
|
- self.success = False
|
|
|
- return
|
|
|
+ #这一步捕捉异常:
|
|
|
+ html = self.driver.html or ""
|
|
|
+ if "请刷新或切换账户试试" in html:
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':page_no,'exception_type':2})
|
|
|
+ return
|
|
|
+
|
|
|
+
|
|
|
+ total_page = int(re.search(r'(\d+)', self.driver.ele('@class:_pagination_total_1gv36_61', timeout=2).text).group(1))
|
|
|
+ self.report_data.update({'total_pages':total_page})
|
|
|
+
|
|
|
+
|
|
|
|
|
|
if self.start_page > 1:
|
|
|
if not self._jump_to_page(self.start_page):
|
|
|
logger.warning("跳页失败,将从第 1 页开始采集")
|
|
|
self.start_page = 1
|
|
|
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':self.start_page,'exception_type ':4})
|
|
|
+ return #说明页数可能出错,直接返回结束任务且汇报
|
|
|
+
|
|
|
+
|
|
|
logger.info(
|
|
|
"采集页码范围: %s ~ %s(共 %s 页)",
|
|
|
self.start_page,
|
|
|
- self.end_page,
|
|
|
+ total_page,
|
|
|
self.end_page - self.start_page + 1,
|
|
|
)
|
|
|
|
|
|
- for page_no in range(self.start_page, self.end_page + 1):
|
|
|
+ for page_no in range(self.start_page, total_page+1):
|
|
|
if self._is_logged_out():
|
|
|
if not self._ensure_logged_in():
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':page_no,'exception_type':1})
|
|
|
+
|
|
|
self.success = False
|
|
|
break
|
|
|
+
|
|
|
self.driver.get(
|
|
|
f"https://search.jd.com/Search?keyword={kw}&enc=utf-8&wq={kw}",
|
|
|
timeout=15,
|
|
|
@@ -641,17 +718,17 @@ class JdCrawlerV2:
|
|
|
if page_no > 1:
|
|
|
self._jump_to_page(page_no)
|
|
|
|
|
|
- if not handle_jd_slider_captcha(self.driver, pause_listen=True):
|
|
|
- logger.warning("滑块验证码处理失败,停止采集")
|
|
|
- self.success = False
|
|
|
- break
|
|
|
+
|
|
|
|
|
|
if self._check_page_blocked():
|
|
|
break
|
|
|
|
|
|
+ self._success_report({'is_finished':0,'need_reassign':0,'current_page':page_no})
|
|
|
+
|
|
|
logger.info("===== 正在爬取第 %s 页 =====", page_no)
|
|
|
search_ele = self.driver.ele("xpath=//div[@id='search-condition']", timeout=10)
|
|
|
if not search_ele:
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':page_no,'exception_type':2})
|
|
|
logger.warning("未找到搜索结果区域,停止采集")
|
|
|
break
|
|
|
|
|
|
@@ -660,28 +737,54 @@ class JdCrawlerV2:
|
|
|
total += page_n
|
|
|
logger.info("累计监听条数: %s", total)
|
|
|
|
|
|
+ if self.scheduler.end ==True:
|
|
|
+ logger.info('心跳失败')
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':0,'current_page':page_no})
|
|
|
+ break
|
|
|
if self.is_no_prodcut > 20:
|
|
|
+
|
|
|
logger.info("连续无匹配商品过多,停止采集")
|
|
|
+ self._success_report({'is_finished':1,'need_reassign':0,'current_page':page_no})
|
|
|
break
|
|
|
|
|
|
if page_no >= self.end_page:
|
|
|
+ self._success_report({'is_finished':1,'need_reassign':0,'current_page':page_no})
|
|
|
break
|
|
|
|
|
|
next_btn = self.driver.ele("text=下一页", timeout=2)
|
|
|
if not next_btn:
|
|
|
logger.info("没有下一页(未找到)")
|
|
|
+ self._success_report({'is_finished':1,'need_reassign':0,'current_page':page_no})
|
|
|
break
|
|
|
+
|
|
|
cls_str = next_btn.attr("class") or ""
|
|
|
if "disabled" in cls_str:
|
|
|
logger.info("没有下一页(已禁用)")
|
|
|
+ self._success_report({'is_finished':1,'need_reassign':0,'current_page':page_no})
|
|
|
+
|
|
|
break
|
|
|
|
|
|
if not self._go_next_page(next_btn):
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':page_no,'exception_type':4})
|
|
|
break
|
|
|
+ if page_no==total_page: #达到最大页数
|
|
|
+ self._success_report({'is_finished':1,'need_reassign':0,'current_page':page_no})
|
|
|
+ break
|
|
|
+ self.scheduler.stop()
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
|
|
|
def run(self):
|
|
|
# 检测账号
|
|
|
- if not self.get_account():
|
|
|
+
|
|
|
+ # 启动守护线程
|
|
|
+ # self._daemon_thread = threading.Thread(target=self._heartbeat_reporter)
|
|
|
+ # self._daemon_thread.daemon = True # 守护线程,主线程退出时自动结束
|
|
|
+ # self._daemon_thread.start()
|
|
|
+
|
|
|
+ #if self.get_account():
|
|
|
+ if 0:
|
|
|
logger.info("==================当前无账号可用==================")
|
|
|
self.success = False
|
|
|
return self.pipeline.crawl_count, self.success
|
|
|
@@ -698,7 +801,7 @@ class JdCrawlerV2:
|
|
|
self.success = False
|
|
|
logger.exception("爬取异常: %s", e)
|
|
|
self.sleep(3, 5)
|
|
|
-
|
|
|
+ self._error_report({'is_finished':0,'need_reassign':1,'current_page':self.start_page,'exception_type':3})
|
|
|
finally:
|
|
|
if self.driver:
|
|
|
self.driver.quit()
|