Selenium 使用 Chrome 瀏覽器 webdriver

如果你不會寫程式,針對電腦只會使用滑鼠的使用者,目前我們有提供點2下就可以動的執行檔,請參考教學影片:
拓元售票系統使用Max自動搶票機器人(2018/11/23)
https://www.youtube.com/watch?v=QX8u2iF1Gm8

附註:如果需要上面影片裡的exe執行檔的下載點,請匯錢給我,謝謝你~

之前 2018-12-14 的版本可以指定日期和區域:https://www.youtube.com/watch?v=tSOBgcrMmuA


也可以試看看這個免費小程式:
Max打勾機器人 (有附下載點)
https://max-everyday.com/2019/05/max-checkbot/
程式功能:幫你把網頁裡的checkbox(複選框)打勾。
原始碼:https://github.com/max32002/MaxCheckBot


如果你是Firefox瀏覽器的愛好者請參考這一篇:Selenium 使用Firefox 瀏覽器 webdriver
http://stackoverflow.max-everyday.com/?p=2483&preview=true


接下來文章內容是要寫給想要「練功」或「有一些電腦基礎」而且有辦法在「命令提示字元」(Windows平台)或的終端機(Linux/Mac平台)執行指令的人。


Step 1:要安裝 python

附註1:如果是 Linux 或 MacOS 平台,可以略過 Step1,因為內建就有 python, python2 或 python3 都可以,不限定版本。
附註2:除了python 如果你懂其他程式語言(例如:java)也可以實作,不限於python程式語言,大同小異)

Step 2:要安裝 pip

附註:這個太簡單,如果你的電腦裡沒有pip 指令,自行google 看看如何安裝。

Step 3:安裝selenium套件,請執行指令:

pip install selenium

如果在 Linux 或 MacOS 平台裡執行pip install 失敗,請先pip install virtualenv. 我自己本身是使用  macOS, 一開始是無法安裝,使用 virtualenv,是一定可以跑,後來不知道修改到什麼,變成不用進入 venv 環境裡也可以直接執行。

如果是 Windows平台,請直接跳到 Step 4.

virtualenv 基礎教學:
http://docs.python-guide.org/en/latest/dev/virtualenvs/


Step 4:下載ChromeDriver

ChromeDriver 說明:

ChromeDriver 可以讓 Selenium Server 呼叫 Google Chrome 執行,ChromeDriver 網站:
https://sites.google.com/a/chromium.org/chromedriver/

ChromeDriver 下載頁面:

https://sites.google.com/a/chromium.org/chromedriver/downloads

附註1:ChromeDriver目前有支援 Linux 64bit / macOS 64bit / Windows 32bit (64bit 也可以執行 32bit程式)

附註2:除了有 ChromeDriver 還有 SafariDriver 可以讓 Selenium Server 呼叫 Safari 瀏覽器來執行,參考看看 Safari Extension,建議使用ChromeDriver即可。

太舊的 chrome 執行起來會有問題,請先更新chrome瀏覽器為最近的版本,更新方式為:「設定」->「關於Chrome」。

完成 step 3 的 selenium 的安裝,和 step 4 下載ChromeDriver 之後,先試看看這個sample code:

from selenium import webdriver   
chromedriver = "/Users/max/Documents/chromedriver"
driver = webdriver.Chrome(chromedriver)
driver.get("http://tw.yahoo.com/")
  • 說明1:這個ChromeDriver 路徑請換成您電腦實際下載的資料夾。
  • 說明2:這個範例會開一個新的 chrome 視窗並連到網址 http://tw.yahoo.com/
  • 附註:目前的範例是透過 python 去控制 selenium + chromedriver,如果你懂其他的程式語言,也是可以實作的出來。

範例 2 號:

from selenium import webdriver
 
chromedriver = "/Users/max/Documents/chromedriver"
driver = webdriver.Chrome(chromedriver)
driver.get('http://www.cwb.gov.tw/V7/')
driver.set_window_position(0,0) #瀏覽器位置
driver.set_window_size(700,700) #瀏覽器大小

driver.find_element_by_link_text('天氣預報').click() #點擊頁面上"天氣預報"的連結

webdriver有許多方法,

範例 2 號使用的是find_element_by_link_text(),還有許多方法如下:

find_element_by_name()
find_element_by_id()
find_element_by_tag_name()
find_element_by_partial_link_text()
find_element_by_css_selector()

另一種用法:

area = el.find_element(By.TAG_NAME, “a”)

  • By.TAG_NAME
  • By.CSS_SELECTOR

用法可以參考SeleniumHQ:

http://www.seleniumhq.org/docs/03_webdriver.jsp


常見問題(Q&A)

Q:網速到底有沒有差?
光世代有分300M/100M,100m/40m,請知道的是如果在同樣的操作環境,人也一樣,那300m的會不會比100m的更高的機率買到票? 到底什麼是比別人快買到票的關鍵因素?? 售票系統的運作原理是什麼? 為何我的手速也很快了,但為什麼一進去票就是都被買了?他們到底是什麼原因比我快進去,快送出購票請求??

A:高速的網路下載速度是有差異的,也許差幾個毫秒吧,以100Mbps和20Mbps來說下載一個拓元的網頁可能差異不大。

買票前建議先試著去買其他表演,事先下載好購票網頁會使用到的 javascript 和 css 檔案,可以透過離線檔案的快取(cache) 加速網頁的反應時間。

建議使用chrome 瀏覽器來搶票,反應時間會快一點。

「售票系統的運作原理」不難,google 一下就可以看到大量的實作和原理教學,在這裡就不詳述。以目前常見的網頁相關技術來說,在網路上大型的網站實作原理都大同小異,大致上會使用負載平衡(load balance)架構分散主機的網頁流量和要求,通常是(但不是絕對)在資料庫的伺服器那一段程式碼來決定那一個要求可以買到票的,大多數伺服器在處理排隊(queue)的要求是先進先出(first in first out),所以如果你的網路早一點下載完網頁,早一點執行完javascript,早一點送出搶票的要求,理論上搶到票的機率會高一點。

Q:您提到,拓元有分 detail 和 game, 連到game的網址,搶票才會快,假如一開始時間還沒到,「立即購票」的按鈕根本還沒出現,用這個方法,要怎麼使用呢??
A:detail 網址和 game 網址是一樣的,是獨立的,在購票流程裡你可以對可以購票的場次各使用 detail 和 game 網址去訂一次票,就可以知道其中差異,速度上 game 網址也可以買到票,由於傳回的網頁裡的資訊還有所執行的javascript較少,理論上也許會快幾個毫秒。

Q:用搶票機器人,跟手動的差別? 這個問題主要是問,機器人的速度跟用手動的速度,會差距明顯嗎?手的速度能不能贏過機器人?
A:我有放示範用的youtube影片,那個反應時間以人類手指和眼球速度應該無法超越。

Q:能開發軟體嗎?讓不懂程式的人也可以用?
A:
目前有執行檔,不懂程式的人可以在 Windows/Mac/Linux 平台上可以直接執行。
附註1:「搶票軟體」無法保證可以買的到票,也許還有很多其他人使用了機器人來搶票。
問題2:「搶票軟體」拓元網頁會改版,也許會造成功能不正常。

Q:有試著安裝及研究python,也成執行了webdriver幾個範例,但是還是不了解要如何寫出與您所說一支小程式來搶票?

A:請先學會如何透過python 程式自動去點畫面上的按鈕,或使用點2下就可以跑的執行檔。

Q:怎麼像你一樣在已開啟及已登入的視窗執行python?

A:python會全開啟一個全新的視窗,請使用該視窗去登入 Google/Facebook/Pixel Pin.

Q:你的教程中,電腦用的是Linux的系統,Windows系統是無法使用嗎?

