lelands_spider.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/5/13 15:54
  5. import random
  6. import time
  7. import inspect
  8. import schedule
  9. from curl_cffi import requests
  10. import user_agent
  11. from loguru import logger
  12. from parsel import Selector
  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://auction.lelands.com/lots/gallery/?page=3
  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. headers = {
  26. "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",
  27. # "accept-language": "en,zh-CN;q=0.9,zh;q=0.8",
  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. # url = 'https://auction.lelands.com/bids/bidplace?itemid=133680'
  70. response = requests.get(url, headers=headers, impersonate=random.choice(client_identifier_list), timeout=10,
  71. proxies=get_proxys(log))
  72. response.raise_for_status()
  73. selector = Selector(response.text)
  74. category = selector.xpath('//a[@id="MainContent_hCategory"]/text()').get()
  75. description = selector.xpath('//*[@id="MainContent_lblOldAuction"]/text()').getall()
  76. description = ' '.join(description).strip() if description else None
  77. # imgs = selector.xpath('//div[@class="col-md-5 col-sm-5"]//a[@class="MagicThumb-swap"]/@href').getall()
  78. imgs = selector.xpath('//div[@class="col-md-5 col-sm-5"]//a[not(@id="Zoomer")]/@href').getall()
  79. imgs = ','.join(imgs) if imgs else None
  80. # print(category, description, imgs)
  81. # 更新数据和状态
  82. sql_pool.update_one_or_dict(
  83. table="lelands_record",
  84. data={"category": category, "description": description, "imgs": imgs, "state": 1},
  85. condition={"id": sql_id}
  86. )
  87. @retry(stop=stop_after_attempt(5), wait=wait_fixed(2), after=after_log)
  88. def get_single_page(log, page, sql_pool):
  89. """
  90. 获取单页数据
  91. :param log: logger对象
  92. :param page: 页码
  93. :param sql_pool: MySQL连接池
  94. :return: 该页数据条数
  95. """
  96. log.info(f">>>>>>>>>>>>>> 正在爬取第 {page} 页数据 <<<<<<<<<<<<<<")
  97. url = "https://auction.lelands.com/lots/gallery/"
  98. params = {
  99. # "page": "1"
  100. "page": f"{page}"
  101. }
  102. # response = requests.get(url, headers=headers, params=params, timeout=22, proxies=get_proxys(log))
  103. with requests.Session() as session:
  104. response = session.get(url, impersonate=random.choice(client_identifier_list), headers=headers, params=params,
  105. proxies=get_proxys(log), timeout=10, allow_redirects=False)
  106. # print(response.text)
  107. # print(response)
  108. response.raise_for_status()
  109. selector = Selector(response.text)
  110. # 实际加载内容有变化,需要调整 XPath 表达式
  111. # tag_div_list = selector.xpath('//div[@class="col-md-9 col-sm-9"]/div[@class="col-lg-3 col-md-4 col-sm-6"]')
  112. tag_div_list = selector.xpath(
  113. '//div[@class="items"]/div/div[@class="row"]//div[@class="col-lg-3 col-md-4 col-sm-6"]')
  114. # print('tag_div_list:', tag_div_list)
  115. info_list = []
  116. for tag_div in tag_div_list:
  117. title = tag_div.xpath('.//p/a/text()').get()
  118. detail_url = tag_div.xpath('.//p/a/@href').get()
  119. # img = tag_div.xpath('.//div[@class="item-image"]/a/img/@src').get()
  120. tag_div_p = tag_div.xpath('.//div/p[2]/strong/text()').getall()
  121. bids = tag_div_p[0] if tag_div_p else None
  122. opening_bid = tag_div_p[1] if len(tag_div_p) > 1 else None
  123. opening_bid = opening_bid.replace('$', '').replace(',', '').strip() if opening_bid else None
  124. status = tag_div_p[2] if len(tag_div_p) > 2 else None
  125. price = tag_div.xpath('.//div[@class="item-price"]/a/text()').get()
  126. price = price.replace('SOLD FOR $', '').replace(',', '').strip() if price else None
  127. data_dict = {
  128. "title": title,
  129. "detail_url": detail_url,
  130. # "img": img,
  131. "bids": bids,
  132. "opening_bid": opening_bid,
  133. "status": status,
  134. "price": price
  135. }
  136. # print('data_dict:', data_dict)
  137. info_list.append(data_dict)
  138. # 保存数据到数据库
  139. if info_list:
  140. sql_pool.insert_many(table="lelands_record", data_list=info_list, ignore=True)
  141. return len(info_list)
  142. def get_sold_list(log, sql_pool):
  143. """
  144. 获取已售列表
  145. :param log: logger对象
  146. :param sql_pool: MySQL连接池
  147. :return: 无
  148. """
  149. page = 1
  150. max_page = 10
  151. while page <= max_page:
  152. try:
  153. len_list = get_single_page(log, page, sql_pool)
  154. except Exception as e:
  155. log.error(f"Error getting page {page}: {e}")
  156. continue
  157. if len_list == 0:
  158. log.warning(f"No data on page {page}, stopping further requests")
  159. break
  160. page += 1
  161. @retry(stop=stop_after_attempt(100), wait=wait_fixed(3600), after=after_log)
  162. def lds_main(log):
  163. """
  164. 主函数
  165. :param log: logger对象
  166. """
  167. log.info(
  168. f'开始运行 {inspect.currentframe().f_code.co_name} 爬虫任务....................................................')
  169. # 配置 MySQL 连接池
  170. sql_pool = MySQLConnectionPool(log=log)
  171. if not sql_pool:
  172. log.error("MySQL数据库连接失败")
  173. raise Exception("MySQL数据库连接失败")
  174. try:
  175. try:
  176. get_sold_list(log, sql_pool)
  177. except Exception as e:
  178. log.error(f'Error getting sold list: {e}')
  179. # 更新详情页
  180. log.debug('Updating detail pages........................... started')
  181. # sql_result = sql_pool.select_all('select id, detail_url from lelands_record where state = 0')
  182. sql_result = sql_pool.select_all('select id, detail_url from lelands_record where state != 1')
  183. # sql_result = sql_pool.select_all('select id, detail_url from lelands_record where imgs is null')
  184. for row in sql_result:
  185. sql_id = row[0]
  186. detail_url = row[1]
  187. try:
  188. get_details(log, detail_url, sql_pool, sql_id)
  189. except Exception as e:
  190. log.error(f'Error getting details for {detail_url}: {e}')
  191. # 更新数据和状态
  192. sql_pool.update_one_or_dict(
  193. table="lelands_record",
  194. data={"state": 2},
  195. condition={"id": sql_id}
  196. )
  197. except Exception as e:
  198. log.error(f'{inspect.currentframe().f_code.co_name} error: {e}')
  199. finally:
  200. log.info(f'爬虫程序 {inspect.currentframe().f_code.co_name} 运行结束,等待下一轮的采集任务............')
  201. def schedule_task():
  202. """
  203. 设置定时任务
  204. """
  205. lds_main(log=logger)
  206. schedule.every().day.at("05:00").do(lds_main, log=logger)
  207. while True:
  208. schedule.run_pending()
  209. time.sleep(1)
  210. if __name__ == '__main__':
  211. # lds_main(log=logger)
  212. schedule_task()
  213. # get_single_page(log=logger, page=1, sql_pool=None)
  214. # get_details(logger, "https://auction.lelands.com/bids/bidplace?itemid=133680", sql_pool=None, sql_id=None)
  215. """
  216. ['https://auction.lelands.com/images_items/item_133680_1_488429.jpg',
  217. 'https://auction.lelands.com/images_items/item_133680_1_488429.jpg',
  218. 'https://auction.lelands.com/images_items/item_133680_2_488430.jpg',
  219. 'https://auction.lelands.com/images_items/item_133680_3_488431.jpg',
  220. 'https://auction.lelands.com/images_items/item_133680_4_488432.jpg',
  221. 'https://auction.lelands.com/images_items/item_133680_5_488433.jpg',
  222. 'https://auction.lelands.com/images_items/item_133680_6_488434.jpg',
  223. 'https://auction.lelands.com/images_items/item_133680_7_488435.jpg',
  224. 'https://auction.lelands.com/images_items/item_133680_8_488436.jpg',
  225. 'https://auction.lelands.com/images_items/item_133680_9_488437.jpg',
  226. 'https://auction.lelands.com/images_items/item_133680_10_488438.jpg',
  227. 'https://auction.lelands.com/images_items/item_133680_11_488439.jpg',
  228. 'https://auction.lelands.com/images_items/item_133680_12_488440.jpg',
  229. 'https://auction.lelands.com/images_items/item_133680_13_488441.jpg']
  230. """