meml_spdier.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/5/18 15:17
  5. import random
  6. import time
  7. import inspect
  8. import schedule
  9. import user_agent
  10. from loguru import logger
  11. from parsel import Selector
  12. from curl_cffi import requests
  13. from curl_cffi.requests import BrowserType
  14. from mysql_pool import MySQLConnectionPool
  15. from tenacity import retry, stop_after_attempt, wait_fixed
  16. """
  17. 目标网站:https://bid.memorylaneinc.com/lots/gallery?page=2
  18. """
  19. logger.remove()
  20. logger.add("./logs/{time:YYYYMMDD}.log", encoding='utf-8', rotation="00:00",
  21. format="[{time:YYYY-MM-DD HH:mm:ss.SSS}] {level} {message}",
  22. level="DEBUG", retention="7 day")
  23. # 直接用库内置的所有浏览器类型,不用手动维护列表
  24. client_identifier_list = [b.value for b in BrowserType]
  25. # print(client_identifier_list)
  26. headers = {
  27. "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  28. "user-agent": user_agent.generate_user_agent()
  29. }
  30. def after_log(retry_state):
  31. """
  32. retry 回调
  33. :param retry_state: RetryCallState 对象
  34. """
  35. # 检查 args 是否存在且不为空
  36. if retry_state.args and len(retry_state.args) > 0:
  37. log = retry_state.args[0] # 获取传入的 logger
  38. else:
  39. log = logger # 使用全局 logger
  40. if retry_state.outcome.failed:
  41. log.warning(
  42. f"Function '{retry_state.fn.__name__}', Attempt {retry_state.attempt_number} Times")
  43. else:
  44. log.info(f"Function '{retry_state.fn.__name__}', Attempt {retry_state.attempt_number} succeeded")
  45. @retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
  46. def get_proxys(log):
  47. http_proxy = "http://u1952150085001297:sJMHl4qc4bM0@proxy.123proxy.cn:36931"
  48. https_proxy = "http://u1952150085001297:sJMHl4qc4bM0@proxy.123proxy.cn:36931"
  49. try:
  50. proxySettings = {
  51. "http": http_proxy,
  52. "https": https_proxy,
  53. }
  54. return proxySettings
  55. except Exception as e:
  56. log.error(f"Error getting proxy: {e}")
  57. raise e
  58. @retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
  59. def get_details(log, url, sql_pool, sql_id):
  60. """
  61. 获取详情数据
  62. :param log: logger对象
  63. :param url: 详情页URL
  64. :param sql_pool: MySQL连接池
  65. :param sql_id: 数据ID
  66. :return: 标题和描述
  67. """
  68. log.info(f">>>>>>>>>>>>>> 正在爬取详情数据URL: {url} <<<<<<<<<<<<<<")
  69. response = requests.get(url, headers=headers, impersonate=random.choice(client_identifier_list), timeout=10,
  70. proxies=get_proxys(log))
  71. response.raise_for_status()
  72. selector = Selector(response.text)
  73. category = selector.xpath('//a[@id="MainContent_hCategory"]/text()').get()
  74. description = selector.xpath('//*[@id="MainContent_lblOldAuction"]/text()').getall()
  75. description = ' '.join(description).strip() if description else None
  76. # imgs = selector.xpath('//div[@class="col-md-5 col-sm-5"]//a[@class="MagicThumb-swap"]/@href').getall()
  77. imgs = selector.xpath('//div[@class="col-md-5 col-sm-5"]//a[not(@id="Zoomer")]/@href').getall()
  78. imgs = ','.join(imgs) if imgs else None
  79. # print(category, description, imgs)
  80. # 更新数据和状态
  81. sql_pool.update_one_or_dict(
  82. table="memory_lane_record",
  83. data={"category": category, "description": description, "imgs": imgs, "state": 1},
  84. condition={"id": sql_id}
  85. )
  86. def get_single_page(log, page, sql_pool):
  87. """
  88. 获取单页数据
  89. :param log: logger对象
  90. :param page: 页码
  91. :param sql_pool: MySQL连接池
  92. :return: 该页数据条数
  93. """
  94. log.info(f"Getting page -> {page} data....................................................")
  95. url = "https://bid.memorylaneinc.com/lots/gallery"
  96. params = {"page": page}
  97. # response = requests.get(url, headers=headers, impersonate="chrome124", params=params, timeout=10)
  98. response = requests.get(url, headers=headers, impersonate=random.choice(client_identifier_list), params=params,
  99. proxies=get_proxys(log), timeout=10)
  100. # print(response.text)
  101. response.raise_for_status()
  102. selector = Selector(response.text)
  103. tag_div_list = selector.xpath(
  104. '//div[@class="items"]/div/div[@class="row"]//div[@class="col-lg-3 col-md-4 col-sm-6"]')
  105. # print('tag_div_list:', tag_div_list)
  106. info_list = []
  107. for tag_div in tag_div_list:
  108. title = tag_div.xpath('.//p/a/text()').get()
  109. detail_url = tag_div.xpath('.//p/a/@href').get()
  110. # img = tag_div.xpath('.//div[@class="item-image"]/a/img/@src').get()
  111. tag_div_p = tag_div.xpath('.//div/p[2]/strong/text()').getall()
  112. bids = tag_div_p[0] if tag_div_p else None
  113. opening_bid = tag_div_p[1] if len(tag_div_p) > 1 else None
  114. opening_bid = opening_bid.replace('$', '').replace(',', '').strip() if opening_bid else None
  115. status = tag_div_p[2] if len(tag_div_p) > 2 else None
  116. current_bid = tag_div.xpath('.//div[@class="item-price"]/a/text()').get()
  117. current_bid = current_bid.replace('CURRENT BID $', '').replace(',', '').strip() if current_bid else None
  118. data_dict = {
  119. "title": title,
  120. "detail_url": detail_url,
  121. # "img": img,
  122. "bids": bids,
  123. "opening_bid": opening_bid,
  124. "status": status,
  125. "current_bid": current_bid
  126. }
  127. # print('data_dict:', data_dict)
  128. info_list.append(data_dict)
  129. # 保存数据到数据库
  130. if info_list:
  131. sql_pool.insert_many(table="memory_lane_record", data_list=info_list, ignore=True)
  132. return len(info_list)
  133. def get_sold_list(log, sql_pool):
  134. """
  135. 获取已售列表
  136. :param log: logger对象
  137. :param sql_pool: MySQL连接池
  138. :return: 无
  139. """
  140. page = 1
  141. max_page = 10
  142. while page <= max_page:
  143. try:
  144. len_list = get_single_page(log, page, sql_pool)
  145. except Exception as e:
  146. log.error(f"Error getting page {page}: {e}")
  147. continue
  148. if len_list == 0:
  149. log.warning(f"No data on page {page}, stopping further requests")
  150. break
  151. page += 1
  152. @retry(stop=stop_after_attempt(100), wait=wait_fixed(3600), after=after_log)
  153. def meml_main(log):
  154. """
  155. 主函数
  156. :param log: logger对象
  157. """
  158. log.info(
  159. f'开始运行 {inspect.currentframe().f_code.co_name} 爬虫任务....................................................')
  160. # 配置 MySQL 连接池
  161. sql_pool = MySQLConnectionPool(log=log)
  162. if not sql_pool:
  163. log.error("MySQL数据库连接失败")
  164. raise Exception("MySQL数据库连接失败")
  165. try:
  166. try:
  167. get_sold_list(log, sql_pool)
  168. except Exception as e:
  169. log.error(f'Error getting sold list: {e}')
  170. # 更新详情页
  171. log.debug('Updating detail pages........................... started')
  172. # sql_result = sql_pool.select_all('select id, detail_url from memory_lane_record where state = 0')
  173. sql_result = sql_pool.select_all('select id, detail_url from memory_lane_record where state != 1 order by id')
  174. for row in sql_result:
  175. sql_id = row[0]
  176. detail_url = row[1]
  177. try:
  178. get_details(log, detail_url, sql_pool, sql_id)
  179. except Exception as e:
  180. log.error(f'Error getting details for {detail_url}: {e}')
  181. # 更新数据和状态
  182. sql_pool.update_one_or_dict(
  183. table="memory_lane_record",
  184. data={"state": 2},
  185. condition={"id": sql_id}
  186. )
  187. except Exception as e:
  188. log.error(f'{inspect.currentframe().f_code.co_name} error: {e}')
  189. finally:
  190. log.info(f'爬虫程序 {inspect.currentframe().f_code.co_name} 运行结束,等待下一轮的采集任务............')
  191. def schedule_task():
  192. """
  193. 设置定时任务
  194. """
  195. meml_main(log=logger)
  196. schedule.every().day.at("05:00").do(meml_main, log=logger)
  197. while True:
  198. schedule.run_pending()
  199. time.sleep(1)
  200. if __name__ == "__main__":
  201. # get_single_page(log=logger, page=1, sql_pool=None)
  202. schedule_task()