A:Windows 也可以使用哦。而且語法相同。ChromeDriver目前有支援 Linux 64bit / macOS 64bit / Windows 32bit (64bit 也可以執行 32bit程式)檔案下載:
http://chromedriver.chromium.org/downloads

Q:如何判斷某一個網址,去做特定事情?
A:

while True:
    time.sleep(0.2)
    url = ""
    try:
        url = driver.current_url
    except Exception as exc:
        pass

    if url is None:
        continue
    else:
        if len(url) == 0:
            continue
    print(url)

Q:google提供的擴充附件外掛 官方是不是會查出來?

A:這不是google提供。不確定官方是不是查的出來,要查的出來的可能很低,難度也很高,難在伺服器端很難判斷用戶是否為機器人。

Q:環境是Win10 64bit 執行畫面顯示,我雙擊 chromedriver 後,畫面顯示「Only local connections are allowed.」,是不是這個機械人不設海外購票?

A:

Starting ChromeDriver 2.44(……) on port 9515
Only local connections are allowed.

直接執行 chromedriver.exe 是會顯示上面的訊息沒錯,ChromeDriver 的架構如下:

說明:上面三個正方形各代表一個執行檔,你雙擊 chromedriver.exe 是執行上面第二個正方形,最右邊的是chrome瀏覽器,最左邊的是我們寫的python應用程式,透過ChromeDriver可以認識的指令去操作ChromeDriver, ChromeDriver 進而在同一台電腦裡遠端去控制chrome瀏覽器。


實際範例:

如果有一個html 長的像醬子:

該按鈕的 html code:

<input class="btn btn-next" data-href="/ticket/area/18_RBTW/4029" name="yt0" type="button" value="立即訂購">

要取到該按鈕的 python code:

el = driver.find_element_by_css_selector('.btn-next')

讓按鈕產生點擊的事件的 python code:

el.click()

有可能會找不到element

How to use the try/except with Selenium Webdriver when having Exceptions on Python

To be able to use required exception you have to import it first with correct name (NoSuchElement -> NoSuchElementException):

from selenium.common.exceptions import NoSuchElementException

try:
    WebDriver.find_element_by_css_selector('div[class="..."')
except NoSuchElementException:
    ActionToRunInCaseNoSuchElementTrue

Using JavaScript

element = driver.execute_script("return $('.cheese')[0]")

設定 select 裡的值:

from selenium.webdriver.support.ui import Select
select = Select(driver.find_element_by_tag_name("select"))
select.select_by_visible_text("Edam")

勾選 checkbox :

You can “toggle” the state of checkboxes, and you can use “click” to set

html:

<input type="checkbox" value="1" name="TicketForm[agree]" id="TicketForm_agree">

python code:

el = driver.find_element(By.CSS_SELECTOR, '#TicketForm_agree')

or

driver.execute_script("$('#TicketForm_agree').prop('checked', true);")

附註:讓輸入框focus 請用 javascript:

$('#TicketForm_verifyCode').focus()

WebDriver Status
https://chromium.googlesource.com/chromium/src/+/master/docs/chromedriver_status.md

這些是目前 WebDriver 支援的指令。


webdriver.py 實際的原始碼,理論上不需要去看,大部份的人也看不懂

"""The WebDriver implementation."""

import base64
import copy
import warnings
from contextlib import contextmanager

from .command import Command
from .webelement import WebElement
from .remote_connection import RemoteConnection
from .errorhandler import ErrorHandler
from .switch_to import SwitchTo
from .mobile import Mobile
from .file_detector import FileDetector, LocalFileDetector
from selenium.common.exceptions import (InvalidArgumentException,
                                        WebDriverException)
from selenium.webdriver.common.by import By
from selenium.webdriver.common.html5.application_cache import ApplicationCache

try:
    str = basestring
except NameError:
    pass


_W3C_CAPABILITY_NAMES = frozenset([
    'acceptInsecureCerts',
    'browserName',
    'browserVersion',
    'platformName',
    'pageLoadStrategy',
    'proxy',
    'setWindowRect',
    'timeouts',
    'unhandledPromptBehavior',
])

_OSS_W3C_CONVERSION = {
    'acceptSslCerts': 'acceptInsecureCerts',
    'version': 'browserVersion',
    'platform': 'platformName'
}


def _make_w3c_caps(caps):
    """Makes a W3C alwaysMatch capabilities object.

    Filters out capability names that are not in the W3C spec. Spec-compliant
    drivers will reject requests containing unknown capability names.

    Moves the Firefox profile, if present, from the old location to the new Firefox
    options object.

    :Args:
     - caps - A dictionary of capabilities requested by the caller.
    """
    caps = copy.deepcopy(caps)
    profile = caps.get('firefox_profile')
    always_match = {}
    if caps.get('proxy') and caps['proxy'].get('proxyType'):
        caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower()
    for k, v in caps.items():
        if v and k in _OSS_W3C_CONVERSION:
            always_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v
        if k in _W3C_CAPABILITY_NAMES or ':' in k:
            always_match[k] = v
    if profile:
        moz_opts = always_match.get('moz:firefoxOptions', {})
        # If it's already present, assume the caller did that intentionally.
        if 'profile' not in moz_opts:
            # Don't mutate the original capabilities.
            new_opts = copy.deepcopy(moz_opts)
            new_opts['profile'] = profile
            always_match['moz:firefoxOptions'] = new_opts
    return {"firstMatch": [{}], "alwaysMatch": always_match}


