├── .gitignore ├── requirements.txt ├── pics └── install_python.png ├── utils ├── decorators.py ├── config.py ├── basic.py └── log.py ├── .github └── workflows │ └── python-app.yml ├── monitor.py ├── readme.md └── visa.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium 2 | webdriver-manager 3 | pyttsx3 -------------------------------------------------------------------------------- /pics/install_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vxwong/spain-visa-monitor/HEAD/pics/install_python.png -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def singleton(cls): 5 | """ 6 | 生成单例的装饰器函数 7 | :param cls: 变成单例的类对象 8 | :return: 9 | """ 10 | instance = {} 11 | 12 | @wraps(cls) 13 | def get_instance(*args, **kwargs): 14 | if cls not in instance: 15 | instance[cls] = cls(*args, **kwargs) 16 | return instance[cls] 17 | 18 | return get_instance 19 | -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | # =============== GENERAL SETTINGS =============== 2 | TIMEOUT = 90 # refresh page every 90s 3 | 4 | # =============== VISA CENTER SELECTION =============== 5 | CENTER = ('England', 'Manchester', 'Normal', 'Tourism') 6 | # CENTER = ('Scotland', 'Edinburgh', 'Normal', 'Tourism') 7 | # CENTER = ('Bristol', 'London', 'Normal', 'Tourist') 8 | # =============== PERSONAL CONFIG FOR VISA =============== 9 | OPENED_PAGE = 'https://uk.blsspainvisa.com/visa4spain/book-date/QwertYU123' # your BLS appointment page link 10 | EMAIL = '123@456.com' # BLS email 11 | PASSWORD = 'BLSpassword' # BLS password 12 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 3.10 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: "3.10" 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: run monitor code 32 | run: | 33 | python monitor.py 34 | -------------------------------------------------------------------------------- /monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pyttsx3 4 | from selenium import webdriver 5 | from webdriver_manager.chrome import ChromeDriverManager 6 | 7 | from utils import config 8 | from utils.log import logger 9 | from visa import Visa 10 | 11 | 12 | def init_driver(): 13 | profile = { 14 | "profile.default_content_setting_values.notifications": 2 # block notifications 15 | } 16 | chrome_options = webdriver.ChromeOptions() 17 | chrome_options.add_experimental_option('prefs', profile) 18 | chrome_options.add_argument("--incognito") 19 | 20 | driver = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options) 21 | driver.implicitly_wait(1) 22 | driver.delete_all_cookies() 23 | return driver 24 | 25 | 26 | def monitor(): 27 | try: 28 | driver = init_driver() 29 | visa = Visa(driver) 30 | visa.go_to_appointment_page() 31 | visa.login() 32 | visa.go_to_book_appointment() 33 | visa.select_centre(config.CENTER[0], config.CENTER[1], config.CENTER[2]) 34 | while True: 35 | dates = visa.check_available_dates() 36 | if dates: 37 | logger.info(f"DAY AVAILABLE: {dates}") 38 | pyttsx3.speak(f"say day available {dates}") 39 | time.sleep(120) 40 | else: 41 | logger.info(f"NO DAY AVAILABLE..") 42 | time.sleep(config.TIMEOUT) 43 | driver.refresh() 44 | 45 | except Exception as e: 46 | logger.error(f'Monitor runtime error. {e}') 47 | monitor() 48 | 49 | 50 | if __name__ == "__main__": 51 | monitor() 52 | -------------------------------------------------------------------------------- /utils/basic.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | from selenium.webdriver.support.wait import WebDriverWait 3 | from selenium.webdriver.support import expected_conditions as ec 4 | 5 | 6 | class Basic: 7 | def __init__(self, driver): 8 | self.driver = driver 9 | 10 | def click_el(self, xpath=None, id=None, name=None, text=None): 11 | locator = None 12 | if xpath: 13 | locator = (By.XPATH, xpath) 14 | elif id: 15 | locator = (By.ID, id) 16 | elif name: 17 | locator = (By.NAME, name) 18 | else: 19 | locator = (By.XPATH, "//*[contains(text(), '{}')]".format(text)) 20 | WebDriverWait(self.driver, 10).until(ec.element_to_be_clickable(locator), message="No element").click() 21 | 22 | def wait_for_loading(self): 23 | WebDriverWait(self.driver, 10).until(ec.invisibility_of_element_located((By.ID, "overlay"))) 24 | 25 | def enter_message(self, message, xpath=None, id=None, name=None, text=None): 26 | if xpath: 27 | locator = (By.XPATH, xpath) 28 | elif id: 29 | locator = (By.ID, id) 30 | elif name: 31 | locator = (By.NAME, name) 32 | else: 33 | locator = (By.XPATH, "//*[contains(text(), '{}')]".format(text)) 34 | element = WebDriverWait(self.driver, 10).until(ec.presence_of_element_located(locator), 35 | message="No element {}".format(locator)) 36 | element.clear() 37 | element.send_keys(message) 38 | 39 | def wait_for_secs(self, secs=1): 40 | WebDriverWait(self.driver, secs) 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## 西班牙签证位置监听 (base英国) 2 | ### 一、简介 3 | 回国需要申根签,但是每天蹲网站刷slot太浪费时间了,故写了一个监听脚本。分享给在🇬🇧有需要的同学。 4 | 5 | 项目是基于 [one-focus/visa-spain](https://github.com/one-focus/visa-spain) 改的,抽出了关键代码,针对英国的BLS的网站改了一下。请不要拿来用作盈利用途,否则会追究责任。 6 | 7 | 在python3.6 + macOS Catalina,python 3.9 + macOS Monterey,Windows 10,Windows 11上运行成功,没在别的环境上做过测试。 8 | 9 | #### 功能 10 | 11 | 如果放了签证预约空位,电脑会自动语音通知你。目前只通知日期,不会精确到小时,但个人用途足矣。 12 | 13 | 14 | ### 二、文件介绍 15 | ```text 16 | . 17 | ├── monitor.py # starter 18 | ├── visa.py # modify xpath for UK BLS 19 | ├── utils 20 | │   ├── basic.py 21 | │   ├── config.py # configuration, you have to change this file! 22 | │   ├── decorators.py 23 | │   └── log.py 24 | ├── requirements.txt # pip install -r requirements.txt 25 | └── readme.md 26 | ``` 27 | 28 | ### 三、运行 29 | 30 | 1. 安装Python 31 | 32 | [Python for Windows](https://www.python.org/ftp/python/3.10.5/python-3.10.5-amd64.exe) 33 | 34 | [Python for Intel Mac](https://www.python.org/ftp/python/3.9.13/python-3.9.13-macosx10.9.pkg) 35 | 36 | [Python for Apple Silicon Mac](https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg) 37 | 38 | 请注意,方便起见,Windows安装时请务必勾选Add Python 3.xx to PATH。Mac安装无需进行此操作 39 | ![个人通知](./pics/install_python.png) 40 | 41 | 2. 修改config.py 42 | 43 | ```python 44 | # 自动刷新时间(不建议小于60s) 45 | TIMEOUT = 90 46 | 47 | # 签证中心一共有三个。都写在了config.py中。默认曼城。如果需要改为其他城市,请注释掉曼城那一行,并解除注释你需要的签证中心那一行。 48 | CENTER = ('England', 'Manchester', 'Normal', 'Tourism') 49 | # CENTER = ('Scotland', 'Edinburgh', 'Normal', 'Tourism') 50 | # CENTER = ('Bristol', 'London', 'Normal', 'Tourist') 51 | 52 | # 必改,下面两项为你登陆BLS的账号密码 53 | EMAIL = 'xxx' 54 | PASSWORD = 'xxx' 55 | 56 | # 必改,你BLS点了Book Appointment之后出现的页面 57 | OPENED_PAGE = 'xxx' 58 | ``` 59 | 60 | 3. 安装依赖 61 | 62 | 打开cmd/终端,输入pip install[空格],将requirements.txt拖入cmd/终端,回车 63 | ```shell 64 | pip install -r requirements.txt 65 | ``` 66 | 67 | 4. 运行 68 | 69 | 在cmd/终端中输入python[空格],将monitor.py拖入cmd/终端,回车 70 | ```shell 71 | python monitor.py 72 | ``` 73 | 程序会自动控制一个Chrome浏览器完成自动登录、选择签证者中心,自动识别可用日期。有slot的话会直接语音提示。 74 | 75 | 5. 致谢 76 | 特别感谢[@daddywolf](https://github.com/daddywolf)对本项目的优化和issue解答,作者维护了一个cloudflare bypass版的,感兴趣的同学可以移步[此处](https://github.com/daddywolf/spain-visa-monitor-cloudflare-bypass)。 77 | -------------------------------------------------------------------------------- /utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import os 4 | from utils.decorators import singleton 5 | 6 | 7 | @singleton 8 | class SingletonLogging: 9 | """ 10 | 单例日志类 11 | """ 12 | 13 | def __init__(self, logger_name, logfile_dir): 14 | """ 15 | 初始化日志属性 16 | :param logger_name: 日志器名称 17 | :param logfile_dir: 日志文件夹名称 18 | """ 19 | self.logger_name = logger_name 20 | 21 | # BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 22 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 23 | BASE_DIR = BASE_DIR.replace("\\", "/") 24 | self.logfile_path = BASE_DIR + logfile_dir 25 | 26 | os.makedirs(self.logfile_path, exist_ok=True) 27 | 28 | def get_logger(self): 29 | """ 30 | 配置日志 31 | :return: 日志器 32 | """ 33 | # 获取日志器 34 | logger = logging.getLogger(self.logger_name) 35 | # 设置日志等级 36 | logger.setLevel(logging.DEBUG) 37 | 38 | # 处理所有日志 39 | # 获取处理器 40 | all_handler = logging.handlers.TimedRotatingFileHandler(filename=self.logfile_path + "/all.log", 41 | encoding="utf-8", when='midnight') 42 | # 获取格式器 43 | all_formatter = logging.Formatter('%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s - %(message)s') 44 | # 添加格式器 45 | all_handler.setFormatter(all_formatter) 46 | # 添加处理器 47 | logger.addHandler(all_handler) 48 | 49 | # Commenting out the stdout streaming for production server. 50 | stream_handler = logging.StreamHandler() 51 | stream_handler.setFormatter(fmt=all_formatter) 52 | logger.addHandler(stream_handler) 53 | 54 | # # 处理error日志 55 | # # 获取处理器 56 | # error_handler = logging.handlers.TimedRotatingFileHandler(filename=self.logfile_path + "/error.log", 57 | # encoding="utf-8", when='midnight') 58 | # # 设置格式器 59 | # error_formatter = logging.Formatter("%(levelname)s - %(asctime)s - %(filename)s[:%(lineno)d] - %(message)s") 60 | # # 添加格式器 61 | # error_handler.setFormatter(error_formatter) 62 | # # 设置处理器处理的日志等级 63 | # error_handler.setLevel(logging.ERROR) 64 | # # 添加处理器 65 | # logger.addHandler(error_handler) 66 | 67 | return logger 68 | 69 | 70 | logger = SingletonLogging(logger_name="monitor", logfile_dir="/logs").get_logger() 71 | 72 | if __name__ == '__main__': 73 | logger.error("Test error log") 74 | 75 | -------------------------------------------------------------------------------- /visa.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from utils import config 4 | from utils.basic import Basic 5 | from utils.log import logger 6 | 7 | 8 | class Visa(Basic): 9 | 10 | def __init__(self, driver): 11 | super().__init__(driver) 12 | 13 | def open_page(self, page): 14 | self.driver.get(page) 15 | 16 | def select_centre(self, county, city, category): 17 | self.wait_for_secs() 18 | self.click_el(name="JurisdictionId") 19 | self.click_el(xpath="//select[@name='JurisdictionId']/option[contains(text(),'{}')]".format(county)) 20 | self.wait_for_loading() 21 | self.click_el(name="centerId") 22 | self.click_el(xpath="//select[@name='centerId']/option[contains(text(),'{}')]".format(city)) 23 | self.wait_for_secs() 24 | self.click_el(name="category") 25 | self.click_el(xpath="//select[@name='category']/option[contains(text(),'{}')]".format(category)) 26 | self.wait_for_secs() 27 | self.click_el(name='checkDate') 28 | logger.info("select centre finished") 29 | 30 | def go_to_appointment_page(self, phone='', email=''): 31 | self.open_page(config.OPENED_PAGE) 32 | # self.select_centre("England", "Manchester", "Normal") 33 | # self.enter_phone_and_email(phone, email) 34 | # self.enter_wrong_code(email, config.PASSWORD) 35 | # self.enter_code_from_email(email) 36 | 37 | def login(self): 38 | try: 39 | # self.click_el(xpath="//a[text() = 'Log in']") 40 | element = self.driver.find_element_by_xpath("//a[contains(text(),'Log in')]") 41 | element.click() 42 | self.wait_for_secs() 43 | self.enter_message(config.EMAIL, name='email') 44 | self.wait_for_secs() 45 | self.enter_message(config.PASSWORD, name='password') 46 | self.wait_for_secs() 47 | self.click_el(name="login") 48 | logger.info("log in finished") 49 | except Exception as e: 50 | logger.error(e) 51 | 52 | def go_to_book_appointment(self): 53 | unique_suffix = config.OPENED_PAGE.split('/')[-1] 54 | link = f'book-appointment/{unique_suffix}' 55 | logger.info(f"date appointment link = [{link}]") 56 | # open a new tab 57 | self.driver.execute_script(f'window.open(\"{link}\","_blank");') 58 | # switch to the new tab 59 | self.driver.switch_to.window(self.driver.window_handles[-1]) 60 | logger.info("go to book appointment finished") 61 | 62 | def check_available_dates(self): 63 | self.click_el(id="VisaTypeId") 64 | self.click_el(xpath="//select[@id='VisaTypeId']/option[contains(text(),'{}')]".format(config.CENTER[3])) 65 | self.wait_for_secs(0) 66 | 67 | # check date 68 | self.click_el(id="app_date") 69 | available_dates = {} 70 | next_button_xpath = "//div[@class = 'datepicker-days']//th[@class = 'next' and @style = 'visibility: visible;']" # next month 71 | while True: 72 | nd = self.get_normal_dates() 73 | if nd: 74 | available_dates.update(nd) 75 | if self.driver.find_elements_by_xpath(next_button_xpath): 76 | self.wait_for_secs(0) 77 | self.click_el(xpath=next_button_xpath) 78 | else: 79 | break 80 | return available_dates 81 | 82 | def get_normal_dates(self): 83 | normal_dates_xpath = "//div[@class='datepicker-days']//td[not(contains(@class, 'disabled'))]" # days in the current month 84 | result_dates = {} 85 | dates = [] 86 | if len(self.driver.find_elements_by_xpath(normal_dates_xpath)): 87 | found_month = self.driver.find_element_by_xpath( 88 | "//div[@class='datepicker-days']//th[@class='datepicker-switch']").text 89 | for day in self.driver.find_elements_by_xpath(normal_dates_xpath): # need refactor fix 90 | dates.append(day.text) 91 | for day in dates: 92 | found_date = datetime.strptime(day + " " + found_month, '%d %B %Y') 93 | result_dates[found_date.strftime("%d/%m/%Y")] = [] 94 | self.click_el(normal_dates_xpath) # 自动点击 95 | 96 | return result_dates 97 | --------------------------------------------------------------------------------