├── .gitignore ├── requirements.txt ├── dbc_api_python3 ├── 2x4.png ├── test.jpg ├── banner.jpg ├── test2.jpg ├── new_recaptcha_token_image.py ├── new_recaptcha_coordinates.py ├── new_hcaptcha.py ├── new_funcaptcha.py ├── new_recaptcha_token_v3.py ├── new_recaptcha_image_group.py ├── deathbycaptcha.py └── readme.html ├── LICENSE ├── README.cn.rst ├── utils.py ├── README.rst └── verify.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | 2captcha-python 3 | selenium 4 | Appium-Python-Client 5 | 2captcha-python 6 | -------------------------------------------------------------------------------- /dbc_api_python3/2x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilerjee/captcha-solver-on-android/HEAD/dbc_api_python3/2x4.png -------------------------------------------------------------------------------- /dbc_api_python3/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilerjee/captcha-solver-on-android/HEAD/dbc_api_python3/test.jpg -------------------------------------------------------------------------------- /dbc_api_python3/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilerjee/captcha-solver-on-android/HEAD/dbc_api_python3/banner.jpg -------------------------------------------------------------------------------- /dbc_api_python3/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lilerjee/captcha-solver-on-android/HEAD/dbc_api_python3/test2.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Liler Jee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dbc_api_python3/new_recaptcha_token_image.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | import json 3 | # Put your DBC account username and password here. 4 | username = "username" 5 | password = "password" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "" 9 | 10 | # Put the proxy and reCaptcha token data 11 | Captcha_dict = { 12 | 'proxy': 'http://user:password@127.0.0.1:1234', 13 | 'proxytype': 'HTTP', 14 | 'googlekey': '6Lc2fhwTAAAAAGatXTzFYfvlQMI2T7B6ji8UVV_f', 15 | 'pageurl': 'http://google.com'} 16 | 17 | # Create a json string 18 | json_Captcha = json.dumps(Captcha_dict) 19 | 20 | client = deathbycaptcha.SocketClient(username, password, authtoken) 21 | # to use http client client = deathbycaptcha.HttpClient(username, password) 22 | # client = deathbycaptcha.HttpClient(username, password, authtoken) 23 | 24 | try: 25 | balance = client.get_balance() 26 | print(balance) 27 | 28 | # Put your CAPTCHA type and Json payload here: 29 | captcha = client.decode(type=4, token_params=json_Captcha) 30 | if captcha: 31 | # The CAPTCHA was solved; captcha["captcha"] item holds its 32 | # numeric ID, and captcha["text"] item its list of "coordinates". 33 | print ("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 34 | 35 | if '': # check if the CAPTCHA was incorrectly solved 36 | client.report(captcha["captcha"]) 37 | except deathbycaptcha.AccessDeniedException: 38 | # Access to DBC API denied, check your credentials and/or balance 39 | print ("error: Access to DBC API denied," + 40 | "check your credentials and/or balance") 41 | -------------------------------------------------------------------------------- /dbc_api_python3/new_recaptcha_coordinates.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | 3 | # Put your DBC account username and password here. 4 | username = "noborderz" 5 | password = r"/+eQm@>;Q:Td8?MA" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "" 9 | captcha_file = 'test_traffic_light.png' # image 10 | # captcha_file = 'test_bus.png' # image 11 | # captcha_file = 'test_vehicles.png' # image 12 | captcha_file = './captcha_small.png' # image 13 | captcha_file = './captcha_puzzle.png' # image 14 | captcha_file = '../FunCaptcha_small.png' # image 15 | 16 | # client = deathbycaptcha.SocketClient(username, password) 17 | #to use http client 18 | client = deathbycaptcha.HttpClient(username, password) 19 | 20 | 21 | try: 22 | balance = client.get_balance() 23 | print(balance) 24 | 25 | # Put your CAPTCHA file name or file-like object, and optional 26 | # solving timeout (in seconds) here: 27 | captcha = client.decode(captcha_file, type=2, timeout=60) 28 | if captcha: 29 | # The CAPTCHA was solved; captcha["captcha"] item holds its 30 | # numeric ID, and captcha["text"] item its list of "coordinates". 31 | print ("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 32 | # print(type(captcha["text"])) 33 | 34 | if '': # check if the CAPTCHA was incorrectly solved 35 | client.report(captcha["captcha"]) 36 | else: 37 | print(captcha) 38 | except deathbycaptcha.AccessDeniedException: 39 | # Access to DBC API denied, check your credentials and/or balance 40 | print ("error: Access to DBC API denied, check your credentials and/or balance") 41 | -------------------------------------------------------------------------------- /dbc_api_python3/new_hcaptcha.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | import json 3 | # Put your DBC account username and password here. 4 | username = "username" 5 | password = "password" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "authtoken" 9 | 10 | # 162.212.173.198:80:bypass:bypass123 11 | # Put the proxy and reCaptcha token data 12 | Captcha_dict = { 13 | 'proxy': 'http://user:password@127.0.0.1:1234', 14 | 'proxytype': 'HTTP', 15 | 'sitekey': '56489210-0c02-58c0-00e5-1763b63dc9d4', 16 | 'pageurl': 'https://clientdemo.demo.com/demo-page'} 17 | 18 | # Create a json string 19 | json_Captcha = json.dumps(Captcha_dict) 20 | 21 | # client = deathbycaptcha.SocketClient(username, password, authtoken) 22 | # to use http client client = deathbycaptcha.HttpClient(username, password) 23 | client = deathbycaptcha.HttpClient(username, password, authtoken) 24 | 25 | try: 26 | balance = client.get_balance() 27 | print(balance) 28 | 29 | # Put your CAPTCHA type and Json payload here: 30 | captcha = client.decode(type=7, hcaptcha_params=json_Captcha) 31 | if captcha: 32 | # The CAPTCHA was solved; captcha["captcha"] item holds its 33 | # numeric ID, and captcha["text"] item its list of "coordinates". 34 | print("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 35 | 36 | if '': # check if the CAPTCHA was incorrectly solved 37 | client.report(captcha["captcha"]) 38 | except deathbycaptcha.AccessDeniedException: 39 | # Access to DBC API denied, check your credentials and/or balance 40 | print("error: Access to DBC API denied," + 41 | "check your credentials and/or balance") 42 | -------------------------------------------------------------------------------- /dbc_api_python3/new_funcaptcha.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | import json 3 | # Put your DBC account username and password here. 4 | username = "username" 5 | password = "password" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "authtoken" 9 | 10 | # 162.212.173.198:80:bypass:bypass123 11 | # Put the proxy and reCaptcha token data 12 | Captcha_dict = { 13 | 'proxy': 'http://user:password@127.0.0.1:1234', 14 | 'proxytype': 'HTTP', 15 | 'publickey': '029EF0D3-41DE-03E1-6971-466539B47725', 16 | 'pageurl': 'https://clientdemo.demo.com/demo-page'} 17 | 18 | # Create a json string 19 | json_Captcha = json.dumps(Captcha_dict) 20 | 21 | # client = deathbycaptcha.SocketClient(username, password, authtoken) 22 | # to use http client client = deathbycaptcha.HttpClient(username, password) 23 | client = deathbycaptcha.HttpClient(username, password, authtoken) 24 | 25 | try: 26 | balance = client.get_balance() 27 | print(balance) 28 | 29 | # Put your CAPTCHA type and Json payload here: 30 | captcha = client.decode(type=6, funcaptcha_params=json_Captcha) 31 | if captcha: 32 | # The CAPTCHA was solved; captcha["captcha"] item holds its 33 | # numeric ID, and captcha["text"] item its list of "coordinates". 34 | print ("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 35 | 36 | if '': # check if the CAPTCHA was incorrectly solved 37 | client.report(captcha["captcha"]) 38 | except deathbycaptcha.AccessDeniedException: 39 | # Access to DBC API denied, check your credentials and/or balance 40 | print ("error: Access to DBC API denied," + 41 | "check your credentials and/or balance") 42 | -------------------------------------------------------------------------------- /dbc_api_python3/new_recaptcha_token_v3.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | import json 3 | # Put your DBC account username and password here. 4 | username = "" 5 | password = "" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "authtoken" 9 | 10 | # Put the proxy and reCaptcha token data 11 | 12 | Captcha_dict = { 13 | # 'proxy': 'http://user:password@127.0.0.1:1234', 14 | # 'proxytype': 'HTTP', 15 | 'googlekey': '6LdyC2cUAAAAACGuDKpXeDorzUDWXmdqeg-xy696', 16 | 'pageurl': 'https://recaptchav3.demo.com/scores.php', 17 | 'action': "examples/v3scores", 18 | 'min_score': "0.3"} 19 | 20 | 21 | # Create a json string 22 | json_Captcha = json.dumps(Captcha_dict) 23 | 24 | # client = deathbycaptcha.SocketClient(username, password, authtoken) 25 | # to use http client client = deathbycaptcha.HttpClient(username, password) 26 | client = deathbycaptcha.HttpClient(username, password, authtoken) 27 | 28 | try: 29 | balance = client.get_balance() 30 | print(balance) 31 | 32 | # Put your CAPTCHA type and Json payload here: 33 | captcha = client.decode(type=5, token_params=json_Captcha) 34 | if captcha: 35 | # The CAPTCHA was solved; captcha["captcha"] item holds its 36 | # numeric ID, and captcha["text"] item its list of "coordinates". 37 | print ("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 38 | 39 | if '': # check if the CAPTCHA was incorrectly solved 40 | client.report(captcha["captcha"]) 41 | except deathbycaptcha.AccessDeniedException: 42 | # Access to DBC API denied, check your credentials and/or balance 43 | print ("error: Access to DBC API denied," + 44 | "check your credentials and/or balance") 45 | -------------------------------------------------------------------------------- /dbc_api_python3/new_recaptcha_image_group.py: -------------------------------------------------------------------------------- 1 | import deathbycaptcha 2 | 3 | # Put your DBC account username and password here. 4 | username = "jrbp1972" 5 | password = "Password001" 6 | # you can use authtoken instead of user/password combination 7 | # activate and get the authtoken from DBC users panel 8 | authtoken = "" 9 | captcha_file = "test2.jpg" # image 10 | banner = "banner.jpg" # image banner 11 | banner_text = "select all pizza:" # banner text 12 | 13 | # client = deathbycaptcha.SocketClient(username, password, authtoken) 14 | #to use http client use: 15 | client = deathbycaptcha.HttpClient(username, password, authtoken) 16 | 17 | 18 | try: 19 | balance = client.get_balance() 20 | print(balance) 21 | 22 | # Put your CAPTCHA file name or file-like object, and optional 23 | # solving timeout (in seconds) here: 24 | captcha = client.decode( 25 | captcha_file, type=3, banner=banner, banner_text=banner_text) 26 | #you can supply optional `grid` argument to decode() call, with a 27 | #string like 3x3 or 2x4, defining what grid individual images were located at 28 | #example: 29 | #captcha = client.decode( 30 | # captcha_file, type=3, banner=banner, banner_text=banner_text, grid="2x4") 31 | #see 2x4.png example image to have an idea what that images look like 32 | #If you wont supply `grid` argument, dbc will attempt to autodetect the grid 33 | 34 | if captcha: 35 | # The CAPTCHA was solved; captcha["captcha"] item holds its 36 | # numeric ID, and captcha["text"] is a json-like list of the index for each image that should be clicked. 37 | print ("CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])) 38 | 39 | if '': # check if the CAPTCHA was incorrectly solved 40 | client.report(captcha["captcha"]) 41 | except deathbycaptcha.AccessDeniedException: 42 | # Access to DBC API denied, check your credentials and/or balance 43 | print ("error: Access to DBC API denied, check your credentials and/or balance") 44 | -------------------------------------------------------------------------------- /README.cn.rst: -------------------------------------------------------------------------------- 1 | 简介 2 | ==== 3 | 4 | 这是个运行在Android平台上的用来解析CAPTCHA人机识别系统(reCAPTCHA/funCAPTCHA)的客户端软件, 5 | 这个客户端使用了其他网站提供的CAPTCHA人机识别系统服务(例如Deathbycaptcha, 2captcha). 6 | 7 | 这个客户端是从我创建的Twitter自动化机器人系统里面提取出来的,没有测试这个提取出来的结果, 8 | 但是我可以保证这个客户端可以解析两种不同的CAPTCHA人机识别系统:reCAPTCHA和funCAPTCHA. 9 | 10 | 你可以基于这个客户端来创建其他类型的CAPTCHA人机系统解析方案客户端. 11 | 12 | 原因 13 | ==== 14 | 15 | 为什么创建运行Android平台上的用来解析CAPTCHA人机识别系统的客户端呢? 16 | 17 | CAPTCHA人机识别系统服务提供者一般提供了用户友好的基于浏览器的客户端及其相关API, 18 | 但是基本上没有提供用户友好的基于Android的客户端,仅仅提供了相关的API,用户不得不自己 19 | 创建可以直接使用的用户友好的基于相关API的客户端。 20 | 21 | 所以为了解析在Android上面的CAPTCHA人机识别系统, 我创建了用户友好的基于相关API的客户端。 22 | 23 | 原理 24 | ==== 25 | 26 | 这个客户端把CAPTCHA图片截图下来,然后把这个截图缩小以满足CAPTCHA服务提供者的大小限制, 27 | 然后通过API把这个缩小的截图发送到CAPTCHA服务器。获取到服务器返回的结果(正确图片的坐标)后, 28 | 客户端就会从原始的坐标计算正确的坐标, 然后点击正确的CAPTCHA图片。 29 | 30 | 这个客户端包含两层,第一层是CAPTCHA人机识别系统解析服务提供者的API客户端,例如 ``DeathByCaptchaUI``, 31 | 用来与解析服务器进行通信与获取解析后的结果;第二层是CAPTCHA人机识别系统的解析API客户端,例如 32 | ``FuncaptchaAndroidUI``, 用来针对具体的CAPTCHA处理特定的解析逻辑。 33 | 34 | 用法 35 | ==== 36 | 37 | #. 安装依赖及CAPTCHA服务提供者提供的客户端:: 38 | 39 | pip install -r requirements.txt 40 | 41 | 如果你想用Deathbycaptcha提供的CAPTCHA解析服务,请按照下面去做: 42 | 43 | - 下载 `Death By Captcha API`__, 解压缩然后放到项目根目录下面(实际上已经存在于这个库里面了) 44 | 45 | __ https://static.deathbycaptcha.com/files/dbc_api_v4_6_3_python3.zip 46 | 47 | #. 在CAPTCHA服务提供者的页面上创建自己的用户名与密码,然后在文件 ``verify.py`` 里面修改它们 48 | (包括图片大小限制):: 49 | 50 | class TwoCaptchaAPI: 51 | image_restrict_size = 1024 * 100 # 100KB 52 | 53 | TWOCAPTCHA_API_KEY = '' 54 | 55 | 56 | class DeathByCaptchaUI: 57 | image_restrict_size = 1024 * 180 # 180KB 58 | 59 | DBC_USERNAME = '' 60 | DBC_PASSWORD = '' 61 | 62 | #. 创建或者选择CAPTCHA的 ``resolver`` (CAPTCHA解析服务提供者的API) 63 | (在脚本里面已经存在了两个resolver: ``DeathByCaptchaUI``, ``TwoCaptchaAPI``):: 64 | 65 | class CaptchaAndroidBaseUI: 66 | def __init__(self, driver, resolver=None, wait_timeout=wait_timeout): 67 | self.driver = driver 68 | if not resolver: 69 | self.resolver = DeathByCaptchaUI(timeout=self.client_timeout, 70 | client_type=self.client_type) 71 | # If you want to use 2captcha, uncomment the following and comment the above line 72 | # self.resolver = TwoCaptchaAPI() 73 | else: 74 | self.resolver = resolver 75 | 76 | #. 有可能需要调整一些元素的定位器(locator)与一些CAPTCHA解析算法 77 | 78 | 刚开始创建这个客户端是为了解析出现在Twitter里面的CAPTCHA人机识别系统, 所以有可能一些元素的定位器 79 | 与页面结构不一样,如果这个客户端不能工作,请根据特定的页面结构调整它们。 80 | 81 | 例如:: 82 | 83 | class FuncaptchaAndroidUI(CaptchaAndroidBaseUI): 84 | """User interface level API for resolving FunCaptcha on android""" 85 | # step1 86 | verify_first_page_frame_xpath = ( 87 | '//android.view.View[@resource-id="FunCaptcha"]') 88 | 89 | verify_heading_xpath = ( 90 | '//android.view.View[@resource-id="home_children_heading"]') 91 | 92 | #. 用下面的代码集成这个客户端到你的脚本里面:: 93 | 94 | from verify import RecaptchaAndroidUI, FuncaptchaAndroidUI 95 | from conf import RECAPTCHA_ALL_RETRY_TIMES, FUNCAPTCHA_ALL_RETRY_TIMES 96 | 97 | RECAPTCHA_ALL_RETRY_TIMES = 15 # the number of captcha images to resolve 98 | FUNCAPTCHA_ALL_RETRY_TIMES = 20 # the number of captcha images to resolve 99 | 100 | # resolve reCAPTCHA 101 | recaptcha = RecaptchaAndroidUI(self.app_driver) 102 | if recaptcha.is_captcha_first_page(): 103 | LOGGER.info('Resovling reCAPTCHA') 104 | if recaptcha.resolve_all_with_coordinates_api( 105 | all_resolve_retry_times=RECAPTCHA_ALL_RETRY_TIMES): 106 | LOGGER.info('reCAPTCHA is resolved') 107 | else: 108 | LOGGER.info('reCAPTCHA cannot be resolved') 109 | 110 | # resolve FunCaptcha 111 | funcaptcha = FuncaptchaAndroidUI(self.app_driver) 112 | if funcaptcha.is_captcha_first_page(): 113 | LOGGER.info('Resovling FunCaptcha') 114 | if funcaptcha.resolve_all_with_coordinates_api( 115 | all_resolve_retry_times=RECAPTCHA_ALL_RETRY_TIMES): 116 | LOGGER.info('FunCaptcha is resolved') 117 | else: 118 | LOGGER.info('FunCaptcha cannot be resolved') 119 | 120 | 许可证 121 | ====== 122 | 123 | MIT License 124 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import random 4 | import time 5 | import uuid 6 | 7 | from pathlib import Path 8 | from PIL import Image 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | def random_sleep(min_sleep_time=1, max_sleep_time=5): 14 | """ 15 | Random sleep 16 | :param min_sleep_time: Min Sleep time 17 | :param max_sleep_time: Max Sleeep time 18 | """ 19 | sleep_time = random.randint(min_sleep_time, max_sleep_time) 20 | LOGGER.debug(f'Random sleep: {sleep_time}') 21 | time.sleep(sleep_time) 22 | 23 | def get_random_file_name(min_len=10, max_len=20, suffix=''): 24 | return ''.join(random.choices(uuid.uuid4().hex, 25 | k=random.randrange(min_len, max_len))) + suffix 26 | 27 | def _add_suffix_name(fname, suffix='_small', repeate=False): 28 | fnames = fname.split('.') 29 | if len(fnames) == 1: 30 | if not repeate: 31 | return fname if fname.endswith(suffix) else (fname + suffix) 32 | else: 33 | return fname + suffix 34 | 35 | else: 36 | if not repeate: 37 | names = '.'.join(fnames[:-1]) 38 | return fname if names.endswith(suffix) else ( 39 | names + suffix + '.' + fnames[-1]) 40 | else: 41 | return '.'.join(fnames[:-1]) + suffix + '.' + fnames[-1] 42 | 43 | def resize_img(img_file, reduce_factor=1): 44 | # reduce the image's size 45 | img = Image.open(img_file) 46 | # LOGGER.debug(f'Original image size: {img.size}') 47 | # LOGGER.debug(f'Original file size: {os.path.getsize(img_file)}') 48 | # LOGGER.debug(f'Resize factor: {reduce_factor}') 49 | 50 | width = int(img.size[0] / reduce_factor) 51 | height = int(img.size[1] / reduce_factor) 52 | 53 | if isinstance(img_file, Path): 54 | img_file_path = str(img_file.absolute()) 55 | else: 56 | img_file_path = img_file 57 | 58 | small_img_file = _add_suffix_name(img_file_path) 59 | 60 | small_img = img.resize((width, height)) 61 | # small_img = img.resize(reduce_factor) 62 | small_img.save(small_img_file) 63 | # LOGGER.debug(f'Resized image size: {small_img.size}') 64 | # LOGGER.debug(f'Resized file size: {os.path.getsize(small_img_file)}') 65 | 66 | return small_img_file 67 | 68 | def restrict_image_size(img_file, reduce_factor, reduce_step, restrict_size): 69 | """Reduce the image file size to let it be less than restricting size""" 70 | img_file_size = os.path.getsize(img_file) 71 | 72 | if img_file_size <= restrict_size: 73 | reduced_img_file = img_file 74 | 75 | times = 0 76 | while img_file_size > restrict_size: 77 | reduce_factor += reduce_step 78 | reduced_img_file = resize_img(img_file, reduce_factor) 79 | img_file_size = os.path.getsize(reduced_img_file) 80 | times += 1 81 | 82 | LOGGER.debug(f'Reduced image file: {reduced_img_file}') 83 | LOGGER.debug(f'After {times} times of reducing, the image file size' 84 | f' {img_file_size} is less than {restrict_size}') 85 | return (reduced_img_file, reduce_factor) 86 | 87 | def reduce_img_size(img_file, reduce_factor=1): 88 | # reduce the image's size 89 | img = Image.open(img_file) 90 | LOGGER.info(f'Original image size: {img.size}') 91 | LOGGER.info(f'Original file size: {os.path.getsize(img_file)}') 92 | LOGGER.info(f'Reduce factor: {reduce_factor}') 93 | 94 | # width = int(img.size[0] // reduce_factor) 95 | # height = int(img.size[1] // reduce_factor) 96 | 97 | if isinstance(img_file, Path): 98 | img_file_path = str(img_file.absolute()) 99 | else: 100 | img_file_path = img_file 101 | 102 | small_img_file = _add_suffix_name(img_file_path) 103 | 104 | # small_img = img.resize((width, height), Image.ANTIALIAS) 105 | small_img = img.reduce(reduce_factor) 106 | small_img.save(small_img_file) 107 | LOGGER.info(f'Reduced image size: {small_img.size}') 108 | LOGGER.info(f'Reduced file size: {os.path.getsize(small_img_file)}') 109 | 110 | return small_img_file 111 | 112 | def get_absolute_path_str(path): 113 | if isinstance(path, Path): 114 | absolute_path = str(path.absolute()) 115 | elif isinstance(path, str): 116 | absolute_path = os.path.abspath(path) 117 | else: 118 | LOGGER.debug(f'Other type of path: {type(path)}') 119 | absolute_path = path 120 | 121 | # LOGGER.debug(f'Absolute path: "{absolute_path}" from "{path}"') 122 | return absolute_path 123 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `中文文档 <./README.cn.rst>`_ 2 | 3 | Introduction 4 | ============ 5 | 6 | This is the CAPTCHA(reCAPTCHA/funCAPTCHA) solving client on Android, which uses the CAPTCHA solving 7 | services provided by other websites(e.g. Deathbycaptcha, 2captcha). 8 | 9 | The client is extracted from the Twitter robot system created by me, and I don't test the extracted 10 | result, but I make sure the client can solve the two different CAPTCHAs: reCAPTCHA, and funCAPTCHA. 11 | 12 | You can create other CAPTCHA solving clients based on the client. 13 | 14 | Reason 15 | ====== 16 | 17 | Why did I create the CAPTCHA solving client on Android? 18 | 19 | The CAPTCHA solving service providers supply user-friendly browser-based client and the relevant 20 | APIs, but cannot provide user-friendly android-based client, just the relevant APIs. The users have 21 | to create the user-friendly client using the relevant APIs which can be used directly. 22 | 23 | So in order to solve the CAPTCHA problem on Android, I create the user-friendly client using the 24 | relevant APIs. 25 | 26 | Rationale 27 | ========= 28 | 29 | The client captures the image of CAPTCHA, then reduced the size of the image in order to meet the 30 | restriction of CAPTCHA server, and sends the reduced size image to the CAPTCHA server using its API. 31 | Since getting the result (the coordinates of right images) from the CAPTCHA server, the client 32 | calculate the right coordinates from the original ones, then click the right images. 33 | 34 | This client consists of two layers, and the first is the CAPTCHA service provider API client, 35 | e.g. ``DeathByCaptchaUI``, which is used to communicate with the CAPTCHA server and get the solving 36 | result from the server; the second is the CAPTCHA solving API client on Android, 37 | e.g. ``FuncaptchaAndroidUI``, which is used to deal with the particular logic of solving specific 38 | CAPTCHA on Android. 39 | 40 | Usage 41 | ===== 42 | 43 | #. Install the dependencies and CAPTCHA client provided by CAPTCHA solving services:: 44 | 45 | pip install -r requirements.txt 46 | 47 | If you want to use the CAPTCHA service by Deathbycaptcha, please do the following: 48 | 49 | - Download `Death By Captcha API`__, then unzip and put it into the root directory 50 | of the project. (This folder has existed in repository) 51 | 52 | __ https://static.deathbycaptcha.com/files/dbc_api_v4_6_3_python3.zip 53 | 54 | #. Create your username and password on webpage of CAPTCHA solving services, 55 | and modify them(including image size restriction) in script file ``verify.py``:: 56 | 57 | class TwoCaptchaAPI: 58 | image_restrict_size = 1024 * 100 # 100KB 59 | 60 | TWOCAPTCHA_API_KEY = '' 61 | 62 | 63 | class DeathByCaptchaUI: 64 | image_restrict_size = 1024 * 180 # 180KB 65 | 66 | DBC_USERNAME = '' 67 | DBC_PASSWORD = '' 68 | 69 | #. Create or select the ``resolver`` (CAPTCHA service provider API) for CAPTCHA 70 | (There are two resolver existing in the script: ``DeathByCaptchaUI``, and ``TwoCaptchaAPI``):: 71 | 72 | class CaptchaAndroidBaseUI: 73 | def __init__(self, driver, resolver=None, wait_timeout=wait_timeout): 74 | self.driver = driver 75 | if not resolver: 76 | self.resolver = DeathByCaptchaUI(timeout=self.client_timeout, 77 | client_type=self.client_type) 78 | # If you want to use 2captcha, uncomment the following and comment the above line 79 | # self.resolver = TwoCaptchaAPI() 80 | else: 81 | self.resolver = resolver 82 | 83 | #. Maybe need to adjust the locators or the algorithm of solving CAPTCHAs. 84 | 85 | The is the client created for CAPTCHAs which appear on Twitter. So maybe the locators of some 86 | elements are different or the page structures are different, If the client cannot work, please 87 | adjust them according the specific page structures. 88 | 89 | For example:: 90 | 91 | class FuncaptchaAndroidUI(CaptchaAndroidBaseUI): 92 | """User interface level API for resolving FunCaptcha on android""" 93 | # step1 94 | verify_first_page_frame_xpath = ( 95 | '//android.view.View[@resource-id="FunCaptcha"]') 96 | 97 | verify_heading_xpath = ( 98 | '//android.view.View[@resource-id="home_children_heading"]') 99 | 100 | 101 | #. Integrate the client into your script by using the following code:: 102 | 103 | from verify import RecaptchaAndroidUI, FuncaptchaAndroidUI 104 | from conf import RECAPTCHA_ALL_RETRY_TIMES, FUNCAPTCHA_ALL_RETRY_TIMES 105 | 106 | RECAPTCHA_ALL_RETRY_TIMES = 15 # the number of captcha images to resolve 107 | FUNCAPTCHA_ALL_RETRY_TIMES = 20 # the number of captcha images to resolve 108 | 109 | # resolve reCAPTCHA 110 | recaptcha = RecaptchaAndroidUI(self.app_driver) 111 | if recaptcha.is_captcha_first_page(): 112 | LOGGER.info('Resovling reCAPTCHA') 113 | if recaptcha.resolve_all_with_coordinates_api( 114 | all_resolve_retry_times=RECAPTCHA_ALL_RETRY_TIMES): 115 | LOGGER.info('reCAPTCHA is resolved') 116 | else: 117 | LOGGER.info('reCAPTCHA cannot be resolved') 118 | 119 | # resolve FunCaptcha 120 | funcaptcha = FuncaptchaAndroidUI(self.app_driver) 121 | if funcaptcha.is_captcha_first_page(): 122 | LOGGER.info('Resovling FunCaptcha') 123 | if funcaptcha.resolve_all_with_coordinates_api( 124 | all_resolve_retry_times=RECAPTCHA_ALL_RETRY_TIMES): 125 | LOGGER.info('FunCaptcha is resolved') 126 | else: 127 | LOGGER.info('FunCaptcha cannot be resolved') 128 | 129 | License 130 | ======= 131 | 132 | MIT License 133 | -------------------------------------------------------------------------------- /dbc_api_python3/deathbycaptcha.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Death by Captcha HTTP and socket API clients. 5 | 6 | There are two types of Death by Captcha (DBC hereinafter) API: HTTP and 7 | socket ones. Both offer the same functionalily, with the socket API 8 | sporting faster responses and using way less connections. 9 | 10 | To access the socket API, use SocketClient class; for the HTTP API, use 11 | HttpClient class. Both are thread-safe. SocketClient keeps a persistent 12 | connection opened and serializes all API requests sent through it, thus 13 | it is advised to keep a pool of them if you're script is heavily 14 | multithreaded. 15 | 16 | Both SocketClient and HttpClient give you the following methods: 17 | 18 | get_user() 19 | Returns your DBC account details as a dict with the following keys: 20 | 21 | "user": your account numeric ID; if login fails, it will be the only 22 | item with the value of 0; 23 | "rate": your CAPTCHA rate, i.e. how much you will be charged for one 24 | solved CAPTCHA in US cents; 25 | "balance": your DBC account balance in US cents; 26 | "is_banned": flag indicating whether your account is suspended or not. 27 | 28 | get_balance() 29 | Returns your DBC account balance in US cents. 30 | 31 | get_captcha(cid) 32 | Returns an uploaded CAPTCHA details as a dict with the following keys: 33 | 34 | "captcha": the CAPTCHA numeric ID; if no such CAPTCHAs found, it will 35 | be the only item with the value of 0; 36 | "text": the CAPTCHA text, if solved, otherwise None; 37 | "is_correct": flag indicating whether the CAPTCHA was solved correctly 38 | (DBC can detect that in rare cases). 39 | 40 | The only argument `cid` is the CAPTCHA numeric ID. 41 | 42 | get_text(cid) 43 | Returns an uploaded CAPTCHA text (None if not solved). The only argument 44 | `cid` is the CAPTCHA numeric ID. 45 | 46 | report(cid) 47 | Reports an incorrectly solved CAPTCHA. The only argument `cid` is the 48 | CAPTCHA numeric ID. Returns True on success, False otherwise. 49 | 50 | upload(captcha) 51 | Uploads a CAPTCHA. The only argument `captcha` can be either file-like 52 | object (any object with `read` method defined, actually, so StringIO 53 | will do), or CAPTCHA image file name. On successul upload you'll get 54 | the CAPTCHA details dict (see get_captcha() method). 55 | 56 | NOTE: AT THIS POINT THE UPLOADED CAPTCHA IS NOT SOLVED YET! You have 57 | to poll for its status periodically using get_captcha() or get_text() 58 | method until the CAPTCHA is solved and you get the text. 59 | 60 | decode(captcha, timeout=DEFAULT_TIMEOUT) 61 | A convenient method that uploads a CAPTCHA and polls for its status 62 | periodically, but no longer than `timeout` (defaults to 60 seconds). 63 | If solved, you'll get the CAPTCHA details dict (see get_captcha() 64 | method for details). See upload() method for details on `captcha` 65 | argument. 66 | 67 | Visit http://www.deathbycaptcha.com/user/api for updates. 68 | 69 | """ 70 | 71 | import base64 72 | import errno 73 | import imghdr 74 | import random 75 | import select 76 | import socket 77 | import sys 78 | import threading 79 | import time 80 | import requests 81 | try: 82 | from json import read as json_decode, write as json_encode 83 | except ImportError: 84 | try: 85 | from json import loads as json_decode, dumps as json_encode 86 | except ImportError: 87 | from simplejson import loads as json_decode, dumps as json_encode 88 | 89 | 90 | # API version and unique software ID 91 | API_VERSION = 'DBC/Python v4.6' 92 | 93 | # Default CAPTCHA timeout and decode() polling interval 94 | DEFAULT_TIMEOUT = 60 95 | DEFAULT_TOKEN_TIMEOUT = 120 96 | POLLS_INTERVAL = [1, 1, 2, 3, 2, 2, 3, 2, 2] 97 | DFLT_POLL_INTERVAL = 3 98 | 99 | # Base HTTP API url 100 | HTTP_BASE_URL = 'http://api.dbcapi.me/api' 101 | 102 | # Preferred HTTP API server's response content type, do not change 103 | HTTP_RESPONSE_TYPE = 'application/json' 104 | 105 | # Socket API server's host & ports range 106 | SOCKET_HOST = 'api.dbcapi.me' 107 | SOCKET_PORTS = list(range(8123, 8131)) 108 | 109 | 110 | def _load_image(captcha): 111 | if hasattr(captcha, 'read'): 112 | img = captcha.read() 113 | else: 114 | img = '' 115 | try: 116 | captcha_file = open(captcha, 'rb') 117 | except Exception: 118 | raise 119 | else: 120 | img = captcha_file.read() 121 | captcha_file.close() 122 | if not len(img): 123 | raise ValueError('CAPTCHA image is empty') 124 | elif imghdr.what(None, img) is None: 125 | raise TypeError('Unknown CAPTCHA image type') 126 | else: 127 | return img 128 | 129 | 130 | class AccessDeniedException(Exception): 131 | pass 132 | 133 | 134 | class Client(object): 135 | 136 | """Death by Captcha API Client.""" 137 | 138 | def __init__(self, username=None, password=None, authtoken=None): 139 | # self.is_verbose = True 140 | self.is_verbose = False 141 | self.userpwd = {'username': username, 'password': password} 142 | if authtoken: 143 | self.authtoken = {'authtoken': authtoken} 144 | else: 145 | self.authtoken = None 146 | 147 | def get_auth(self): 148 | 149 | if self.authtoken: 150 | return self.authtoken.copy() 151 | else: 152 | return self.userpwd.copy() 153 | 154 | def _log(self, cmd, msg=''): 155 | if self.is_verbose: 156 | print('%d %s %s' % (time.time(), cmd, msg.rstrip())) 157 | return self 158 | 159 | def close(self): 160 | pass 161 | 162 | def connect(self): 163 | pass 164 | 165 | def get_user(self): 166 | """Fetch user details -- ID, balance, rate and banned status.""" 167 | raise NotImplementedError() 168 | 169 | def get_balance(self): 170 | """Fetch user balance (in US cents).""" 171 | return self.get_user().get('balance') 172 | 173 | def get_captcha(self, cid): 174 | """Fetch a CAPTCHA details -- ID, text and correctness flag.""" 175 | raise NotImplementedError() 176 | 177 | def get_text(self, cid): 178 | """Fetch a CAPTCHA text.""" 179 | return self.get_captcha(cid).get('text') or None 180 | 181 | def report(self, cid): 182 | """Report a CAPTCHA as incorrectly solved.""" 183 | raise NotImplementedError() 184 | 185 | def upload(self, captcha): 186 | """Upload a CAPTCHA. 187 | 188 | Accepts file names and file-like objects. Returns CAPTCHA details 189 | dict on success. 190 | 191 | """ 192 | raise NotImplementedError() 193 | 194 | def decode(self, captcha=None, timeout=None, **kwargs): 195 | """ 196 | Try to solve a CAPTCHA. 197 | 198 | See Client.upload() for arguments details. 199 | 200 | Uploads a CAPTCHA, polls for its status periodically with arbitrary 201 | timeout (in seconds), returns CAPTCHA details if (correctly) solved. 202 | 203 | """ 204 | if not timeout: 205 | if not captcha: 206 | timeout = DEFAULT_TOKEN_TIMEOUT 207 | else: 208 | timeout = DEFAULT_TIMEOUT 209 | 210 | deadline = time.time() + (max(0, timeout) or DEFAULT_TIMEOUT) 211 | uploaded_captcha = self.upload(captcha, **kwargs) 212 | if uploaded_captcha: 213 | intvl_idx = 0 # POLL_INTERVAL index 214 | while deadline > time.time() and not uploaded_captcha.get('text'): 215 | intvl, intvl_idx = self._get_poll_interval(intvl_idx) 216 | time.sleep(intvl) 217 | uploaded_captcha = self.get_captcha(uploaded_captcha['captcha']) 218 | if (uploaded_captcha.get('text') and 219 | uploaded_captcha.get('is_correct')): 220 | return uploaded_captcha 221 | 222 | def _get_poll_interval(self, idx): 223 | """Returns poll interval and next index depending on index provided""" 224 | 225 | if len(POLLS_INTERVAL) > idx: 226 | intvl = POLLS_INTERVAL[idx] 227 | else: 228 | intvl = DFLT_POLL_INTERVAL 229 | idx += 1 230 | 231 | return intvl, idx 232 | 233 | 234 | class HttpClient(Client): 235 | 236 | """Death by Captcha HTTP API client.""" 237 | 238 | def __init__(self, *args): 239 | Client.__init__(self, *args) 240 | 241 | def _call(self, cmd, payload=None, headers=None, files=None): 242 | if headers is None: 243 | headers = {} 244 | if not payload: 245 | payload = {} 246 | headers['Accept'] = HTTP_RESPONSE_TYPE 247 | headers['User-Agent'] = API_VERSION 248 | self._log('SEND', '%s %d %s' % (cmd, len(payload), payload)) 249 | if payload: 250 | response = requests.post(HTTP_BASE_URL + '/' + cmd.strip('/'), 251 | data=payload, 252 | files=files, 253 | headers=headers) 254 | else: 255 | response = requests.get( 256 | HTTP_BASE_URL + '/' + cmd.strip('/'), headers=headers) 257 | status = response.status_code 258 | if 403 == status: 259 | raise AccessDeniedException('Access denied, please check' 260 | ' your credentials and/or balance') 261 | elif status in (400, 413): 262 | raise ValueError("CAPTCHA was rejected by the service, check" 263 | " if it's a valid image") 264 | elif 503 == status: 265 | raise OverflowError("CAPTCHA was rejected due to service" 266 | " overload, try again later") 267 | if not response.ok: 268 | raise RuntimeError('Invalid API response') 269 | self._log('RECV', '%d %s' % (len(response.text), response.text)) 270 | try: 271 | return json_decode(response.text) 272 | except Exception: 273 | raise RuntimeError('Invalid API response') 274 | return {} 275 | 276 | def get_user(self): 277 | return self._call('user', self.get_auth()) or {'user': 0} 278 | 279 | def get_captcha(self, cid): 280 | return self._call('captcha/%d' % cid) or {'captcha': 0} 281 | 282 | def report(self, cid): 283 | return not self._call('captcha/%d/report' % cid, 284 | self.get_auth()).get('is_correct') 285 | 286 | def upload(self, captcha=None, **kwargs): 287 | banner = kwargs.get('banner', '') 288 | data = self.get_auth() 289 | data.update(kwargs) 290 | files ={} 291 | if captcha: 292 | files = {"captchafile": _load_image(captcha)} 293 | if banner: 294 | files.update({"banner": _load_image(banner)}) 295 | response = self._call('captcha', payload=data, files=files) or {} 296 | if response.get('captcha'): 297 | return response 298 | 299 | 300 | class SocketClient(Client): 301 | 302 | """Death by Captcha socket API client.""" 303 | 304 | TERMINATOR = bytes('\r\n', 'ascii') 305 | 306 | def __init__(self, *args): 307 | Client.__init__(self, *args) 308 | self.socket_lock = threading.Lock() 309 | self.socket = None 310 | 311 | def close(self): 312 | if self.socket: 313 | self._log('CLOSE') 314 | try: 315 | self.socket.shutdown(socket.SHUT_RDWR) 316 | except socket.error: 317 | pass 318 | finally: 319 | self.socket.close() 320 | self.socket = None 321 | 322 | def connect(self): 323 | if not self.socket: 324 | self._log('CONN') 325 | host = (socket.gethostbyname(SOCKET_HOST), 326 | random.choice(SOCKET_PORTS)) 327 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 328 | self.socket.settimeout(0) 329 | try: 330 | self.socket.connect(host) 331 | except socket.error as err: 332 | if (err.errno not in 333 | (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS)): 334 | self.close() 335 | raise err 336 | return self.socket 337 | 338 | def __del__(self): 339 | self.close() 340 | 341 | def _sendrecv(self, sock, buf): 342 | self._log('SEND', buf) 343 | fds = [sock] 344 | buf = bytes(buf, 'utf-8') + self.TERMINATOR 345 | response = bytes() 346 | intvl_idx = 0 347 | while True: 348 | intvl, intvl_idx = self._get_poll_interval(intvl_idx) 349 | rds, wrs, exs = select.select((not buf and fds) or [], 350 | (buf and fds) or [], 351 | fds, 352 | intvl) 353 | if exs: 354 | raise IOError('select() failed') 355 | try: 356 | if wrs: 357 | while buf: 358 | buf = buf[wrs[0].send(buf):] 359 | elif rds: 360 | while True: 361 | s = rds[0].recv(256) 362 | if not s: 363 | raise IOError('recv(): connection lost') 364 | else: 365 | response += s 366 | except socket.error as err: 367 | if (err.errno not in 368 | (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS)): 369 | raise err 370 | if response.endswith(self.TERMINATOR): 371 | self._log('RECV', response) 372 | return str(response.rstrip(self.TERMINATOR), 'utf-8') 373 | raise IOError('send/recv timed out') 374 | 375 | def _call(self, cmd, data=None): 376 | if data is None: 377 | data = {} 378 | data['cmd'] = cmd 379 | data['version'] = API_VERSION 380 | request = json_encode(data) 381 | 382 | response = None 383 | for i in range(2): 384 | if not self.socket and cmd != 'login': 385 | self._call('login', self.get_auth()) 386 | self.socket_lock.acquire() 387 | try: 388 | sock = self.connect() 389 | response = self._sendrecv(sock, request) 390 | except IOError as err: 391 | sys.stderr.write(str(err) + "\n") 392 | self.close() 393 | except socket.error as err: 394 | sys.stderr.write(str(err) + "\n") 395 | self.close() 396 | raise IOError('Connection refused') 397 | else: 398 | break 399 | finally: 400 | self.socket_lock.release() 401 | 402 | if response is None: 403 | raise IOError('Connection lost timed out during API request') 404 | 405 | try: 406 | response = json_decode(response) 407 | except Exception: 408 | raise RuntimeError('Invalid API response') 409 | 410 | if not response.get('error'): 411 | return response 412 | 413 | error = response['error'] 414 | if error in ('not-logged-in', 'invalid-credentials'): 415 | raise AccessDeniedException('Access denied, check your credentials') 416 | elif 'banned' == error: 417 | raise AccessDeniedException('Access denied, account is suspended') 418 | elif 'insufficient-funds' == error: 419 | raise AccessDeniedException( 420 | 'CAPTCHA was rejected due to low balance') 421 | elif 'invalid-captcha' == error: 422 | raise ValueError('CAPTCHA is not a valid image') 423 | elif 'service-overload' == error: 424 | raise OverflowError( 425 | 'CAPTCHA was rejected due to service overload, try again later') 426 | else: 427 | self.socket_lock.acquire() 428 | self.close() 429 | self.socket_lock.release() 430 | raise RuntimeError('API server error occured: %s' % error) 431 | 432 | def get_user(self): 433 | return self._call('user') or {'user': 0} 434 | 435 | def get_captcha(self, cid): 436 | return self._call('captcha', {'captcha': cid}) or {'captcha': 0} 437 | 438 | def upload(self, captcha=None, **kwargs): 439 | data = {} 440 | if captcha: 441 | data['captcha'] = str(base64.b64encode(_load_image(captcha)), 'ascii') 442 | if kwargs: 443 | banner = kwargs.get('banner', '') 444 | if banner: 445 | kwargs['banner'] = str(base64.b64encode( 446 | _load_image(banner)), 'ascii') 447 | data.update(kwargs) 448 | response = self._call('upload', data) 449 | if response.get('captcha'): 450 | uploaded_captcha = dict( 451 | (k, response.get(k)) 452 | for k in ('captcha', 'text', 'is_correct') 453 | ) 454 | if not uploaded_captcha['text']: 455 | uploaded_captcha['text'] = None 456 | return uploaded_captcha 457 | 458 | def report(self, cid): 459 | return not self._call('report', {'captcha': cid}).get('is_correct') 460 | 461 | 462 | if '__main__' == __name__: 463 | # Put your DBC username & password here: 464 | print(len(sys.argv)) 465 | print(sys.argv) 466 | if len(sys.argv) == 2: 467 | client = HttpClient(None, None, sys.argv[1]) 468 | # client = SocketClient(None, None, sys.argv[1]) 469 | elif len(sys.argv) >= 3: 470 | # client = HttpClient(sys.argv[1], sys.argv[2], None) 471 | client = SocketClient(sys.argv[1], sys.argv[2], None) 472 | # else: 473 | # raise AccessDeniedException('Access denied, please check' 474 | # ' your credentials and/or balance') 475 | client.is_verbose = True 476 | 477 | print('Your balance is %s US cents' % client.get_balance()) 478 | 479 | for fn in sys.argv[3:]: 480 | try: 481 | # Put your CAPTCHA image file name or file-like object, and optional 482 | # solving timeout (in seconds) here: 483 | captcha = client.decode(fn, DEFAULT_TIMEOUT) 484 | except Exception as err: 485 | sys.stderr.write('Failed uploading CAPTCHA: %s\n' % (err, )) 486 | captcha = None 487 | 488 | if captcha: 489 | print('CAPTCHA %d solved: %s' % ( 490 | captcha['captcha'], captcha['text'])) 491 | 492 | # Report as incorrectly solved if needed. Make sure the CAPTCHA was 493 | # in fact incorrectly solved! 494 | # try: 495 | # client.report(captcha['captcha']) 496 | # except Exception, err: 497 | # sys.stderr.write('Failed reporting CAPTCHA: %s\n' % (err, )) 498 | -------------------------------------------------------------------------------- /dbc_api_python3/readme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | DeathByCaptcha API Clients 4 | 5 |