class WebDriver(object):
    """
    Controls a browser by sending commands to a remote server.
    This server is expected to be running the WebDriver wire protocol
    as defined at
    https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol

    :Attributes:
     - session_id - String ID of the browser session started and controlled by this WebDriver.
     - capabilities - Dictionaty of effective capabilities of this browser session as returned
         by the remote server. See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
     - command_executor - remote_connection.RemoteConnection object used to execute commands.
     - error_handler - errorhandler.ErrorHandler object used to handle errors.
    """

    _web_element_cls = WebElement

    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
                 desired_capabilities=None, browser_profile=None, proxy=None,
                 keep_alive=False, file_detector=None, options=None):
        """
        Create a new driver that will issue commands using the wire protocol.

        :Args:
         - command_executor - Either a string representing URL of the remote server or a custom
             remote_connection.RemoteConnection object. Defaults to 'http://127.0.0.1:4444/wd/hub'.
         - desired_capabilities - A dictionary of capabilities to request when
             starting the browser session. Required parameter.
         - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object.
             Only used if Firefox is requested. Optional.
         - proxy - A selenium.webdriver.common.proxy.Proxy object. The browser session will
             be started with given proxy settings, if possible. Optional.
         - keep_alive - Whether to configure remote_connection.RemoteConnection to use
             HTTP keep-alive. Defaults to False.
         - file_detector - Pass custom file detector object during instantiation. If None,
             then default LocalFileDetector() will be used.
         - options - instance of a driver options.Options class
        """
        if desired_capabilities is None:
            raise WebDriverException("Desired Capabilities can't be None")
        if not isinstance(desired_capabilities, dict):
            raise WebDriverException("Desired Capabilities must be a dictionary")
        if proxy is not None:
            warnings.warn("Please use FirefoxOptions to set proxy",
                          DeprecationWarning)
            proxy.add_to_capabilities(desired_capabilities)
        if options is not None:
            desired_capabilities.update(options.to_capabilities())
        self.command_executor = command_executor
        if type(self.command_executor) is bytes or isinstance(self.command_executor, str):
            self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive)
        self._is_remote = True
        self.session_id = None
        self.capabilities = {}
        self.error_handler = ErrorHandler()
        self.start_client()
        if browser_profile is not None:
            warnings.warn("Please use FirefoxOptions to set browser profile",
                          DeprecationWarning)
        self.start_session(desired_capabilities, browser_profile)
        self._switch_to = SwitchTo(self)
        self._mobile = Mobile(self)
        self.file_detector = file_detector or LocalFileDetector()

    def __repr__(self):
        return '<{0.__module__}.{0.__name__} (session="{1}")>'.format(
            type(self), self.session_id)

    @contextmanager
    def file_detector_context(self, file_detector_class, *args, **kwargs):
        """
        Overrides the current file detector (if necessary) in limited context.
        Ensures the original file detector is set afterwards.

        Example:

        with webdriver.file_detector_context(UselessFileDetector):
            someinput.send_keys('/etc/hosts')

        :Args:
         - file_detector_class - Class of the desired file detector. If the class is different
             from the current file_detector, then the class is instantiated with args and kwargs
             and used as a file detector during the duration of the context manager.
         - args - Optional arguments that get passed to the file detector class during
             instantiation.
         - kwargs - Keyword arguments, passed the same way as args.
        """
        last_detector = None
        if not isinstance(self.file_detector, file_detector_class):
            last_detector = self.file_detector
            self.file_detector = file_detector_class(*args, **kwargs)
        try:
            yield
        finally:
            if last_detector is not None:
                self.file_detector = last_detector

    @property
    def mobile(self):
        return self._mobile

    @property
    def name(self):
        """Returns the name of the underlying browser for this instance.

        :Usage:
            name = driver.name
        """
        if 'browserName' in self.capabilities:
            return self.capabilities['browserName']
        else:
            raise KeyError('browserName not specified in session capabilities')

    def start_client(self):
        """
        Called before starting a new session. This method may be overridden
        to define custom startup behavior.
        """
        pass

    def stop_client(self):
        """
        Called after executing a quit command. This method may be overridden
        to define custom shutdown behavior.
        """
        pass

    def start_session(self, capabilities, browser_profile=None):
        """
        Creates a new session with the desired capabilities.

        :Args:
         - browser_name - The name of the browser to request.
         - version - Which browser version to request.
         - platform - Which platform to request the browser on.
         - javascript_enabled - Whether the new session should support JavaScript.
         - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. Only used if Firefox is requested.
        """
        if not isinstance(capabilities, dict):
            raise InvalidArgumentException("Capabilities must be a dictionary")
        if browser_profile:
            if "moz:firefoxOptions" in capabilities:
                capabilities["moz:firefoxOptions"]["profile"] = browser_profile.encoded
            else:
                capabilities.update({'firefox_profile': browser_profile.encoded})
        w3c_caps = _make_w3c_caps(capabilities)
        parameters = {"capabilities": w3c_caps,
                      "desiredCapabilities": capabilities}
        response = self.execute(Command.NEW_SESSION, parameters)
        if 'sessionId' not in response:
            response = response['value']
        self.session_id = response['sessionId']
        self.capabilities = response.get('value')

        # if capabilities is none we are probably speaking to
        # a W3C endpoint
        if self.capabilities is None:
            self.capabilities = response.get('capabilities')

        # Double check to see if we have a W3C Compliant browser
        self.w3c = response.get('status') is None
        self.command_executor.w3c = self.w3c

    def _wrap_value(self, value):
        if isinstance(value, dict):
            converted = {}
            for key, val in value.items():
                converted[key] = self._wrap_value(val)
            return converted
        elif isinstance(value, self._web_element_cls):
            return {'ELEMENT': value.id, 'element-6066-11e4-a52e-4f735466cecf': value.id}
        elif isinstance(value, list):
            return list(self._wrap_value(item) for item in value)
        else:
            return value

    def create_web_element(self, element_id):
        """Creates a web element with the specified `element_id`."""
        return self._web_element_cls(self, element_id, w3c=self.w3c)

    def _unwrap_value(self, value):
        if isinstance(value, dict):
            if 'ELEMENT' in value or 'element-6066-11e4-a52e-4f735466cecf' in value:
                wrapped_id = value.get('ELEMENT', None)
                if wrapped_id:
                    return self.create_web_element(value['ELEMENT'])
                else:
                    return self.create_web_element(value['element-6066-11e4-a52e-4f735466cecf'])
            else:
                for key, val in value.items():
                    value[key] = self._unwrap_value(val)
                return value
        elif isinstance(value, list):
            return list(self._unwrap_value(item) for item in value)
        else:
            return value

    def execute(self, driver_command, params=None):
        """
        Sends a command to be executed by a command.CommandExecutor.

        :Args:
         - driver_command: The name of the command to execute as a string.
         - params: A dictionary of named parameters to send with the command.

        :Returns:
          The command's JSON response loaded into a dictionary object.
        """
        if self.session_id is not None:
            if not params:
                params = {'sessionId': self.session_id}
            elif 'sessionId' not in params:
                params['sessionId'] = self.session_id

        params = self._wrap_value(params)
        response = self.command_executor.execute(driver_command, params)
        if response:
            self.error_handler.check_response(response)
            response['value'] = self._unwrap_value(
                response.get('value', None))
            return response
        # If the server doesn't send a response, assume the command was
        # a success
        return {'success': 0, 'value': None, 'sessionId': self.session_id}

    def get(self, url):
        """
        Loads a web page in the current browser session.
        """
        self.execute(Command.GET, {'url': url})

    @property
    def title(self):
        """Returns the title of the current page.

        :Usage:
            title = driver.title
        """
        resp = self.execute(Command.GET_TITLE)
        return resp['value'] if resp['value'] is not None else ""

    def find_element_by_id(self, id_):
        """Finds an element by id.

        :Args:
         - id\_ - The id of the element to be found.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_id('foo')
        """
        return self.find_element(by=By.ID, value=id_)

    def find_elements_by_id(self, id_):
        """
        Finds multiple elements by id.

        :Args:
         - id\_ - The id of the elements to be found.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_id('foo')
        """
        return self.find_elements(by=By.ID, value=id_)

    def find_element_by_xpath(self, xpath):
        """
        Finds an element by xpath.

        :Args:
         - xpath - The xpath locator of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_xpath('//div/td[1]')
        """
        return self.find_element(by=By.XPATH, value=xpath)

    def find_elements_by_xpath(self, xpath):
        """
        Finds multiple elements by xpath.

        :Args:
         - xpath - The xpath locator of the elements to be found.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_xpath("//div[contains(@class, 'foo')]")
        """
        return self.find_elements(by=By.XPATH, value=xpath)

    def find_element_by_link_text(self, link_text):
        """
        Finds an element by link text.

        :Args:
         - link_text: The text of the element to be found.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_link_text('Sign In')
        """
        return self.find_element(by=By.LINK_TEXT, value=link_text)

    def find_elements_by_link_text(self, text):
        """
        Finds elements by link text.

        :Args:
         - link_text: The text of the elements to be found.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_link_text('Sign In')
        """
        return self.find_elements(by=By.LINK_TEXT, value=text)

    def find_element_by_partial_link_text(self, link_text):
        """
        Finds an element by a partial match of its link text.

        :Args:
         - link_text: The text of the element to partially match on.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_partial_link_text('Sign')
        """
        return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text)

    def find_elements_by_partial_link_text(self, link_text):
        """
        Finds elements by a partial match of their link text.

        :Args:
         - link_text: The text of the element to partial match on.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_partial_link_text('Sign')
        """
        return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text)

    def find_element_by_name(self, name):
        """
        Finds an element by name.

        :Args:
         - name: The name of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_name('foo')
        """
        return self.find_element(by=By.NAME, value=name)

    def find_elements_by_name(self, name):
        """
        Finds elements by name.

        :Args:
         - name: The name of the elements to find.

        :Returns:
         - list of webelement - a list with elements if any was found.  an
           empty list if not

        :Usage:
            elements = driver.find_elements_by_name('foo')
        """
        return self.find_elements(by=By.NAME, value=name)

    def find_element_by_tag_name(self, name):
        """
        Finds an element by tag name.

        :Args:
         - name - name of html tag (eg: h1, a, span)

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_tag_name('h1')
        """
        return self.find_element(by=By.TAG_NAME, value=name)

    def find_elements_by_tag_name(self, name):
        """
        Finds elements by tag name.

        :Args:
         - name - name of html tag (eg: h1, a, span)

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_tag_name('h1')
        """
        return self.find_elements(by=By.TAG_NAME, value=name)

    def find_element_by_class_name(self, name):
        """
        Finds an element by class name.

        :Args:
         - name: The class name of the element to find.

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_class_name('foo')
        """
        return self.find_element(by=By.CLASS_NAME, value=name)

    def find_elements_by_class_name(self, name):
        """
        Finds elements by class name.

        :Args:
         - name: The class name of the elements to find.

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_class_name('foo')
        """
        return self.find_elements(by=By.CLASS_NAME, value=name)

    def find_element_by_css_selector(self, css_selector):
        """
        Finds an element by css selector.

        :Args:
         - css_selector - CSS selector string, ex: 'a.nav#home'

        :Returns:
         - WebElement - the element if it was found

        :Raises:
         - NoSuchElementException - if the element wasn't found

        :Usage:
            element = driver.find_element_by_css_selector('#foo')
        """
        return self.find_element(by=By.CSS_SELECTOR, value=css_selector)

    def find_elements_by_css_selector(self, css_selector):
        """
        Finds elements by css selector.

        :Args:
         - css_selector - CSS selector string, ex: 'a.nav#home'

        :Returns:
         - list of WebElement - a list with elements if any was found.  An
           empty list if not

        :Usage:
            elements = driver.find_elements_by_css_selector('.foo')
        """
        return self.find_elements(by=By.CSS_SELECTOR, value=css_selector)

    def execute_script(self, script, *args):
        """
        Synchronously Executes JavaScript in the current window/frame.

        :Args:
         - script: The JavaScript to execute.
         - \*args: Any applicable arguments for your JavaScript.

        :Usage:
            driver.execute_script('return document.title;')
        """
        converted_args = list(args)
        command = None
        if self.w3c:
            command = Command.W3C_EXECUTE_SCRIPT
        else:
            command = Command.EXECUTE_SCRIPT

        return self.execute(command, {
            'script': script,
            'args': converted_args})['value']

    def execute_async_script(self, script, *args):
        """
        Asynchronously Executes JavaScript in the current window/frame.

        :Args:
         - script: The JavaScript to execute.
         - \*args: Any applicable arguments for your JavaScript.

        :Usage:
            script = "var callback = arguments[arguments.length - 1]; " \
                     "window.setTimeout(function(){ callback('timeout') }, 3000);"
            driver.execute_async_script(script)
        """
        converted_args = list(args)
        if self.w3c:
            command = Command.W3C_EXECUTE_SCRIPT_ASYNC
        else:
            command = Command.EXECUTE_ASYNC_SCRIPT

        return self.execute(command, {
            'script': script,
            'args': converted_args})['value']

    @property
    def current_url(self):
        """
        Gets the URL of the current page.

        :Usage:
            driver.current_url
        """
        return self.execute(Command.GET_CURRENT_URL)['value']

    @property
    def page_source(self):
        """
        Gets the source of the current page.

        :Usage:
            driver.page_source
        """
        return self.execute(Command.GET_PAGE_SOURCE)['value']

    def close(self):
        """
        Closes the current window.

        :Usage:
            driver.close()
        """
        self.execute(Command.CLOSE)

    def quit(self):
        """
        Quits the driver and closes every associated window.

        :Usage:
            driver.quit()
        """
        try:
            self.execute(Command.QUIT)
        finally:
            self.stop_client()

    @property
    def current_window_handle(self):
        """
        Returns the handle of the current window.

        :Usage:
            driver.current_window_handle
        """
        if self.w3c:
            return self.execute(Command.W3C_GET_CURRENT_WINDOW_HANDLE)['value']
        else:
            return self.execute(Command.GET_CURRENT_WINDOW_HANDLE)['value']

    @property
    def window_handles(self):
        """
        Returns the handles of all windows within the current session.

        :Usage:
            driver.window_handles
        """
        if self.w3c:
            return self.execute(Command.W3C_GET_WINDOW_HANDLES)['value']
        else:
            return self.execute(Command.GET_WINDOW_HANDLES)['value']

    def maximize_window(self):
        """
        Maximizes the current window that webdriver is using
        """
        command = Command.MAXIMIZE_WINDOW
        if self.w3c:
            command = Command.W3C_MAXIMIZE_WINDOW
        self.execute(command, {"windowHandle": "current"})

    def fullscreen_window(self):
        """
        Invokes the window manager-specific 'full screen' operation
        """
        self.execute(Command.FULLSCREEN_WINDOW)

    def minimize_window(self):
        """
        Invokes the window manager-specific 'minimize' operation
        """
        self.execute(Command.MINIMIZE_WINDOW)

    @property
    def switch_to(self):
        """
        :Returns:
            - SwitchTo: an object containing all options to switch focus into

        :Usage:
            element = driver.switch_to.active_element
            alert = driver.switch_to.alert
            driver.switch_to.default_content()
            driver.switch_to.frame('frame_name')
            driver.switch_to.frame(1)
            driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0])
            driver.switch_to.parent_frame()
            driver.switch_to.window('main')
        """
        return self._switch_to

    # Target Locators
    def switch_to_active_element(self):
        """ Deprecated use driver.switch_to.active_element
        """
        warnings.warn("use driver.switch_to.active_element instead", DeprecationWarning)
        return self._switch_to.active_element

    def switch_to_window(self, window_name):
        """ Deprecated use driver.switch_to.window
        """
        warnings.warn("use driver.switch_to.window instead", DeprecationWarning)
        self._switch_to.window(window_name)

    def switch_to_frame(self, frame_reference):
        """ Deprecated use driver.switch_to.frame
        """
        warnings.warn("use driver.switch_to.frame instead", DeprecationWarning)
        self._switch_to.frame(frame_reference)

    def switch_to_default_content(self):
        """ Deprecated use driver.switch_to.default_content
        """
        warnings.warn("use driver.switch_to.default_content instead", DeprecationWarning)
        self._switch_to.default_content()

    def switch_to_alert(self):
        """ Deprecated use driver.switch_to.alert
        """
        warnings.warn("use driver.switch_to.alert instead", DeprecationWarning)
        return self._switch_to.alert

    # Navigation
    def back(self):
        """
        Goes one step backward in the browser history.

        :Usage:
            driver.back()
        """
        self.execute(Command.GO_BACK)

    def forward(self):
        """
        Goes one step forward in the browser history.

        :Usage:
            driver.forward()
        """
        self.execute(Command.GO_FORWARD)

    def refresh(self):
        """
        Refreshes the current page.

        :Usage:
            driver.refresh()
        """
        self.execute(Command.REFRESH)

    # Options
    def get_cookies(self):
        """
        Returns a set of dictionaries, corresponding to cookies visible in the current session.

        :Usage:
            driver.get_cookies()
        """
        return self.execute(Command.GET_ALL_COOKIES)['value']

    def get_cookie(self, name):
        """
        Get a single cookie by name. Returns the cookie if found, None if not.

        :Usage:
            driver.get_cookie('my_cookie')
        """
        cookies = self.get_cookies()
        for cookie in cookies:
            if cookie['name'] == name:
                return cookie
        return None

    def delete_cookie(self, name):
        """
        Deletes a single cookie with the given name.

        :Usage:
            driver.delete_cookie('my_cookie')
        """
        self.execute(Command.DELETE_COOKIE, {'name': name})

    def delete_all_cookies(self):
        """
        Delete all cookies in the scope of the session.

        :Usage:
            driver.delete_all_cookies()
        """
        self.execute(Command.DELETE_ALL_COOKIES)

    def add_cookie(self, cookie_dict):
        """
        Adds a cookie to your current session.

        :Args:
         - cookie_dict: A dictionary object, with required keys - "name" and "value";
            optional keys - "path", "domain", "secure", "expiry"

        Usage:
            driver.add_cookie({'name' : 'foo', 'value' : 'bar'})
            driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/'})
            driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/', 'secure':True})

        """
        self.execute(Command.ADD_COOKIE, {'cookie': cookie_dict})

    # Timeouts
    def implicitly_wait(self, time_to_wait):
        """
        Sets a sticky timeout to implicitly wait for an element to be found,
           or a command to complete. This method only needs to be called one
           time per session. To set the timeout for calls to
           execute_async_script, see set_script_timeout.

        :Args:
         - time_to_wait: Amount of time to wait (in seconds)

        :Usage:
            driver.implicitly_wait(30)
        """
        if self.w3c:
            self.execute(Command.SET_TIMEOUTS, {
                'implicit': int(float(time_to_wait) * 1000)})
        else:
            self.execute(Command.IMPLICIT_WAIT, {
                'ms': float(time_to_wait) * 1000})

    def set_script_timeout(self, time_to_wait):
        """
        Set the amount of time that the script should wait during an
           execute_async_script call before throwing an error.

        :Args:
         - time_to_wait: The amount of time to wait (in seconds)

        :Usage:
            driver.set_script_timeout(30)
        """
        if self.w3c:
            self.execute(Command.SET_TIMEOUTS, {
                'script': int(float(time_to_wait) * 1000)})
        else:
            self.execute(Command.SET_SCRIPT_TIMEOUT, {
                'ms': float(time_to_wait) * 1000})

    def set_page_load_timeout(self, time_to_wait):
        """
        Set the amount of time to wait for a page load to complete
           before throwing an error.

        :Args:
         - time_to_wait: The amount of time to wait

        :Usage:
            driver.set_page_load_timeout(30)
        """
        try:
            self.execute(Command.SET_TIMEOUTS, {
                'pageLoad': int(float(time_to_wait) * 1000)})
        except WebDriverException:
            self.execute(Command.SET_TIMEOUTS, {
                'ms': float(time_to_wait) * 1000,
                'type': 'page load'})

    def find_element(self, by=By.ID, value=None):
        """
        'Private' method used by the find_element_by_* methods.

        :Usage:
            Use the corresponding find_element_by_* instead of this.

        :rtype: WebElement
        """
        if self.w3c:
            if by == By.ID:
                by = By.CSS_SELECTOR
                value = '[id="%s"]' % value
            elif by == By.TAG_NAME:
                by = By.CSS_SELECTOR
            elif by == By.CLASS_NAME:
                by = By.CSS_SELECTOR
                value = ".%s" % value
            elif by == By.NAME:
                by = By.CSS_SELECTOR
                value = '[name="%s"]' % value
        return self.execute(Command.FIND_ELEMENT, {
            'using': by,
            'value': value})['value']

    def find_elements(self, by=By.ID, value=None):
        """
        'Private' method used by the find_elements_by_* methods.

        :Usage:
            Use the corresponding find_elements_by_* instead of this.

        :rtype: list of WebElement
        """
        if self.w3c:
            if by == By.ID:
                by = By.CSS_SELECTOR
                value = '[id="%s"]' % value
            elif by == By.TAG_NAME:
                by = By.CSS_SELECTOR
            elif by == By.CLASS_NAME:
                by = By.CSS_SELECTOR
                value = ".%s" % value
            elif by == By.NAME:
                by = By.CSS_SELECTOR
                value = '[name="%s"]' % value

        # Return empty list if driver returns null
        # See https://github.com/SeleniumHQ/selenium/issues/4555
        return self.execute(Command.FIND_ELEMENTS, {
            'using': by,
            'value': value})['value'] or []

    @property
    def desired_capabilities(self):
        """
        returns the drivers current desired capabilities being used
        """
        return self.capabilities

    def get_screenshot_as_file(self, filename):
        """
        Saves a screenshot of the current window to a PNG image file. Returns
           False if there is any IOError, else returns True. Use full paths in
           your filename.

        :Args:
         - filename: The full path you wish to save your screenshot to. This
           should end with a `.png` extension.

        :Usage:
            driver.get_screenshot_as_file('/Screenshots/foo.png')
        """
        if not filename.lower().endswith('.png'):
            warnings.warn("name used for saved screenshot does not match file "
                          "type. It should end with a `.png` extension", UserWarning)
        png = self.get_screenshot_as_png()
        try:
            with open(filename, 'wb') as f:
                f.write(png)
        except IOError:
            return False
        finally:
            del png
        return True

    def save_screenshot(self, filename):
        """
        Saves a screenshot of the current window to a PNG image file. Returns
           False if there is any IOError, else returns True. Use full paths in
           your filename.

        :Args:
         - filename: The full path you wish to save your screenshot to. This
           should end with a `.png` extension.

        :Usage:
            driver.save_screenshot('/Screenshots/foo.png')
        """
        return self.get_screenshot_as_file(filename)

    def get_screenshot_as_png(self):
        """
        Gets the screenshot of the current window as a binary data.

        :Usage:
            driver.get_screenshot_as_png()
        """
        return base64.b64decode(self.get_screenshot_as_base64().encode('ascii'))

    def get_screenshot_as_base64(self):
        """
        Gets the screenshot of the current window as a base64 encoded string
           which is useful in embedded images in HTML.

        :Usage:
            driver.get_screenshot_as_base64()
        """
        return self.execute(Command.SCREENSHOT)['value']

    def set_window_size(self, width, height, windowHandle='current'):
        """
        Sets the width and height of the current window. (window.resizeTo)

        :Args:
         - width: the width in pixels to set the window to
         - height: the height in pixels to set the window to

        :Usage:
            driver.set_window_size(800,600)
        """
        if self.w3c:
            if windowHandle != 'current':
                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
            self.set_window_rect(width=int(width), height=int(height))
        else:
            self.execute(Command.SET_WINDOW_SIZE, {
                'width': int(width),
                'height': int(height),
                'windowHandle': windowHandle})

    def get_window_size(self, windowHandle='current'):
        """
        Gets the width and height of the current window.

        :Usage:
            driver.get_window_size()
        """
        command = Command.GET_WINDOW_SIZE
        if self.w3c:
            if windowHandle != 'current':
                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
            size = self.get_window_rect()
        else:
            size = self.execute(command, {'windowHandle': windowHandle})

        if size.get('value', None) is not None:
            size = size['value']

        return {k: size[k] for k in ('width', 'height')}

    def set_window_position(self, x, y, windowHandle='current'):
        """
        Sets the x,y position of the current window. (window.moveTo)

        :Args:
         - x: the x-coordinate in pixels to set the window position
         - y: the y-coordinate in pixels to set the window position

        :Usage:
            driver.set_window_position(0,0)
        """
        if self.w3c:
            if windowHandle != 'current':
                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
            return self.set_window_rect(x=int(x), y=int(y))
        else:
            self.execute(Command.SET_WINDOW_POSITION,
                         {
                             'x': int(x),
                             'y': int(y),
                             'windowHandle': windowHandle
                         })

    def get_window_position(self, windowHandle='current'):
        """
        Gets the x,y position of the current window.

        :Usage:
            driver.get_window_position()
        """
        if self.w3c:
            if windowHandle != 'current':
                warnings.warn("Only 'current' window is supported for W3C compatibile browsers.")
            position = self.get_window_rect()
        else:
            position = self.execute(Command.GET_WINDOW_POSITION,
                                    {'windowHandle': windowHandle})['value']

        return {k: position[k] for k in ('x', 'y')}

    def get_window_rect(self):
        """
        Gets the x, y coordinates of the window as well as height and width of
        the current window.

        :Usage:
            driver.get_window_rect()
        """
        return self.execute(Command.GET_WINDOW_RECT)['value']

    def set_window_rect(self, x=None, y=None, width=None, height=None):
        """
        Sets the x, y coordinates of the window as well as height and width of
        the current window.

        :Usage:
            driver.set_window_rect(x=10, y=10)
            driver.set_window_rect(width=100, height=200)
            driver.set_window_rect(x=10, y=10, width=100, height=200)
        """
        if (x is None and y is None) and (height is None and width is None):
            raise InvalidArgumentException("x and y or height and width need values")

        return self.execute(Command.SET_WINDOW_RECT, {"x": x, "y": y,
                                                      "width": width,
                                                      "height": height})['value']

    @property
    def file_detector(self):
        return self._file_detector

    @file_detector.setter
    def file_detector(self, detector):
        """
        Set the file detector to be used when sending keyboard input.
        By default, this is set to a file detector that does nothing.

        see FileDetector
        see LocalFileDetector
        see UselessFileDetector

        :Args:
         - detector: The detector to use. Must not be None.
        """
        if detector is None:
            raise WebDriverException("You may not set a file detector that is null")
        if not isinstance(detector, FileDetector):
            raise WebDriverException("Detector has to be instance of FileDetector")
        self._file_detector = detector

    @property
    def orientation(self):
        """
        Gets the current orientation of the device

        :Usage:
            orientation = driver.orientation
        """
        return self.execute(Command.GET_SCREEN_ORIENTATION)['value']

    @orientation.setter
    def orientation(self, value):
        """
        Sets the current orientation of the device

        :Args:
         - value: orientation to set it to.

        :Usage:
            driver.orientation = 'landscape'
        """
        allowed_values = ['LANDSCAPE', 'PORTRAIT']
        if value.upper() in allowed_values:
            self.execute(Command.SET_SCREEN_ORIENTATION, {'orientation': value})
        else:
            raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'")

    @property
    def application_cache(self):
        """ Returns a ApplicationCache Object to interact with the browser app cache"""
        return ApplicationCache(self)

    @property
    def log_types(self):
        """
        Gets a list of the available log types

        :Usage:
            driver.log_types
        """
        return self.execute(Command.GET_AVAILABLE_LOG_TYPES)['value']

    def get_log(self, log_type):
        """
        Gets the log for a given log type

        :Args:
         - log_type: type of log that which will be returned

        :Usage:
            driver.get_log('browser')
            driver.get_log('driver')
            driver.get_log('client')
            driver.get_log('server')
        """
        return self.execute(Command.GET_LOG, {'type': log_type})['value']

