def send_mail(content, html=False): """ 邮件通知 :param content:str email content :param html:boolean 是否为 html 格式 :return: """ try: sender = config.email['sender'] receivers = config.email["receivers"] subject = '恭喜,您已订票成功!' username = config.email["username"] password = config.email["password"] host = config.email["host"] if html: msg = MIMEMultipart() msg.attach(MIMEText(content, 'html', 'utf-8')) else: msg = MIMEText(content, 'plain', 'utf-8') # 中文需参数 utf-8,单字节字符不需要 msg['Subject'] = Header(subject, 'utf-8') msg['From'] = sender msg['To'] = ",".join(receivers) try: smtp = smtplib.SMTP_SSL(host) smtp.connect(host) except socket.error: smtp = smtplib.SMTP() smtp.connect(host) smtp.connect(host) smtp.login(username, password) smtp.sendmail(sender, receivers, msg.as_string()) smtp.quit() console.print(u"邮件已通知, 请查收", style="bold green") except Exception as e: console.print(u"邮件配置有误{}".format(e), style="bold red")
def checkForView(self): try: console.print('Capturing screen', 0) self.getScreen() except ValueError: console.print( f'{console.icho.bold}{console.icho.red}' 'Error capturing screen' f'{console.icho.normal}', 0) return False idx = len(self.viewList) for view in self.viewList: position = (len(self.viewList) - idx) // len(self.viewList) console.print(f'View: {view.name}', position) idx -= 1 if view.isView(self._tmpImage): console.print( f'View: {view.name} ' f'{console.icho.bold}' f'{console.icho.cyan}' f'OK{console.icho.normal}', 1) for touch in view.touchArray: self.touchScreen(touch) sleep(view.touchDelay / 1000) return True console.print('No view found', 1) return False
def show_alert_info(text=None, split=' '): """ @param text 添加到打印信息头部 @param split 打印信息分隔符 打印通知警告信息 """ print_info = [] if text: print_info.append(text) # 对话框 check_el = browser.find_el_if_exist('content_checkticketinfo_id', by=By.ID) if check_el and check_el.is_displayed(): check_text = check_el.find_element_by_id('sy_ticket_num_id').text console.print(check_text, style="bold red") # 提示框 notice_el = browser.find_el_if_exist('content_transforNotice_id', by=By.ID) if notice_el and notice_el.is_displayed(): el = browser.find_el_if_exist('#orderResultInfo_id > div > span') if el: print_info.append(f'[red]{el.text}[/red]') el = find_el_if_exist(notice_el, 'p', by=By.TAG_NAME) if el: print_info.append(el.text) console.print(split.join(print_info))
def check_error(self): """ 检查是否被关小黑屋 。。。 """ from core import error_page if self.current_url.startswith(error_page): # IP 被限制,等待五分钟后重试 console.print('[yellow]网络错误,五分钟后重试![/yellow]') self.wait_unblock()
def _check_ticket(self): """ 检查是否有余票 """ train_trs = tk.find_train_info_trs() if len(train_trs) == 0: console.print(':raccoon: [yellow]未查询到车次信息[/yellow]: ', config.from_time, f"[{str.join(',', config.trains)}]") else: self._try_submit(train_trs)
def _try_submit(self, index, left_tr, train_name): """ 尝试提交 :param index: 表格索引 :param left_tr: 列车信息行 :param train_name: 列车车次名称 """ left_tr_id = left_tr.get_attribute('id') submit_btn = browser.find_el_if_exist('#' + left_tr_id + ' > td.no-br > a') if submit_btn: ss, es, st, et, du = _get_train_info(index) if str.startswith(left_tr_id, 'ticket_'): serial_num = left_tr_id[len('ticket_'):].split('_')[0] # 座位类型是否匹配 for seat_type in config.seat_types: if seat_type_dict[seat_type]: seat_selector = '#' + seat_type_dict[ seat_type] + '_' + serial_num seat = left_tr.find_element_by_css_selector( seat_selector) if seat: ticket_info = { "address": f"{ss} - {es}", "time": f"{st} - {et}", "duration": du, 'train': train_name } # 判断是否有余票,如果有余票就尝试提交 def try_submit(seat_el): if seat_el.text and seat_el.text is not '无' \ and seat_el.text is not '--': self._running = False ticket_info['seat_type'] = seat_type ticket_info['value'] = seat_el.text self._ticket_info = ticket_info console.print( _table_ticket_info(ticket_info)) submit_btn.click() return True return False seat_div = browser.find_el_if_exist(seat_selector + ' > div') if try_submit(seat_div if seat_div else seat): return True console.print( f":vampire: {config.from_time} - {train_name} - {seat_type}", "[red]暂无余票[/red]") else: console.print(f":vampire: {config.from_time} - {train_name}", "[red]暂无余票[/red]") return False
def _init(): # 跳转到登录页 browser.get(login_page) time.sleep(1) if browser.find_el_if_exist('ERROR', by=By.ID) is not None: console.print('[red]12306请求过于频繁,请稍等重试 . . . [/red]') browser.wait_unblock() # 等待用户密码登录按钮可以点击,切换到用户密码登录 Tab wait.WebDriverWait(browser, 5).until( ec.element_to_be_clickable( (By.CLASS_NAME, 'login-hd-account'))).click() time.sleep(1)
def try_submit(seat_el): if seat_el.text and seat_el.text is not '无' \ and seat_el.text is not '--': self._running = False ticket_info['seat_type'] = seat_type ticket_info['value'] = seat_el.text self._ticket_info = ticket_info console.print( _table_ticket_info(ticket_info)) submit_btn.click() return True return False
def _try_submit(self, train_trs): """ 尝试提交 :param train_trs: 列车车次名称 """ for train_tr in train_trs: submit_btn = find_el_if_exist(train_tr, 'td.no-br > a') ss, es, st, et, du, tn = tk.get_train_info(train_tr) if submit_btn and submit_btn: left_tr_id = train_tr.get_attribute('id') serial_num = left_tr_id[len('ticket_'):].split('_')[0] # 座位类型是否匹配 for seat_type in config.seat_types: if tk.seat_type_dict[seat_type]: seat_type_id = tk.seat_type_dict[seat_type][ "code"] + '_' + serial_num seat = train_tr.find_element_by_id(seat_type_id) if seat: ticket_info = { "address": f"{ss} - {es}", "time": f"{st} - {et}", "duration": du, 'train': tn } # 判断是否有余票,如果有余票就尝试提交 seat_text = seat.text if seat_text == '有' or re.match( "^\\d+$", seat_text): self._running = False ticket_info['seat_type'] = seat_type ticket_info['value'] = seat_text self._ticket_info = ticket_info console.print( tk.table_ticket_info(ticket_info)) submit_btn.click() return True else: console.print(f"未找到坐席类型 :[red] {seat_type} [/red]") console.print("请按照要求配置坐席类型:", ",".join(tk.seat_type_dict.keys())) sys.exit(-1) console.print( f":vampire: {config.from_time} - {tn} - {seat_type}", "[red]暂无余票[/red]") else: console.print(f":vampire: {config.from_time} - {tn}", "[red]暂无余票[/red]") return False
def _parse_captcha(self): # 这里也可以使用自建打码服务器 code_url = f'{config.code_server["scheme"]}://{config.code_server["host"]}{config.code_server["path"]}' data = {"imageFile": self._code_img} count = 0 while count < 10: count += 1 try: resp = requests.post(code_url, data=data) if resp.status_code is 200: resp_json = resp.json() if resp_json and resp_json.get("code") is 0: return code_xy(resp_json.get("data")).split(',') except Exception as e: console.print('[red]打码失败[/red]') print(e) raise e
def login(self): # 初始化 _init() # 自动打码 self.captcha.code_captcha() # 开始登陆 browser.find_element_by_id('J-userName').send_keys(config.username) browser.find_element_by_id('J-password').send_keys(config.password) browser.find_element_by_id('J-login').click() time.sleep(0.5) # 验证滑块验证码 slider_captcha() try: # 等待跳转到首页 wait.WebDriverWait(browser, 10).until( lambda driver: driver.current_url.startswith(index_page)) except TimeoutException: console.print("登录失败,请稍后重试 . . .", style="bold red") browser.wait_unblock()
def _check_ticket(self): """ 检查是否有余票 """ exist_train = False left_tr_list = browser.find_els_if_exist('#queryLeftTable > tr') for index in range(len(left_tr_list)): # 跳过空表格行 if index % 2 == 0: left_tr = left_tr_list[index] train_tr = left_tr_list[index + 1] train_name = train_tr.get_attribute('datatran') for t in config.trains: if t == train_name: exist_train = True if self._try_submit(index, left_tr, train_name): return if not exist_train: console.print(':vampire: [yellow]未查询到车次信息[/yellow]: ', config.from_time, f"[{str.join(',', config.trains)}]")
def check(self): count = 0 sleep_time = 0 while self._running: random_time = random.randint(300, 1000) / 1000 time.sleep(random_time) now = datetime.now().strftime('%H:%M:%S') # 判断是否在预约时间内 if config.start_time <= now <= config.end_time: count += 1 browser.find_element_by_id('query_ticket').click() time.sleep(0.5) console.print(f':vampire: 第 [red]{count}[/red] 次点击查询 . . .') self._check_ticket() else: sleep_time += 1 # 为了防止长时间不操作,session 失效 if sleep_time >= 60: sleep_time = 0 console.print(f':raccoon: [{now}] 刷新浏览器') browser.refresh() self._create_order() message = "恭喜您,抢到票了,请及时前往12306支付订单!" console.print(f":smiley: {message}") if config.mail['enable'] is True: send_mail(message) if config.ftqq_server['enable'] is True: send_ftqq(_md_ticket_info(self._ticket_info))
def code_xy(select): """ 根据打码服务器返回的 offset 获取页面中真实的坐标 """ post = [] offsets_x = 0 # 选择的答案的left值,通过浏览器点击8个小图的中点得到的,这样基本没问题 offsets_y = 0 # 选择的答案的top值 for offset in select: if offset == '1': offsets_y = 77 offsets_x = 40 elif offset == '2': offsets_y = 77 offsets_x = 112 elif offset == '3': offsets_y = 77 offsets_x = 184 elif offset == '4': offsets_y = 77 offsets_x = 256 elif offset == '5': offsets_y = 149 offsets_x = 40 elif offset == '6': offsets_y = 149 offsets_x = 112 elif offset == '7': offsets_y = 149 offsets_x = 184 elif offset == '8': offsets_y = 149 offsets_x = 256 else: pass post.append(offsets_x) post.append(offsets_y) rand_code = str(post).replace(']', '').replace('[', '').replace("'", '').replace(' ', '') console.print(f":thumbs_up: 验证码识别成功: [{rand_code}]") return rand_code
def slider_captcha(): try_time = 1 div = browser.find_el_if_exist('nc_1_n1z', by=By.ID) while div and div.is_displayed(): if try_time > 10: console.print('滑块验证码验证失败 . . .', style='bold red') sys.exit(-1) # 动作链 action = ActionChains(browser) # 点击长按指定的标签 action.click_and_hold(div) # 处理滑动模块 for i in range(5): # perform()立即执行动作链操作 # move_by_offset(x,y):x水平方向 y竖直方向 try: action.move_by_offset(350, 0).perform() # 速度为30mm except WebDriverException: time.sleep(0.5) time.sleep(0.5) action.release() try_time += 1 div = browser.find_el_if_exist('nc_1_n1z', by=By.ID)
def _create_order(self): """ 创建订单 """ time.sleep(0.5) passenger_list = browser.find_els_if_exist('#normal_passenger_id > li') # 选择乘坐人 passenger_index = 1 self._ticket_info['passengers'] = [] for passenger in config.passengers: for passenger_li in passenger_list: passenger_input = passenger_li.find_element_by_tag_name( 'input') if passenger_li.find_element_by_tag_name( 'label').text == passenger: console.print(f"选择乘坐人 [{passenger}] . . .") self._ticket_info['passengers'].append(passenger) passenger_input.click() warning_alert = browser.find_el_if_exist( 'content_defaultwarningAlert_id', by=By.ID) if warning_alert: warning_alert.find_element_by_id( 'qd_closeDefaultWarningWindowDialog_id').click() time.sleep(0.5) # 选择席座 console.print( f"开始选择席座 [{self._ticket_info['seat_type']}] . . .") seat_select = browser.find_element_by_id( f"seatType_{str(passenger_index)}") _, seat_type_value = _get_seat_type_index_value( self._ticket_info['seat_type']) if seat_type_value != 0: Select(seat_select).select_by_value( str(seat_type_value)) passenger_index += 1 time.sleep(0.5) if passenger_index == 1: console.print("未找到乘客信息 ... 提交订单失败", style="bold red") print('正在提交订单 . . .') wait.WebDriverWait(browser, 5).until( ec.element_to_be_clickable((By.ID, 'submitOrder_id'))).click() print('正在确认订单 . . .') wait.WebDriverWait(browser, 5).until( ec.element_to_be_clickable((By.ID, 'qr_submit_id'))).click() # 截取屏幕,订单信息页 print('预订成功,正在截取屏幕 . . .') time.sleep(5) browser.screenshot( f'{self.fs}_{self.ts}_{config.from_time.replace("-", "")}_{self._ticket_info["train"].lower()}.png' ) time.sleep(60)
def wait_unblock(self): """ 关小黑屋后,在这里等待。。。 """ from core import ticket_url console.print("欢迎来到小黑屋 。。。") console.print( """|-----------------------------------------------------------------------|""" ) console.print( """| o \ o / _ o __| \ / |__ o _ \ o / o |""" ) console.print( """| /|\ | /\ ___\o \o | o/ o/__ /\ | /|\ |""" ) console.print( """| / \ / \ | \ /) | ( \ /o\ / ) | (\ / | / \ / \ |""" ) console.print( """|-----------------------------------------------------------------------|""" ) time.sleep(config.block_time) self.get(ticket_url) time.sleep(1)
def check(self): browser.get(ticket_url) time.sleep(1) check_alert() count = 0 while self._running: # 检查时候登录失效, 判断是否存在登出按钮 if not has_login(): return # 检查是否出错 browser.check_error() # 随机休眠 0.3-1 秒 # random_time = random.randint(300, 1000) / 1000 # time.sleep(random_time) # 把开始时间,结束时间字符串转换为日期 now = datetime.now() def convert_time_to_datetime(time_str): now_date_str = now.strftime(date_format_str) return datetime.strptime(f"{now_date_str} {time_str}", datetime_format_str) start_ts = convert_time_to_datetime(config.start_time).timestamp() end_ts = convert_time_to_datetime(config.end_time).timestamp() now_ts = now.timestamp() # 判断是否在预约时间内 if (start_ts - 120) <= now_ts <= end_ts: try: wait.WebDriverWait(browser, 5).until( ec.element_to_be_clickable( (By.ID, 'query_ticket'))).click() except ElementClickInterceptedException: continue time.sleep(0.5) count += 1 console.print(":raccoon:", f'第 [red]{count}[/red] 次点击查询 . . .') self._check_ticket() # 每查询 1000 次刷新一次浏览器 if count % 1000 == 0: browser.refresh() time.sleep(1) # 检测是否出现提醒框 check_alert() else: # 使用睡眠方案,提前两分钟唤醒,登录超时会自动登录 now_time_str = now.strftime(time_format_str) # 取今天日期 当前时间小于开始抢票时间 if now_time_str < config.start_time: date_str = now.strftime(date_format_str) # 取明天的日期 else: tomorrow = datetime.fromtimestamp(now.timestamp() + 60 * 60 * 24) date_str = tomorrow.strftime(date_format_str) next_run_datetime = datetime.strptime( f"{date_str} {config.start_time}", datetime_format_str) sleep_second = next_run_datetime.timestamp() - now.timestamp( ) - 120 console.print("未到达开始开放抢票时间,程序即将休眠 ... ", style="bold yellow") console.print( f"程序将于 [{next_run_datetime.strftime(datetime_format_str)}] 重新启动 !", style="bold green") time.sleep(sleep_second) # 创建订单 if self._create_order(): message = "恭喜您,抢到票了,请及时前往12306支付订单!" console.print(f":smiley: {message}") # 发送通知邮件 if config.email['enable'] is True: send_mail(tk.html_ticket_info(self._ticket_info), html=True) # 发送 server 酱通知 if config.server_chan['enable'] is True: send_server_chan(tk.md_ticket_info(self._ticket_info)) sys.exit(0) self.check()
from core.login import Login from core.ticket import Ticket from utils import console from utils.file import read_as_str def main(): Login().login() Ticket().check() if __name__ == '__main__': console.print(read_as_str('./banner.txt')) while True: main()
def _create_order(self): """ 创建订单 """ time.sleep(1) passenger_list = browser.find_els_if_exist('#normal_passenger_id > li') # 选择乘坐人 passenger_index = 1 self._ticket_info['passengers'] = [] # 检查余票数量 ticket_value = self._ticket_info['value'] surplus_ticket_number = int( ticket_value) if ticket_value != '有' else 9999 for passenger in config.passengers[:surplus_ticket_number]: for passenger_li in passenger_list: passenger_input = passenger_li.find_element_by_tag_name( 'input') if passenger_li.find_element_by_tag_name( 'label').text == passenger: console.print(f"选择乘坐人 [{passenger}] . . .") self._ticket_info['passengers'].append(passenger) passenger_input.click() warning_alert = browser.find_el_if_exist( 'content_defaultwarningAlert_id', by=By.ID) if warning_alert: warning_alert.find_element_by_id( 'qd_closeDefaultWarningWindowDialog_id').click() time.sleep(0.5) # 选择席座 console.print( f"开始选择席座 [{self._ticket_info['seat_type']}] . . .") seat_select = browser.find_element_by_id( f"seatType_{str(passenger_index)}") seat_type = tk.seat_type_dict[ self._ticket_info['seat_type']] if not seat_type: console.print(f"未找到坐席类型 :[red] {seat_type} [/red]") sys.exit(-1) seat_type_value = seat_type['value'] if seat_type_value != '0': Select(seat_select).select_by_value(seat_type_value) passenger_index += 1 time.sleep(0.5) if passenger_index == 1: console.print("未找到乘客信息 ... 无法选择乘客和坐席", style="bold red") try: print('正在提交订单 . . .') wait.WebDriverWait(browser, 10).until( ec.element_to_be_clickable((By.ID, 'submitOrder_id'))).click() time.sleep(1) # 确认提交订单 print('正在确认订单 . . .') wait.WebDriverWait(browser, 10).until( ec.element_to_be_clickable((By.ID, 'qr_submit_id'))).click() # 等待跳转到支付页面, 如果超时未跳转, 说明订单生成失败 wait.WebDriverWait(browser, 10).until( lambda driver: driver.current_url.startswith(pay_order_url)) submit_btn = browser.find_el_if_exist('qr_submit_id', by=By.ID) if not submit_btn or not submit_btn.is_displayed(): console.print(f'该车次无法提交[{self._ticket_info["train"]}]。', style="bold yellow") config.trains.remove(self._ticket_info["train"]) show_alert_info('该车次无法提交[{self._ticket_info["train"]}]。', '\n') return False except WebDriverException as e: browser.screenshot( f'{datetime.now().strftime(datetime_format_str)}-error.png') show_alert_info('提交订单失败:\n') raise e # 截取屏幕,订单信息页 print('预订成功,正在截取屏幕 . . .') browser.screenshot( f'{self.fs}_{self.ts}_{config.from_time.replace("-", "")}_{self._ticket_info["train"].lower()}.png' ) return True