settings.py 21 KB

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