DeathByCaptcha API Clients

6 | 7 |

Introduction

8 |

DeathByCaptcha offers APIs of two types — HTTP and socket-based, 9 | with the latter being recommended for having faster responses and overall 10 | better performance. Switching between different APIs is usually as easy as 11 | changing the client class and/or package name, the interface stays the same.

12 |

When using the socket API, please make sure that outgoing TCP traffic to 13 | api.dbcapi.me to the ports range 8123–8130 14 | is not blocked on your side.

15 | 16 |

How to Use DBC API Clients

17 | 18 |

Thread-safety notes

19 |

.NET, Java and Python clients are thread-safe, 20 | means it is perfectly fine to share a client between multiple threads (although 21 | in a heavily multithreaded applications it is a better idea to keep a pool of 22 | clients).

23 |

PHP itself is not multithreaded so the clients are not thread-safe.

24 |

Perl clients are not thread-safe at this moment, use a client instance 25 | per thread.

26 | 27 |

Common Clients' Interface

28 |

All the clients have to be instantiated with two string arguments: your 29 | DeathByCaptcha account's username and password.

30 |

All the clients provide a few methods to handle your CAPTCHAs and your 31 | DBC account. Below you will find those methods' short summary summary and 32 | signatures in pseudo-code. Check the example scripts and the clients' source 33 | code for more details.