從目前的 source code 可以清楚的看到如何去使用 webdriver 物件。

下面的這個 chromedriver 切換 frame 的功能,如果你要是搶「熱門」的票,是遇不到的,熱門的場次都是「自動畫位」,所以不必自己去選坐位,自動選坐位會彈出在 iframe 裡,可以使用下面這行指令即可切換到選位的 iframe:

driver.switch_to.frame(driver.find_element_by_xpath("//iframe[contains(@src,'/ticket/selectSeat/')]"))

selenium定位页面元素的时候会遇到定位不到的问题,明明元素就在那儿,用檢視原素也可以看到,就是定位不到,这种情况很有可能是frame在搞鬼。

frame标签有frameset、frame、iframe三种,frameset跟其他普通标签没有区别,不会影响到正常的定位,而frame与iframe对selenium定位而言是一样的,selenium有一组方法对frame进行操作。

1.怎么切到frame中(switch_to.frame())

selenium提供了switch_to.frame()方法来切换frame

switch_to.frame(reference)

reference是传入的参数,用来定位frame,可以传入id、name、index以及selenium的WebElement对象,假设有如下HTML代码 index.html:

<html lang="en">
<head>
 <title>FrameTest</title>
</head>
<body>
<iframe src="a.html" id="frame1" name="myframe"></iframe>
</body>
</html>

