france_slot_api.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. import time
  2. import json
  3. import inspect
  4. import requests
  5. import uuid
  6. import random
  7. import urllib3
  8. import asyncio
  9. import aiohttp
  10. import logging
  11. from datetime import datetime, timedelta
  12. # from book_data_buiilder import troov_dublin_visas_book_data_builder
  13. # 禁止显示 urllib3 的 SSL 警告信息
  14. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  15. logger = logging.getLogger(__name__)
  16. TROOV_EMBASSY = {
  17. # 都柏林
  18. 'TroovFr_Dublin_Visas': {
  19. 'code': 'ambassade-de-france-en-irlande',
  20. 'name': 'Visas',
  21. 'teamId': '621540d353069dec25bd0045',
  22. 'zoneId': '624317926863643fe83c8548',
  23. 'chooseService': 'Visas',
  24. 'submitCountry': 'IE',
  25. 'submitCity': 'DUB',
  26. 'travelCountry': 'FR',
  27. 'visaCategory': 'Visas',
  28. 'website': 'https://consulat.gouv.fr/en/ambassade-de-france-en-irlande/appointment?name=Visas'
  29. },
  30. # 日本 - 测试环境
  31. 'TroovFr_Tokyo_Visa': {
  32. 'code': 'ambassade-de-france-a-tokyo',
  33. 'name': 'Visa',
  34. 'teamId': '6238b4dfb1e5a274ff4c0f09',
  35. 'zoneId': '6242f7463f8ce81dec596054',
  36. 'chooseService': 'Visa',
  37. 'submitCountry': 'JP',
  38. 'submitCity': 'TYO',
  39. 'travelCountry': 'FR',
  40. 'visaCategory': 'Visa',
  41. 'website': 'https://consulat.gouv.fr/en/ambassade-de-france-a-tokyo/appointment?name=Visa'
  42. },
  43. }
  44. class SessionExpiredError(Exception):
  45. """会话过期异常"""
  46. def __init__(self, message="SESSION EXPIRED"):
  47. super().__init__(message)
  48. def check_responsed_session_expired(resp_text: str):
  49. """检查响应内容是否包含会话过期标识,如有则抛出异常"""
  50. if "SESSION_EXPIRED" in resp_text or "SESSION_NOT_FOUND" in resp_text:
  51. raise SessionExpiredError("SESSION EXPIRED OR NOT FOUND")
  52. def is_session_remaining_life_zero(session_dic, max_lifetime_minutes=5):
  53. def should_expire(probability=0.6):
  54. """以给定概率返回 True(表示失效),否则返回 False"""
  55. return random.random() < probability
  56. now = datetime.utcnow()
  57. now_hour, now_minute = now.hour, now.minute
  58. session_time = datetime.strptime(session_dic['session_create_at'], "%Y-%m-%dT%H:%M:%S.%fZ")
  59. session_hour, session_minute = session_time.hour, session_time.minute
  60. # 🕒 计算 session 已存活时长(分钟)
  61. elapsed_minutes = (now - session_time).total_seconds() / 60
  62. # ✅ 新增逻辑:session 超过 n 分钟就强制过期
  63. if elapsed_minutes >= max_lifetime_minutes:
  64. return True
  65. # 🧠 原逻辑保持不变
  66. if (now_minute >= 45 and session_hour == now_hour and 45 <= session_minute < 60) or \
  67. (now_minute < 5 and session_hour == now_hour) or \
  68. (now_minute >= 5 and now_minute < 45):
  69. return False
  70. expired = should_expire()
  71. return expired
  72. async def troov_handshake(embassy, proxy):
  73. url = "https://51.254.177.49/api/handshake"
  74. headers = {
  75. 'accept': 'application/json, text/plain, */*',
  76. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  77. 'origin': 'https://consulat.gouv.fr',
  78. 'referer': embassy['website'],
  79. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  80. 'x-gouv-web': 'fr.gouv.consulat'
  81. }
  82. session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  83. try:
  84. async with session.head(url, headers=headers, proxy=proxy) as response:
  85. if response.status == 200:
  86. return (
  87. response.headers.get('X-Gouv-App-Id'),
  88. response.headers.get('X-Gouv-Handshake')
  89. )
  90. return None
  91. finally:
  92. await session.close()
  93. async def troov_create_session(proxy, captcha, embassy=TROOV_EMBASSY['TroovFr_Dublin_Visas']):
  94. handshake_ret = await troov_handshake(embassy, proxy)
  95. if not handshake_ret:
  96. logger.error(f'troov_handshake failed')
  97. return None
  98. x_gouv_app_id, x_gouv_handshake = handshake_ret
  99. reservation_session_ret = await troov_make_reservation_session(embassy, proxy, captcha, x_gouv_app_id, x_gouv_handshake)
  100. if not reservation_session_ret:
  101. logger.error(f'troov_make_reservation_session failed')
  102. return None
  103. x_gouv_handshake2, session_create_at, session_id = reservation_session_ret
  104. session_dic = {
  105. 'embassy': embassy,
  106. 'x_gouv_app_id':x_gouv_app_id,
  107. 'x-csrf-token': x_gouv_handshake2,
  108. 'session_create_at': session_create_at,
  109. 'session_id': session_id
  110. }
  111. status = await troov_update_dynamic_steps(proxy, session_dic)
  112. if status:
  113. return session_dic
  114. logger.error(f'troov_update_dynamic_steps failed')
  115. return None
  116. async def troov_refresh_session(proxy, session_dic):
  117. handshake_ret = await troov_handshake(proxy, session_dic['center'])
  118. if not handshake_ret:
  119. return None
  120. x_gouv_app_id, x_gouv_handshake = handshake_ret
  121. session_dic['x_gouv_app_id'] = x_gouv_app_id
  122. session_dic['x-csrf-token'] = x_gouv_handshake
  123. reservation_session_ret = await troov_get_reservation_session(proxy, session_dic)
  124. if not reservation_session_ret:
  125. return None
  126. return session_dic
  127. async def troov_make_reservation_session(embassy, proxy, capcha_str, handshake_gouv_appid, handshake_gouv_handshake):
  128. url = f"https://51.254.177.49/api/team/{embassy['teamId']}/reservations-session"
  129. payload = json.dumps({
  130. "standaloneServiceName": embassy["name"],
  131. "sessionId": None,
  132. "captcha": capcha_str
  133. })
  134. headers = {
  135. 'accept': 'application/json, text/plain, */*',
  136. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  137. 'content-type': 'application/json',
  138. 'origin': f'https://consulat.gouv.fr',
  139. 'referer': embassy["website"],
  140. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  141. 'x-csrf-token': handshake_gouv_handshake,
  142. 'x-gouv-app-id': handshake_gouv_appid,
  143. 'x-gouv-web': 'fr.gouv.consulat'
  144. }
  145. session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  146. try:
  147. async with session.post(url, headers=headers, data=payload, proxy=proxy) as response:
  148. response_text = await response.text()
  149. if response.status == 200:
  150. response_dic = json.loads(response_text)
  151. return response.headers['X-Gouv-Handshake'], response_dic['created_at'], response_dic['_id']
  152. logger.error(f'troov_make_reservation_session {response.status}, {response_text}')
  153. return None
  154. finally:
  155. await session.close()
  156. async def troov_get_reservation_session(proxy, session_dic):
  157. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations-session"
  158. headers = {
  159. 'accept': 'application/json, text/plain, */*',
  160. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  161. 'origin': 'https://consulat.gouv.fr',
  162. 'referer': session_dic['embassy']['website'],
  163. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  164. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  165. 'x-gouv-web': 'fr.gouv.consulat',
  166. }
  167. params = {
  168. 'sessionId': session_dic['session_id'],
  169. 'standaloneServiceName': session_dic['embassy']['name']
  170. }
  171. session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  172. try:
  173. async with session.get(url, params=params, headers=headers, proxy=proxy) as response:
  174. if response.status == 200:
  175. return await response.json()
  176. else:
  177. rtext = await response.text()
  178. check_responsed_session_expired(rtext)
  179. return None
  180. finally:
  181. await session.close()
  182. async def troov_update_dynamic_steps(proxy, session_dic):
  183. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations-session/{session_dic['session_id']}/update-dynamic-steps"
  184. payload = json.dumps({
  185. "key": "slotsSteps",
  186. "steps": [
  187. {
  188. "stepType": "slotsStep",
  189. "name": "Visas",
  190. "numberOfSlots": 1,
  191. "dynamicStepIndex": 0,
  192. "zone_id": session_dic['embassy']['zoneId'],
  193. "value": {
  194. "lastSelectedDate": "",
  195. "label": session_dic['embassy']['name'],
  196. "accessibleCalendar": False,
  197. "hasSwitchedCalendar": False,
  198. "slots": {}
  199. }
  200. }
  201. ]
  202. })
  203. headers = {
  204. 'accept': 'application/json, text/plain, */*',
  205. 'content-type': 'application/json',
  206. 'origin': f'https://consulat.gouv.fr',
  207. 'referer': session_dic["embassy"]["website"],
  208. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  209. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  210. 'x-gouv-web': 'fr.gouv.consulat',
  211. }
  212. session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  213. try:
  214. async with session.post(url, headers=headers, data=payload, proxy=proxy) as response:
  215. if response.status == 200:
  216. return True
  217. else:
  218. rtext = await response.text()
  219. check_responsed_session_expired(rtext)
  220. logger.error(f'troov_make_reservation_session {response.status}, {rtext}')
  221. return False
  222. finally:
  223. await session.close()
  224. async def troov_update_step_value(proxy, session_dic, slot_datetime_list: list):
  225. slots_data = {}
  226. for slot_datetime in slot_datetime_list:
  227. slot_date = slot_datetime['date']
  228. slot_time = slot_datetime['time']
  229. if slot_date not in slots_data:
  230. slots_data[slot_date] = []
  231. _ = {
  232. "time": slot_time,
  233. "rate": "0.00",
  234. "capacity": 1,
  235. "numberOfApplicants": 1,
  236. "date": slot_date
  237. }
  238. slots_data[slot_date].append(_)
  239. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations-session/{session_dic['session_id']}/update-step-value"
  240. payload = json.dumps({
  241. "key": "slotsStep",
  242. "value": {
  243. "lastSelectedDate": list(slots_data.keys())[-1],
  244. "label": session_dic['embassy']['name'],
  245. "accessibleCalendar": False,
  246. "hasSwitchedCalendar": False,
  247. "slots": slots_data
  248. },
  249. "stepIndex": 2,
  250. "dynamicStepIndex": 0
  251. })
  252. headers = {
  253. 'accept': 'application/json, text/plain, */*',
  254. 'content-type': 'application/json',
  255. 'origin': 'https://consulat.gouv.fr',
  256. 'referer': session_dic['embassy']['website'],
  257. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  258. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  259. 'x-gouv-web': 'fr.gouv.consulat',
  260. }
  261. timeout = aiohttp.ClientTimeout(total=10)
  262. # 为每个请求创建全新的连接,不使用连接池
  263. connector = aiohttp.TCPConnector(ssl=False, limit=1, force_close=True)
  264. async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
  265. async with session.post(url, headers=headers, data=payload, proxy=proxy) as response:
  266. rtext = await response.text()
  267. logger.info(f'update-step-value retcode={response.status}')
  268. if response.status == 200:
  269. logger.info(f'update-step-value retcode={response.status}, text={rtext}')
  270. return slot_datetime_list
  271. if response.status == 404:
  272. check_responsed_session_expired(rtext)
  273. response_dic = json.loads(rtext)
  274. taken_slots = response_dic.get('message', {}).get('takenSlots', [])
  275. taken_slot_datetimes = []
  276. for ts in taken_slots:
  277. sdt = {
  278. "date": ts['slotDate'].split('T')[0],
  279. "time": ts['slotDate'].split('T')[1]
  280. }
  281. taken_slot_datetimes.append(sdt)
  282. return [x for x in slot_datetime_list if x not in taken_slot_datetimes]
  283. return None
  284. async def troov_get_exclude_days(proxy, session_dic):
  285. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/exclude-days"
  286. payload = json.dumps({
  287. "session": {
  288. session_dic['embassy']['zoneId']: True
  289. },
  290. "sessionId": session_dic['session_id']
  291. })
  292. headers = {
  293. 'accept': 'application/json, text/plain, */*',
  294. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  295. 'content-type': 'application/json',
  296. 'origin': f'https://consulat.gouv.fr',
  297. 'referer': session_dic['embassy']['website'],
  298. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  299. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  300. 'x-gouv-web': 'fr.gouv.consulat'
  301. }
  302. session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  303. try:
  304. async with session.post(url, headers=headers, data=payload, proxy=proxy) as response:
  305. if response.status == 200:
  306. return await response.json()
  307. else:
  308. rtext = await response.text()
  309. check_responsed_session_expired(rtext)
  310. return None
  311. finally:
  312. await session.close()
  313. async def troov_get_interval(proxy, session_dic):
  314. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/get-interval?serviceId={session_dic['embassy']['zoneId']}"
  315. headers = {
  316. 'accept': 'application/json, text/plain, */*',
  317. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  318. 'origin': f'https://consulat.gouv.fr',
  319. 'referer': session_dic["embassy"]["website"],
  320. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  321. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  322. 'x-gouv-web': 'fr.gouv.consulat'
  323. }
  324. async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
  325. async with session.get(url, headers=headers, proxy=proxy) as response:
  326. if response.status == 200:
  327. return await response.json()
  328. else:
  329. rtext = await response.text()
  330. check_responsed_session_expired(rtext)
  331. return None
  332. async def troov_get_available_times(proxy, session_dic, date, places=1, capacity=2):
  333. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/availability?name=Visas&date={date}&places={places}&matching=&maxCapacity={capacity}&sessionId={session_dic['session_id']}"
  334. headers = {
  335. 'accept': 'application/json, text/plain, */*',
  336. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  337. 'origin': f'https://consulat.gouv.fr',
  338. 'referer': session_dic['embassy']['website'],
  339. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  340. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  341. 'x-gouv-web': 'fr.gouv.consulat'
  342. }
  343. async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
  344. async with session.get(url, headers=headers, proxy=proxy) as response:
  345. if response.status == 200:
  346. return await response.json()
  347. else:
  348. rtext = await response.text()
  349. check_responsed_session_expired(rtext)
  350. return None
  351. # async def troov_book(proxy, session_dic, date, slot, uinfo, captcha):
  352. # url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/family"
  353. # book_body = troov_dublin_visas_book_data_builder(session_dic['session_id'], date, slot, uinfo, captcha)
  354. # payload = json.dumps(book_body)
  355. # headers = {
  356. # 'accept': 'application/json, text/plain, */*',
  357. # 'content-type': 'application/json',
  358. # 'origin': f'https://consulat.gouv.fr',
  359. # 'referer': session_dic['embassy']['website'],
  360. # 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  361. # 'x-csrf-token': session_dic['x-csrf-token'],
  362. # 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  363. # 'x-gouv-web': 'fr.gouv.consulat',
  364. # }
  365. # session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False))
  366. # try:
  367. # async with session.post(url, headers=headers, data=payload, proxy=proxy) as response:
  368. # if response.status == 200:
  369. # resp_text = await response.text()
  370. # if "qrcode" in resp_text:
  371. # return resp_text
  372. # else:
  373. # rtext = await response.text()
  374. # check_responsed_session_expired(rtext)
  375. # return None
  376. # finally:
  377. # await session.close()
  378. async def troov_get_open_days(proxy, session_dic):
  379. def get_dates_in_range(date_range):
  380. start_date = datetime.strptime(date_range["start"], "%Y-%m-%d")
  381. end_date = datetime.strptime(date_range["end"], "%Y-%m-%d")
  382. dates_list = []
  383. current_date = start_date
  384. while current_date <= end_date:
  385. dates_list.append(current_date.strftime("%Y-%m-%d"))
  386. current_date += timedelta(days=1)
  387. return dates_list
  388. # 🚀 并发执行两个请求
  389. interval, exclude_days = await asyncio.gather(
  390. troov_get_interval(proxy, session_dic),
  391. troov_get_exclude_days(proxy, session_dic)
  392. )
  393. # logger.info(f'interval={interval}, exclude_days={exclude_days}')
  394. if interval and exclude_days:
  395. all_days = get_dates_in_range(interval)
  396. available_dates = [
  397. day for day in all_days
  398. if day not in exclude_days and datetime.strptime(day, "%Y-%m-%d").weekday() not in (5, 6)
  399. ]
  400. return available_dates
  401. return None
  402. async def concurrency_full_coverage_lock(proxies, session_dic, slot_datetime_list, timeout=5):
  403. start_time = time.perf_counter()
  404. sem = asyncio.Semaphore(len(slot_datetime_list))
  405. async def one_call(slot_datetime: dict):
  406. async with sem:
  407. t0 = time.perf_counter()
  408. try:
  409. result = await asyncio.wait_for(
  410. troov_update_step_value(random.choice(proxies), session_dic, [slot_datetime]),
  411. timeout=timeout
  412. )
  413. locked = bool(result)
  414. status = "locked" if locked else "success"
  415. except asyncio.TimeoutError:
  416. locked = None
  417. status = "timeout"
  418. except SessionExpiredError: # ⚠️ 不吞掉这个异常,直接向上传递
  419. raise
  420. except Exception as e:
  421. locked = None
  422. status = f"error: {repr(e)}"
  423. print(f'e={e}')
  424. finally:
  425. t1 = time.perf_counter()
  426. duration = t1 - t0
  427. return {
  428. "slot_datetime": slot_datetime,
  429. "locked": locked,
  430. "duration": duration,
  431. "status": status
  432. }
  433. try:
  434. tasks = [one_call(slot_datetime) for slot_datetime in slot_datetime_list]
  435. results = await asyncio.gather(*tasks)
  436. except SessionExpiredError:
  437. # 🚨 如果任意任务抛出 SessionExpiredError,这里会捕获到
  438. raise # 交由外部处理
  439. end_time = time.perf_counter()
  440. total_time = end_time - start_time
  441. # 统计结果
  442. locked_count = sum(1 for r in results if r["locked"] is True)
  443. success_count = sum(1 for r in results if r["locked"] is False)
  444. error_count = sum(1 for r in results if r["locked"] is None)
  445. avg_task_time = sum(r["duration"] for r in results) / len(results) if results else 0
  446. locked_datetimes = [r['slot_datetime'] for r in results if r["locked"] is True]
  447. return {
  448. "total": len(results),
  449. "success": success_count,
  450. "locked": locked_count,
  451. "error": error_count,
  452. "slot_locked": locked_count > 0,
  453. "elapsed_time": total_time,
  454. "avg_task_time": avg_task_time,
  455. "session_dic": session_dic,
  456. "locked_datetimes": locked_datetimes
  457. }
  458. async def concurrency_lock(proxies, session_dic, slot_datetime_list, num_of_concurrency_size: int, timeout=5):
  459. start_time = time.perf_counter()
  460. sem = asyncio.Semaphore(num_of_concurrency_size)
  461. async def one_call(task_id: int):
  462. async with sem:
  463. t0 = time.perf_counter()
  464. try:
  465. result = await asyncio.wait_for(
  466. troov_update_step_value(random.choice(proxies), session_dic, slot_datetime_list),
  467. timeout=timeout
  468. )
  469. locked = bool(result)
  470. status = "locked" if locked else "success"
  471. except asyncio.TimeoutError:
  472. locked = None
  473. status = "timeout"
  474. except SessionExpiredError: # ⚠️ 不吞掉这个异常,直接向上传递
  475. raise
  476. except Exception as e:
  477. locked = None
  478. status = f"error: {repr(e)}"
  479. finally:
  480. t1 = time.perf_counter()
  481. duration = t1 - t0
  482. return {
  483. "id": task_id,
  484. "locked": locked,
  485. "duration": duration,
  486. "status": status
  487. }
  488. try:
  489. tasks = [one_call(i) for i in range(num_of_concurrency_size)]
  490. results = await asyncio.gather(*tasks)
  491. except SessionExpiredError:
  492. # 🚨 如果任意任务抛出 SessionExpiredError,这里会捕获到
  493. raise # 交由外部处理
  494. end_time = time.perf_counter()
  495. total_time = end_time - start_time
  496. # 统计结果
  497. locked_count = sum(1 for r in results if r["locked"] is True)
  498. success_count = sum(1 for r in results if r["locked"] is False)
  499. error_count = sum(1 for r in results if r["locked"] is None)
  500. avg_task_time = sum(r["duration"] for r in results) / len(results) if results else 0
  501. return {
  502. "total": len(results),
  503. "success": success_count,
  504. "locked": locked_count,
  505. "error": error_count,
  506. "slot_locked": locked_count > 0,
  507. "elapsed_time": total_time,
  508. "avg_task_time": avg_task_time,
  509. "session_dic": session_dic
  510. }
  511. ########################################################!!!同步函数!!!########################################################
  512. def troov_handshake_old(embassy, proxy):
  513. url = "https://51.254.177.49/api/handshake"
  514. headers = {
  515. 'accept': 'application/json, text/plain, */*',
  516. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  517. 'origin': 'https://consulat.gouv.fr',
  518. 'referer': embassy["website"],
  519. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  520. 'x-gouv-web': 'fr.gouv.consulat'
  521. }
  522. proxies = {
  523. "http": proxy,
  524. "https": proxy
  525. }
  526. response = requests.head(url, headers=headers, proxies=proxies, verify=False)
  527. if response.status_code == 200:
  528. return (
  529. response.headers.get('X-Gouv-App-Id'),
  530. response.headers.get('X-Gouv-Handshake')
  531. )
  532. return None
  533. def troov_create_session_old(proxy, captcha, embassy=TROOV_EMBASSY["TroovFr_Dublin_Visas"]):
  534. handshake_ret = troov_handshake_old(embassy, proxy)
  535. if not handshake_ret:
  536. return None
  537. x_gouv_app_id, x_gouv_handshake = handshake_ret
  538. reservation_session_ret = troov_make_reservation_session_old(embassy, proxy, captcha, x_gouv_app_id, x_gouv_handshake)
  539. if not reservation_session_ret:
  540. return None
  541. x_gouv_handshake2, session_create_at, session_id = reservation_session_ret
  542. session_dic = {
  543. 'embassy': embassy,
  544. 'x_gouv_app_id':x_gouv_app_id,
  545. 'x-csrf-token': x_gouv_handshake2,
  546. 'session_create_at': session_create_at,
  547. 'session_id': session_id
  548. }
  549. status = troov_update_dynamic_steps_old(proxy, session_dic)
  550. if status:
  551. return session_dic
  552. return None
  553. def troov_refresh_session_old(proxy, session_dic):
  554. handshake_ret = troov_handshake_old(session_dic['embassy'], proxy)
  555. if not handshake_ret:
  556. return None
  557. x_gouv_app_id, x_gouv_handshake = handshake_ret
  558. session_dic['x_gouv_app_id'] = x_gouv_app_id
  559. session_dic['x-csrf-token'] = x_gouv_handshake
  560. reservation_session_ret = troov_get_reservation_session_old(proxy, session_dic)
  561. if not reservation_session_ret:
  562. return None
  563. return session_dic
  564. def troov_make_reservation_session_old(embassy, proxy, capcha_str, handshake_gouv_appid, handshake_gouv_handshake):
  565. url = f"https://51.254.177.49/api/team/{embassy['teamId']}/reservations-session"
  566. payload = json.dumps({
  567. "standaloneServiceName": embassy['name'],
  568. "sessionId": None,
  569. "captcha": capcha_str
  570. })
  571. headers = {
  572. 'accept': 'application/json, text/plain, */*',
  573. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  574. 'content-type': 'application/json',
  575. 'origin': f'https://consulat.gouv.fr',
  576. 'referer': embassy['website'],
  577. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  578. 'x-csrf-token': handshake_gouv_handshake,
  579. 'x-gouv-app-id': handshake_gouv_appid,
  580. 'x-gouv-web': 'fr.gouv.consulat'
  581. }
  582. proxies = {
  583. "http": proxy,
  584. "https": proxy
  585. }
  586. response = requests.post(url, headers=headers, data=payload, proxies=proxies, verify=False)
  587. if response.status_code == 200:
  588. response_dic = response.json()
  589. return response.headers['X-Gouv-Handshake'], response_dic['created_at'], response_dic['_id']
  590. logger.error(f'troov_make_reservation_session_old {response.status_code}, {response.text}')
  591. return None
  592. def troov_update_dynamic_steps_old(proxy, session_dic):
  593. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations-session/{session_dic['session_id']}/update-dynamic-steps"
  594. payload = json.dumps({
  595. "key": "slotsSteps",
  596. "steps": [
  597. {
  598. "stepType": "slotsStep",
  599. "name": session_dic['embassy']['name'],
  600. "numberOfSlots": 1,
  601. "dynamicStepIndex": 0,
  602. "zone_id": session_dic['embassy']['zoneId'],
  603. "value": {
  604. "lastSelectedDate": "",
  605. "label": session_dic['embassy']['name'],
  606. "accessibleCalendar": False,
  607. "hasSwitchedCalendar": False,
  608. "slots": {}
  609. }
  610. }
  611. ]
  612. })
  613. headers = {
  614. 'accept': 'application/json, text/plain, */*',
  615. 'content-type': 'application/json',
  616. 'origin': f'https://consulat.gouv.fr',
  617. 'referer': session_dic['embassy']['website'],
  618. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  619. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  620. 'x-gouv-web': 'fr.gouv.consulat',
  621. }
  622. proxies = {
  623. "http": proxy,
  624. "https": proxy
  625. }
  626. response = requests.post(url, headers=headers, data=payload, proxies=proxies, verify=False)
  627. if response.status_code == 200:
  628. return True
  629. else:
  630. rtext = response.text
  631. check_responsed_session_expired(rtext)
  632. return False
  633. def troov_get_reservation_session_old(proxy, session_dic):
  634. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations-session"
  635. headers = {
  636. 'accept': 'application/json, text/plain, */*',
  637. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  638. 'origin': 'https://consulat.gouv.fr',
  639. 'referer': session_dic['embassy']['website'],
  640. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  641. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  642. 'x-gouv-web': 'fr.gouv.consulat',
  643. }
  644. params = {
  645. 'sessionId': session_dic['session_id'],
  646. 'standaloneServiceName': session_dic['embassy']['name']
  647. }
  648. proxies = {
  649. "http": proxy,
  650. "https": proxy
  651. }
  652. response = requests.get(url, params=params, headers=headers, proxies=proxies, verify=False)
  653. if response.status_code == 200:
  654. return response.json()
  655. else:
  656. rtext = response.text
  657. check_responsed_session_expired(rtext)
  658. return None
  659. def troov_get_interval_old(proxy, session_dic):
  660. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/get-interval?serviceId={session_dic['embassy']['zoneId']}"
  661. headers = {
  662. 'accept': 'application/json, text/plain, */*',
  663. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  664. 'origin': f'https://consulat.gouv.fr',
  665. 'referer': session_dic['embassy']['website'],
  666. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  667. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  668. 'x-gouv-web': 'fr.gouv.consulat'
  669. }
  670. proxies = {
  671. "http": proxy,
  672. "https": proxy
  673. }
  674. response = requests.get(url, headers=headers, proxies=proxies, verify=False)
  675. if response.status_code == 200:
  676. return response.json()
  677. else:
  678. rtext = response.text
  679. check_responsed_session_expired(rtext)
  680. return None
  681. def troov_get_exclude_days_old(proxy, session_dic):
  682. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/exclude-days"
  683. payload = json.dumps({
  684. "session": {
  685. session_dic['embassy']['zoneId']: True
  686. },
  687. "sessionId": session_dic['session_id']
  688. })
  689. headers = {
  690. 'accept': 'application/json, text/plain, */*',
  691. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  692. 'content-type': 'application/json',
  693. 'origin': f'https://consulat.gouv.fr',
  694. 'referer': session_dic['embassy']['website'],
  695. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  696. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  697. 'x-gouv-web': 'fr.gouv.consulat'
  698. }
  699. proxies = {
  700. "http": proxy,
  701. "https": proxy
  702. }
  703. response = requests.post(url, headers=headers, data=payload, proxies=proxies, verify=False)
  704. if response.status_code == 200:
  705. return response.json()
  706. else:
  707. rtext = response.text
  708. check_responsed_session_expired(rtext)
  709. return None
  710. def troov_get_open_days_old(proxy, session_dic):
  711. def get_dates_in_range(date_range):
  712. start_date = datetime.strptime(date_range["start"], "%Y-%m-%d")
  713. end_date = datetime.strptime(date_range["end"], "%Y-%m-%d")
  714. current_date = start_date
  715. dates_list = []
  716. while current_date <= end_date:
  717. dates_list.append(current_date.strftime("%Y-%m-%d"))
  718. current_date += timedelta(days=1)
  719. return dates_list
  720. interval = troov_get_interval_old(proxy, session_dic)
  721. exclude_days = troov_get_exclude_days_old(proxy, session_dic)
  722. # exclude_days = ['2025-05-31']
  723. if interval and exclude_days:
  724. all_days = get_dates_in_range(interval)
  725. available_dates = [
  726. day for day in all_days
  727. if day not in exclude_days and datetime.strptime(day, "%Y-%m-%d").weekday() not in (5, 6)
  728. ]
  729. return available_dates
  730. return None
  731. def troov_get_available_times_old(proxy, session_dic, date, places=1, capacity=2):
  732. url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/availability?name=Visas&date={date}&places={places}&matching=&maxCapacity={capacity}&sessionId={session_dic['session_id']}"
  733. headers = {
  734. 'accept': 'application/json, text/plain, */*',
  735. 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
  736. 'origin': f'https://consulat.gouv.fr',
  737. 'referer': session_dic['embassy']['website'],
  738. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  739. 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  740. 'x-gouv-web': 'fr.gouv.consulat'
  741. }
  742. proxies = {
  743. "http": proxy,
  744. "https": proxy
  745. }
  746. response = requests.get(url, headers=headers, proxies=proxies, verify=False)
  747. if response.status_code == 200:
  748. return response.json()
  749. else:
  750. rtext = response.text
  751. check_responsed_session_expired(rtext)
  752. return None
  753. # def troov_book_old(proxy, session_dic, date, slot, uinfo, captcha):
  754. # try:
  755. # url = f"https://51.254.177.49/api/team/{session_dic['embassy']['teamId']}/reservations/family"
  756. # book_body = troov_dublin_visas_book_data_builder(session_dic['session_id'], date, slot, uinfo, captcha)
  757. # payload = json.dumps(book_body)
  758. # headers = {
  759. # 'accept': 'application/json, text/plain, */*',
  760. # 'content-type': 'application/json',
  761. # 'origin': f'https://consulat.gouv.fr',
  762. # 'referer': session_dic['embassy']['website'],
  763. # 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
  764. # 'x-csrf-token': session_dic['x-csrf-token'],
  765. # 'x-gouv-app-id': session_dic['x_gouv_app_id'],
  766. # 'x-gouv-web': 'fr.gouv.consulat',
  767. # }
  768. # proxy_config = {
  769. # "http": proxy,
  770. # "https": proxy
  771. # }
  772. # response = requests.post(url, headers=headers, data=payload, verify=False, proxies=proxy_config)
  773. # resp_text = response.text
  774. # logger.info(f'book code={response.status_code}, text={response.text}')
  775. # if response.status_code == 200:
  776. # if "qrcode" in resp_text:
  777. # return resp_text
  778. # else:
  779. # check_responsed_session_expired(resp_text)
  780. # except Exception as e:
  781. # logger.error(f"proxy={proxy}, session_dic={session_dic}, troov_book_old exception: {e}")
  782. # return None