34 | 35 |

Upload()

36 |

Uploads a CAPTCHA to the DBC service for solving, returns uploaded CAPTCHA details on success, NULL otherwise. Here are the signatures in pseudo-code:

37 |
38 |
.NET
39 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Upload(byte[] imageData)
40 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Upload(Stream imageStream)
41 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Upload(string imageFileName)
42 |
Java
43 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.upload(byte[] imageData)
44 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.upload(InputStream imageStream)
45 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.upload(File imageFile)
46 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.upload(String imageFileName)
47 |
Perl
48 |
hash DeathByCaptcha.Client->upload(string $imageFileName)
49 |
PHP
50 |
array DeathByCaptcha_Client->upload(resource $imageFile)
51 |
array DeathByCaptcha_Client->upload(string $imageFileName)
52 |
Python
53 |
dict deathbycaptcha.Client.upload(file imageFile)
54 |
dict deathbycaptcha.Client.upload(str imageFileName)
55 |
56 | 57 |

GetCaptcha()

58 |

Fetches uploaded CAPTCHA details, returns NULL on failures.

59 |
60 |
.NET
61 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.GetCaptcha(int captchaId)
62 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.GetCaptcha(DeathByCaptcha.Captcha captcha)
63 |
Java
64 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.getCaptcha(int captchaId)
65 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.getCaptcha(com.DeathByCaptcha.Captcha captcha)
66 |
Perl
67 |
hash DeathByCaptcha.Client->getCaptcha(int $captchaId)
68 |
PHP
69 |
array DeathByCaptcha_Client->get_captcha(int $captchaId)
70 |
Python
71 |
dict deathbycaptcha.Client.get_captcha(dict imageFileName)
72 |
73 | 74 |