想要定位其中的iframe并切进去,可以通过如下代码:

from selenium import webdriver
driver = webdriver.Firefox()
driver.switch_to.frame(0) # 1.用frame的index来定位,第一个是0
# driver.switch_to.frame("frame1") # 2.用id来定位
# driver.switch_to.frame("myframe") # 3.用name来定位
# driver.switch_to.frame(driver.find_element_by_tag_name("iframe")) # 4.用WebElement对象来定位

通常采用id和name就能够解决绝大多数问题。但有时候frame并无这两项属性,则可以用index和WebElement来定位:

index从0开始,传入整型参数即判定为用index定位,传入str参数则判定为用id/name定位
WebElement对象,即用find_element系列方法所取得的对象,我们可以用tag_name、xpath等来定位frame对象
举个例子:

<iframe src="myframetest.html" />

用xpath定位,传入WebElement对象:

driver.switch_to.frame(driver.find_element_by_xpath("//iframe[contains(@src,'myframe')]"))

2.从frame中切回主文档(switch_to.default_content())

切到frame中之后,我们便不能继续操作主文档的元素,这时如果想操作主文档内容,则需切回主文档。

driver.switch_to.default_content()

3.嵌套frame的操作(switch_to.parent_frame())

有时候我们会遇到嵌套的frame,如下:

