pdd_new1.py 64 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465
  1. import requests
  2. import base64
  3. import uiautomator2 as u2
  4. import time
  5. import sys
  6. import subprocess
  7. import re
  8. import random
  9. import json
  10. from aip import AipOcr
  11. import numpy as np
  12. import cv2
  13. import os
  14. from pdd_config import Config
  15. import logging
  16. from logger import setup_logger
  17. import pymysql
  18. from 拼多多盒数处理脚本.main import extract_box_number
  19. import datetime
  20. setup_logger("pdd_spider") # 初始化日志
  21. def get_mysql():
  22. return pymysql.connect(
  23. host='39.108.116.125', # "localhost", # 修改后的主机
  24. port=3306, # 3306, # 添加端口号
  25. user='drug_retrieve', # 'root', # 修改后的用户名
  26. password='Pem287cwM58jNpe2', # 修改后的密码
  27. db='drug_retrieve', #
  28. charset='utf8mb4'
  29. )
  30. def get_task_mysql():
  31. task_cnn = pymysql.connect(
  32. host='39.108.116.125', # "localhost", # 修改后的主机
  33. port=3306, # 3306, # 添加端口号
  34. user='drug_retrieve', # 'root', # 修改后的用户名
  35. password='Pem287cwM58jNpe2', # 修改后的密码
  36. db='drug_retrieve', # "drug_data", # 修改后的数据库名
  37. charset='utf8mb4'
  38. )
  39. try:
  40. cursor = task_cnn.cursor()
  41. result = {} # 用于存储两个表的返回数据
  42. # ---------- 任务表 ----------
  43. sql1 = "SELECT * FROM retrieve_collect_task_allocate WHERE platform = 3 AND status = 1"
  44. cursor.execute(sql1)
  45. row1 = cursor.fetchone()
  46. print(row1)
  47. if row1:
  48. result['data'] = row1
  49. else:
  50. print("表1没有找到 platform=3 且 status=1 的记录")
  51. result['data'] = None
  52. # ---------- 设备表----------
  53. # 条件:name 包含 'pdd' 且 status = 0
  54. keyword = "pdd"
  55. dynamic_id = row1[2]
  56. sql2 = "SELECT * FROM retrieve_collect_equipment WHERE name LIKE %s AND id = %s"
  57. cursor.execute(sql2, (f'%{keyword}%', dynamic_id))
  58. row2 = cursor.fetchone()
  59. if row2:
  60. result['device_id'] = row2[2]
  61. else:
  62. print("设备表没有找到 name 包含 'pdd' 且 status=0 的记录")
  63. result['device_id'] = None
  64. # 提交所有更新
  65. task_cnn.commit()
  66. return result
  67. except Exception as e:
  68. print(f"操作出错: {e}")
  69. task_cnn.rollback()
  70. return None
  71. finally:
  72. cursor.close()
  73. task_cnn.close()
  74. def get_all_tasks():
  75. """
  76. 获取所有符合条件的任务,返回任务列表
  77. 返回格式: [{'data': row1, 'device_id': device_id}, ...]
  78. """
  79. task_cnn = pymysql.connect(
  80. host='39.108.116.125',
  81. port=3306,
  82. user='drug_retrieve',
  83. password='Pem287cwM58jNpe2',
  84. db='drug_retrieve',
  85. charset='utf8mb4'
  86. )
  87. result_list = []
  88. try:
  89. cursor = task_cnn.cursor()
  90. # ---------- 获取所有符合条件的任务 ----------
  91. sql1 = "SELECT * FROM retrieve_collect_task_allocate WHERE platform = 3 AND status = 1"
  92. cursor.execute(sql1)
  93. rows = cursor.fetchall() # 获取所有记录
  94. if not rows:
  95. print("没有找到 platform=3 且 status=1 的任务记录")
  96. return result_list
  97. print(f"找到 {len(rows)} 个待执行任务")
  98. # ---------- 遍历每个任务,获取对应的设备 ----------
  99. for row in rows:
  100. task_info = {}
  101. task_info['data'] = row
  102. # 获取 dynamic_id(假设在索引2的位置,请根据实际表结构调整)
  103. dynamic_id = row[2] # 如果索引不对,请修改
  104. keyword = "pdd"
  105. # 查询设备表
  106. sql2 = "SELECT * FROM retrieve_collect_equipment WHERE name LIKE %s AND id = %s"
  107. cursor.execute(sql2, (f'%{keyword}%', dynamic_id))
  108. device_row = cursor.fetchone()
  109. if device_row:
  110. task_info['device_id'] = device_row[2] # 假设设备ID在索引2
  111. print(f"任务 {dynamic_id} 分配到设备: {task_info['device_id']}")
  112. else:
  113. print(f"警告: 任务 {dynamic_id} 没有找到可用设备")
  114. task_info['device_id'] = None
  115. result_list.append(task_info)
  116. task_cnn.commit()
  117. return result_list
  118. except Exception as e:
  119. print(f"操作出错: {e}")
  120. task_cnn.rollback()
  121. return []
  122. finally:
  123. cursor.close()
  124. task_cnn.close()
  125. def report_api(task_id,page=None,start=None,end_page=None,end_time=None):
  126. params = {
  127. "collect_task_allocate_id": task_id,
  128. "statr_page":page,
  129. "end_page":end_page,
  130. "status": start,
  131. "finish_status": 0,
  132. "start_time": int(time.time()),
  133. "end_time": '',
  134. }
  135. print(params)
  136. url = "http://schedule.dfwy.tech/api/collect_equipment_execute/result_report"
  137. res = requests.get(url, params=params, timeout=20)
  138. print(res.text)
  139. # 获取滑块验证中滑块需要移动的距离
  140. def slide_verify(img_path):
  141. with open(img_path, 'rb') as f:
  142. b = base64.b64encode(f.read()).decode() ## 图片二进制流base64字符串
  143. url = "http://api.jfbym.com/api/YmServer/customApi"
  144. data = {
  145. ## 关于参数,一般来说有3个;不同类型id可能有不同的参数个数和参数名,找客服获取
  146. "token": "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk",
  147. "type": "22222",
  148. "image": b,
  149. }
  150. _headers = {
  151. "Content-Type": "application/json"
  152. }
  153. response = requests.request("POST", url, headers=_headers, json=data).json()
  154. print(response)
  155. if response.get("msg") == "识别成功":
  156. # 获取 data 中的 data 字段
  157. result = response.get("data", {}).get("data")
  158. if result:
  159. print(result) # 输出结果
  160. else:
  161. print("无法获取数据")
  162. else:
  163. print("识别未成功")
  164. return result
  165. class PDD:
  166. def __init__(
  167. self,
  168. search_key,
  169. device_id,
  170. title_key=None,
  171. spec_list=None,
  172. brand="",
  173. save_search_key=None,
  174. max_counts_limit=None,
  175. direct_shop_lookup=False,
  176. sort=None,
  177. platform = None,
  178. task_id = None
  179. ):
  180. self.package_name = 'com.xunmeng.pinduoduo'
  181. self.APP_ID = '116857964'
  182. self.API_KEY = '1gAzACJOAr7BeILKqkqPOETh'
  183. self.SECRET_KEY = 'ZNArANb9GwJYgLKg4EfYhukKBfPdl1n3'
  184. self.client = AipOcr(self.APP_ID, self.API_KEY, self.SECRET_KEY)
  185. self.table_name = "retrieve_scrape_data" # "pdd_drug"
  186. self.shop_table_name = "retrieve_scrape_data" # "pdd_shop_info"
  187. self.loggerPdd = logging.getLogger()
  188. self.clipboard = "" # 初始化剪切板的内容为空
  189. self.task_id = task_id
  190. self.platform = platform
  191. self.sort = sort
  192. self.sort_key = 0
  193. self.search_key = search_key # 参苓健脾胃颗粒 香砂平胃颗粒 舒肝颗粒 清肺化痰丸
  194. self.title_key = title_key if title_key is not None else search_key
  195. self.spec_list = self._normalize_rule_list(spec_list)
  196. self.brand = brand
  197. self.save_search_key = save_search_key or search_key
  198. self.max_counts_limit = max_counts_limit
  199. self.direct_shop_lookup = direct_shop_lookup
  200. self.unrelated_data = 0 # 无关数据数量
  201. self.device_id = device_id
  202. self.page = 0
  203. # 统计售罄数量
  204. self.sold_out_counts = 0
  205. # 程序启动时间
  206. self.program_start_time = self.app_start_time()
  207. # 统计商品数量
  208. # 最大量数据阈值
  209. self.max_counts = 0
  210. # 统计点击商品的次数
  211. self.click_counts = 0
  212. # 商品在列表的位置
  213. self.search_key_loc = 0
  214. # oss配置
  215. self.oss_config = {
  216. "access_key_id": Config.access_key_id,
  217. "access_key_secret": Config.access_key_secret,
  218. "endpoint": Config.endpoint, # 例: oss-cn-beijing.aliyuncs.com
  219. "bucket_name": Config.bucket_name,
  220. "oss_prefix": Config.oss_prefix # OSS中存放截图的前缀(虚拟文件夹)
  221. }
  222. # 异常处理
  223. def wr_re(self, mod, device_id, sort=None, page=None):
  224. file_path = f'./ycwj/{device_id}_{self.title_key}.txt'
  225. if mod == "写":
  226. try:
  227. data = {
  228. "page": page if page else "",
  229. "sort": sort if sort else "",
  230. }
  231. os.makedirs(os.path.dirname(file_path), exist_ok=True)
  232. with open(file_path, 'w', encoding='utf-8') as f:
  233. json.dump(data, f, ensure_ascii=False, indent=2)
  234. print(f"进度保存成功:{sort},{page}页")
  235. except Exception as e:
  236. print("保存进度失败")
  237. elif mod == "读":
  238. try:
  239. if not os.path.exists(file_path):
  240. return None
  241. with open(file_path, 'r', encoding='utf-8') as f:
  242. data = json.load(f)
  243. print(self.sort)
  244. if self.sort and self.sort_key == 0:
  245. self.li_or_lo(self.sort)
  246. if data['page'] != '':
  247. i = 0
  248. while True:
  249. if i == data['page']:
  250. self.page = data['page']
  251. break
  252. else:
  253. i += 1
  254. end_y = 300
  255. self.d.swipe(200, 1400, 200, end_y, 0.4)
  256. else:
  257. return None
  258. return data
  259. except Exception as e:
  260. print(f"读取进度失败", e)
  261. return None
  262. return None
  263. # 排序
  264. def li_or_lo(self, key):
  265. if key == "升序":
  266. self.sort_key += 1
  267. self.d.xpath('//*[@text="价格"]').click()
  268. n = self.d.xpath('//*[@text="总价低到高"]')
  269. if n.exists:
  270. n.click()
  271. time.sleep(self.get_sleep_time())
  272. if key == "降序":
  273. self.sort_key += 1
  274. self.d.xpath('//*[@text="价格"]').click()
  275. n = self.d.xpath('//*[@text="单粒价格低到高"]')
  276. if n:
  277. n.click()
  278. else:
  279. self.d.xpath('//*[@text="价格"]').click()
  280. # 返回列表页
  281. def back_to_list_page(self):
  282. for i in range(10):
  283. if self.distinct_target():
  284. return True
  285. print(f'第{i}次尝试退回到列表页')
  286. self.swipe_back(1)
  287. time.sleep(1)
  288. print('页面出错,没有退回到列表页')
  289. return False
  290. def get_drug_lis(self, idx):
  291. if idx == 0:
  292. drug_lis = self.d.xpath(
  293. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.FrameLayout[2]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.FrameLayout').all()
  294. else:
  295. for i in range(1, 6):
  296. drug_lis = self.d.xpath(
  297. f'/hierarchy/android.widget.FrameLayout[{i}]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.FrameLayout').all()
  298. if drug_lis:
  299. break
  300. return drug_lis
  301. # 代码运行那时候的时间
  302. def app_current_time(self):
  303. return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  304. def slide_link(self):
  305. value_tag = None
  306. if self.d.xpath('//*[@text="微信"]').exists:
  307. value_tag = self.d.xpath('//*[@text="微信"]').info['bounds']
  308. self.d.swipe(400, value_tag['top'], 100, value_tag['top'], 0.3)
  309. return
  310. if self.d.xpath('//*[@text="朋友圈"]').exists:
  311. value_tag = self.d.xpath('//*[@text="朋友圈"]').info['bounds']
  312. self.d.swipe(400, value_tag['top'], 100, value_tag['top'], 0.3)
  313. return
  314. if self.d.xpath('//*[@text="QQ好友"]').exists:
  315. value_tag = self.d.xpath('//*[@text="QQ好友"]').info['bounds']
  316. self.d.swipe(400, value_tag['top'], 100, value_tag['top'], 0.3)
  317. return
  318. def app_start_time(self):
  319. """
  320. 获取app启动时间
  321. :return:
  322. """
  323. return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  324. def stop_app(self):
  325. self.d.app_stop(self.package_name)
  326. time.sleep(5)
  327. def start_app(self):
  328. self.d.app_start(self.package_name)
  329. time.sleep(5)
  330. def restart_app(self):
  331. """
  332. 重启app
  333. :return:
  334. """
  335. self.stop_app()
  336. self.start_app()
  337. @staticmethod
  338. def get_sleep_time():
  339. return random.randint(1, 2)
  340. # return random.randint(5, 8)
  341. @staticmethod
  342. def get_current_date():
  343. return datetime.datetime.now().strftime('%Y/%m/%d')
  344. @staticmethod
  345. def _normalize_rule_list(value):
  346. if value is None:
  347. return []
  348. if isinstance(value, (list, tuple, set)):
  349. raw_values = value
  350. else:
  351. raw_values = [value]
  352. result = []
  353. for item in raw_values:
  354. item_str = str(item).strip()
  355. if item_str:
  356. result.append(item_str)
  357. return result
  358. @staticmethod
  359. def _normalize_match_text(value):
  360. return re.sub(r'\s+', '', str(value or '')).lower()
  361. def _match_any_keyword(self, text, keywords):
  362. keyword_list = self._normalize_rule_list(keywords)
  363. if not keyword_list:
  364. return True
  365. normalized_text = self._normalize_match_text(text)
  366. return any(self._normalize_match_text(keyword) in normalized_text for keyword in keyword_list)
  367. def is_link_spec_useful(self, product_title, specifications=''):
  368. if not self.spec_list:
  369. return True
  370. title_text = self._normalize_match_text(product_title)
  371. spec_text = self._normalize_match_text(specifications)
  372. for spec in self.spec_list:
  373. normalized_spec = self._normalize_match_text(spec)
  374. if normalized_spec in title_text or normalized_spec in spec_text:
  375. return True
  376. return False
  377. def is_link_useful(self, product_title, specifications=''):
  378. if not self._match_any_keyword(product_title, self.title_key):
  379. print(f"当前商品名称:{product_title} 不包含{self.title_key}关键字")
  380. return False
  381. if not self._match_any_keyword(product_title, self.brand):
  382. print(f"当前商品名称:{product_title} 不包含{self.brand}品牌")
  383. return False
  384. if not self.is_link_spec_useful(product_title, specifications):
  385. print(f"当前商品名称:{product_title} 不包含{self.spec_list}品规")
  386. return False
  387. return True
  388. def remove_watermark(self, img_path):
  389. """
  390. 图片去水印(将水印部分变成白色背景)并将数据转化为二进制数据
  391. :param img_path: 图片路径
  392. :return: 二进制图片数据
  393. """
  394. img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), -1)
  395. endswith = os.path.splitext(img_path)[1]
  396. new = np.clip(1.4057577998008846 * img - 38.33089999653017, 0, 255).astype(np.uint8)
  397. _, img_binary = cv2.imencode(endswith, new)
  398. return img_binary
  399. def get_shop_name(self):
  400. """
  401. 获取店铺名
  402. :return:
  403. """
  404. try:
  405. xpath = '//*[@text="进店"]/preceding-sibling::android.view.ViewGroup/android.widget.LinearLayout/android.widget.TextView'
  406. if self.d.xpath(xpath).exists:
  407. shop_name = self.d.xpath(xpath).text
  408. self.loggerPdd.info(f'1-获取到店铺名:{shop_name}')
  409. else:
  410. # 进入店铺新页面
  411. shop_btn_xpath = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]'
  412. if self.d.xpath(shop_btn_xpath).exists:
  413. self.d.xpath(shop_btn_xpath).click()
  414. time.sleep(1)
  415. # self.d.xpath('//*[@text="店铺"]').click()
  416. xpath_shop_name = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]/android.widget.RelativeLayout[1]/android.widget.LinearLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.RelativeLayout[1]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.TextView[1]'
  417. if self.d.xpath(xpath_shop_name).exists:
  418. shop_name = self.d.xpath(xpath_shop_name).text
  419. self.loggerPdd.info(f'2-获取到店铺名:{shop_name}')
  420. else:
  421. shop_name = ''
  422. self.loggerPdd.info(f'3-获取到店铺名:{shop_name}')
  423. self.swipe_back(1) #
  424. else:
  425. shop_name = ''
  426. self.loggerPdd.info('4-因为shop_btn_xpath不存在,获取到店铺名为空')
  427. # time.sleep(10000)
  428. return shop_name
  429. except Exception as e:
  430. print(f'获取店铺名出错:{e}')
  431. self.loggerPdd.error(f'获取店铺名出错:{e}')
  432. return None
  433. def save_to_database(self, data):
  434. print(f'保存数据到数据库:{data}')
  435. max_retries = 5
  436. for attempt in range(max_retries):
  437. conn = None
  438. try:
  439. conn = get_mysql()
  440. with conn.cursor() as cur:
  441. add_sql = """
  442. INSERT INTO retrieve_scrape_data (
  443. enterprise_id, platform_id, platform_item_id, province_id, city_id,
  444. province_name, city_name, area_info, product_name, product_specs,
  445. one_box_price, manufacture_date, expiry_date, manufacturer, approval_number,
  446. is_sold_out, online_posting_count, continuous_listing_count, link_url,
  447. store_name, store_url, shipment_province_id, shipment_province_name,
  448. shipment_city_id, shipment_city_name, company_name, qualification_number,
  449. scrape_date, min_price, number, sales, inventory, snapshot_url
  450. ) VALUES (
  451. %s, %s, %s, %s, %s,
  452. %s, %s, %s, %s, %s,
  453. %s, %s, %s, %s, %s,
  454. %s, %s, %s, %s,
  455. %s, %s, %s, %s,
  456. %s, %s, %s, %s,
  457. %s, %s, %s, %s, %s, %s
  458. )
  459. """
  460. cur.execute(add_sql, (
  461. data['enterprise_id'],
  462. data['platform_id'],
  463. data['platform_item_id'],
  464. data['province_id'],
  465. data['city_id'],
  466. data['province_name'],
  467. data['city_name'],
  468. data['area_info'],
  469. data['product_name'],
  470. data['product_specs'],
  471. data['one_box_price'],
  472. data['manufacture_date'],
  473. data['expiry_date'],
  474. data['manufacturer'],
  475. data['approval_number'],
  476. data['is_sold_out'],
  477. data['online_posting_count'],
  478. data['continuous_listing_count'],
  479. data['link_url'],
  480. data['store_name'],
  481. data['store_url'],
  482. data['shipment_province_id'],
  483. data['shipment_province_name'],
  484. data['shipment_city_id'],
  485. data['shipment_city_name'],
  486. data['company_name'],
  487. data['qualification_number'],
  488. data['scrape_date'],
  489. data['min_price'],
  490. data['number'],
  491. data['sales'],
  492. data['inventory'],
  493. data['snapshot_url'],
  494. ))
  495. conn.commit()
  496. # 如果你有计数器,可以取消下面注释
  497. self.max_counts += 1
  498. print("存入数据库成功")
  499. return True
  500. except Exception as e:
  501. print(f'保存数据库异常 (尝试 {attempt + 1}/{max_retries}): {e}')
  502. if conn:
  503. conn.rollback()
  504. conn.close()
  505. if attempt == max_retries - 1:
  506. print("达到最大重试次数,保存失败")
  507. return False
  508. time.sleep(2)
  509. def click_target_product_by_search_key(self, fuzzy_match=False, timeout=10):
  510. """
  511. 动态匹配self.search_key对应的商品并点击
  512. :param fuzzy_match: 是否模糊匹配(应对商品名带额外后缀/前缀的情况) 不模糊匹配
  513. :param timeout: 等待元素出现的超时时间(秒)
  514. :return: 点击是否成功(bool)
  515. """
  516. try:
  517. # 1. 定义定位条件(动态使用self.search_key)
  518. if fuzzy_match:
  519. # 模糊匹配:包含search_key即可(推荐,适配搜索结果商品名略有差异)
  520. locator = self.d(textContains=self.search_key)
  521. print(f"🔍 模糊匹配商品:包含「{self.search_key}」的元素")
  522. else:
  523. # 精确匹配:商品名与search_key完全一致
  524. locator = self.d(text=self.search_key)
  525. print(f"🔍 精确匹配商品:「{self.search_key}」")
  526. # 2. 等待元素出现(核心:避免元素未加载就点击)
  527. if locator.wait(timeout=timeout):
  528. print(f"✅ 找到匹配的商品,准备点击")
  529. # 执行点击(优先点击可点击的元素)
  530. locator.click()
  531. print(f"✅ 成功点击「{self.search_key}」对应的商品")
  532. # 点击后等待页面加载
  533. time.sleep(self.get_sleep_time())
  534. return True
  535. else:
  536. # print(f"❌ 超时未找到「{self.search_key}」对应的商品,尝试滑动刷新")
  537. # # 容错:向上滑动(加载更多)后重试
  538. # self.swipe_up()
  539. # 滑动后再等待5秒重试
  540. # if locator.wait(timeout=5):
  541. # locator.click()
  542. # print(f"✅ 滑动后找到并点击「{self.search_key}」对应的商品")
  543. # return True
  544. # else:
  545. print(f"❌ 滑动后仍未找到「{self.search_key}」对应的商品")
  546. return False
  547. except Exception as e:
  548. print(f"❌ 点击「{self.search_key}」对应商品时异常:{e}")
  549. return False
  550. def swipe_down(self):
  551. """
  552. 下滑(模拟真人操作,抗风控+设备适配+容错)
  553. 核心:起点在屏幕上方,终点在屏幕下方(和上滑相反)
  554. :return: None
  555. """
  556. try:
  557. # 1. 获取屏幕尺寸(兼容不同设备,给默认值避免获取失败)
  558. screen_width = self.d.info.get('displayWidth', 1080) # 默认1080px宽度
  559. screen_height = self.d.info.get('displayHeight', 2400) # 默认2400px高度
  560. # 2. 随机滑动时长(0.1~0.3秒,避免固定值被风控,且不设0秒)
  561. duration_rate = random.uniform(0.1, 0.3)
  562. # 3. 计算滑动坐标(用屏幕比例,适配所有设备)
  563. start_x = screen_width // 2 # 水平居中(和上滑一致,符合真人操作习惯)
  564. start_y = screen_height * 0.2 # 起点:屏幕20%高度(上方偏下)
  565. end_y = screen_height * 0.8 # 终点:屏幕80%高度(下方偏上)
  566. # 强制确保起点y < 终点y(必为向下滑,避免逻辑错误)
  567. start_y, end_y = min(start_y, end_y - 10), max(end_y, start_y + 10)
  568. # 4. 核心向下滑动操作
  569. self.d.swipe(start_x, start_y, start_x, end_y, duration=duration_rate)
  570. # 滑动后全局等待(确保页面加载,避免元素定位失败)
  571. time.sleep(self.get_sleep_time())
  572. except Exception as e:
  573. # 异常捕获:避免设备断开/滑动失败导致程序崩溃
  574. print(f"向下滑动失败:{e}")
  575. # 兜底方案:用固定坐标重试(适配主流1080x2400设备)
  576. self.d.swipe(540, 480, 540, 1920, duration=0.2)
  577. time.sleep(self.get_sleep_time())
  578. def swipe_up(self):
  579. """
  580. 上滑
  581. :return:
  582. """
  583. screen_width = self.d.info['displayWidth']
  584. screen_height = self.d.info['displayHeight']
  585. duration_rate = random.uniform(0, 0.3)
  586. self.d.swipe(screen_width // 2, screen_height - 100, screen_width // 2, 100, duration=duration_rate)
  587. no = random.uniform(0, 1)
  588. if no > 0.85:
  589. # 有的时候卡着 再稍微往上滑一点点
  590. self.d.swipe_ext("up", 0.1)
  591. time.sleep(self.get_sleep_time())
  592. def swipe_back(self, no):
  593. """
  594. 返回
  595. :param no: 回退次数
  596. :return:
  597. """
  598. if not self.distinct_target():
  599. for idx in range(no):
  600. self.d.press('back')
  601. time.sleep(self.get_sleep_time())
  602. def drug_price(self):
  603. """
  604. 获取药品价格
  605. :return:
  606. """
  607. try:
  608. xpath = '//*[@text="¥"]/following-sibling::android.widget.TextView[1]'
  609. price_str = self.d.xpath(xpath).text
  610. price = float(re.search(r'[\d\.]+', price_str).group())
  611. print(f'获取到价格:{price}')
  612. return float(price)
  613. except Exception as e:
  614. print(f'提取价格出错-->{e}')
  615. return None
  616. def drug_price_ex(self):
  617. price_str = '' # 价格初始化
  618. ext = '' # 初始化已选择的信息
  619. price = ''
  620. # 这是点击进入品规的按钮
  621. button_xpath_1 = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[2]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.view.ViewGroup[last()]'
  622. button_xpath_2 = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[2]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[1]/android.view.ViewGroup[last()]'
  623. # 调试
  624. # test_button = self.d.xpath(button_xpath_1).exists
  625. # print(test_button)
  626. # test_button_2 = self.d.xpath(button_xpath_2).exists
  627. # print(test_button_2)
  628. # time.sleep(1000)
  629. # if self.d.xpath('//*[@text="发起拼单"]').exists:
  630. # self.d.xpath('//*[@text="发起拼单"]').click()
  631. # elif self.d.xpath('//*[@text="去复诊开药"]').exists:
  632. # self.d.xpath('//*[@text="去复诊开药"]').click()
  633. if self.d.xpath(button_xpath_1).exists:
  634. self.d.xpath(button_xpath_1).click()
  635. elif self.d.xpath(button_xpath_2).exists:
  636. self.d.xpath(button_xpath_2).click()
  637. else:
  638. print("button1 and button_2 all not exist")
  639. return price, ext
  640. select_xpath_1 = '//*[@resource-id="android:id/content"]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.TextView[last()]'
  641. select_xpath_2 = '//*[@resource-id="android:id/content"]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.TextView[last()]'
  642. select_xpath_3 = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.view.ViewGroup[2]/android.widget.LinearLayout[1]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.view.ViewGroup[1]/android.widget.TextView[last()]'
  643. select_xpath_3_2 = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.view.ViewGroup[2]/android.widget.LinearLayout[1]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.view.ViewGroup[1]/android.widget.TextView[last()-1]'
  644. price_xpath_1 = '//*[@resource-id="android:id/content"]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.TextView[1]'
  645. price_xpath_2 = '//*[@resource-id="android:id/content"]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.TextView[1]'
  646. price_xpath_3 = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.view.ViewGroup[2]/android.widget.LinearLayout[1]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.view.ViewGroup[1]//android.widget.TextView[1]'
  647. if self.d.xpath(select_xpath_1).exists:
  648. text1 = self.d.xpath(select_xpath_1).text
  649. print(f"select_xpath_1--text1={text1}")
  650. if '已选' in text1:
  651. if self.d.xpath(price_xpath_1).exists:
  652. price_str = self.d.xpath(price_xpath_1).text
  653. print(f"select_xpath_1--price_str-1={price_str}")
  654. else:
  655. print("select_xpath_1--price_xpath_1-1 not exist")
  656. ext = text1
  657. elif '请选择' in text1:
  658. # 需要再下面点击选择
  659. scroll_xpath_1 = '//*[@resource-id="android:id/content"]//android.widget.ScrollView[1]/android.widget.LinearLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.LinearLayout[last()]/android.view.ViewGroup[1]/android.view.ViewGroup[last()]'
  660. scroll_xpath_2 = ''
  661. if self.d.xpath(scroll_xpath_1).exists:
  662. self.d.xpath(scroll_xpath_1).click()
  663. time.sleep(2) # 延时2秒钟,选择了之后价格会刷新
  664. if self.d.xpath(select_xpath_1).exists:
  665. text2 = self.d.xpath(select_xpath_1).text
  666. if '已选' in text2:
  667. print(f"select_xpath_1--已选择2:text2={text2}")
  668. if self.d.xpath(price_xpath_1).exists:
  669. price_str = self.d.xpath(price_xpath_1).text
  670. print(f"select_xpath_1--price_str-2={price_str}")
  671. else:
  672. print("select_xpath_1--price_xpath_1-2 not exist")
  673. ext = text2
  674. else:
  675. print("select_xpath_1--scroll_xpath_1 not exist")
  676. elif self.d.xpath(select_xpath_2).exists:
  677. text1 = self.d.xpath(select_xpath_2).text
  678. print(f"xpath2--text1={text1}")
  679. if '已选' in text1:
  680. ext = text1
  681. if self.d.xpath(price_xpath_2).exists:
  682. price_str = self.d.xpath(price_xpath_2).text
  683. print(f"select_xpath_2--price_str-2={price_str}")
  684. else:
  685. print("select_xpath_2--price_xpath_2-1 not exist")
  686. elif '请选择' in text1:
  687. print('come in here')
  688. # 需要再下面点击选择
  689. scroll_xpath_1 = '//*[@resource-id="android:id/content"]//android.widget.ScrollView[1]/android.widget.LinearLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.LinearLayout[last()]/android.view.ViewGroup[1]/android.view.ViewGroup[1]'
  690. if self.d.xpath(scroll_xpath_1).exists:
  691. print("scroll_xpath_1 exists")
  692. self.d.xpath(scroll_xpath_1).click()
  693. time.sleep(2) # 延时2秒钟,选择了之后价格可能会刷新
  694. if self.d.xpath(select_xpath_2).exists:
  695. text2 = self.d.xpath(select_xpath_2).text
  696. if '已选' in text2:
  697. ext = text2
  698. print(f"select_xpath_2--已选择2:text2={text2}")
  699. if self.d.xpath(price_xpath_2).exists:
  700. price_str = self.d.xpath(price_xpath_2).text
  701. print(f"select_xpath_2--price_str-2={price_str}")
  702. else:
  703. print("select_xpath_2--price_xpath_2-2 not exist")
  704. else:
  705. print("scroll_xpath_1 not exists")
  706. else:
  707. print("not exist 请选择 or 已选")
  708. elif self.d.xpath(select_xpath_3).exists:
  709. text1 = self.d.xpath(select_xpath_3).text
  710. print(f"xpath3--text1-1={text1}")
  711. if ('请选择' not in text1) and ('已选' not in text1):
  712. text1 = self.d.xpath(select_xpath_3_2).text
  713. print(f"xpath3--text1-2={text1}")
  714. if '已选' in text1:
  715. ext = text1
  716. if self.d.xpath(price_xpath_3).exists:
  717. price_str = self.d.xpath(price_xpath_3).text
  718. print(f"select_xpath_3--price_str-3-3-1={price_str}")
  719. else:
  720. print("select_xpath_3--price_xpath_3-3-1 not exist")
  721. elif '请选择' in text1:
  722. print('come in here')
  723. # 需要再下面点击选择
  724. scroll_xpath_1 = '//*[@resource-id="android:id/content"]//android.widget.ScrollView[1]/android.widget.LinearLayout[1]/android.support.v7.widget.RecyclerView[1]/android.widget.LinearLayout[last()]/android.view.ViewGroup[1]/android.view.ViewGroup[1]'
  725. recycler_view_xpath = '//*[@resource-id="android:id/content"]//android.support.v7.widget.RecyclerView[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[last()]/android.view.ViewGroup[1]/android.view.ViewGroup[1]'
  726. if self.d.xpath(scroll_xpath_1).exists:
  727. print("scroll_xpath_1 exists")
  728. self.d.xpath(scroll_xpath_1).click()
  729. time.sleep(2) # 延时2秒钟,选择了之后价格可能会刷新
  730. if self.d.xpath(select_xpath_3).exists:
  731. text2 = self.d.xpath(select_xpath_3).text
  732. if '已选' in text2:
  733. ext = text2
  734. print(f"select_xpath_3--已选择2:text2={text2}")
  735. if self.d.xpath(price_xpath_3).exists:
  736. price_str = self.d.xpath(price_xpath_3).text
  737. print(f"select_xpath_3--price_str-3-2={price_str}")
  738. else:
  739. print("select_xpath_3--price_xpath_3-3-2 not exist")
  740. elif self.d.xpath(recycler_view_xpath).exists:
  741. self.d.xpath(recycler_view_xpath).click()
  742. time.sleep(2) # 延时2秒钟,选择了之后价格可能会刷新
  743. if self.d.xpath(select_xpath_3).exists:
  744. text2 = self.d.xpath(select_xpath_3).text
  745. if '已选' in text2:
  746. ext = text2
  747. print(f"select_xpath_3--已选择2:text2={text2}")
  748. if self.d.xpath(price_xpath_3).exists:
  749. price_str = self.d.xpath(price_xpath_3).text
  750. print(f"select_xpath_3--price_str-3-3={price_str}")
  751. else:
  752. print("select_xpath_3--price_xpath_3-3-3 not exist")
  753. else:
  754. print("scroll_xpath_1 not exists")
  755. else:
  756. print(f"xpath3--text1-不包含请选择和已选择")
  757. else:
  758. print("select_xpath_1 and select_xpath_2 and select_xpath_3 all not exist")
  759. if price_str:
  760. # price = float(re.search('[\d\.]+', price_str).group())
  761. match = re.search(r'¥([\d\.]+)', price_str)
  762. if match:
  763. price = float(match.group(1))
  764. else:
  765. price = ''
  766. # price = float(re.search(r'¥([\d\.]+)', price_str).group(1))
  767. print(f'获取到价格:{price}')
  768. print(f"ext={ext}")
  769. self.swipe_back(1) #
  770. return price, ext
  771. def restart_uiautomator_services(self, device_id):
  772. """
  773. 重启atx的uiautomator 服务
  774. :param device_id:
  775. :return:
  776. """
  777. stop_uiautomator_services = f'adb -s {device_id} shell /data/local/tmp/atx-agent server -d --stop'
  778. start_uiautomator_services = f'adb -s {device_id} shell /data/local/tmp/atx-agent server -d'
  779. # result = subprocess.run(stop_uiautomator_services, capture_output=True, text=True, shell=True)
  780. # print(result.stdout)
  781. subprocess.run(stop_uiautomator_services, capture_output=True, text=True, shell=True)
  782. time.sleep(self.get_sleep_time())
  783. subprocess.run(start_uiautomator_services, capture_output=True, text=True, shell=True)
  784. time.sleep(self.get_sleep_time())
  785. def connect_devices(self, device_id):
  786. """
  787. 连接设备
  788. :return:
  789. """
  790. try:
  791. self.d = u2.connect_usb(device_id)
  792. # 设置隐形等待时间
  793. # self.d.implicitly_wait(5)
  794. self.restart_uiautomator_services(device_id)
  795. print(f'[{self.program_start_time}]连接到设备:{device_id}')
  796. except Exception as e:
  797. print(f'{device_id} 连接错误: {e}')
  798. raise Exception(e)
  799. def get_ocr_res(self, img):
  800. try:
  801. image = self.remove_watermark(img)
  802. res_image = self.client.basicGeneral(image)
  803. data = res_image.get('words_result', '')
  804. print(f'百度api返回结果:{data}')
  805. return data
  806. except:
  807. return None
  808. def get_title(self):
  809. try:
  810. print('开始提取标题')
  811. time.sleep(self.get_sleep_time())
  812. title_xpath = '//*[@resource-id="com.xunmeng.pinduoduo:id/tv_title"]'
  813. if self.d.xpath(title_xpath).exists:
  814. title = self.d.xpath(title_xpath).info['contentDescription'].strip()
  815. else:
  816. return None
  817. # title = self.d.xpath('//*[@resource-id="com.xunmeng.pinduoduo:id/tv_title"]').info['contentDescription'].strip()
  818. print(f'提取到标题:{title}')
  819. return title
  820. except Exception as e:
  821. print(f'获取标题出错:{e}')
  822. return None
  823. # 从里面匹配出药品名和规格
  824. # drugs_name
  825. # specifications
  826. # match = re.search(r'([^\d]+)([\d\D]+)', title)
  827. # match = re.search(r'(\[[^\]]+\])(.+?)(\d+.*)', title)
  828. # if match:
  829. # drugs_name = match.group(1).strip() + match.group(2).strip()
  830. # specifications = match.group(3).strip()
  831. # print("药品名:", drugs_name)
  832. # print("规格:", specifications)
  833. # print('完整药名:', drugs_name + specifications)
  834. # return drugs_name, specifications
  835. # else:
  836. # print("没有匹配到预期格式")
  837. def enter_shop(self):
  838. """
  839. 进店,方便提取资质环境
  840. :return:
  841. """
  842. # self.d.xpath('//*[@text="进店"]').click()
  843. self.d.xpath('//*[@text="店铺"]').click()
  844. time.sleep(self.get_sleep_time())
  845. def data_is_exists(self, data):
  846. # 1. 验证必要字段
  847. required_keys = ['min_price', 'shop', 'scrape_date', 'platform']
  848. if not all(key in data for key in required_keys):
  849. missing = [key for key in required_keys if key not in data]
  850. print(f"缺少必要字段: {', '.join(missing)}")
  851. return None
  852. conn = None
  853. try:
  854. conn = get_mysql()
  855. with conn.cursor() as cur:
  856. query_sql = """
  857. SELECT * FROM {}
  858. WHERE min_price = %s
  859. AND store_name = %s
  860. AND scrape_date = %s
  861. AND platform_id = %s
  862. """.format(self.table_name)
  863. cur.execute(query_sql, (
  864. data['min_price'],
  865. data['shop'],
  866. data['scrape_date'],
  867. data['platform']
  868. ))
  869. result = cur.fetchone()
  870. return bool(result) # 如果存在返回True,否则False
  871. except Exception as e:
  872. print(f"MySQL 错误: {str(e)}")
  873. finally:
  874. if conn:
  875. conn.close()
  876. def get_instructions_data(self):
  877. """
  878. 确定有详情页之后之后,提取所有的详情页数据
  879. :return:
  880. """
  881. # 下面的for循环已经有滑动的操作了,不要一进来就滑动。
  882. # self.d.swipe_ext("up", scale=0.5)
  883. for i in range(8):
  884. if self.d.xpath('//*[@text="品牌"]').exists or self.d.xpath('//*[@text="药品通用名"]').exists:
  885. self.d.swipe_ext("up", scale=0.1)
  886. print('开始采集详情数据')
  887. break
  888. self.d.swipe_ext("up", scale=0.5)
  889. time.sleep(self.get_sleep_time())
  890. # 点击查看全部
  891. if self.d.xpath('//*[@text="品牌"]').exists:
  892. self.d.xpath('//*[@text="品牌"]').click()
  893. else:
  894. self.d.xpath('//*[@text="药品通用名"]').click()
  895. time.sleep(self.get_sleep_time())
  896. attr = dict()
  897. # # 获取详情页信息
  898. xpath = '//*[starts-with(@text,"商品参数")]/parent::*/parent::*/following-sibling::*/*/*/android.view.ViewGroup//android.widget.TextView'
  899. ddd = self.d.xpath(xpath).all()
  900. for i in range(0, len(ddd), 2):
  901. group = ddd[i:i + 2]
  902. attr[group[0].text] = group[1].text
  903. # 截图获取未获取到的数据
  904. # if not all(i in ['有效期', '生产企业', '批准文号', '药品规格', '产品规格'] for i in attr.keys()):
  905. if not all(i in ['有效期', '生产企业', '批准文号', '药品规格'] for i in attr.keys()):
  906. self.d.swipe_ext("up", 0.4)
  907. time.sleep(self.get_sleep_time())
  908. xpath = '//*[starts-with(@text,"商品参数")]/parent::*/parent::*/following-sibling::*/*/*/android.view.ViewGroup//android.widget.TextView'
  909. ddd = self.d.xpath(xpath).all()
  910. for i in range(0, len(ddd), 2):
  911. group = ddd[i:i + 2]
  912. attr[group[0].text] = group[1].text
  913. print(f'当前说明书规格参数:{attr}')
  914. res_data = {
  915. # "有效期": attr.get('有效期',''),
  916. # "生产单位": attr['生产企业'],
  917. # "批准文号": attr['批准文号'],
  918. # "产品规格": attr.get('药品规格') if attr.get('药品规格', '') else attr.get('药品规格')
  919. "有效期": attr.get('有效期', ''),
  920. "生产单位": attr.get('生产企业', ''),
  921. "批准文号": attr.get('批准文号', ''),
  922. "产品规格": attr.get('药品规格', '')
  923. }
  924. print(f'当前规格参数字典数据:{res_data}')
  925. return res_data
  926. def has_instructions(self):
  927. """
  928. 是否有详情页
  929. :return:如果有详情页返回True,否则返回False
  930. """
  931. # 没有说明书的无法采集具体数据
  932. max_attempts = 12 # 最大尝试次数
  933. attempt = 0 # 当前尝试次数
  934. while attempt < max_attempts:
  935. time.sleep(0.5)
  936. xpath = '//*[@text="商品详情"]'
  937. is_has_instructions = self.d.xpath(xpath).exists
  938. if is_has_instructions:
  939. return True # 如果找到“商品详情”,则返回True
  940. self.d.swipe_ext("up", 0.3)
  941. attempt += 1
  942. return False # 如果尝试次数达到最大次数,则返回False
  943. def distinct_target(self):
  944. result = False
  945. is_position = self.d.xpath('//*[@content-desc="拍照搜索"]').exists
  946. is_position2 = self.d.xpath('//*[@text="年货节大促"]').exists
  947. is_position3 = self.d.xpath('//*[@text="筛选"]').exists
  948. is_position4 = self.d.xpath('//*[@text="回头客常拼"]').exists
  949. list_page_xpath = '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[2]/android.view.ViewGroup[1]/android.widget.LinearLayout[1]//android.support.v7.widget.RecyclerView[1]'
  950. is_position_new = self.d.xpath(list_page_xpath).exists
  951. print(f'is_position_new={is_position_new}')
  952. if is_position or is_position2 or is_position3 or is_position4 or is_position_new:
  953. result = True
  954. return result
  955. def enter_target_page(self):
  956. self.d.xpath(
  957. '//*[@resource-id="android:id/content"]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]').click()
  958. time.sleep(self.get_sleep_time())
  959. self.d(className='android.widget.EditText').click()
  960. time.sleep(self.get_sleep_time())
  961. self.d.send_keys(self.search_key, clear=True)
  962. time.sleep(self.get_sleep_time())
  963. self.d.xpath('//*[@text="搜索"]').click()
  964. time.sleep(self.get_sleep_time())
  965. if self.sort and self.sort_key == 0:
  966. self.li_or_lo(self.sort)
  967. self.wr_re("读", self.device_id)
  968. def get_clipboard(self):
  969. self.loggerPdd.info(f"Clipboard content:{self.d.clipboard}") # 打印调试信息
  970. clipboard_content = self.d.clipboard
  971. if clipboard_content is None:
  972. return ''
  973. return clipboard_content.strip()
  974. def get_product_link(self):
  975. product_link = ''
  976. print('开始获取商品链接')
  977. content_frame = self.d.xpath('//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]').exists
  978. print(content_frame)
  979. relative_layout = self.d.xpath(
  980. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]').exists
  981. print(relative_layout)
  982. relative_layout2 = self.d.xpath(
  983. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]').exists
  984. print(relative_layout2)
  985. Frame_Layout = self.d.xpath(
  986. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[2]').exists
  987. print(Frame_Layout)
  988. ImageView = self.d.xpath(
  989. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[2]/android.view.View[1]').exists
  990. print(ImageView)
  991. ImageView2 = self.d.xpath(
  992. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[3]/android.view.View[1]').exists
  993. print(ImageView2)
  994. # 多种可能的“分享”按钮
  995. dots_xpaths = [
  996. # '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[2]/android.view.View[1]',
  997. '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[1]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[last()]/android.view.View[1]',
  998. # '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[2]/android.widget.RelativeLayout[1]/android.widget.FrameLayout[2]/android.view.View[1]',
  999. # '//*[@resource-id="android:id/content"]/android.widget.FrameLayout[1]/android.widget.RelativeLayout[2]/android.widget.RelativeLayout[2]/android.widget.FrameLayout[3]/android.widget.ImageView[1]',
  1000. ]
  1001. max_retry = 5 # 最多尝试次数
  1002. for idx in range(1, max_retry + 1):
  1003. if product_link: # 已经拿到则退出
  1004. break
  1005. for xp in dots_xpaths:
  1006. if self.d.xpath(xp).exists:
  1007. # print(f'{idx}-进入分享点点点')
  1008. self.loggerPdd.info(f'{idx}-进入分享点点点')
  1009. self.d.xpath(xp).click()
  1010. time.sleep(1)
  1011. self.loggerPdd.info('开始滑动')
  1012. self.slide_link()
  1013. time.sleep(0.2)
  1014. self.d.xpath('//*[@text="复制链接"]').click_exists()
  1015. time.sleep(1)
  1016. product_link = self.get_clipboard()
  1017. time.sleep(0.5)
  1018. self.loggerPdd.info(f'{idx}-商品链接:{product_link}')
  1019. break # 找到并执行后跳出内层循环
  1020. if not product_link and idx < max_retry:
  1021. time.sleep(0.5) # 最后一次不需要再等待
  1022. # time.sleep(100000)
  1023. return product_link
  1024. def integrate_data_v2(self):
  1025. """
  1026. 基于入口配置统一校验标题、品牌和品规,替代内部大量硬编码分支。
  1027. """
  1028. min_price, ext = self.drug_price_ex()
  1029. title_info = self.get_title()
  1030. if not title_info:
  1031. print('标题获取为空')
  1032. self.swipe_back(1)
  1033. return
  1034. if not self.is_link_useful(title_info):
  1035. self.swipe_back(1)
  1036. self.unrelated_data += 1
  1037. return
  1038. if not min_price:
  1039. min_price = self.drug_price()
  1040. if not min_price:
  1041. print('提取价格出错,回退到列表页')
  1042. self.swipe_back(1)
  1043. self.unrelated_data += 1
  1044. return
  1045. product_link = self.get_product_link()
  1046. time.sleep(2)
  1047. if self.direct_shop_lookup:
  1048. shop = self.get_shop_name()
  1049. else:
  1050. for _ in range(15):
  1051. if self.d(textStartsWith="进店").exists:
  1052. print('开始获取店铺名')
  1053. break
  1054. self.d.swipe_ext("up", scale=0.3)
  1055. time.sleep(self.get_sleep_time())
  1056. if self.d(textStartsWith="进店").exists:
  1057. print('可以开始获取店铺名')
  1058. shop = self.get_shop_name()
  1059. if not shop:
  1060. print('当前店铺名称为空')
  1061. self.swipe_back(1)
  1062. self.unrelated_data += 1
  1063. return
  1064. scrape_date = self.get_current_date()
  1065. dup_data = {
  1066. 'min_price': min_price,
  1067. 'shop': shop,
  1068. 'scrape_date': scrape_date,
  1069. 'platform': '3'
  1070. }
  1071. if self.data_is_exists(dup_data):
  1072. print('存在相同数据不入库')
  1073. self.back_to_list_page()
  1074. return
  1075. is_has_instructions = self.has_instructions()
  1076. self.loggerPdd.info(f'是否有说明书:{is_has_instructions}')
  1077. manufacture_date = ''
  1078. credit_code = ext
  1079. if is_has_instructions:
  1080. try:
  1081. instructions_info = self.get_instructions_data()
  1082. expiry_date = instructions_info['有效期'].strip('。')
  1083. manufacturer = instructions_info['生产单位'].strip('。')
  1084. approval_number = instructions_info['批准文号'].strip('。')
  1085. specifications = instructions_info['产品规格'].strip('。')
  1086. except Exception as e:
  1087. print(f'获取详情页规格参数出错:{e}')
  1088. self.swipe_back(2)
  1089. return
  1090. else:
  1091. expiry_date = ''
  1092. manufacturer = ''
  1093. approval_number = ''
  1094. specifications = ''
  1095. if not self.is_link_useful(title_info, specifications):
  1096. self.swipe_back(1)
  1097. self.unrelated_data += 1
  1098. return
  1099. self.unrelated_data = 0
  1100. if extract_box_number(credit_code):
  1101. one_box_price = min_price / extract_box_number(credit_code)
  1102. else:
  1103. print("单瓶药品价格没处理成功")
  1104. one_box_price = 0
  1105. save_data = {
  1106. 'enterprise_id': 0,
  1107. 'platform_id': self.platform,
  1108. 'platform_item_id': '',
  1109. 'province_id': 0,
  1110. 'city_id': 0,
  1111. 'province_name': '',
  1112. 'city_name': '',
  1113. 'area_info': "",
  1114. 'product_name': title_info,
  1115. 'product_specs': specifications,
  1116. 'one_box_price': one_box_price,
  1117. 'manufacture_date': manufacture_date,
  1118. 'expiry_date': expiry_date,
  1119. 'manufacturer': manufacturer,
  1120. 'approval_number': approval_number,
  1121. 'is_sold_out': 0,
  1122. 'online_posting_count': 1,
  1123. 'continuous_listing_count': 1,
  1124. 'link_url': product_link,
  1125. 'store_name': shop,
  1126. 'store_url': '',
  1127. 'shipment_province_id': 0,
  1128. 'shipment_province_name': "",
  1129. 'shipment_city_id': 0,
  1130. 'shipment_city_name': "",
  1131. 'company_name': "",
  1132. 'qualification_number': "",
  1133. 'scrape_date': scrape_date,
  1134. 'min_price': min_price,
  1135. 'number': 0,
  1136. 'sales': "",
  1137. 'inventory': "",
  1138. 'snapshot_url': "",
  1139. 'insert_time': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  1140. }
  1141. self.save_to_database(save_data)
  1142. def main(self, device_id, search_key_length, keyword_idx):
  1143. spider_no = 0
  1144. self.connect_devices(device_id)
  1145. time.sleep(self.get_sleep_time())
  1146. if keyword_idx == 0:
  1147. print("搜索前,先重启APP")
  1148. self.restart_app()
  1149. # 搜索关键字
  1150. self.enter_target_page()
  1151. else:
  1152. print("清空前面的文字,再输入关键词")
  1153. self.d.send_keys(self.search_key, clear=True)
  1154. time.sleep(1)
  1155. print("点击搜索")
  1156. self.d.xpath('//*[@text="搜索"]').click()
  1157. time.sleep(1)
  1158. # 上报状态
  1159. report_api(self.task_id, self.page, 2)
  1160. for idx in range(300):
  1161. print(f'第{idx + self.page}页')
  1162. self.wr_re("写", self.device_id, self.sort, idx + self.page)
  1163. if spider_no > 30:
  1164. time.sleep(300)
  1165. spider_no = 0
  1166. if self.unrelated_data > 10:
  1167. print(f'[{self.program_start_time}]----{self.search_key}----连续超过10个不达标的数据则停止采集')
  1168. print(
  1169. f"[程序启动时间:{self.program_start_time}-----程序结束时间:{self.app_current_time()}]----搜索关键词:{self.search_key}----点击了{self.click_counts}个商品")
  1170. self.swipe_down()
  1171. time.sleep(self.get_sleep_time()) # 下滑后等待页面稳定
  1172. click_success = self.click_target_product_by_search_key(fuzzy_match=False)
  1173. if not click_success:
  1174. print(f"⚠️ 关键词「{self.search_key}」商品点击失败")
  1175. return
  1176. print("点击搜索框")
  1177. self.d(className='android.widget.EditText').click()
  1178. time.sleep(self.get_sleep_time())
  1179. if keyword_idx == search_key_length - 1:
  1180. print("程序最后一个品规采集完毕,返回主屏幕")
  1181. report_api(self.task_id, end_page=idx, start=3, end_time=int(time.time()))
  1182. # self.d.press("home")
  1183. # 连续超过10个不达标的数据则停止采集
  1184. break
  1185. if self.max_counts_limit and self.max_counts >= self.max_counts_limit:
  1186. report_api(self.task_id, end_page=idx, start=3, end_time=int(time.time()))
  1187. print('enough')
  1188. # 向下滑
  1189. self.swipe_down()
  1190. time.sleep(self.get_sleep_time())
  1191. # 点击搜索框
  1192. click_success = self.click_target_product_by_search_key(fuzzy_match=False)
  1193. if not click_success:
  1194. print(f"关键词「{self.search_key}」商品点击失败")
  1195. return
  1196. print("点击搜索框")
  1197. self.d(className='android.widget.EditText').click()
  1198. time.sleep(self.get_sleep_time())
  1199. break
  1200. # 售罄次数大于4基本就是号废了但是如果下次点击不会出现这种情况就要重置为0
  1201. if self.sold_out_counts > 4:
  1202. report_api(self.task_id, end_page=idx,start=4,end_time=int(time.time()))
  1203. print(
  1204. f"[程序启动时间:{self.program_start_time}-----程序结束时间:{self.app_current_time()}]----搜索关键词:{self.search_key}----点击了{self.click_counts}个商品")
  1205. print("====商品已售罄4次,结束采集(号不能用)")
  1206. break
  1207. drug_lis = self.get_drug_lis(idx)
  1208. print('数量', len(drug_lis))
  1209. for idd, drug_one in enumerate(drug_lis):
  1210. print(idd + 1, drug_one.info)
  1211. time.sleep(self.get_sleep_time())
  1212. top = drug_one.info['bounds']['top']
  1213. bottom = drug_one.info['bounds']['bottom']
  1214. if bottom <= 1524 and top >= 258:
  1215. drug_one.click()
  1216. self.click_counts += 1
  1217. time.sleep(self.get_sleep_time())
  1218. # 先判断是否售罄次数是否大于4
  1219. if self.sold_out_counts >= 4:
  1220. print(
  1221. f"[程序启动时间:{self.program_start_time}-----程序结束时间:{self.app_current_time()}]----搜索关键词:{self.search_key}----点击了{self.click_counts}个商品")
  1222. print("====这是在第一页有两个,商品已售罄4次,结束采集(号不能用)====")
  1223. time.sleep(self.get_sleep_time())
  1224. report_api(self.task_id, end_page=idx, start=4, end_time=int(time.time()))
  1225. self.d.press('home')
  1226. return
  1227. if self.d.xpath('//*[contains(@text, "商品已售罄")]').wait(timeout=5):
  1228. print("======商品已售罄======")
  1229. self.sold_out_counts += 1
  1230. if self.back_to_list_page():
  1231. continue
  1232. # 采集药品信息
  1233. try:
  1234. # 重置商品售罄次数
  1235. self.sold_out_counts = 0
  1236. self.integrate_data_v2()
  1237. # 检测下是否回退到列表页
  1238. if self.back_to_list_page():
  1239. print('回退到列表页', True)
  1240. else:
  1241. print(f'[{self.app_current_time()}] 回退到列表页失败')
  1242. print(
  1243. f"[程序启动时间:{self.program_start_time}-----结束时间:{self.app_current_time()}]----搜索关键词:{self.search_key}----点击了{self.click_counts}个商品")
  1244. report_api(self.task_id, end_page=idx, start=4, end_time=int(time.time()))
  1245. return
  1246. time.sleep(self.get_sleep_time())
  1247. spider_no += 1
  1248. except Exception as e:
  1249. self.loggerPdd.error(f'采集药品详情数据出错:{e}')
  1250. if not self.back_to_list_page():
  1251. print('页面出错,退回列表')
  1252. else:
  1253. continue
  1254. if self.d(textStartsWith="抱歉,没有更多商品啦~").exists:
  1255. report_api(self.task_id, end_page=idx,start=3,end_time=int(time.time()))
  1256. print('已经到达列表页最底部')
  1257. break
  1258. print('开始滑入下一页')
  1259. end_y = 300
  1260. self.d.swipe(200, 1400, 200, end_y, 0.4)
  1261. time.sleep(self.get_sleep_time())
  1262. # pdd
  1263. def main():
  1264. # task = get_task_mysql()
  1265. task = get_all_tasks()
  1266. for i in range(len(task)):
  1267. if task[i]["data"] and task[i]["device_id"]:
  1268. print("任务开始")
  1269. device_list = {
  1270. task[i]["device_id"]: [
  1271. {
  1272. "search_key": task[i]["data"][7] + task[i]["data"][5],
  1273. "title_key": task[i]["data"][5],
  1274. "spec_list": task[i]["data"][6],
  1275. "brand": task[i]["data"][7],
  1276. "save_search_key": task[i]["data"][7] + task[i]["data"][5],
  1277. "max_counts_limit": 300,
  1278. "sort": "升序",
  1279. "platform": task[i]["data"][4],
  1280. "task_id": task[i]["data"][0]
  1281. },
  1282. ],
  1283. }
  1284. # if len(sys.argv) < 2:
  1285. # print('需要输入设备id')
  1286. # return
  1287. # device_id = sys.argv[1]
  1288. # print(f"设备id: {device_id}")
  1289. device_id = task[i]["device_id"]
  1290. if device_id not in device_list:
  1291. print(f"设备id没有配置: {device_id}")
  1292. return
  1293. tasks = device_list[device_id]
  1294. task_length = len(tasks)
  1295. for keyword_idx, task in enumerate(tasks):
  1296. print(f"正在处理第 {keyword_idx + 1}/{task_length} 个关键词:{task['search_key']}")
  1297. pdd = PDD(
  1298. task["search_key"],
  1299. device_id,
  1300. title_key=task.get("title_key"),
  1301. spec_list=task.get("spec_list"),
  1302. brand=task.get("brand", ""),
  1303. save_search_key=task.get("save_search_key"),
  1304. max_counts_limit=task.get("max_counts_limit"),
  1305. direct_shop_lookup=task.get("direct_shop_lookup", False),
  1306. sort=task.get("sort"),
  1307. platform=task.get("platform"),
  1308. task_id=task.get("task_id"),
  1309. )
  1310. pdd.main(device_id, task_length, keyword_idx)
  1311. else:
  1312. if not task["data"]:
  1313. print("没有任务")
  1314. if not task["device_id"]:
  1315. print("设备被占用")
  1316. if __name__ == '__main__':
  1317. main()