Report()

75 |

Reports incorrectly solved CAPTCHA for refund, returns true on success, false otherwise.

76 |

Please make sure the CAPTCHA you're reporting was in fact incorrectly solved, do not just report them thoughtlessly, or else you'll be flagged as abuser and banned.

77 |
78 |
.NET
79 |
bool DeathByCaptcha.Client.Report(int captchaId)
80 |
bool DeathByCaptcha.Client.Report(DeathByCaptcha.Captcha captcha)
81 |
Java
82 |
boolean com.DeathByCaptcha.Client.report(int captchaId)
83 |
boolean com.DeathByCaptcha.Client.report(com.DeathByCaptcha.Captcha captcha)
84 |
Perl
85 |
bool DeathByCaptcha.Client->report(int $captchaId)
86 |
PHP
87 |
bool DeathByCaptcha.Client->report(int $captchaId)
88 |
Python
89 |
bool deathbycaptcha.Client.report(int captchaId)
90 |
91 | 92 |

Decode()

93 |

This method uploads a CAPTCHA, then polls for its status until it's solved or times out; returns solved CAPTCHA details on success, NULL otherwise.

94 |
95 |
.NET
96 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Decode(byte[] imageData, int timeout)
97 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Decode(Stream imageStream, int timeout)
98 |
DeathByCaptcha.Captcha DeathByCaptcha.Client.Decode(string imageFileName, int timeout)
99 |
Java
100 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.decode(byte[] imageData, int timeout)
101 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.decode(InputStream imageStream, int timeout)
102 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.decode(File imageFile, int timeout)
103 |
com.DeathByCaptcha.Captcha com.DeathByCaptcha.Client.decode(string imageFileName, int timeout)
104 |
Perl
105 |
hash DeathByCaptcha.Client->decode(string $imageFileName, int $timeout)
106 |
PHP
107 |
array DeathByCaptcha.Client->decode(resource $imageFile, int $timeout)
108 |
array DeathByCaptcha.Client->decode(string $imageFileName, int $timeout)
109 |
Python
110 |
dict deathbycaptcha.Client.decode(file imageFile, int timeout)
111 |
dict deathbycaptcha.Client.decode(str imageFileName, int timeout)
112 |
113 | 114 |

GetBalance()

115 |

Fetches your current DBC credit balance (in US cents).

116 |
117 |
.NET
118 |
double DeathByCaptcha.Client.GetBalance()
119 |
Java
120 |
double com.DeathByCaptcha.Client.getBalance()
121 |
Perl
122 |
float DeathByCaptcha.Client->getBalance()
123 |
PHP
124 |
float DeathByCaptcha.Client->get_balance()
125 |
Python
126 |
float deathbycaptcha.Client.get_balance()
127 |
128 | 129 |

CAPTCHA objects/details hashes

130 |

.NET and Java clients wrap CAPTCHA details in DeathByCaptcha.Captcha and com.DeathByCaptcha.Captcha objects respectively, exposing CAPTCHA details through the following properties and methods:

131 |
    132 |
  • CAPTCHA numeric ID as integer Id (.NET) and id (Java) properties;
  • 133 |
  • CAPTCHA text as string Text (.NET) and text (Java) properties;
  • 134 |
  • a flag showing whether the CAPTCHA was uploaded, as boolean Uploaded property (.NET) and isUploaded() (Java) method;
  • 135 |
  • a flag showing whether the CAPTCHA was solved, as boolean Solved property (.NET) and isSolved() (Java) method;
  • 136 |
  • a flag showing whether the CAPTCHA was solved correctly, as boolean Correct property (.NET) and isCorrect() (Java) method.
  • 137 |
138 |

Clients in other languages use simple hashes (dictionaries, associative arrays etc.) to store CAPTCHA details, keeping numeric IDs under "captcha" key, CAPTCHA text under "text" key, and the correctness flag under "is_correct" key.

139 | 140 | 141 |

Examples

142 |

Below you can find a few DBC API clients' usage examples.

143 | 144 |

C#

145 |
146 |     using DeathByCaptcha;
147 | 
148 |     /* Put your DeathByCaptcha account username and password here.
149 |        Use HttpClient for HTTP API. */
150 |     Client client = (Client)new SocketClient(username, password);
151 |     try {
152 |         double balance = client.GetBalance();
153 | 
154 |         /* Put your CAPTCHA file name, or file object, or arbitrary stream,
155 |            or an array of bytes, and optional solving timeout (in seconds) here: */
156 |         Captcha captcha = client.Decode(captchaFileName, timeout);
157 |         if (null != captcha) {
158 |             /* The CAPTCHA was solved; captcha.Id property holds its numeric ID,
159 |                and captcha.Text holds its text. */
160 |             Console.WriteLine("CAPTCHA {0} solved: {1}", captcha.Id, captcha.Text);
161 | 
162 |             if (/* check if the CAPTCHA was incorrectly solved */) {
163 |                 client.Report(captcha);
164 |             }
165 |         }
166 |     } catch (AccessDeniedException e) {
167 |         /* Access to DBC API denied, check your credentials and/or balance */
168 |     }
169 | 
170 | 171 |

Java

172 |
173 |     import com.DeathByCaptcha.AccessDeniedException;
174 |     import com.DeathByCaptcha.Captcha;
175 |     import com.DeathByCaptcha.Client;
176 |     import com.DeathByCaptcha.SocketClient;
177 |     import com.DeathByCaptcha.HttpClient;
178 | 
179 |     /* Put your DeathByCaptcha account username and password here.
180 |        Use HttpClient for HTTP API. */
181 |     Client client = (Client)new SocketClient(username, password);
182 |     try {
183 |         double balance = client.getBalance();
184 | 
185 |         /* Put your CAPTCHA file name, or file object, or arbitrary input stream,
186 |            or an array of bytes, and optional solving timeout (in seconds) here: */
187 |         Captcha captcha = client.decode(captchaFileName, timeout);
188 |         if (null != captcha) {
189 |             /* The CAPTCHA was solved; captcha.id property holds its numeric ID,
190 |                and captcha.text holds its text. */
191 |             System.out.println("CAPTCHA " + captcha.id + " solved: " + captcha.text);
192 | 
193 |             if (/* check if the CAPTCHA was incorrectly solved */) {
194 |                 client.report(captcha);
195 |             }
196 |         }
197 |     } catch (AccessDeniedException e) {
198 |         /* Access to DBC API denied, check your credentials and/or balance */
199 |     }
200 | 
201 | 202 |

PHP

203 |
204 |     require_once "deathbycaptcha.php";
205 | 
206 |     /* Put your DBC account username and password here.
207 |        Use DeathByCaptcha_HttpClient for HTTP API. */
208 |     $client = new DeathByCaptcha_SocketClient($username, $password);
209 |     try {
210 |         $balance = $client->get_balance();
211 | 
212 |         /* Put your CAPTCHA file name or opened file handler, and optional
213 |            solving timeout (in seconds) here: */
214 |         $captcha = $client->decode($captcha_file_name, $timeout);
215 |         if ($captcha) {
216 |             /* The CAPTCHA was solved; captcha["captcha"] item holds its
217 |                numeric ID, and captcha["text"] item its text. */
218 |             echo "CAPTCHA {$captcha["captcha"]} solved: {$captcha["text"]}";
219 | 
220 |             if (/* check if the CAPTCHA was incorrectly solved */) {
221 |                 $client->report($captcha["captcha"]);
222 |             }
223 |         }
224 |     } catch (DeathByCaptcha_AccessDeniedException) {
225 |         /* Access to DBC API denied, check your credentials and/or balance */
226 |     }
227 | 
228 | 229 |

Python