<html>
 <iframe id="frame1">
 <iframe id="frame2" / >
 </iframe>
</html>

1.从主文档切到frame2,一层层切进去

driver.switch_to.frame("frame1")
driver.switch_to.frame("frame2")

2.从frame2再切回frame1,这里selenium给我们提供了一个方法能够从子frame切回到父frame,而不用我们切回主文档再切进来。

driver.switch_to.parent_frame() # 如果当前已是主文档,则无效果

有了parent_frame()这个相当于后退的方法,我们可以随意切换不同的frame,随意的跳来跳去了。

所以只要善用以下三个方法,遇到frame分分钟搞定:

driver.switch_to.frame(reference)
driver.switch_to.parent_frame()
driver.switch_to.default_content()

如何在Server side(伺服器端)檢查使用者有沒開 chromedriver?

Can a website detect when you are using selenium with chromedriver?
https://stackoverflow.com/questions/33225947/can-a-website-detect-when-you-are-using-selenium-with-chromedriver

上面文章不用去看了,我檢查過新的版本的 selenium 完全不會多產生上面的 key 值。


拓元在 2018-05-12 之後更新的javascript 如下:

function order_check() {
 var count = valueCount(["WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek="]),
 maxQuota = 4;

if(!$("#TicketForm_agree").prop("checked")) {
 alert("\u8acb\u5148\u8a73\u95b1\u4e14\u540c\u610f\u6703\u54e1\u670d\u52d9\u689d\u6b3e\u5f8c\u518d\u884c\u9001\u51fa\u52d5\u4f5c\u3002");
 } else if (count > maxQuota) {
 alert("\u55ae\u7b46\u4ea4\u6613\u6700\u591a\u53ef\u8cb7 \" + maxQuota + \" \u5f35");
 } else if (count == 0) {
 alert("\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u7a2e\u7968\u7a2e");
 
 } else {
 var ticketType = ["WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek="],
 ticketTypeSelector = $("[name=\"" + ticketType.join("\"], [name=\"") + "\"]");

ticketTypeSelector.each(function() {
 $(this).attr("name", "TicketForm[ticketPrice][" + $(this).get(0).name + "]");
 });
 return true;
 }

return false;
}

function valueCount(elements) {
 elements = countValById(elements);
 return elements.reduce(function(total, element) {
 return total + element;
 }, 0);
}

$("select[id=\"WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek=\"]").on("click", function(event) {
 if (!!event.originalEvent.isTrusted && !event.isTrigger) {
 $("#TicketForm_checked").attr("name", "TicketForm[ticketPrice][vQmwBD+sVq5AOWaOJrdiOQ5oIjAlhU38AxsBgnL1qkU=]");
 }
});
$("#TicketForm_agree").on("click", function(event) {
 if (!!event.originalEvent.isTrusted && !$(this).checked && !event.isTrigger) {
 $(this).attr("name", "TicketForm[agree][YnuMm9Vok/JcdY82p5pho4QaTg8m+p735VSWpPyjOfE=]");
 }
})

