settings.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.10.8
  4. # Date : 2025/3/24 15:05
  5. import inspect
  6. import requests
  7. from loguru import logger
  8. from bs4 import BeautifulSoup
  9. from tenacity import retry, stop_after_attempt, wait_fixed
  10. logger.remove()
  11. logger.add("./logs/{time:YYYYMMDD}.log", encoding='utf-8', rotation="00:00",
  12. format="[{time:YYYY-MM-DD HH:mm:ss.SSS}] {level} {message}",
  13. level="DEBUG", retention="7 day")
  14. HEADERS = {
  15. "User-Agent": "Dart/3.5 (dart:io)",
  16. "Accept-Encoding": "gzip",
  17. "Content-Type": "application/json",
  18. "deviceid": "110bec91-951f-4346-8195-95700fdb8c1d",
  19. "brand": "google",
  20. "channel": "JC",
  21. "os": "android",
  22. "content-type": "application/json; charset=utf-8",
  23. # "authori-zation": "a-209a2707889f473688a742c4bac06eed",
  24. "systemversion": "30",
  25. "theme": "light",
  26. "lang": "zh",
  27. "verse-ua": "37ba2436e1eec03479ded38f6acb8fd9",
  28. "version": "1.1.5",
  29. "isphysicaldevice": "true",
  30. "sktime": "1776653418408",
  31. "cid": "08295441",
  32. "sk": "d6fb6fcce1243c11dc940183af6cdad6" # 每次版本变化 也需要修改
  33. }
  34. def after_log(retry_state):
  35. """
  36. retry 回调
  37. :param retry_state: RetryCallState 对象
  38. """
  39. # 检查 args 是否存在且不为空
  40. if retry_state.args and len(retry_state.args) > 0:
  41. log = retry_state.args[0] # 获取传入的 logger
  42. else:
  43. log = logger # 使用全局 logger
  44. if retry_state.outcome.failed:
  45. log.warning(
  46. f"Function '{retry_state.fn.__name__}', Attempt {retry_state.attempt_number} Times")
  47. else:
  48. log.info(f"Function '{retry_state.fn.__name__}', Attempt {retry_state.attempt_number} succeeded")
  49. @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
  50. def get_proxys(log):
  51. """
  52. 获取代理
  53. :return: 代理
  54. """
  55. tunnel = "x371.kdltps.com:15818"
  56. kdl_username = "t13753103189895"
  57. kdl_password = "o0yefv6z"
  58. try:
  59. proxies = {
  60. "http": "http://%(user)s:%(pwd)s@%(proxy)s/" % {"user": kdl_username, "pwd": kdl_password, "proxy": tunnel},
  61. "https": "http://%(user)s:%(pwd)s@%(proxy)s/" % {"user": kdl_username, "pwd": kdl_password, "proxy": tunnel}
  62. }
  63. return proxies
  64. except Exception as e:
  65. log.error(f"Error getting proxy: {e}")
  66. raise e
  67. @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
  68. def make_request(log, method, url, params=None, data=None, headers=None, proxies=None, timeout=22, token=None):
  69. """
  70. 通用请求函数
  71. :param log: logger对象
  72. :param method: 请求方法 ('GET' 或 'POST')
  73. :param url: 请求的URL
  74. :param params: GET请求的查询参数
  75. :param data: POST请求的数据
  76. :param headers: 请求头
  77. :param proxies: 代理
  78. :param timeout: 请求超时时间
  79. :param token: token
  80. :return: 响应的JSON数据
  81. """
  82. if headers is None:
  83. headers = HEADERS
  84. if 'getHitCardReport' or 'getCardPublicly' or 'productDetailDynamics' or 'productShowList' in url:
  85. if not token:
  86. token = "a-87768ad775bf4513bec14f2c4fd2cd0c"
  87. headers["authori-zation"] = token
  88. if proxies is None:
  89. proxies = get_proxys(log)
  90. try:
  91. with requests.Session() as session:
  92. if method.upper() == 'GET':
  93. if proxies is None:
  94. response = session.get(url, headers=headers, params=params, timeout=timeout)
  95. else:
  96. response = session.get(url, headers=headers, params=params, proxies=proxies, timeout=timeout)
  97. elif method.upper() == 'POST':
  98. if proxies is None:
  99. response = session.post(url, headers=headers, json=data, timeout=timeout)
  100. # print(response.text)
  101. else:
  102. response = session.post(url, headers=headers, json=data, proxies=proxies, timeout=timeout)
  103. else:
  104. log.error(f"Unsupported request method: {method}")
  105. return None
  106. response.raise_for_status()
  107. data = response.json()
  108. # print(response.text)
  109. if data["code"] == 200:
  110. log.info(f"Successfully fetched {method} request to {url}")
  111. return data
  112. else:
  113. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: {data['message']}")
  114. return {}
  115. except requests.exceptions.RequestException as e:
  116. log.error(f"Error making {method} request to {url}: {e}")
  117. raise e
  118. except ValueError as e:
  119. log.error(f"Error parsing JSON for {method} request to {url}: {e}")
  120. raise e
  121. except Exception as e:
  122. log.error(f"Error making {method} request to {url}: {e}")
  123. raise e
  124. def get_play_back(log, product_id, token):
  125. """
  126. 获取 视频回放链接
  127. :param log: logger对象
  128. :param product_id: product_id
  129. :param token: token
  130. """
  131. log.info(f"Starting to fetch playback for product_id {product_id}")
  132. url = "https://api.joycard.xyz/api/front/c/product/productDetailDynamics"
  133. params = {
  134. # "code": "LCS1254174"
  135. "code": product_id
  136. }
  137. try:
  138. response = make_request(log, 'GET', url, params=params, token=token)
  139. if response:
  140. items = response.get("data", {})
  141. normalLiving = items.get("normalLiving", {})
  142. playback = normalLiving.get("playback")
  143. return playback
  144. else:
  145. return None
  146. except Exception as e:
  147. log.error(f"Error fetching playback {product_id}: {e}")
  148. return None
  149. def clean_texts(html_text):
  150. """
  151. 使用 BeautifulSoup 解析并获取纯文本
  152. :param html_text: 待解析的HTML格式的数据
  153. :return: clean_text -> 解析后的数据
  154. """
  155. if not html_text:
  156. return ""
  157. soup = BeautifulSoup(html_text, 'html.parser')
  158. # clean_text = soup.get_text(separator=' ', strip=True)
  159. clean_text = soup.get_text(strip=True)
  160. # 替换   为普通空格
  161. clean_text = clean_text.replace(' ', ' ')
  162. return clean_text
  163. def parse_product_items(log, items, sql_pool, product_id, token):
  164. """
  165. 解析 产品信息
  166. :param log: logger对象
  167. :param items: 请求response
  168. :param sql_pool: MySQL连接池对象
  169. :param product_id: product_id
  170. :param token: token
  171. """
  172. if not items:
  173. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: No items found")
  174. return
  175. no = items.get("id")
  176. create_time = items.get("publishTime")
  177. title = items.get("productName")
  178. img = items.get("productImageIndex")
  179. price_sale = items.get("unitPriceStr")
  180. total_price = items.get("totalSalePrice")
  181. sale_num = items.get("saleCount") # 售出数量
  182. spec_config = items.get("hitCardStandard") # 规格
  183. sort = items.get("series") # 分类 0:全部 1:原盒 2:幸运盒 3:福盒?
  184. state = items.get("status")
  185. shop_id = items.get("merchantCode")
  186. shop_name = items.get("merchantName")
  187. category = items.get("brandId")
  188. on_sale_time = items.get("onlineTime")
  189. end_time = items.get("endTime")
  190. finish_time = items.get("finishTime")
  191. # content = items.get("purchaseNotes")
  192. # if content:
  193. # content = content.replace("<p>", "").replace("</p>", "")
  194. # brief = items.get("brief")
  195. product_detail = items.get("productDetail")
  196. if product_detail:
  197. product_detail = clean_texts(product_detail)
  198. # print('product_detail:',product_detail)
  199. video_url = get_play_back(log, product_id, token)
  200. hit_card_desc = items.get("hitCardDesc") # 赠品介绍
  201. open_mode = items.get("openMode") # 随机球队
  202. open_mode_comment = items.get("openModeComment") # 随机球队 说明
  203. random_mode = items.get("randomMode") # 即买即随
  204. random_mode_comment = items.get("randomModeComment") # 即买即随 说明
  205. info_dict = {
  206. "no": no,
  207. "create_time": create_time,
  208. "title": title,
  209. "img": img,
  210. "price_sale": price_sale,
  211. "total_price": total_price,
  212. "sale_num": sale_num,
  213. "spec_config": spec_config,
  214. "sort": sort,
  215. "state": state,
  216. "shop_id": shop_id,
  217. "shop_name": shop_name,
  218. "category": category,
  219. "on_sale_time": on_sale_time,
  220. "end_time": end_time,
  221. "finish_time": finish_time,
  222. "product_detail": product_detail,
  223. "video_url": video_url,
  224. "hit_card_desc": hit_card_desc,
  225. "open_mode": open_mode,
  226. "open_mode_comment": open_mode_comment,
  227. "random_mode": random_mode,
  228. "random_mode_comment": random_mode_comment,
  229. }
  230. # print(info_dict)
  231. # sql_pool.insert_one_or_dict(table="yueka_product_record", data=info_dict)
  232. sql_pool.update_one_or_dict(table="yueka_product_record", data=info_dict, condition={"product_id": product_id})
  233. @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
  234. def get_product_details(log, product_id, sql_pool, token):
  235. """
  236. 获取 商品详情 单条 信息
  237. :param log: logger对象
  238. :param product_id: product_id
  239. :param sql_pool: MySQL连接池对象
  240. :param token: token
  241. """
  242. log.debug(f"Getting product details for {product_id}")
  243. url = "https://api.joycard.xyz/api/front/c/product/productDetail"
  244. params = {
  245. # "code": "LCS1254079"
  246. "code": product_id
  247. }
  248. try:
  249. response = make_request(log, 'GET', url, params=params, token=token)
  250. if response:
  251. parse_product_items(log, response.get("data"), sql_pool, product_id, token)
  252. else:
  253. log.error(f"Error getting product details for {product_id}: {response.get('msg')}")
  254. except Exception as e:
  255. log.error(f"Error getting product details for {product_id}: {e}")
  256. def get_product_detail_list(log, sql_pool, token):
  257. """
  258. 获取 商品详情 列表 信息
  259. :param log: logger对象
  260. :param sql_pool: MySQL连接池对象
  261. :param token: token
  262. """
  263. sql_product_id_list = sql_pool.select_all("SELECT product_id FROM yueka_product_record WHERE no IS NULL")
  264. sql_product_id_list = [item[0] for item in sql_product_id_list]
  265. for product_id in sql_product_id_list:
  266. try:
  267. get_product_details(log, product_id, sql_pool, token)
  268. except Exception as e:
  269. log.error(f"Error get_product_detail_list fetching product {product_id}: {e}")
  270. continue
  271. def parse_player_items(log, items, sql_pool, product_id):
  272. """
  273. 解析 卡密公示 信息
  274. :param log: logger对象
  275. :param items: 请求response
  276. :param product_id: product_id
  277. :param sql_pool: MySQL连接池对象
  278. """
  279. if not items:
  280. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: No items found")
  281. return
  282. player_list = []
  283. for item in items:
  284. # print(item)
  285. user_code = item.get("userCode")
  286. user_id = item.get("userId")
  287. user_name = item.get("nickName")
  288. num = item.get("cardCount")
  289. # info = (product_id, user_code, num, user_id, user_name)
  290. info_dict = {
  291. "product_id": product_id,
  292. "user_code": user_code,
  293. "num": num,
  294. "user_id": user_id,
  295. "user_name": user_name
  296. }
  297. # print(info_dict)
  298. player_list.append(info_dict)
  299. sql_pool.insert_many(table='yueka_player_record', data_list=player_list)
  300. sql_pool.update_one("update yueka_product_record set km_state = 1 where product_id = %s", (product_id,))
  301. @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
  302. def get_player_list(log, product_id, sql_pool, token):
  303. """
  304. 抓取 kami公示 信息
  305. :param log: logger对象
  306. :param product_id: product_id
  307. :param sql_pool: MySQL连接池对象
  308. :param token: token
  309. """
  310. log.debug(f"Getting player list for {product_id}")
  311. url = "https://api.joycard.xyz/api/front/c/card/getCardPublicly"
  312. last_id = 0 # 初始lastId为0
  313. total_players = 0
  314. while True:
  315. data = {
  316. "keyword": "",
  317. "lastUserId": last_id,
  318. "productCode": product_id,
  319. "publiclyType": 2, # 1:赠品维度 2:玩家维度
  320. }
  321. # print(data)
  322. try:
  323. response = make_request(log, 'POST', url, data=data, token=token)
  324. if not response:
  325. log.error(f"Error getting player list for {product_id}: Empty response")
  326. break
  327. items = response.get("data", [])
  328. if not items:
  329. log.info(f"No more players found for product {product_id}")
  330. sql_pool.update_one("update yueka_product_record set km_state = 3 where product_id = %s", (product_id,))
  331. break
  332. # 处理当前页数据
  333. parse_player_items(log, items, sql_pool, product_id)
  334. total_players += len(items)
  335. # 如果获取数量超过50条,说明已经获取到所有数据,结束循环
  336. if total_players > 50:
  337. log.debug(f"Total players found for product {product_id}: {total_players}")
  338. break
  339. # 如果获取数量不足20条,说明是最后一页
  340. if len(items) < 20:
  341. log.info(f"Last page detected for product {product_id} (got {len(items)} items)")
  342. break
  343. # 更新lastId为最后一条的userId
  344. last_id = items[-1].get("userId")
  345. # print(last_id)
  346. if not last_id:
  347. log.error("API response missing userId in last item, cannot paginate")
  348. break
  349. # 避免频繁请求
  350. # time.sleep(0.5)
  351. except Exception as e:
  352. log.error(f"Error getting player list for {product_id} at lastId {last_id}: {e}")
  353. break
  354. log.info(f"Finished fetching players for product {product_id}, total: {total_players}")
  355. def get_players(log, sql_pool, token):
  356. """
  357. 抓取 kami公示 信息
  358. :param log: logger对象
  359. :param sql_pool: MySQL连接池对象
  360. :param token: token
  361. """
  362. product_list = sql_pool.select_all("SELECT product_id FROM yueka_product_record WHERE km_state IN (0, 3)")
  363. product_list = [product_id[0] for product_id in product_list]
  364. # token = sql_pool.select_one("SELECT token FROM yueka_token")
  365. # token = token[0]
  366. if not product_list:
  367. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: No product_id found")
  368. return
  369. else:
  370. log.info(f"Start fetching players data. Total products: {len(product_list)}")
  371. for product_id in product_list:
  372. try:
  373. get_player_list(log, product_id, sql_pool, token)
  374. except Exception as e:
  375. log.error(f"Error fetching product {product_id}: {e}")
  376. continue
  377. @retry(stop=stop_after_attempt(5), wait=wait_fixed(1), after=after_log)
  378. def get_report_one_page(log, sql_pool, productCode, page, last_id, token):
  379. """
  380. 获取 拆卡报告 单页的信息
  381. :param log: logger对象
  382. :param sql_pool: MySQL连接池对象
  383. :param productCode: product_id
  384. :param page: 页码
  385. :param last_id: last_id
  386. :param token: token
  387. """
  388. url = "https://api.joycard.xyz/api/front/c/card/getHitCardReport"
  389. data = {
  390. "keyword": "",
  391. "page": page,
  392. "lastId": last_id,
  393. # "productCode": "LCS1254213"
  394. "productCode": productCode
  395. }
  396. log.info(f"Getting report data for: {productCode}, Page: {page}")
  397. try:
  398. response = make_request(log, 'POST', url, data=data, token=token)
  399. # print(response)
  400. if response:
  401. items = response.get("data", [])
  402. if items:
  403. info_list = []
  404. for item in items:
  405. card_id = item.get("orderNo")
  406. card_name = item.get("cardSecret")
  407. create_time = item.get("drawTime")
  408. imgs = item.get("hitPic")
  409. user_id = item.get("userCode")
  410. user_name = item.get("nickName")
  411. shop_id = item.get("merchantCode")
  412. shop_name = item.get("merchantName")
  413. card_desc = item.get("hitCardDesc")
  414. # info = (card_id, card_name, create_time, imgs, user_id, user_name, shop_id, shop_name, card_desc)
  415. info_dict = {
  416. "product_id": productCode,
  417. "card_id": card_id,
  418. "card_name": card_name,
  419. "create_time": create_time,
  420. "imgs": imgs,
  421. "user_id": user_id,
  422. "user_name": user_name,
  423. "shop_id": shop_id,
  424. "shop_name": shop_name,
  425. "card_desc": card_desc
  426. }
  427. # print(info_dict)
  428. info_list.append(info_dict)
  429. sql_pool.insert_many(table='yueka_report_record', data_list=info_list)
  430. log.info(f"Successfully saved {len(items)} report items")
  431. return items[-1].get("userCode"), len(items)
  432. else:
  433. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: No items found")
  434. sql_pool.update_one("update yueka_product_record set report_state = 3 where product_id = %s",
  435. (productCode,))
  436. return 0, 0
  437. else:
  438. log.error(f"Error getting report data: {response.get('msg')}")
  439. return 0
  440. except Exception as e:
  441. log.error(f"Error getting report data: {e}")
  442. raise e
  443. def get_report_list(log, sql_pool, product_id, token):
  444. """
  445. 抓取 拆卡报告 单个product_id 所有页码的 信息
  446. :param log: logger对象
  447. :param sql_pool: MySQL连接池对象
  448. :param product_id: product_id
  449. :param token: token
  450. """
  451. # log.info(f"Start fetching report data. Product id: {product_id}")
  452. page = 1
  453. last_id = 0
  454. # while True:
  455. try:
  456. last_d, len_item = get_report_one_page(log, sql_pool, product_id, page, last_id, token)
  457. # if len_item != 0 and len_item < 20:
  458. log.info(f"Finished fetching report data for product {product_id}, total: {len_item}")
  459. sql_pool.update_one("update yueka_product_record set report_state = 1 where product_id = %s", (product_id,))
  460. # # 如果获取数量不足20条,说明是最后一页 ***暂时没找到第二页的***
  461. # if len_item < 20:
  462. # log.info(f"Last page detected for product {product_id} (got {len_item} items)")
  463. # break
  464. #
  465. # # 更新lastId为最后一条的userId
  466. # last_id = last_d
  467. # if not last_id:
  468. # log.error("API response missing userId in last item, cannot paginate")
  469. # break
  470. #
  471. # page += 1
  472. except Exception as e:
  473. log.error(f"Error getting report data: {e}")
  474. # break
  475. def get_reports(log, sql_pool, token):
  476. """
  477. 抓取 拆卡报告 信息
  478. :param log: logger对象
  479. :param sql_pool: MySQL连接池对象
  480. :param token: token
  481. """
  482. product_list = sql_pool.select_all("SELECT product_id FROM yueka_product_record WHERE report_state IN (0, 3)")
  483. product_list = [product_id[0] for product_id in product_list]
  484. # token = sql_pool.select_one("SELECT token FROM yueka_token")
  485. # token = token[0]
  486. if not product_list:
  487. log.warning(f"Warning {inspect.currentframe().f_code.co_name}: No product_id found")
  488. return
  489. else:
  490. log.info(f"Start fetching report data. Total products: {len(product_list)}")
  491. for product_id in product_list:
  492. try:
  493. get_report_list(log, sql_pool, product_id, token)
  494. except Exception as e:
  495. log.error(f"Error fetching product {product_id}: {e}")
  496. continue
  497. if __name__ == '__main__':
  498. pass
  499. # pid = 'LCS1254213'
  500. # pid = 'LCS1253418'
  501. # pid = 'LCS1256332'
  502. # from mysql_pool import MySQLConnectionPool
  503. # sql_pool_ = MySQLConnectionPool(log=logger)
  504. # get_reports(logger, None)
  505. # get_player_list(logger, pid, None)
  506. # get_product_details(logger, 'LCS1255968', sql_pool_)