230 |
231 |     import deathbycaptcha
232 | 
233 |     # Put your DBC account username and password here.
234 |     # Use deathbycaptcha.HttpClient for HTTP API.
235 |     client = deathbycaptcha.SocketClient(username, password)
236 |     try:
237 |         balance = client.get_balance()
238 | 
239 |         # Put your CAPTCHA file name or file-like object, and optional
240 |         # solving timeout (in seconds) here:
241 |         captcha = client.decode(captcha_file_name, timeout)
242 |         if captcha:
243 |             # The CAPTCHA was solved; captcha["captcha"] item holds its
244 |             # numeric ID, and captcha["text"] item its text.
245 |             print "CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])
246 | 
247 |             if ...:  # check if the CAPTCHA was incorrectly solved
248 |                 client.report(captcha["captcha"])
249 |     except deathbycaptcha.AccessDeniedException:
250 |         # Access to DBC API denied, check your credentials and/or balance
251 | 
252 | 253 |
254 |

New Recaptcha API support

255 |

What's "new reCAPTCHA/noCAPTCHA"?

256 |

They're new reCAPTCHA challenges that typically require the user to identify and click on certain images. They're not to be confused with traditional word/number reCAPTCHAs (those have no images).

257 | 258 |

For your convinience, we implemented support for New Recaptcha API. If your software works with it, and supports minimal configuration, you should be able to decode captchas using New Recaptcha API in no time.

259 |

We provide two different types of New Recaptcha API:

260 |
    261 |
  • Coordinates API: Provided a screenshot, the API returns a group of coordinates to click.
  • 262 |
  • Image Group API: Provided a group of (base64-encoded) images, the API returns the indexes of the images to click.
  • 263 |
264 | 265 |

Coordinates API FAQ:

266 |
267 |
What's the Coordinates API URL?
268 |
269 |

To use the Coordinates API you will have to send a HTTP POST Request to http://api.dbcapi.me/api/captcha

270 |
271 |
272 | 273 |
274 |
What are the POST parameters for the Coordinates API?
275 |
276 |

277 |

    278 |
  • username: Your DBC account username
  • 279 |
  • password: Your DBC account password
  • 280 |
  • captchafile: a Base64 encoded or Multipart file contents with a valid New Recaptcha screenshot
  • 281 |
  • type=2: Type 2 specifies this is a New Recaptcha Coordinates API
  • 282 |
283 |

284 |
285 | 286 |
What's the response from the Coordinates API?
287 |
288 |

captcha: id of the provided captcha, if the text field is null, you will have to pool the url http://api.dbcapi.me/api/captcha/captcha_id until it becomes available

289 |

is_correct:(0 or 1) specifying if the captcha was marked as incorrect or unreadable

290 |

text: a json-like nested list, with all the coordinates (x, y) to click relative to the image, for example: 291 |

292 |               [[23.21, 82.11]]
293 |           
294 | where the X coordinate is 23.21 and the Y coordinate is 82.11 295 |

296 |

297 |
298 |
299 | 300 |

Image Group API FAQ:

301 |
302 |
What's the Image Group API URL?
303 |
304 |

To use the Image Group API you will have to send a HTTP POST Request to http://api.dbcapi.me/api/captcha

305 |
306 |
307 | 308 |
309 |
What are the POST parameters for the Image Group API?
310 |
311 |