$("#TicketForm").on("change", function(event) {
 var ticketType = ["WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek="];

if (ticketType.indexOf(event.target.id) != -1) {
 var count = valueCount(ticketType),
 maxQuota = 4;

if (count > maxQuota) {
 var num = parseInt($(event.target).val()) + (maxQuota - count);

alert("\u55ae\u7b46\u4ea4\u6613\u6700\u591a\u53ef\u8cb7 \" + maxQuota + \" \u5f35\uff0c\u60a8\u5171\u9078\u64c7\u4e86 \" + count + \" \u5f35");

while ($(event.target).find("option[value=" + num + "]").length < 1 && num != 0) {
 num--;
 }

$(event.target).val(num);
 }

$("#ticketQuota").text(maxQuota - valueCount(ticketType));
 }
});

$("#TicketForm select").change();

 <select class="mobile-select" name="WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek=" id="WMeBWmQEOdoAKQq0wKU8kv4k5VcwA3GjyISDUmtPZek=">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
 </select>

針對上面的程式碼,如果使用

driver.execute_script("$('#TicketForm_agree').prop('checked', true);")

並不會觸發 onclick 事件。請改服用:

form_checkbox = None
try:
 form_checkbox = driver.find_element(By.ID, 'TicketForm_agree')
 if form_checkbox is not None:
 try:
 form_checkbox.click()
 except Exception as exc:
 print "click TicketForm_agree fail"
 pass
except NoSuchElementException:
 print "find TicketForm_agree fail"

附註, select box 比照 checkbox 的 code, 先產生 click 再去選取預期的張數即可。


2018年09月某一天 發現拓元的 javascript 又增加了幾個新的檢查點,javascript 如下:

function countValById(arr) {
    return arr.map(function(val) {
        return parseInt($("[id=\"" + val + "\"]").val());
    });
}

function order_check() {
    var count = valueCount(["gqQB0FXCgar2OyOvqUFZR1xujK9R1sO+OR6V6m\/unzY="]),
        maxQuota = 4;

    if(!$("#TicketForm_agree").prop("checked")) {
        alert("\u8acb\u5148\u8a73\u95b1\u4e14\u540c\u610f\u6703\u54e1\u670d\u52d9\u689d\u6b3e\u5f8c\u518d\u884c\u9001\u51fa\u52d5\u4f5c\u3002");
    } else if (count > maxQuota) {
        alert("單筆交易最多可買 " + maxQuota + " 張");
    } else if (count == 0) {
        alert("\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u7a2e\u7968\u7a2e");
    
    } else {
        var ticketType = ["gqQB0FXCgar2OyOvqUFZR1xujK9R1sO+OR6V6m\/unzY="],
            ticketTypeSelector = $("[name=\"" + ticketType.join("\"], [name=\"") + "\"]");

        ticketTypeSelector.each(function() {
            $(this).attr("name", "TicketForm[ticketPrice][" + $(this).get(0).name + "]");
        });
        return true;
    }

    return false;
}

function valueCount(elements) {
    elements = countValById(elements);
    return elements.reduce(function(total, element) {
        return total + element;
    }, 0);
}

$(document).ready(function() {
    var ticketType = ["gqQB0FXCgar2OyOvqUFZR1xujK9R1sO+OR6V6m\/unzY="],
        ticketTypeSelector = "[id='" + ticketType.join("'], [id='") + "']";

    $("#TicketForm_agree").attr("name", "TicketForm[agrees]");
    $("#TicketForm_checked").attr("name", "TicketForm[ticketPrice][checks][ + $(ticketTypeSelector).length + ]");

    ravenCheck(ticketTypeSelector);

    $("#TicketForm").on("mousedown click touchstart", ticketTypeSelector, function(event) {
        $("#TicketForm_checked").attr("name", "s_" + event.originalEvent.isTrusted + "_" + event.isTrigger);
        if (event.originalEvent.isTrusted !== false && !event.isTrigger) {
            $("#TicketForm_checked").attr("name", "TicketForm[ticketPrice][5BQBWTBCQoAEPama/ehw8qJhA1lkdF0fH8J5eua5HSw=]");
        }
    }).on("mousedown click touchstart", "#TicketForm_agree", function(event) {
        $(this).attr("name", "s_" + event.originalEvent.isTrusted + "_" + event.isTrigger);
        if (event.originalEvent.isTrusted !== false && !$(this).checked && !event.isTrigger) {
            $(this).attr("name", "TicketForm[agree][Dbk4t/2b/yoQPK0qAnhBljhX0JBLgehzAlwuEOBdEik=]");
        }
    }).on("change", function(event) {
        if (ticketType.indexOf(event.target.id) != -1) {
            var count = valueCount(ticketType),
                maxQuota = 4;

            if (count > maxQuota) {
                var num = parseInt($(event.target).val()) + (maxQuota - count);

                alert("單筆交易最多可買 " + maxQuota + " 張,您共選擇了 " + count + " 張");

                while ($(event.target).find("option[value=" + num + "]").length < 1 && num != 0) {
                    num--;
                }

                $(event.target).val(num);
            }

            $("#ticketQuota").text(maxQuota - valueCount(ticketType));
        }
    });

    $("#TicketForm select").change();
});

jQuery(document).on('click', '#yw0', function(){
    jQuery.ajax({
        url: "\/ticket\/captcha?refresh=1",
        dataType: 'json',
        cache: false,
        success: function(data) {
            jQuery('#yw0').attr('src', data['url']);
            jQuery('body').data('captcha.hash', [data['hash1'], data['hash2']]);
        }
    });
    return false;
});

event.originalEvent.isTrusted

Event 介面的 isTrusted 唯讀屬性為一個布林值,若事件物件是由使用者操作而產生,則 isTrusted 值為 true。若事件物件是由程式碼所建立、修改,或是透過 EventTarget.dispatchEvent() 來觸發,則 isTrusted 值為 false。

Event.isTrusted

The isTrusted read-only property of the Event interface is a Boolean that is truewhen the event was generated by a user action such as mouse click, and false when the event was scripted or invoked via dispatchEvent.

This new property is intended primarily for use by browser extensions, to determine if an event was dispatched by a script running in the main world or not.