312 |

    313 |
  • username: Your DBC account username
  • 314 |
  • password: Your DBC account password
  • 315 |
  • captchafile: the Base64 encoded file contents with a valid New Recaptcha screenshot. 316 | You must send each image in a single "captchafile" parameter. The order you send them matters
  • 317 |
  • banner: the Base64 encoded banner image (the example image that appears on the upper right)
  • 318 |
  • banner_text: the banner text (the text that appears on the upper left)
  • 319 |
  • type=3: Type 3 specifies this is a New Recaptcha Image Group API
  • 320 |
  • grid: Optional grid parameter specifies what grid individual images in captcha are aligned to (string, width+"x"+height, Ex.: "2x4", if images aligned to 4 rows with 2 images in each. If not supplied, dbc will attempt to autodetect the grid.
  • 321 |
322 |

323 |
324 | 325 |
What's the response from the Image Group API?
326 |
327 |

captcha: id of the provided captcha, if the text field is null, you will have to pool the url http://api.dbcapi.me/api/captcha/captcha_id until it becomes available

328 |

is_correct:(0 or 1) specifying if the captcha was marked as incorrect or unreadable

329 |

text: a json-like list of the index for each image that should be clicked. for example: 330 |

331 |               [1, 4, 6]
332 |           
333 | where the images that should be clicked are the first, the fourth and the six, counting from left to right and up to bottom 334 |

335 |

Examples 336 |

Python and Recaptcha Coordinates API

337 |
338 |                   import deathbycaptcha
339 | 
340 |                   # Put your DBC account username and password here.
341 |                   username = "user"  
342 |                   password = "password"
343 |                   captcha_file = 'test.jpg' # image
344 | 
345 |                   client = deathbycaptcha.SocketClient(username, password) 
346 |                   # to use http client use: client = deathbycaptcha.HttpClient(username, password)
347 | 
348 | 
349 |                   try:
350 |                       balance = client.get_balance()
351 | 
352 |                       # Put your CAPTCHA file name or file-like object, and optional
353 |                       # solving timeout (in seconds) here:
354 |                       captcha = client.decode(captcha_file, type=2)
355 |                       if captcha:
356 |                           # The CAPTCHA was solved; captcha["captcha"] item holds its
357 |                           # numeric ID, and captcha["text"] item its list of "coordinates".
358 |                           print "CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])
359 | 
360 |                           if '':  # check if the CAPTCHA was incorrectly solved
361 |                               client.report(captcha["captcha"])
362 |                   except deathbycaptcha.AccessDeniedException:
363 |                       # Access to DBC API denied, check your credentials and/or balance
364 |                       print "error: Access to DBC API denied, check your credentials and/or balance"
365 |               
366 |

Python and Recaptcha Image Group

367 |
368 |                   import deathbycaptcha
369 | 
370 |                   # Put your DBC account username and password here.
371 |                   username = "user"  
372 |                   password = "password"
373 |                   captcha_file = "test2.jpg"  # image
374 |                   banner = "banner.jpg"  # image banner
375 |                   banner_text = "select all pizza:"
376 | 
377 |                   #client = deathbycaptcha.SocketClient(username, password) 
378 |                   client = deathbycaptcha.HttpClient(username, password)
379 |                   # to use http client use: client = deathbycaptcha.HttpClient(username, password)
380 | 
381 | 
382 |                   try:
383 |                       balance = client.get_balance()
384 | 
385 |                       # Put your CAPTCHA file name or file-like object, and optional
386 |                       # solving timeout (in seconds) here:
387 |                       captcha = client.decode(
388 |                           captcha_file, type=3, banner=banner, banner_text=banner_text)
389 |                       #you can supply optional `grid` argument to decode() call, with a 
390 |                       #string like 3x3 or 2x4, defining what grid individual images were located at
391 |                       #example: 
392 |                       #captcha = client.decode(
393 |                       #    captcha_file, type=3, banner=banner, banner_text=banner_text, grid="2x4")
394 |                       #see 2x4.png example image to have an idea what that images look like
395 |                       #If you wont supply `grid` argument, dbc will attempt to autodetect the grid
396 |                       if captcha:
397 |                           # The CAPTCHA was solved; captcha["captcha"] item holds its
398 |                           # numeric ID, and captcha["text"] is a json-like list of the index for each image that should be clicked.
399 |                           print "CAPTCHA %s solved: %s" % (captcha["captcha"], captcha["text"])
400 | 
401 |                           if '':  # check if the CAPTCHA was incorrectly solved
402 |                               client.report(captcha["captcha"])
403 |                   except deathbycaptcha.AccessDeniedException:
404 |                       # Access to DBC API denied, check your credentials and/or balance
405 |                       print "error: Access to DBC API denied, check your credentials and/or balance"
406 |               
407 | 408 | 409 | 410 |

PHP and Recaptcha Coordinates API

411 |
412 |                   /**
413 |                    * Death by Captcha PHP API recaptcha coordinates usage example
414 |                    *
415 |                    * @package DBCAPI
416 |                    * @subpackage PHP
417 |                    */
418 | 
419 |                   /**
420 |                    * DBC API clients
421 |                    */
422 |                   require_once 'deathbycaptcha.php';
423 | 
424 |                   // Put your DBC username & password here.
425 |                   $username = "username";
426 |                   $password = "password";
427 |                   $captcha_filename = "test.jpg";  # your captchafile
428 |                   $extra = ['type'=>2];   # extra parameters in an array
429 |                   // Use DeathByCaptcha_HttpClient() class if you want to use HTTP API.
430 |                   $client = new DeathByCaptcha_SocketClient($username, $password);
431 |                   $client->is_verbose = true;
432 | 
433 |                   echo "Your balance is {$client->balance} US cents\n";
434 | 
435 |                   // Put your CAPTCHA image file name, file resource, or vector of bytes,
436 |                   // and optional solving timeout (in seconds) here; you'll get CAPTCHA
437 |                   // details array on success.
438 |                   if ($captcha = $client->decode($captcha_filename, $extra)) {
439 |                       echo "CAPTCHA {$captcha['captcha']} uploaded\n";
440 | 
441 |                       sleep(DeathByCaptcha_Client::DEFAULT_TIMEOUT);
442 | 
443 |                           // Poll for CAPTCHA coordinates:
444 |                           if ($text = $client->get_text($captcha['captcha'])) {
445 |                               echo "CAPTCHA {$captcha['captcha']} solved: {$text}\n";
446 | 
447 |                               // Report an incorrectly solved CAPTCHA.
448 |                               // Make sure the CAPTCHA was in fact incorrectly solved!
449 |                               //$client->report($captcha['captcha']);
450 |                           }
451 |                       }
452 |               
453 |

PHP and Recaptcha Image Group

454 |
455 |                   /**
456 |                    * Death by Captcha PHP API recaptcha coordinates usage example
457 |                    *
458 |                    * @package DBCAPI
459 |                    * @subpackage PHP
460 |                    */
461 | 
462 |                   /**
463 |                    * DBC API clients
464 |                    */
465 |                   require_once 'deathbycaptcha.php';
466 | 
467 |                   // Put your DBC username & password here.
468 |                   $username = "username";
469 |                   $password = "password";
470 |                   $captcha_filename = "test2.jpg";   # your captchafile
471 |                   // extra parameters in an array
472 |                   $extra = [
473 |                       'type'=>3,
474 |                       'banner'=> 'banner.jpg',  # banner img
475 |                       'banner_text'=> "select all pizza:"  # banner text 
476 |                        #'grid' => "3x2" #optional paramater for specifying what grid 
477 |                                         #images are aligned to. 
478 |                                         #If ommitted, dbc would try to autodetect the grid.
479 |                   ];
480 |                   // Use DeathByCaptcha_HttpClient() class if you want to use HTTP API.
481 |                   $client = new DeathByCaptcha_SocketClient($username, $password);
482 |                   $client->is_verbose = true;
483 | 
484 |                   echo "Your balance is {$client->balance} US cents\n";
485 | 
486 |                   if ($captcha = $client->decode($captcha_filename, $extra)) {
487 |                       echo "CAPTCHA {$captcha['captcha']} uploaded\n";
488 | 
489 |                       sleep(DeathByCaptcha_Client::DEFAULT_TIMEOUT);
490 | 
491 |                           // Poll for CAPTCHA indexes:
492 |                           if ($text = $client->get_text($captcha['captcha'])) {
493 |                               echo "CAPTCHA {$captcha['captcha']} solved: {$text}\n";
494 | 
495 |                               // Report an incorrectly solved CAPTCHA.
496 |                               // Make sure the CAPTCHA was in fact incorrectly solved!
497 |                               //$client->report($captcha['captcha']);
498 |                           }
499 |                       }
500 |               
501 |

502 |
503 |
504 |
505 | 506 | 507 | 508 | -------------------------------------------------------------------------------- /verify.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | import time 4 | import random 5 | import logging 6 | import twocaptcha 7 | 8 | from appium.webdriver.common.touch_action import TouchAction 9 | from selenium.common.exceptions import NoSuchElementException, TimeoutException 10 | from selenium.webdriver.support.ui import WebDriverWait 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.common.by import By 13 | from pathlib import Path 14 | from PIL import Image 15 | from io import BytesIO 16 | from dbc_api_python3 import deathbycaptcha 17 | from twocaptcha import TwoCaptcha 18 | 19 | from utils import reduce_img_size, random_sleep, get_absolute_path_str 20 | from utils import resize_img, restrict_image_size, get_random_file_name 21 | from utils import _add_suffix_name 22 | 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | CAPTCHA_IMAGE_DIR = Path(__file__).parent / 'temp' 26 | 27 | class CaptchaTooManyRetryException(Exception): 28 | pass 29 | 30 | class CaptchaErrorTooManyRetryException(Exception): 31 | pass 32 | 33 | class TwoCaptchaAPI: 34 | """ 35 | User interface level API to resolve sorts of captcha using APIs by 2captcha.com 36 | 37 | API url: https://2captcha.com/2captcha-api 38 | 39 | Python client: https://github.com/2captcha/2captcha-python 40 | 41 | - **New Recaptcha API** 42 | 43 | **What's "new reCAPTCHA/noCAPTCHA"?** 44 | 45 | They're new reCAPTCHA challenges that typically require the user to 46 | identify and click on certain images. They're not to be confused with 47 | traditional word/number reCAPTCHAs (those have no images). 48 | 49 | Two different types of New Recaptcha API: 50 | 51 | - **Coordinates API**: Provided a screenshot, the API returns a group of 52 | coordinates to click. 53 | - **Image Group API**: Provided a group of (base64-encoded) images, 54 | the API returns the indexes of the images to click. 55 | 56 | **Image upload restrictions.** 57 | 58 | Image file size is limited to less than 100 KB. 59 | When the image will be encoded in base64, the size should be lower than 120 KB. 60 | The supported image formats are JPG, PNG, GIF and BMP. 61 | """ 62 | 63 | image_restrict_size = 1024 * 100 # 100KB 64 | 65 | TWOCAPTCHA_API_KEY = '' 66 | 67 | def __init__(self, api_key=TWOCAPTCHA_API_KEY, timeout=30): 68 | """ 69 | Two Captcha API 70 | :param api_key: API Key of 2Captcha 71 | :param timeout: Time to wait for response 72 | """ 73 | self.api_key = api_key 74 | self.client = None 75 | self.timeout = timeout 76 | self.client = TwoCaptcha(self.api_key, defaultTimeout=timeout) 77 | 78 | def get_balance(self): 79 | """ 80 | Returns balance of 2Captcha 81 | :return: Returns balance of 2Captcha 82 | """ 83 | balance = self.client.balance() 84 | LOGGER.info(f'The balance is {balance}') 85 | return balance 86 | 87 | def report_failure(self, cid, reason=''): 88 | """ 89 | It reports when captcha solved wrongly 90 | :param cid: captcha ID 91 | :param reason: Reason of failure 92 | """ 93 | if reason: 94 | LOGGER.debug(f'Report failed resolving for captcha: {cid},' 95 | f' because of {reason}') 96 | else: 97 | LOGGER.debug(f'Report failed resolving for captcha: {cid}') 98 | self.client.report(cid, correct=False) 99 | 100 | def report_success(self, cid, reason=''): 101 | """ 102 | Reports when captcha solved correctly 103 | :param cid: captcha ID 104 | :param reason: Reason of success 105 | """ 106 | if reason: 107 | LOGGER.debug(f'Report failed resolving for captcha: {cid}, because of {reason}') 108 | else: 109 | LOGGER.debug(f'Report failed resolving for captcha: {cid}') 110 | self.client.report(cid, correct=True) 111 | 112 | def resolve_recaptcha_with_coordinates_api(self, image_file, hint_text): 113 | """ 114 | Resolve New Recaptcha from the image file using coordinates API. 115 | :param image_file: It should be file path 116 | :param hint_text: Hint text to solve captcha 117 | """ 118 | real_coordinates = [] 119 | try: 120 | reduce_factor, b64_img = self.get_restricted_encoded_image(image_file) 121 | LOGGER.info(f'Captcha image reduce factor: {reduce_factor}') 122 | captcha = self.client.coordinates(b64_img, hintText=hint_text) 123 | 124 | if 'captchaId' in captcha: 125 | cid = captcha['captchaId'] 126 | code = captcha['code'] 127 | LOGGER.debug(f"CAPTCHA {cid} solved: {code}") 128 | coordinates = re.findall('x=(\d+),y=(\d+)', code) 129 | for x, y in coordinates: 130 | real_coordinates.append((int(int(x) * reduce_factor), 131 | int(int(y) * reduce_factor))) 132 | LOGGER.info(f'Real coordinates: {real_coordinates}') 133 | if not real_coordinates: 134 | self.report_failure(cid, reason="co-ordinates are not present") 135 | else: 136 | LOGGER.debug(f'CAPTCHA: {captcha}') 137 | except KeyboardInterrupt as e: 138 | raise e 139 | except twocaptcha.TimeoutException as e: 140 | LOGGER.debug(e) 141 | except Exception as e: 142 | LOGGER.debug(f'Error: {e} while solving captcha') 143 | random_sleep(5, 6) 144 | return real_coordinates 145 | 146 | @staticmethod 147 | def get_restricted_encoded_image(image_file, reduce_factor=1, reduce_step=0.125, 148 | max_img_size=100): 149 | """ 150 | It returns base64 format of Image with image size less than `max_img_size`KB 151 | :param image_file: Captcha image path 152 | :param reduce_factor: Currently reduced factor of original captcha image 153 | :param reduce_step: Reduce step of every time 154 | :param max_img_size: Max size of image in KB 155 | """ 156 | image_file = get_absolute_path_str(image_file) 157 | img = Image.open(image_file) 158 | buffer = BytesIO() 159 | img.save(buffer, format='PNG') 160 | 161 | while (buffer.getbuffer().nbytes / 1024) > max_img_size - 1: 162 | reduce_factor += reduce_step 163 | width = int(img.width // reduce_factor) 164 | height = int(img.height // reduce_factor) 165 | 166 | reduced_img = img.resize((width, height)) 167 | buffer.seek(0) 168 | buffer.truncate(0) 169 | reduced_img.save(buffer, format='PNG') 170 | LOGGER.info(f'Image file reduced to {buffer.getbuffer().nbytes / 1024}kB') 171 | b64_img = base64.b64encode(buffer.getvalue()).decode() 172 | return reduce_factor, b64_img 173 | 174 | class DeathByCaptchaUI: 175 | """ 176 | User interface level API to resolve sorts of captcha using APIs by deathbycaptcha.com 177 | 178 | API url: https://deathbycaptcha.com/api 179 | 180 | - **New Recaptcha API** 181 | 182 | **What's "new reCAPTCHA/noCAPTCHA"?** 183 | 184 | They're new reCAPTCHA challenges that typically require the user to 185 | identify and click on certain images. They're not to be confused with 186 | traditional word/number reCAPTCHAs (those have no images). 187 | 188 | Two different types of New Recaptcha API: 189 | 190 | - **Coordinates API**: Provided a screenshot, the API returns a group of 191 | coordinates to click. 192 | - **Image Group API**: Provided a group of (base64-encoded) images, 193 | the API returns the indexes of the images to click. 194 | 195 | **Image upload restrictions.** 196 | 197 | Image file size is limited to less than 180 KB. 198 | When the image will be encoded in base64, the size should be lower than 120 KB. 199 | The supported image formats are JPG, PNG, GIF and BMP. 200 | """ 201 | 202 | image_restrict_size = 1024 * 180 # 180KB 203 | 204 | DBC_USERNAME = '' 205 | DBC_PASSWORD = '' 206 | 207 | def __init__(self, username=DBC_USERNAME, password=DBC_PASSWORD, 208 | authtoken=None, timeout=60, client_type='http'): 209 | self.username = username 210 | self.password = password 211 | self.authtoken = authtoken 212 | self.client = None 213 | self.timeout = timeout 214 | self.client_type = client_type 215 | 216 | def get_client(self, client_type='http'): 217 | client_type = str.lower(client_type) 218 | if client_type == 'http': 219 | self.client = deathbycaptcha.HttpClient(self.username, self.password, self.authtoken) 220 | elif client_type == 'socket': 221 | self.client = deathbycaptcha.SocketClient(self.username, self.password, self.authtoken) 222 | else: 223 | LOGGER.error('Wrong client type, just use "http" or "socket"') 224 | 225 | return self.client 226 | 227 | def get_same_client(self, client_type='http'): 228 | """Get the same client for all operations""" 229 | if not self.client: 230 | return self.get_client(client_type) 231 | return self.client 232 | 233 | def get_balance(self): 234 | self.get_client() 235 | balance = self.client.get_balance() 236 | LOGGER.info(f'The balance of "{self.username}": {balance}') 237 | return balance 238 | 239 | def report_failed_resolving(self, cid, reason=''): 240 | if reason: 241 | LOGGER.debug(f'Report failed resolving for captcha: {cid}, because of {reason}') 242 | else: 243 | LOGGER.debug(f'Report failed resolving for captcha: {cid}') 244 | self.client.report(cid) 245 | 246 | def resolve_newrecaptcha_with_coordinates_api(self, image_file, 247 | timeout=None, same_client=True, report_blank_list=False): 248 | """Resolve New Recaptcha from the image file using coordinates API. 249 | 250 | Coordinates API FAQ: 251 | 252 | What's the Coordinates API URL? 253 | To use the Coordinates API you will have to send a HTTP POST Request 254 | to http://api.dbcapi.me/api/captcha 255 | 256 | 257 | What are the POST parameters for the Coordinates API? 258 | These are:: 259 | 260 | username: Your DBC account username 261 | password: Your DBC account password 262 | captchafile: a Base64 encoded or Multipart file contents with 263 | a valid New Recaptcha screenshot type=2: Type 2 specifies this is 264 | a New Recaptcha Coordinates API 265 | 266 | What's the response from the Coordinates API? 267 | 268 | captcha: id of the provided captcha, if the text field is null, 269 | you will have to pool the url 270 | http://api.dbcapi.me/api/captcha/captcha_id until it becomes available 271 | 272 | is_correct:(0 or 1) specifying if the captcha was marked as 273 | incorrect or unreadable 274 | 275 | text: a json-like nested list, with all the coordinates (x, y) 276 | to click relative to the image, for example:: 277 | 278 | [[23.21, 82.11]] 279 | 280 | where the X coordinate is 23.21 and the Y coordinate is 82.11 281 | """ 282 | # get one client every time or just use the same client for all operations 283 | if same_client: 284 | self.get_same_client(client_type=self.client_type) 285 | else: 286 | self.get_client(client_type=self.client_type) 287 | 288 | if isinstance(image_file, Path) or isinstance(image_file, str): 289 | captcha_file = get_absolute_path_str(image_file) 290 | else: 291 | captcha_file = image_file 292 | 293 | if timeout is None: 294 | timeout = self.timeout 295 | 296 | # Put your CAPTCHA file name or file-like object, and optional 297 | # solving timeout (in seconds) here: 298 | captcha = self.client.decode(captcha_file, type=2, timeout=timeout) 299 | if captcha: 300 | # The CAPTCHA was solved; captcha["captcha"] item holds its 301 | # numeric ID, and captcha["text"] item its list of "coordinates". 302 | cid = captcha['captcha'] 303 | coordinates = captcha['text'] 304 | # LOGGER.debug(f"CAPTCHA: {captcha}") 305 | LOGGER.debug(f"CAPTCHA {cid} solved: {coordinates}") 306 | 307 | if not coordinates: # check if the CAPTCHA was incorrectly solved 308 | self.report_failed_resolving(cid) 309 | return False 310 | else: 311 | # the coordinates list is string 312 | result = eval(coordinates) 313 | 314 | # if the result is blank list 315 | if not result and report_blank_list: 316 | self.report_failed_resolving(cid, 317 | reason='blank list of result') 318 | return result 319 | else: 320 | LOGGER.debug(f'CAPTCHA: {captcha}') 321 | return None 322 | 323 | def resolve_newrecaptcha_ui_with_coordinates_api(self, image_file, 324 | reduce_factor=1, reduce_step=0.125, retry_times=2, timeout=None, 325 | report_blank_list=False): 326 | """User interface for resolving New Recaptcha using coordinates API 327 | 328 | :return: (coordinates, reduce_factor) or False 329 | """ 330 | # reduce image's size 331 | (image_file, last_reduce_factor) = restrict_image_size(image_file, 332 | reduce_factor, reduce_step, self.image_restrict_size) 333 | # if reduce_factor > 1: 334 | # # image_file = reduce_img_size(image_file, reduce_factor) 335 | # image_file = resize_img(image_file, reduce_factor) 336 | 337 | times = 0 338 | while times <= retry_times: 339 | try: 340 | LOGGER.info('Resolve captcha with coordinates API') 341 | coordinates = self.resolve_newrecaptcha_with_coordinates_api( 342 | image_file, timeout=timeout, 343 | report_blank_list=report_blank_list) 344 | if coordinates: 345 | return (coordinates, last_reduce_factor) 346 | elif (not report_blank_list) and (coordinates == []): 347 | return (coordinates, last_reduce_factor) 348 | else: 349 | times += 1 350 | if times <= retry_times: 351 | LOGGER.warning('Failed to resolve captcha,' 352 | f' then retry: {times}') 353 | continue 354 | except deathbycaptcha.AccessDeniedException as e: 355 | # Access to DBC API denied, check your credentials and/or balance 356 | # LOGGER.info("error: Access to DBC API denied, check your credentials and/or balance") 357 | 358 | LOGGER.error(e) 359 | # check if the balance is bellow zero 360 | balance = self.get_balance() 361 | if balance < 0: 362 | LOGGER.error('Balance is bellow zero, balance: {balance}') 363 | return False 364 | 365 | times += 1 366 | if times <= retry_times: 367 | LOGGER.debug(f'AccessDeniedException, then retry: {times}') 368 | 369 | # LOGGER.debug("Now reduce image's size and then retry") 370 | # reduce_factor += reduce_step 371 | # LOGGER.debug(f'Reduce factor added by {reduce_step}: ' 372 | # f'{reduce_factor}') 373 | # return self.resolve_newrecaptcha_ui_with_coordinates_api( 374 | # image_file, reduce_factor, reduce_step, retry_times, 375 | # timeout) 376 | except (OverflowError, RuntimeError) as e: 377 | # LOGGER.error(e) 378 | raise e 379 | except Exception as e: 380 | # LOGGER.exception(e) 381 | LOGGER.error(e) 382 | times += 1 383 | if times <= retry_times: 384 | LOGGER.debug(f'Other exception, then retry: {times}') 385 | 386 | return False 387 | 388 | class CaptchaAndroidBaseUI: 389 | """Base user interface level API for resolving Captcha on android""" 390 | wait_timeout = 5 391 | 392 | captcha_image_path = CAPTCHA_IMAGE_DIR 393 | captcha_image_file_name_suffix = '_captcha' 394 | captcha_image_file_extension = 'png' 395 | 396 | # client_type = 'socket' 397 | client_type = 'http' 398 | client_timeout = 30 399 | 400 | def __init__(self, driver, resolver=None, wait_timeout=wait_timeout): 401 | self.driver = driver 402 | if not resolver: 403 | self.resolver = DeathByCaptchaUI(timeout=self.client_timeout, 404 | client_type=self.client_type) 405 | # If you want to use 2captcha, uncomment the following and comment the above line 406 | # self.resolver = TwoCaptchaAPI() 407 | else: 408 | self.resolver = resolver 409 | 410 | self.wait_obj = WebDriverWait(self.driver, wait_timeout) 411 | 412 | def find_element(self, element, locator, locator_type=By.XPATH, page=None): 413 | """Waint for an element, then return it or None""" 414 | try: 415 | ele = self.wait_obj.until( 416 | EC.presence_of_element_located( 417 | (locator_type, locator))) 418 | if page: 419 | LOGGER.debug(f'Find the element "{element}" in the page "{page}"') 420 | else: 421 | LOGGER.debug(f'Find the element: {element}') 422 | return ele 423 | except (NoSuchElementException, TimeoutException) as e: 424 | if page: 425 | LOGGER.warning(f'Cannot find the element "{element}" in the page "{page}"') 426 | else: 427 | LOGGER.warning(f'Cannot find the element: {element}') 428 | 429 | def click_element(self, element, locator, locator_type=By.XPATH): 430 | """Find an element, then click and return it, or return None""" 431 | ele = self.find_element(element, locator, locator_type) 432 | if ele: 433 | ele.click() 434 | LOGGER.debug(f'Click the element: {element}') 435 | return ele 436 | 437 | def find_page(self, page, element, locator, locator_type=By.XPATH): 438 | """Find en element of a page, then return it or return None""" 439 | return self.find_element(element, locator, locator_type, page) 440 | 441 | def save_captcha_effect_img(self, captcha_img_locator, captcha_img_locator_type=By.XPATH, 442 | img_file=None): 443 | """Save the effective part of captcha image to a file 444 | 445 | This method is virtual for being overridden by the subclass. 446 | 447 | If file name is None, then create it with random name. 448 | """ 449 | LOGGER.debug('Use the base class method to save effective captcha image') 450 | return self.save_captcha_img(captcha_img_locator, captcha_img_locator_type, img_file) 451 | 452 | def save_captcha_img(self, captcha_img_locator, captcha_img_locator_type=By.XPATH, 453 | img_file=None): 454 | """Save the captcha image into a file. 455 | 456 | If no file, then create the random file. 457 | """ 458 | if not img_file: 459 | LOGGER.debug('Get random image file name, and save captcha image to the file') 460 | img_file_name = get_random_file_name(suffix=self.captcha_image_file_name_suffix) 461 | img_file = self.captcha_image_path / ( 462 | f'{img_file_name}.{self.captcha_image_file_extension}') 463 | img_file_path = get_absolute_path_str(img_file) 464 | # LOGGER.debug(f'CAPTCHA image file: {img_file_path}') 465 | 466 | LOGGER.debug(f'captcha image locator: {captcha_img_locator}') 467 | LOGGER.debug(f'captcha image locator type: {captcha_img_locator_type}') 468 | captcha_img = self.driver.find_element(by=captcha_img_locator_type, 469 | value=captcha_img_locator) 470 | 471 | if captcha_img.screenshot(img_file_path): 472 | LOGGER.debug(f'Saved CAPTCHA image to file: {img_file_path}') 473 | return img_file_path 474 | else: 475 | LOGGER.info(f'Cannot save the captcha image to the file: {img_file_path}') 476 | 477 | def crop_img(self, src_img_file, dest_img_file, box_size): 478 | # LOGGER.debug( 479 | # f'Crop the image "{src_img_file}" to "{dest_img_file}"') 480 | LOGGER.debug(f'Crop box size: {box_size}') 481 | 482 | with Image.open(src_img_file) as im: 483 | im_crop = im.crop(box_size) 484 | im_crop.save(dest_img_file) 485 | 486 | return True 487 | 488 | def crop_captcha_img_vertically(self, src_img_file, parent_element, from_element, to_element, 489 | dest_img_file=None, crop_file_suffix='_crop'): 490 | """Crop captcha image vertically from one element to another element""" 491 | # create destination image file if gaving no one 492 | if not dest_img_file: 493 | src_img_file_path = get_absolute_path_str(src_img_file) 494 | dest_img_file = _add_suffix_name(src_img_file_path, suffix=crop_file_suffix) 495 | 496 | parent_size = parent_element.size 497 | parent_location = parent_element.location 498 | from_size = from_element.size 499 | from_location = from_element.location 500 | to_size = to_element.size 501 | to_location = to_element.location 502 | 503 | # LOGGER.debug(f'parent_size: {parent_size},' 504 | # f' parent_location: {parent_location}') 505 | # LOGGER.debug(f'from_size: {from_size},' 506 | # f' from_location: {from_location}') 507 | # LOGGER.debug(f'to_size: {to_size},' 508 | # f' to_location: {to_location}') 509 | # 510 | # LOGGER.debug(f'parent bounds: {parent_element.get_attribute("bounds")}') 511 | # LOGGER.debug(f'from bounds: {from_element.get_attribute("bounds")}') 512 | # LOGGER.debug(f'to bounds: {to_element.get_attribute("bounds")}') 513 | 514 | from_left = from_location['x'] - parent_location['x'] 515 | to_left = to_location['x'] - parent_location['x'] 516 | left = min(from_left, to_left) 517 | 518 | from_right = from_left + from_size['width'] 519 | to_right = to_left + to_size['width'] 520 | right = max(from_right, to_right) 521 | 522 | upper = from_location['y'] - parent_location['y'] 523 | lower = to_location['y'] + to_size['height'] - parent_location['y'] 524 | # lower = upper + from_size['height'] + to_size['height'] 525 | 526 | LOGGER.debug(f'Crop captcha image from one element to another') 527 | if self.crop_img(src_img_file, dest_img_file, (left, upper, right, lower)): 528 | return dest_img_file 529 | 530 | def resolve_one_with_coordinates_api(self, captcha_img_locator, 531 | captcha_img_crop_start_locator, reduce_factor=1, 532 | reduce_step=0.125, retry_times=3, timeout=30, 533 | report_blank_list=True, captcha_img_locator_type=By.XPATH, 534 | captcha_img_crop_start_locator_type=By.XPATH, 535 | img_file=None, tap_interval=2, need_press=False): 536 | """Resolve one time for one Captcha image 537 | 538 | report_blank_list = True # FunCaptcha has no skip operation 539 | 540 | If the captcha image is not cropped, then captcha_img_locator is 541 | the same with captcha_img_crop_start_locator. 542 | 543 | Resolve successfully, return True; 544 | Resolve unsuccessfully, return False; 545 | Resolve successfully and no image to click, return None; 546 | """ 547 | LOGGER.info('Resolve one time for one captcha image') 548 | # save captcha image 549 | captcha_img_file = self.save_captcha_effect_img(captcha_img_locator, 550 | captcha_img_locator_type=captcha_img_locator_type, 551 | img_file=img_file) 552 | 553 | # get resolving results from the saved captcha image 554 | LOGGER.debug('Get resolving results from the saved captcha image') 555 | results = self.resolver.resolve_newrecaptcha_ui_with_coordinates_api( 556 | captcha_img_file, 557 | reduce_factor=reduce_factor, 558 | reduce_step=reduce_step, 559 | retry_times=retry_times, 560 | timeout=timeout, 561 | report_blank_list=report_blank_list) 562 | 563 | if not results: 564 | LOGGER.debug('Cannot resolve it') 565 | return False 566 | 567 | coordinates = results[0] 568 | last_reduce_factor = results[1] 569 | 570 | # No other images to click, just click skip button 571 | if (coordinates == []) and (not report_blank_list): 572 | LOGGER.debug('No images to click') 573 | return None 574 | 575 | # find the captcha image element 576 | LOGGER.info('Find the captcha image coordinates element, and get the location') 577 | LOGGER.debug(f'captcha_img_crop_start_locator: ' 578 | f'{captcha_img_crop_start_locator} ' 579 | f'captcha_img_crop_start_locator_type: ' 580 | f'{captcha_img_crop_start_locator_type}') 581 | captcha_img = self.driver.find_element( 582 | by=captcha_img_crop_start_locator_type, 583 | value=captcha_img_crop_start_locator) 584 | form_x = captcha_img.location['x'] 585 | form_y = captcha_img.location['y'] 586 | LOGGER.debug(f'form_x: {form_x}, form_y: {form_y}') 587 | 588 | LOGGER.info('Click the image with the resolving coordinates') 589 | LOGGER.debug(f'last_reduce_factor: {last_reduce_factor}') 590 | for x, y in coordinates: 591 | real_x = int(x * last_reduce_factor) + form_x 592 | real_y = int(y * last_reduce_factor) + form_y 593 | LOGGER.debug(f'Image coordinates: ({real_x}, {real_y})') 594 | 595 | action = TouchAction(self.driver) 596 | if need_press: 597 | LOGGER.debug('Press the image') 598 | # action.long_press(x=real_x, y=real_y).release().perform() 599 | action.press(x=real_x, y=real_y).release().perform() 600 | LOGGER.debug('Tap the image') 601 | action.tap(x=real_x, y=real_y).perform() 602 | 603 | if tap_interval > 0: 604 | LOGGER.debug(f'Tap interval: {tap_interval}') 605 | time.sleep(tap_interval) 606 | 607 | return True 608 | 609 | class FuncaptchaAndroidUI(CaptchaAndroidBaseUI): 610 | """User interface level API for resolving FunCaptcha on android""" 611 | # step1 612 | verify_first_page_frame_xpath = ( 613 | '//android.view.View[@resource-id="FunCaptcha"]') 614 | 615 | verify_heading_xpath = ( 616 | '//android.view.View[@resource-id="home_children_heading"]') 617 | verify_body_xpath = ( 618 | '//android.view.View[@resource-id="home_children_body"]') 619 | 620 | verify_button_xpath = ( 621 | '//android.view.View[@resource-id="home_children_button"]') 622 | verify_button_xpath1 = ( 623 | '//android.widget.Button[@resource-id="home_children_button"]') 624 | verify_button_xpath2 = ( 625 | '//android.widget.Button[@resource-id="verifyButton"]') 626 | verify_button_id = 'home_children_button' 627 | 628 | # step2 629 | # captcha_form_xpath = ( 630 | # '//android.view.View[@resource-id="CaptchaFrame"]') 631 | captcha_form_xpath = ( 632 | '//android.view.View[@resource-id="fc-iframe-wrap"]') 633 | 634 | reload_button_xpath = ('//android.view.View[@resource-id="fc-iframe-wrap"]' 635 | '/android.view.View/android.view.View/android.view.View[3]' 636 | '/android.view.View[2]/android.widget.Button[1]') 637 | reload_button_xpath1 = ('//android.webkit.WebView/android.webkit.WebView' 638 | '/android.view.View[2]/android.view.View/android.view.View/' 639 | 'android.view.View/android.view.View/android.view.View/' 640 | 'android.view.View/android.view.View[2]/android.view.View/' 641 | 'android.view.View[1]/android.widget.Button') 642 | 643 | captcha_img_form_xpath = ( 644 | '//android.view.View[@resource-id="game_children_wrapper"]') 645 | captcha_img_form_game_header_xpath = ( 646 | '//android.view.View[@resource-id="game-header"]') 647 | captcha_img_group_xapth = ( 648 | '//android.view.View[@resource-id="game_children_wrapper"]') 649 | 650 | try_again_button_xpath = ( 651 | '//android.view.View[@resource-id="wrong_children_button"]') 652 | 653 | # step2, except: Working, please wait 654 | check_loading_xpath = ( 655 | '//android.widget.Image[@resource-id="checking_children_loadingImg"]') 656 | 657 | wait_timeout = 5 658 | 659 | # captcha_image_path = PRJ_PATH / 'temp' 660 | captcha_image_file_name_suffix = '_funcaptcha' 661 | captcha_image_file_extension = 'png' 662 | 663 | def __init__(self, driver, resolver=None, wait_timeout=wait_timeout): 664 | super().__init__(driver, resolver, wait_timeout) 665 | 666 | def click_verify_button(self): 667 | ele = self.click_element('verify button by xpath', self.verify_button_xpath) 668 | if ele: 669 | return ele 670 | 671 | ele = self.click_element('verify button by xpath1', self.verify_button_xpath1) 672 | if ele: 673 | return ele 674 | 675 | ele = self.click_element('verify button by xpath2', self.verify_button_xpath2) 676 | if ele: 677 | return ele 678 | 679 | def click_reload_button(self): 680 | self.click_element('reload button', self.reload_button_xpath) 681 | 682 | def click_tryagain_button(self): 683 | self.click_element('try again button', self.try_again_button_xpath) 684 | 685 | # check if this is the FunCaptcha regardless of which captcha page 686 | def is_captcha_page(self): 687 | return self.find_page('FunCaptcha page', 'FunCaptcha frame', 688 | self.verify_first_page_frame_xpath) 689 | 690 | # check if this is the FunCaptcha from outside 691 | def is_captcha_first_page(self): 692 | if self.driver.find_elements_by_xpath(self.verify_heading_xpath): 693 | if self.is_captcha_page(): 694 | return True 695 | return False 696 | 697 | def is_in_captcha_img_page(self): 698 | return self.find_page('captcha image page by form xpath', 'captcha image form', 699 | self.captcha_img_form_xpath) or self.find_page( 700 | 'captcha image page by header xpath', 701 | 'image form header', 702 | self.captcha_img_form_game_header_xpath) 703 | 704 | def is_in_wrong_result_page(self): 705 | return self.find_page('wrong result page', 'try again button', self.try_again_button_xpath) 706 | 707 | def is_in_verify_button_page(self): 708 | return self.find_page('start verify page', 'verify button', self.verify_button_xpath) 709 | 710 | def is_in_check_loading_page(self): 711 | return self.find_page('Check loading page', 'loading image', self.check_loading_xpath) 712 | 713 | def resolve_all_with_coordinates_api(self, click_start=True, 714 | reduce_factor=1, reduce_step=0.125, retry_times=3, timeout=20, 715 | report_blank_list=True, img_file=None, tap_interval=2, 716 | need_press=False, all_resolve_retry_times=15): 717 | """Resolve all FunCaptcha images in one step""" 718 | LOGGER.debug(f'All retry times of resolving: {all_resolve_retry_times}') 719 | if click_start: 720 | LOGGER.info('Resolve all FunCaptcha images in one step') 721 | self.click_verify_button() 722 | 723 | img_page_flag = False 724 | # check if it is in the page of captcha image 725 | if self.is_in_captcha_img_page(): 726 | img_page_flag = True 727 | try: 728 | result = self.resolve_one_with_coordinates_api( 729 | captcha_img_locator=self.captcha_img_group_xapth, 730 | captcha_img_crop_start_locator=self.captcha_img_group_xapth, 731 | reduce_factor=reduce_factor, 732 | reduce_step=reduce_step, 733 | retry_times=retry_times, 734 | timeout=timeout, 735 | report_blank_list=report_blank_list, 736 | captcha_img_locator_type=By.XPATH, 737 | captcha_img_crop_start_locator_type=By.XPATH, 738 | img_file=img_file, 739 | tap_interval=tap_interval, 740 | need_press=need_press 741 | ) 742 | 743 | all_resolve_retry_times -= 1 744 | if all_resolve_retry_times <= 0: 745 | LOGGER.info('Retry times of resolving are zero, now exit it') 746 | raise CaptchaTooManyRetryException 747 | # return False 748 | except Exception as e: 749 | LOGGER.error(e) 750 | return False 751 | 752 | if img_page_flag and result is False: 753 | LOGGER.info('Cannot resolve it, then click reload button,' 754 | ' and play the game again') 755 | self.click_reload_button() # change captcha image 756 | return self.resolve_all_with_coordinates_api(click_start=False, 757 | all_resolve_retry_times=all_resolve_retry_times) 758 | 759 | # this condition doesn't exist, because it will report blank list 760 | if img_page_flag and result is None: 761 | LOGGER.warning('Result of resolving is None') 762 | self.click_verify_button() 763 | return self.resolve_all_with_coordinates_api(click_start=False, 764 | all_resolve_retry_times=all_resolve_retry_times) 765 | 766 | # if the game is still going, then continue to verify the captcha 767 | if self.is_in_captcha_img_page(): 768 | LOGGER.debug('The game is still going, then continue to play') 769 | # random_sleep(1, 3) # wait for new image 770 | return self.resolve_all_with_coordinates_api(click_start=False, 771 | all_resolve_retry_times=all_resolve_retry_times) 772 | 773 | # judge the result by the result page 774 | if self.is_in_wrong_result_page(): 775 | LOGGER.debug('Wrong resolving, then click try again button,' 776 | ' and play the game again') 777 | self.click_tryagain_button() 778 | return self.resolve_all_with_coordinates_api(click_start=True, 779 | all_resolve_retry_times=all_resolve_retry_times) 780 | 781 | # if it is in start verify page, then play it again 782 | if self.is_in_verify_button_page(): 783 | LOGGER.debug('Wrong somethings happend, then play it again') 784 | return self.resolve_all_with_coordinates_api(click_start=True, 785 | all_resolve_retry_times=all_resolve_retry_times) 786 | 787 | # if it is in checking loading page, then press reload button 788 | if self.is_in_check_loading_page(): 789 | LOGGER.debug('Checking loading') 790 | return self.resolve_all_with_coordinates_api(click_start=True, 791 | all_resolve_retry_times=all_resolve_retry_times) 792 | 793 | return True 794 | 795 | class RecaptchaAndroidUI(CaptchaAndroidBaseUI): 796 | """User interface level API for resolving reCaptcha on android""" 797 | # step1 798 | verify_first_page_frame_xpath = ( 799 | '//android.view.View[@resource-id="recaptcha_element"]') 800 | 801 | verify_heading_xpath = ( 802 | '//android.view.View[@resource-id="home_children_heading"]') 803 | verify_body_xpath = ( 804 | '//android.view.View[@resource-id="home_children_body"]') 805 | 806 | # verify_button_xpath = ( 807 | # '//android.view.View[@resource-id="home_children_button"]') 808 | not_robot_checkbox_xpath = ( 809 | '//android.widget.CheckBox[@resource-id="recaptcha-anchor"]') 810 | 811 | # step1, except: Cannot contact reCAPTCHA 812 | not_contact_title_xpath = ( 813 | '//android.widget.TextView[@resource-id="android:id/alertTitle"]') 814 | not_contact_message_xpath = ( 815 | '//android.widget.TextView[@resource-id="android:id/message"]') 816 | not_contact_ok_button_xpath = ( 817 | '//android.widget.Button[@resource-id="android:id/button1"]') 818 | 819 | not_contact_frame_id = 'android:id/content' 820 | not_contact_title_id = 'android:id/alertTitle' 821 | not_contact_message_id = 'android:id/message' 822 | not_contact_ok_button_id = 'android:id/button1' 823 | 824 | # step2: select captcha image 825 | # captcha_form_xpath = ( 826 | # '//android.view.View[@resource-id="CaptchaFrame"]') 827 | captcha_form_xpath = ( 828 | '//android.view.View[@resource-id="rc-imageselect"]') 829 | 830 | # this element is used to determine if there is a sample image 831 | # If having two elements of the xpath, then there is a sample image, 832 | # or there is no sample image. 833 | above_part_two_elements_xpath = (f'{captcha_form_xpath}/' 834 | 'android.view.View[1]/android.view.View/android.view.View') 835 | 836 | sample_img_xpath = (f'{captcha_form_xpath}/android.view.View[1]/' 837 | 'android.view.View/android.view.View[1]/android.view.View') 838 | 839 | instruction_for_one_xpath = (f'{captcha_form_xpath}/android.view.View[1]/' 840 | 'android.view.View/android.view.View') 841 | instruction_first_for_one_xpath = (f'{instruction_for_one_xpath}' 842 | '/android.view.View[1]') 843 | instruction_second_for_one_xpath = (f'{instruction_for_one_xpath}' 844 | '/android.view.View[2]') 845 | instruction_third_for_one_xpath = (f'{instruction_for_one_xpath}' 846 | '/android.view.View[3]') 847 | 848 | instruction_for_two_xpath = (f'{captcha_form_xpath}/android.view.View[1]/' 849 | 'android.view.View/android.view.View[2]') 850 | instruction_first_for_two_xpath = (f'{instruction_for_two_xpath}' 851 | '/android.view.View[1]') 852 | instruction_second_for_two_xpath = (f'{instruction_for_two_xpath}' 853 | '/android.view.View[2]') 854 | instruction_third_for_two_xpath = (f'{instruction_for_two_xpath}' 855 | '/android.view.View[3]') 856 | 857 | captcha_instruction_xpath = f'{captcha_form_xpath}/android.view.View[1]' 858 | captcha_img_xpath = f'{captcha_form_xpath}/android.view.View[2]' 859 | 860 | captcha_instruction_xpath1 = f'{captcha_form_xpath}/android.view.View[2]' 861 | captcha_img_xpath1 = f'{captcha_form_xpath}/android.view.View[3]' 862 | 863 | reload_button_xpath = ( 864 | '//android.widget.Button[@resource-id="recaptcha-reload-button"]') 865 | audio_button_xpath = ( 866 | '//android.widget.Button[@resource-id="recaptcha-audio-button"]') 867 | verify_button_xpath = ( 868 | '//android.widget.Button[@resource-id="recaptcha-verify-button"]') 869 | 870 | # step except: check new images or try again 871 | try_again_tips_xpath = check_new_images_tips_xpath = ( 872 | f'{captcha_form_xpath}/android.view.View[3]/android.view.View') 873 | 874 | # step3, continue 875 | continue_button_xpath = ( 876 | '//android.widget.Button[@resource-id="continue_button"]') 877 | # tips of CheckBox of 'not a robot': You are verifiedI'm not a robot 878 | # tips of CheckBox for exception: Verification expired, 879 | # check the checkbox again for a new challengeI'm not a robot 880 | 881 | wait_timeout = 5 882 | # captcha_image_path = PRJ_PATH / 'temp' 883 | captcha_image_file_name_suffix = '_recaptcha' 884 | captcha_image_file_extension = 'png' 885 | 886 | def __init__(self, driver, resolver=None, wait_timeout=wait_timeout): 887 | super().__init__(driver, resolver, wait_timeout) 888 | 889 | def click_not_robot_checkbox(self): 890 | self.click_element('not robot checkbox', self.not_robot_checkbox_xpath) 891 | 892 | def click_verify_button(self): 893 | self.click_element('verify button', self.verify_button_xpath) 894 | 895 | def click_reload_button(self): 896 | self.click_element('reload button', self.reload_button_xpath) 897 | 898 | def click_not_contact_ok_button(self): 899 | self.click_element('not contact ok button', 900 | self.not_contact_ok_button_id, By.ID) 901 | 902 | def click_continue_button(self): 903 | self.click_element('continue button', self.continue_button_xpath) 904 | 905 | def save_captcha_img_from_form(self, img_file): 906 | return self.save_captcha_img(img_file=img_file, 907 | captcha_img_locator=self.captcha_form_xpath) 908 | 909 | def save_captcha_img_from_itself(self, img_file): 910 | return self.save_captcha_img(img_file=img_file, 911 | captcha_img_locator=self.captcha_img_xpath) 912 | 913 | def save_captcha_effect_img(self, captcha_img_locator, captcha_img_locator_type=By.XPATH, 914 | img_file=None, dest_img_file=None): 915 | """Save the effective part of captcha image to a file 916 | 917 | First, save all parts of it to the file img_file, 918 | then crop the image from the file to another effective image, then save 919 | it to the file dest_img_file. 920 | 921 | If file name is None, then create it with random name. 922 | """ 923 | LOGGER.debug('Use subclass method to save effective part of ' 924 | 'captcha image via cropping') 925 | real_src_img_file = self.save_captcha_img(self.captcha_form_xpath, img_file=img_file) 926 | 927 | parent_element = self.driver.find_element_by_xpath(self.captcha_form_xpath) 928 | from_element = self.driver.find_element_by_xpath(self.captcha_instruction_xpath) 929 | 930 | # different page structure 931 | if parent_element.size == from_element.size: 932 | LOGGER.debug('different page structure') 933 | from_element = self.driver.find_element_by_xpath(self.captcha_instruction_xpath1) 934 | to_element = self.driver.find_element_by_xpath(self.captcha_img_xpath1) 935 | else: 936 | to_element = self.driver.find_element_by_xpath(self.captcha_img_xpath) 937 | effect_captcha_img_file = self.crop_captcha_img_vertically( 938 | real_src_img_file, parent_element, from_element, to_element, dest_img_file) 939 | LOGGER.debug(f'Effect captcha image file: {effect_captcha_img_file}') 940 | 941 | return effect_captcha_img_file 942 | 943 | # check if this is the reCAPTCHA regardless of which captcha page 944 | def is_captcha_page(self): 945 | return self.find_page('reCAPTCHA page', 'reCAPTCHA frame', 946 | self.verify_first_page_frame_xpath) 947 | 948 | # check if this is the reCAPTCHA from outside 949 | def is_captcha_first_page(self): 950 | if self.driver.find_elements_by_xpath(self.not_robot_checkbox_xpath): 951 | if self.is_captcha_page(): 952 | return True 953 | return False 954 | 955 | def is_in_captcha_page(self): 956 | return self.find_page('captcha page', 'captcha image form', self.captcha_form_xpath) 957 | 958 | def is_in_captcha_img_page(self): 959 | return self.find_page('captcha image page', 'captcha verify button', 960 | self.verify_button_xpath) 961 | 962 | def is_in_not_contact_page(self): 963 | return self.find_page('not contact page', 'not contact title', 964 | self.not_contact_title_id, By.ID) 965 | 966 | def is_in_start_verify_page(self): 967 | return self.find_page('start verify page', 'not robot checkbox', 968 | self.not_robot_checkbox_xpath) 969 | 970 | def resolve_all_with_coordinates_api(self, click_start=True, 971 | reduce_factor=2, reduce_step=0.125, retry_times=2, timeout=20, 972 | report_blank_list=False, img_file=None, tap_interval=4, 973 | need_press=False, all_resolve_retry_times=15, all_error_retry_times=3): 974 | """Resolve all reCaptcha images in one step""" 975 | LOGGER.debug(f'All retry times of resolving: {all_resolve_retry_times}') 976 | if click_start: 977 | LOGGER.info('Resolve all reCaptcha images in one step') 978 | self.click_not_robot_checkbox() 979 | 980 | img_page_flag = False 981 | # check if it is in the page of captcha image 982 | if self.is_in_captcha_img_page(): 983 | img_page_flag = True 984 | try: 985 | result = self.resolve_one_with_coordinates_api( 986 | captcha_img_locator=self.captcha_form_xpath, 987 | captcha_img_crop_start_locator=self.captcha_form_xpath, 988 | reduce_factor=reduce_factor, 989 | reduce_step=reduce_step, 990 | retry_times=retry_times, 991 | timeout=timeout, 992 | report_blank_list=report_blank_list, 993 | captcha_img_locator_type=By.XPATH, 994 | captcha_img_crop_start_locator_type=By.XPATH, 995 | img_file=img_file, 996 | tap_interval=tap_interval, 997 | need_press=need_press 998 | ) 999 | 1000 | all_resolve_retry_times -= 1 1001 | if all_resolve_retry_times <= 0: 1002 | LOGGER.info('Retry times of resolving are zero, now exit it') 1003 | raise CaptchaTooManyRetryException 1004 | # return False 1005 | except Exception as e: 1006 | LOGGER.error(e) 1007 | return False 1008 | 1009 | if img_page_flag and result is False: 1010 | LOGGER.info('Cannot resolve it, then click reload button, and play the game again') 1011 | self.click_reload_button() # change captcha image 1012 | return self.resolve_all_with_coordinates_api(click_start=False, 1013 | all_resolve_retry_times=all_resolve_retry_times) 1014 | 1015 | # no other image to click, just click skip button 1016 | # after clicking skip button, then go on to check the page to 1017 | # check if the resolving is successful. 1018 | if img_page_flag and result is None: 1019 | self.click_verify_button() 1020 | # check if there are images to click 1021 | ele = self.find_element('select all matching images', self.check_new_images_tips_xpath) 1022 | if ele: 1023 | tips = ele.text 1024 | LOGGER.debug(f'Select tips: {tips}') 1025 | if 'select all matching' in tips.lower(): 1026 | return self.resolve_all_with_coordinates_api(click_start=False, 1027 | all_resolve_retry_times=all_resolve_retry_times) 1028 | 1029 | if img_page_flag and result is True: 1030 | LOGGER.debug('Clicked all matched images, then click verify button') 1031 | self.click_verify_button() 1032 | 1033 | # if the game is still going, then continue to verify the captcha 1034 | if self.is_in_captcha_img_page(): 1035 | LOGGER.debug('The game is still going, then continue to play') 1036 | # random_sleep(1, 3) 1037 | return self.resolve_all_with_coordinates_api(click_start=False, 1038 | all_resolve_retry_times=all_resolve_retry_times) 1039 | 1040 | # if it is in the page of start verify, then click the checkbox of not a robot 1041 | checkbox = self.is_in_start_verify_page() 1042 | if checkbox: 1043 | text = checkbox.text 1044 | LOGGER.debug(f'CheckBox text: {text}') 1045 | 1046 | if 'expired' in text.lower() or not text: # No text or expired 1047 | LOGGER.debug('In "not a robot" page, verification expired ' 1048 | 'or new verification, then click the checkbox') 1049 | return self.resolve_all_with_coordinates_api(click_start=True, 1050 | all_resolve_retry_times=all_resolve_retry_times) 1051 | 1052 | if 'verified' in text.lower(): # You are verified "I'm not a robot" 1053 | self.click_continue_button() 1054 | return True 1055 | 1056 | # if it is in the page of not contact, then click button OK 1057 | if self.is_in_not_contact_page(): 1058 | all_error_retry_times -= 1 1059 | if all_error_retry_times <= 0: 1060 | LOGGER.error('More than all_error_retry_times: {all_error_retry_times}') 1061 | raise CaptchaErrorTooManyRetryException 1062 | LOGGER.debug('In "not contact" page, then click the button OK') 1063 | self.click_not_contact_ok_button() 1064 | return self.resolve_all_with_coordinates_api(click_start=True, 1065 | all_resolve_retry_times=all_resolve_retry_times, 1066 | all_error_retry_times=all_error_retry_times-1) 1067 | 1068 | return False 1069 | --------------------------------------------------------------------------------