如果單純使用之前的程式,會讓 $(“#TicketForm_checked”).attr(“name”); 取得的結果為:

s_false_undefined

然後會造成搶票失敗,帳號會被登出。解法:

其實大多的情況下 Selenium 讓 Event.isTrusted 的結果是 True,調整一下網頁裡事件的執行順序就解決了,拓元很難透過網頁檢測是使用者透過機器人來輔助買票,如果你寫的程式可以被驗測到,代表調整一下順序應該就可以跳過檢查。


上面對很多沒學過 javascript 的應該看起來像是火星文,建議先學一下簡單的 javascript 和 jQuery.

不能確定下次開演唱會時,Max的搶票程式是否可以使用,因為拓元定期會改版。

selenium 的程式有一些限制,必需先知道:

  • 1:會開出新的「視窗」,所以需要重新登入帳號。
  • 2:不能使用新的「分頁」,程式只能控制第一個分頁,所以建議你開2~3個 selenium,放在背景,而且先都登入好會員,如果第一個視窗在搶票時遇到圈圈狂轉,這時候趕快派2號視窗上場救援。
    (附註:其實是可以控制其他分頁,會比較麻煩一點,難度也會比較高,只處理第一個分頁會簡單很多。)
  • 3:想看我的程式可以匯錢給我,歡迎與我聯絡:[email protected]

自動關掉 alert 對話框的工具:
https://chrome.google.com/webstore/detail/alert-control/ofjjanaennfbgpccfpbghnmblpdblbef

如果有討人厭的alert 彈出式的javascript 語法,可以節省大約一秒的時間。

要下載 chrome extension 可以使用 “Get CRX” extension 來取得:
https://chrome.google.com/webstore/detail/get-crx/dijpllakibenlejkbajahncialkbdkjc


在 selenium 裡載入 extension 程式碼:

extension_path = Root_Dir + "webdriver/Alert_Control.crx"
chrome_options = webdriver.ChromeOptions()
chrome_options.add_extension(extension_path)
chromedriver_path =Root_Dir+ "webdriver/chromedriver"
driver = webdriver.Chrome(chrome_options=chrome_options, executable_path=chromedriver_path)

當人們在網站註冊或購物時,經常會出現圖像驗證碼的輸入要求,為了區分「真人」與「機器人」,圖片會出現線條及扭曲分隔開的文字,讓電腦程式的機器人難以辨識,目前Max的搶票機器人沒有辦法處理「驗證碼」的部份,需要人工去輸入,機器人幫忙處理其他的欄位的輸入。

人外有人,天外有天,驗證碼的部份應該也可以透過程式識別,識別也是有分難度的,簡單的驗證碼可以透過去切割圖片,再把切割好的圖片採用深度學習(Deep learning)的方式去訓練就可以有效提升識別率。難一點的驗證碼就是字元會變形、重疊還有雜訊。拓元網站是屬於中上等級,只有變形和重疊。

沒有推薦的實作方向,因為我也沒有研究過。


相關文章:

Max的拓元搶票機器人
http://max-everyday.com/2018/03/tixcraft-bot/

Max自動掛號機器人
http://max-everyday.com/2018/08/max-auto-reg-bot/

Max 學習 tkinter:
http://stackoverflow.max-everyday.com/tag/tkinter/

Max 學習 selenium:
http://stackoverflow.max-everyday.com/tag/selenium/

[Python] 如何取出字串中的選項
http://stackoverflow.max-everyday.com/2018/12/python-regular-expression-findall/
這篇文章目是透過程式取得「驗證問題」裡的選項,再用程式來自動把選項用來回答問題。

相關文章

60 comments

  1. 您好,請問能教我怎麼設定嗎 我看了您的YT影片 進來這看不太懂,

    謝謝您的回覆。

  2. 你好 不知道這個有沒有更好理解或操作的><
    因為完全沒有理工背景 全部都看不懂….

    1. 可正常操作,目前卡在:「欲購票者,建議於節目開賣前25小時,完成加入會員及手機號碼驗證。(驗證通過24小時後,才可購票)」,要過 24小時後,才能做新的影片出來,Demo 如何在改版後自動買票。

      Your mobile phone number has been verified at 2018/05/04 09:41. You may start to purchase tickets at 2018/05/05 09:41.
      系統說,我明天才能買票。

  3. 流程基本上應能正常操作,但聽說使用程式/機器人實際登入後購票好像會被強制登出

    1. hello, 我目前是使用 MacOS 內建的 Python 2.7.10,理論上 Python3 應該也可以執行。

  4. 請問輸入完程式碼後要怎麼儲存才能讓他自動跑呢?
    我要邊輸入程式碼網頁才會跟著動耶 還是我哪裡做錯了嗎?

    1. 你的問題太難,我無法理解。要執行python 的腳本,就是在命令列模式下去輸入 python your-filename.py

  5. 您好,

    感謝您的教學,我有一個小疑問請教
    我設定好了一個Selenium腳本,不過寫法都只會執行一次
    如果我想要每點開一個新頁面就讓它執行一次腳本(抓element 自動典擊等等)
    請問這邊是加入一個time函數讓他去重複跑?
    或是有什麼方法 可以達成這個目標?

    1. 您的問題太抽像,看不懂。上次我搶安室的票中間有多一個步驟是要求客人多打入一串字,類似多問使用者一個問題。

  6. 您好:
    請問若是需要如購票驗證(進入選區域畫面之前) 這種狀況有辦法繞過嗎?
    謝謝您

    1. 可以寫程式去固定選取最前面的坐位,這個程式不難,需要跨frameset的寫法,我有描述在文章裡。通常需要手動去選位的活動,似乎訂票情況都不是很搶手。

  7. Max您好
    小弟自學Python之後也完成了類似的流程
    從登入到買票都ok,但最後按下確定購票時,果然被強制登出了(回覆中也有人提到)
    目前一直卡在最後一步,不知道拓元是怎麼判斷的
    是否方便提供您的程式碼讓我參考呢,感謝!

  8. Max您好
    小弟自學Python之後也寫好了類似的流程
    從登入到買票都ok,但最後按下確定購票時,果然被強制登出了(回覆中也有人提到)
    目前一直卡在最後一步
    是否方便提供您的程式碼讓我參考呢,感謝!

    1. 被登出的原因,只要看一下拓元的 javascript 就可以知道,是因為透過程式去觸發的事件較少,把 javascript 裡需要補足的事件補齊即可通過javascript的檢查。

  9. 不好意思 有個關於選區域的問題請要請教
    用seleuium 來寫的話
    如果我用像是 , get innerText的方式 去抓字元判斷
    但是這時候會發現他不支援中文編碼
    也就是很難用這個方式去鎖定區域選擇
    請問有較好的方式嗎? 或是如何讓他支援中文編碼?
    感謝~

  10. 你好:
    我在測試你給的範例Code時
    當我執行他會說:NameError: name ‘web’ is not defined
    請問這是什麼原因呢

    1. 因為「範例 2 號」 用的變數 web 沒有被定義,我已修改掉掉範例2號裡的變數名稱web,改成和範例1號使用同一個,這樣子就不會出錯了。

  11. 你好,根據你的範例,如果有很多個”立即訂購”按鈕,我要如何選擇指定的按鈕呢?(因為每個按鈕都是find_element_by_css_selector(‘.btn-next’)?)謝謝!

  12. 您好,想詢問程式購買需要多少呢?以及如購買後需修改內容可否直接幫忙呢?

    1. 文章裡有寫,是一次性服務,不確定未來官方改版後我還能夠找的到解法。

  13. 您好!請問如果搶票需要粉絲回答完問題正確以後,才能進入座位區選位,這個指令還適用嗎?謝謝!

    要粉絲為答問題例如:這位歌手的mv破億順序.隊長是誰之類的…

    1. 已重新修改範例2號,不能執行的原因是因為使用了 time.sleep(5) 卻沒有 import time, 解法是刪除 time 相關程式碼即可。

  14. 你好!使用python開網頁後進入訂票模式+驗證碼那邊都ok能順利運行
    但是想請教中間選擇區域該怎麼由上往下去挑選可以進入的做點擊
    可以給一點點提示嗎QQ

    1. 不知道什麼是「全網」,這個機器人只能客製化「特定」的網頁。

    1. 沒有提供代搶票的服務,因為沒搶到票,不好意思跟你拿錢。萬一跟你拿了錢又沒搶到票,可能被說是我偷懶沒做事還收錢。與其有爭議,多一事不如少一事。

  15. 請教一下 在選取特定的票種的張數,該如何去思考呢?能給點提示嗎?目前卡在這邊 感謝MAX大的教學

    1. 拓元只有一個下拉框去選張數。我猜測你問的應該是Kktix. 使用 For 迴圈一行一行取值出來判斷即可完成。

  16. 你好 請問現在自動同意可用的方法是什麼?
    attr prop trigger都被過濾了

      1. 試過用$(‘#TicketForm_agree’).click();
        發現執行之後還是會變成s_false_undefined
        我用的是tampermonkey 會不會是這個影響?

  17. 你好,請問這個BOT是通用的嗎?

    在不同網站購票,只需要更改相2網址跟設定就好了?

    還有請問價錢是多少?

    1. 不是通用,原理是要處理網站的HTML,需要一個一個網頁做「客製化」。價格我私下用email通知你。

發佈回覆給「PC」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *