├── setup.cfg ├── requirements_dev.txt ├── docs ├── images │ ├── pychrome_tab_management.png │ └── pychrome_with_debug_env.png ├── api │ ├── browser.md │ └── tab.md └── index.md ├── MANIFEST.in ├── mkdocs.yml ├── pychrome ├── __init__.py ├── exceptions.py ├── browser.py ├── cli.py └── tab.py ├── tox.ini ├── .coveragerc ├── examples ├── demo2.py ├── demo1.py ├── multi_tabs_pdf.py ├── multi_tabs_screenshot.py ├── post_request.py └── multi_tabs_navigate.py ├── .travis.yml ├── .gitignore ├── LICENSE ├── setup.py ├── tests ├── test_multi_tabs.py ├── test_browser.py └── test_single_tab.py └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | tox>=2.3.1 2 | pytest>=3.6 3 | pytest-cov>=2.5.1 4 | codecov>=2.0.9 5 | pluggy>=0.7 -------------------------------------------------------------------------------- /docs/images/pychrome_tab_management.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fate0/pychrome/HEAD/docs/images/pychrome_tab_management.png -------------------------------------------------------------------------------- /docs/images/pychrome_with_debug_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fate0/pychrome/HEAD/docs/images/pychrome_with_debug_env.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | exclude __pycache__ 4 | recursive-include tests * 5 | recursive-include examples * 6 | recursive-exclude tests *.pyc -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pychrome 2 | theme: readthedocs 3 | site_author: fate0 4 | repo_url: https://github.com/fate0/pychrome 5 | pages: 6 | - ['index.md', 'Intro'] 7 | - ['api/browser.md', 'API'] 8 | - ['api/tab.md', 'API'] -------------------------------------------------------------------------------- /pychrome/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | from .browser import * 7 | from .tab import * 8 | from .exceptions import * 9 | 10 | __version__ = '0.2.4' 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, flake8 3 | 4 | [testenv:flake8] 5 | basepython=python 6 | deps=flake8 7 | commands=flake8 --ignore=E501,F401,E402,F403,F405 pychrome 8 | 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}:{toxinidir}/pychrome 12 | commands = py.test -v -s --cov=./ 13 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = pychrome 4 | 5 | [report] 6 | ignore_errors = True 7 | omit = 8 | pychrome/cli.py 9 | examples/* 10 | tests/* 11 | setup.py 12 | 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | pragma: no cover 16 | 17 | # Don't complain about missing debug-only code: 18 | def __repr__ 19 | def __str__ 20 | raise 21 | warnings.warn 22 | 23 | # Don't complain if non-runnable code isn't run: 24 | if 0: 25 | if __name__ == .__main__.: -------------------------------------------------------------------------------- /examples/demo2.py: -------------------------------------------------------------------------------- 1 | import pychrome 2 | 3 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 4 | tab = browser.new_tab() 5 | 6 | def request_will_be_sent(**kwargs): 7 | print("loading: %s" % kwargs.get('request').get('url')) 8 | 9 | 10 | tab.set_listener("Network.requestWillBeSent", request_will_be_sent) 11 | 12 | tab.start() 13 | tab.call_method("Network.enable") 14 | tab.call_method("Page.navigate", url="https://github.com/fate0/pychrome", _timeout=5) 15 | 16 | tab.wait(5) 17 | tab.stop() 18 | 19 | browser.close_tab(tab) 20 | -------------------------------------------------------------------------------- /pychrome/exceptions.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | 7 | class PyChromeException(Exception): 8 | pass 9 | 10 | 11 | class UserAbortException(PyChromeException): 12 | pass 13 | 14 | 15 | class TabConnectionException(PyChromeException): 16 | pass 17 | 18 | 19 | class CallMethodException(PyChromeException): 20 | pass 21 | 22 | 23 | class TimeoutException(PyChromeException): 24 | pass 25 | 26 | 27 | class RuntimeException(PyChromeException): 28 | pass 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: "2.7" 6 | env: TOXENV=py27 7 | - python: "3.4" 8 | env: TOXENV=py34 9 | - python: "3.5" 10 | env: TOXENV=py35 11 | - python: "3.6" 12 | env: TOXENV=py36 13 | 14 | addons: 15 | chrome: stable 16 | before_install: 17 | - # start your web application and listen on `localhost` 18 | - google-chrome-stable --headless --no-sandbox --disable-gpu --remote-debugging-port=9222 http://localhost & 19 | 20 | install: 21 | - pip install -r requirements_dev.txt 22 | - python setup.py install 23 | 24 | script: tox -e ${TOXENV} 25 | 26 | after_success: 27 | - codecov 28 | 29 | -------------------------------------------------------------------------------- /examples/demo1.py: -------------------------------------------------------------------------------- 1 | import pychrome 2 | 3 | # create a browser instance 4 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 5 | 6 | # create a tab 7 | tab = browser.new_tab() 8 | 9 | # register callback if you want 10 | def request_will_be_sent(**kwargs): 11 | print("loading: %s" % kwargs.get('request').get('url')) 12 | 13 | tab.Network.requestWillBeSent = request_will_be_sent 14 | 15 | # start the tab 16 | tab.start() 17 | 18 | # call method 19 | tab.Network.enable() 20 | # call method with timeout 21 | tab.Page.navigate(url="https://github.com/fate0/pychrome", _timeout=5) 22 | 23 | # wait for loading 24 | tab.wait(5) 25 | 26 | # stop the tab (stop handle events and stop recv message from chrome) 27 | tab.stop() 28 | 29 | # close tab 30 | browser.close_tab(tab) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | .idea/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | .DS_Store 7 | site/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | # pyenv python configuration file 65 | .python-version 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2017, fate0 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from this 18 | software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 27 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 28 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | import ast 6 | from setuptools import setup, find_packages 7 | 8 | with open('README.md') as readme_file: 9 | readme = readme_file.read() 10 | 11 | 12 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 13 | 14 | 15 | with open('pychrome/__init__.py', 'rb') as f: 16 | version = str(ast.literal_eval(_version_re.search( 17 | f.read().decode('utf-8')).group(1))) 18 | 19 | 20 | requirements = [ 21 | 'click>=6.0', 22 | 'websocket-client>=0.44.0', 23 | 'requests>=2.13.0', 24 | ] 25 | 26 | setup( 27 | name='pychrome', 28 | version=version, 29 | description="A Python Package for the Google Chrome Dev Protocol", 30 | long_description=readme, 31 | long_description_content_type="text/markdown", 32 | author="fate0", 33 | author_email='fate0@fatezero.org', 34 | url='https://github.com/fate0/pychrome', 35 | packages=find_packages(), 36 | package_dir={}, 37 | entry_points={ 38 | 'console_scripts': [ 39 | 'pychrome=pychrome.cli:main' 40 | ] 41 | }, 42 | include_package_data=True, 43 | install_requires=requirements, 44 | license="BSD license", 45 | zip_safe=False, 46 | keywords='pychrome', 47 | classifiers=[ 48 | 'Development Status :: 5 - Production/Stable', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Natural Language :: English', 52 | 'Programming Language :: Python :: 2.7', 53 | 'Programming Language :: Python :: 3.4', 54 | 'Programming Language :: Python :: 3.5', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Topic :: Internet :: WWW/HTTP', 57 | 'Topic :: Internet :: WWW/HTTP :: Browsers' 58 | ], 59 | ) 60 | 61 | -------------------------------------------------------------------------------- /docs/api/browser.md: -------------------------------------------------------------------------------- 1 | ## class: Browser 2 | 3 | #### browser.new_tab([url][, timeout]) 4 | - `url` <[string]> 5 | - `timeout` <[int]> 6 | - return: <[Tab]> 7 | 8 | example: 9 | ```python 10 | import pychrome 11 | 12 | browser = pychrome.Browser() 13 | print(browser.new_tab("http://www.fatezero.org")) 14 | ``` 15 | 16 | output: 17 | ``` 18 | 19 | ``` 20 | 21 | #### browser.list_tab([timeout]) 22 | - `timeout` 23 | - return: list 24 | 25 | example: 26 | ```python 27 | import pychrome 28 | 29 | browser = pychrome.Browser() 30 | print(browser.list_tab()) 31 | ``` 32 | 33 | output: 34 | ``` 35 | [, ] 36 | ``` 37 | 38 | #### browser.activate_tab(tab_id[, timeout]) 39 | - `tab_id` 40 | - return: string 41 | 42 | example: 43 | ```python 44 | import pychrome 45 | 46 | browser = pychrome.Browser() 47 | print(browser.activate_tab('0261adad-1b83-4d87-946f-08f0b50ca175')) 48 | ``` 49 | 50 | output: 51 | ``` 52 | Target activated 53 | ``` 54 | 55 | #### browser.close_tab(tab_id[, timeout]) 56 | - `tab_id` 57 | - `timeout` 58 | 59 | example: 60 | ```python 61 | import pychrome 62 | 63 | browser = pychrome.Browser() 64 | print(browser.close_tab('0261adad-1b83-4d87-946f-08f0b50ca175')) 65 | ``` 66 | 67 | output: 68 | ``` 69 | Target is closing 70 | ``` 71 | 72 | #### browser.version([timeout]) 73 | - `timeout` 74 | - return: string 75 | 76 | example: 77 | ```python 78 | import pychrome 79 | 80 | browser = pychrome.Browser() 81 | print(browser.version()) 82 | ``` 83 | 84 | output: 85 | ``` 86 | {'webSocketDebuggerUrl': 'ws://127.0.0.1:9222/devtools/browser/36d5044d-4ef2-421b-b105-35c79edf7fea', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3197.0 Safari/537.36', 'Protocol-Version': '1.2', 'Browser': 'HeadlessChrome/62.0.3197.0', 'WebKit-Version': '537.36 (@a19b1504d1a1f40e6c5358ec9880eb06b506b007)', 'V8-Version': '6.2.369'} 87 | ``` 88 | -------------------------------------------------------------------------------- /tests/test_multi_tabs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import logging 5 | import pychrome 6 | import functools 7 | 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | logging.getLogger("urllib3").setLevel(logging.WARNING) 11 | 12 | 13 | def close_all_tabs(browser): 14 | if len(browser.list_tab()) == 0: 15 | return 16 | 17 | logging.debug("[*] recycle") 18 | for tab in browser.list_tab(): 19 | browser.close_tab(tab) 20 | 21 | time.sleep(1) 22 | assert len(browser.list_tab()) == 0 23 | 24 | 25 | def setup_function(function): 26 | browser = pychrome.Browser() 27 | close_all_tabs(browser) 28 | 29 | 30 | def teardown_function(function): 31 | browser = pychrome.Browser() 32 | close_all_tabs(browser) 33 | 34 | 35 | def new_multi_tabs(browser, n): 36 | tabs = [] 37 | for i in range(n): 38 | tabs.append(browser.new_tab()) 39 | 40 | return tabs 41 | 42 | 43 | def test_normal_callmethod(): 44 | browser = pychrome.Browser() 45 | tabs = new_multi_tabs(browser, 10) 46 | 47 | for tab in tabs: 48 | tab.start() 49 | result = tab.Page.navigate(url="http://www.fatezero.org") 50 | assert result['frameId'] 51 | 52 | time.sleep(3) 53 | 54 | for tab in tabs: 55 | result = tab.Runtime.evaluate(expression="document.domain") 56 | assert result['result']['type'] == 'string' 57 | assert result['result']['value'] == 'www.fatezero.org' 58 | tab.stop() 59 | 60 | 61 | def test_set_event_listener(): 62 | browser = pychrome.Browser() 63 | tabs = new_multi_tabs(browser, 10) 64 | 65 | def request_will_be_sent(tab, **kwargs): 66 | tab.stop() 67 | 68 | for tab in tabs: 69 | tab.start() 70 | tab.Network.requestWillBeSent = functools.partial(request_will_be_sent, tab) 71 | tab.Network.enable() 72 | try: 73 | tab.Page.navigate(url="chrome://newtab/") 74 | except pychrome.UserAbortException: 75 | pass 76 | 77 | for tab in tabs: 78 | if not tab.wait(timeout=5): 79 | assert False, "never get here" 80 | tab.stop() 81 | -------------------------------------------------------------------------------- /docs/api/tab.md: -------------------------------------------------------------------------------- 1 | ## class: Tab 2 | 3 | #### attribute: id 4 | 5 | tab id 6 | 7 | #### attribute: type 8 | 9 | tab type 10 | 11 | #### attribute: status 12 | 13 | tab status, could be `initial`, `started`, `stopped` 14 | 15 | #### attribute: debug 16 | 17 | example: 18 | ```python 19 | import pychrome 20 | 21 | browser = pychrome.Browser() 22 | tab = browser.new_tab("http://www.fatezero.org") 23 | 24 | tab.debug = True 25 | 26 | tab.start() 27 | tab.call_method("Network.enable") 28 | tab.call_method("Page.navigate", url="https://github.com/fate0/pychrome", _timeout=5) 29 | tab.stop() 30 | 31 | browser.close_tab(tab) 32 | ``` 33 | 34 | output: 35 | ``` 36 | SEND ► {"method": "Network.enable", "id": 1001, "params": {}} 37 | ◀ RECV {"id":1001,"result":{}} 38 | SEND ► {"method": "Page.navigate", "id": 1002, "params": {"url": "https://github.com/fate0/pychrome"}} 39 | ◀ RECV {"method":"Network.loadingFailed","params":{"requestId":"34905.1","timestamp":87792.94521,"type":"Other","errorText":"net::ERR_ABORTED","canceled":true}} 40 | ◀ RECV {"method":"Network.requestWillBeSent","params":{"requestId":"34905.2","loaderId":"34905.1","documentURL":"https://github.com/fate0/pychrome","request":{"url":"https://github.com/fate0/pychrome","method":"GET","headers":{"Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3197.0 Safari/537.36"},"mixedContentType":"none","initialPriority":"VeryHigh","referrerPolicy":"no-referrer-when-downgrade"},"timestamp":87792.945539,"wallTime":1503817729.18642,"initiator":{"type":"other"},"type":"Document","frameId":"34905.1"}} 41 | ◀ RECV {"id":1002,"result":{"frameId":"34905.1"}} 42 | ``` 43 | 44 | 45 | #### start() 46 | - return: bool 47 | 48 | Start the tab's activity 49 | 50 | #### stop() 51 | - return: bool 52 | 53 | #### wait([timeout]) 54 | - timeout: int 55 | - return: bool 56 | 57 | Wait until the tab stop or timeout 58 | 59 | #### call_method() 60 | - return: 61 | 62 | ```python 63 | import pychrome 64 | 65 | browser = pychrome.Browser() 66 | tab = browser.new_tab() 67 | 68 | tab.call_method("Page.navigate", url="https://github.com/fate0/pychrome", _timeout=5) 69 | tab.stop() 70 | ``` 71 | 72 | #### set_listener() 73 | - return: bool 74 | 75 | #### get_listener() 76 | - return: bool 77 | 78 | #### del_all_listeners() 79 | - return: bool 80 | 81 | delete all listeners 82 | -------------------------------------------------------------------------------- /pychrome/browser.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | import requests 7 | 8 | from .tab import Tab 9 | 10 | 11 | __all__ = ["Browser"] 12 | 13 | 14 | class Browser(object): 15 | _all_tabs = {} 16 | 17 | def __init__(self, url="http://127.0.0.1:9222"): 18 | self.dev_url = url 19 | 20 | if self.dev_url not in self._all_tabs: 21 | self._tabs = self._all_tabs[self.dev_url] = {} 22 | else: 23 | self._tabs = self._all_tabs[self.dev_url] 24 | 25 | def new_tab(self, url=None, timeout=None): 26 | url = url or '' 27 | rp = requests.put("%s/json/new?%s" % (self.dev_url, url), json=True, timeout=timeout) 28 | tab = Tab(**rp.json()) 29 | self._tabs[tab.id] = tab 30 | return tab 31 | 32 | def list_tab(self, timeout=None): 33 | rp = requests.get("%s/json" % self.dev_url, json=True, timeout=timeout) 34 | tabs_map = {} 35 | for tab_json in rp.json(): 36 | if tab_json['type'] != 'page': # pragma: no cover 37 | continue 38 | 39 | if tab_json['id'] in self._tabs and self._tabs[tab_json['id']].status != Tab.status_stopped: 40 | tabs_map[tab_json['id']] = self._tabs[tab_json['id']] 41 | else: 42 | tabs_map[tab_json['id']] = Tab(**tab_json) 43 | 44 | self._tabs = tabs_map 45 | return list(self._tabs.values()) 46 | 47 | def activate_tab(self, tab_id, timeout=None): 48 | if isinstance(tab_id, Tab): 49 | tab_id = tab_id.id 50 | 51 | rp = requests.get("%s/json/activate/%s" % (self.dev_url, tab_id), timeout=timeout) 52 | return rp.text 53 | 54 | def close_tab(self, tab_id, timeout=None): 55 | if isinstance(tab_id, Tab): 56 | tab_id = tab_id.id 57 | 58 | tab = self._tabs.pop(tab_id, None) 59 | if tab and tab.status == Tab.status_started: # pragma: no cover 60 | tab.stop() 61 | 62 | rp = requests.get("%s/json/close/%s" % (self.dev_url, tab_id), timeout=timeout) 63 | return rp.text 64 | 65 | def version(self, timeout=None): 66 | rp = requests.get("%s/json/version" % self.dev_url, json=True, timeout=timeout) 67 | return rp.json() 68 | 69 | def __str__(self): 70 | return '' % self.dev_url 71 | 72 | __repr__ = __str__ 73 | -------------------------------------------------------------------------------- /examples/multi_tabs_pdf.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import base64 6 | import pychrome 7 | 8 | import threading 9 | 10 | 11 | urls = [ 12 | "http://fatezero.org", 13 | "http://blog.fatezero.org", 14 | "http://github.com/fate0", 15 | "http://github.com/fate0/pychrome" 16 | ] 17 | 18 | 19 | class EventHandler(object): 20 | pdf_lock = threading.Lock() 21 | 22 | def __init__(self, browser, tab): 23 | self.browser = browser 24 | self.tab = tab 25 | self.start_frame = None 26 | 27 | def frame_started_loading(self, frameId): 28 | if not self.start_frame: 29 | self.start_frame = frameId 30 | 31 | def frame_stopped_loading(self, frameId): 32 | if self.start_frame == frameId: 33 | self.tab.Page.stopLoading() 34 | 35 | with self.pdf_lock: 36 | # must activate current tab 37 | print(self.browser.activate_tab(self.tab.id)) 38 | 39 | try: 40 | data = self.tab.Page.printToPDF() 41 | 42 | with open("%s.pdf" % time.time(), "wb") as fd: 43 | fd.write(base64.b64decode(data['data'])) 44 | finally: 45 | self.tab.stop() 46 | 47 | 48 | def close_all_tabs(browser): 49 | if len(browser.list_tab()) == 0: 50 | return 51 | 52 | for tab in browser.list_tab(): 53 | try: 54 | tab.stop() 55 | except pychrome.RuntimeException: 56 | pass 57 | 58 | browser.close_tab(tab) 59 | 60 | time.sleep(1) 61 | assert len(browser.list_tab()) == 0 62 | 63 | 64 | def main(): 65 | browser = pychrome.Browser() 66 | 67 | close_all_tabs(browser) 68 | 69 | tabs = [] 70 | for i in range(len(urls)): 71 | tabs.append(browser.new_tab()) 72 | 73 | for i, tab in enumerate(tabs): 74 | eh = EventHandler(browser, tab) 75 | tab.Page.frameStartedLoading = eh.frame_started_loading 76 | tab.Page.frameStoppedLoading = eh.frame_stopped_loading 77 | 78 | tab.start() 79 | tab.Page.stopLoading() 80 | tab.Page.enable() 81 | tab.Page.navigate(url=urls[i]) 82 | 83 | for tab in tabs: 84 | tab.wait(60) 85 | tab.stop() 86 | browser.close_tab(tab.id) 87 | 88 | print('Done') 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /examples/multi_tabs_screenshot.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import base64 6 | import pychrome 7 | import threading 8 | 9 | 10 | urls = [ 11 | "http://fatezero.org", 12 | "http://blog.fatezero.org", 13 | "http://github.com/fate0", 14 | "http://github.com/fate0/pychrome" 15 | ] 16 | 17 | 18 | class EventHandler(object): 19 | screen_lock = threading.Lock() 20 | 21 | def __init__(self, browser, tab): 22 | self.browser = browser 23 | self.tab = tab 24 | self.start_frame = None 25 | 26 | def frame_started_loading(self, frameId): 27 | if not self.start_frame: 28 | self.start_frame = frameId 29 | 30 | def frame_stopped_loading(self, frameId): 31 | if self.start_frame == frameId: 32 | self.tab.Page.stopLoading() 33 | 34 | with self.screen_lock: 35 | # must activate current tab 36 | print(self.browser.activate_tab(self.tab.id)) 37 | 38 | try: 39 | data = self.tab.Page.captureScreenshot() 40 | with open("%s.png" % time.time(), "wb") as fd: 41 | fd.write(base64.b64decode(data['data'])) 42 | finally: 43 | self.tab.stop() 44 | 45 | 46 | def close_all_tabs(browser): 47 | if len(browser.list_tab()) == 0: 48 | return 49 | 50 | for tab in browser.list_tab(): 51 | try: 52 | tab.stop() 53 | except pychrome.RuntimeException: 54 | pass 55 | 56 | browser.close_tab(tab) 57 | 58 | time.sleep(1) 59 | assert len(browser.list_tab()) == 0 60 | 61 | 62 | def main(): 63 | browser = pychrome.Browser() 64 | 65 | close_all_tabs(browser) 66 | 67 | tabs = [] 68 | for i in range(len(urls)): 69 | tabs.append(browser.new_tab()) 70 | 71 | for i, tab in enumerate(tabs): 72 | eh = EventHandler(browser, tab) 73 | tab.Page.frameStartedLoading = eh.frame_started_loading 74 | tab.Page.frameStoppedLoading = eh.frame_stopped_loading 75 | 76 | tab.start() 77 | tab.Page.stopLoading() 78 | tab.Page.enable() 79 | tab.Page.navigate(url=urls[i]) 80 | 81 | for tab in tabs: 82 | tab.wait(60) 83 | tab.stop() 84 | browser.close_tab(tab) 85 | 86 | print('Done') 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | -------------------------------------------------------------------------------- /tests/test_browser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import logging 5 | import pychrome 6 | 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | def close_all_tabs(browser): 12 | if len(browser.list_tab()) == 0: 13 | return 14 | 15 | for tab in browser.list_tab(): 16 | browser.close_tab(tab) 17 | 18 | time.sleep(1) 19 | assert len(browser.list_tab()) == 0 20 | 21 | 22 | def setup_function(function): 23 | browser = pychrome.Browser() 24 | close_all_tabs(browser) 25 | 26 | 27 | def teardown_function(function): 28 | browser = pychrome.Browser() 29 | close_all_tabs(browser) 30 | 31 | 32 | def test_chome_version(): 33 | browser = pychrome.Browser() 34 | browser_version = browser.version() 35 | assert isinstance(browser_version, dict) 36 | 37 | 38 | def test_browser_list(): 39 | browser = pychrome.Browser() 40 | tabs = browser.list_tab() 41 | assert len(tabs) == 0 42 | 43 | 44 | def test_browser_new(): 45 | browser = pychrome.Browser() 46 | browser.new_tab() 47 | tabs = browser.list_tab() 48 | assert len(tabs) == 1 49 | 50 | 51 | def test_browser_activate_tab(): 52 | browser = pychrome.Browser() 53 | tabs = [] 54 | for i in range(10): 55 | tabs.append(browser.new_tab()) 56 | 57 | for tab in tabs: 58 | browser.activate_tab(tab) 59 | 60 | 61 | def test_browser_tabs_map(): 62 | browser = pychrome.Browser() 63 | 64 | tab = browser.new_tab() 65 | assert tab in browser.list_tab() 66 | assert tab in browser.list_tab() 67 | 68 | browser.close_tab(tab) 69 | assert tab not in browser.list_tab() 70 | 71 | 72 | def test_browser_new_10_tabs(): 73 | browser = pychrome.Browser() 74 | tabs = [] 75 | for i in range(10): 76 | tabs.append(browser.new_tab()) 77 | 78 | time.sleep(1) 79 | assert len(browser.list_tab()) == 10 80 | 81 | for tab in tabs: 82 | browser.close_tab(tab.id) 83 | 84 | time.sleep(1) 85 | assert len(browser.list_tab()) == 0 86 | 87 | 88 | def test_browser_new_100_tabs(): 89 | browser = pychrome.Browser() 90 | tabs = [] 91 | for i in range(100): 92 | tabs.append(browser.new_tab()) 93 | 94 | time.sleep(1) 95 | assert len(browser.list_tab()) == 100 96 | 97 | for tab in tabs: 98 | browser.close_tab(tab) 99 | 100 | time.sleep(1) 101 | assert len(browser.list_tab()) == 0 102 | -------------------------------------------------------------------------------- /examples/post_request.py: -------------------------------------------------------------------------------- 1 | import pychrome 2 | import time 3 | import urllib 4 | 5 | 6 | 7 | class ChromiumClient(object): 8 | """ Client to interact with Chromium """ 9 | 10 | def __init__(self): 11 | self.browser = pychrome.Browser(url="http://127.0.0.1:9222") 12 | 13 | def do_post(self): 14 | self.tab = self.browser.new_tab() 15 | 16 | event_handler = EventHandler() 17 | 18 | event_handler.set_token('asdkflj497564dsklf') 19 | event_handler.set_post_data({ 20 | 'param1': 'value1', 21 | 'param2': 'value2' 22 | }) 23 | 24 | url_pattern_object = {'urlPattern': '*fate0*'} 25 | self.tab.Network.setRequestInterception(patterns=[url_pattern_object]) 26 | 27 | self.tab.Network.requestIntercepted = event_handler.on_request_intercepted 28 | 29 | self.tab.start() 30 | 31 | self.tab.Network.enable() 32 | self.tab.Page.enable() 33 | 34 | self.tab.Page.navigate(url='https://github.com/fate0/pychrome') 35 | 36 | self.tab.wait(5) 37 | self.tab.stop() 38 | 39 | self.browser.close_tab(self.tab.id) 40 | 41 | 42 | class EventHandler(object): 43 | def __init__(self): 44 | self.tab = None 45 | self.token = None 46 | self.is_first_request = False 47 | self.post_data = {} 48 | 49 | def set_tab(self, t): 50 | self.tab = t 51 | 52 | def set_token(self, t): 53 | self.token = t 54 | 55 | def set_post_data(self, pd): 56 | self.post_data = pd 57 | 58 | def on_request_intercepted(self, **kwargs): 59 | new_args = {'interceptionId': kwargs['interceptionId']} 60 | 61 | if self.is_first_request: 62 | # Modify first request only, following are media/static 63 | # requests... 64 | self.is_first_request = False 65 | 66 | extra_headers = { 67 | 'Requested-by': 'Chromium', 68 | 'Authorization': 'Token ' + self.token 69 | } 70 | 71 | request = kwargs.get('request') 72 | request['headers'].update(extra_headers) 73 | 74 | new_args.update({ 75 | 'url': request['url'], 76 | 'method': 'POST', 77 | 'headers': request['headers'], 78 | 'postData': urllib.urlencode(self.post_data) 79 | }) 80 | 81 | self.tab.Network.continueInterceptedRequest(**new_args) 82 | 83 | 84 | if __name__ == '__main__': 85 | client = ChromiumClient() 86 | 87 | client.do_post() -------------------------------------------------------------------------------- /examples/multi_tabs_navigate.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | require chrome version >= 61.0.3119.0 6 | headless mode 7 | """ 8 | 9 | 10 | import time 11 | import pychrome 12 | 13 | 14 | class EventHandler(object): 15 | def __init__(self, browser, tab): 16 | self.browser = browser 17 | self.tab = tab 18 | self.start_frame = None 19 | self.is_first_request = True 20 | self.html_content = None 21 | 22 | def frame_started_loading(self, frameId): 23 | if not self.start_frame: 24 | self.start_frame = frameId 25 | 26 | def request_intercepted(self, interceptionId, request, **kwargs): 27 | if self.is_first_request: 28 | self.is_first_request = False 29 | headers = request.get('headers', {}) 30 | headers['Test-key'] = 'test-value' 31 | self.tab.Network.continueInterceptedRequest( 32 | interceptionId=interceptionId, 33 | headers=headers, 34 | method='POST', 35 | postData="hello post data: %s" % time.time() 36 | ) 37 | else: 38 | self.tab.Network.continueInterceptedRequest( 39 | interceptionId=interceptionId 40 | ) 41 | 42 | def frame_stopped_loading(self, frameId): 43 | if self.start_frame == frameId: 44 | self.tab.Page.stopLoading() 45 | result = self.tab.Runtime.evaluate(expression="document.documentElement.outerHTML") 46 | self.html_content = result.get('result', {}).get('value', "") 47 | print(self.html_content) 48 | self.tab.stop() 49 | 50 | 51 | def close_all_tabs(browser): 52 | if len(browser.list_tab()) == 0: 53 | return 54 | 55 | for tab in browser.list_tab(): 56 | try: 57 | tab.stop() 58 | except pychrome.RuntimeException: 59 | pass 60 | 61 | browser.close_tab(tab) 62 | 63 | time.sleep(1) 64 | assert len(browser.list_tab()) == 0 65 | 66 | 67 | def main(): 68 | browser = pychrome.Browser() 69 | 70 | close_all_tabs(browser) 71 | 72 | tabs = [] 73 | for i in range(4): 74 | tabs.append(browser.new_tab()) 75 | 76 | for i, tab in enumerate(tabs): 77 | eh = EventHandler(browser, tab) 78 | tab.Network.requestIntercepted = eh.request_intercepted 79 | tab.Page.frameStartedLoading = eh.frame_started_loading 80 | tab.Page.frameStoppedLoading = eh.frame_stopped_loading 81 | 82 | tab.start() 83 | tab.Page.stopLoading() 84 | tab.Page.enable() 85 | tab.Network.setRequestInterceptionEnabled(enabled=True) 86 | tab.Page.navigate(url="http://httpbin.org/post") 87 | 88 | for tab in tabs: 89 | tab.wait(60) 90 | tab.stop() 91 | browser.close_tab(tab) 92 | 93 | print('Done') 94 | 95 | 96 | if __name__ == '__main__': 97 | main() 98 | -------------------------------------------------------------------------------- /pychrome/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import json 6 | import click 7 | import pychrome 8 | 9 | 10 | click.disable_unicode_literals_warning = True 11 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 12 | 13 | 14 | shared_options = [ 15 | click.option("--host", "-t", type=click.STRING, default='127.0.0.1', help="HTTP frontend host"), 16 | click.option("--port", "-p", type=click.INT, default=9222, help="HTTP frontend port"), 17 | click.option("--secure", "-s", is_flag=True, help="HTTPS/WSS frontend") 18 | ] 19 | 20 | 21 | def add_shared_options(func): 22 | for option in shared_options: 23 | func = option(func) 24 | 25 | return func 26 | 27 | 28 | class JSONTabEncoder(json.JSONEncoder): 29 | def default(self, obj): 30 | if isinstance(obj, pychrome.Tab): 31 | return obj._kwargs 32 | 33 | return super(JSONTabEncoder, self).default(self, obj) 34 | 35 | 36 | @click.group(context_settings=CONTEXT_SETTINGS) 37 | @click.version_option(pychrome.__version__) 38 | def main(): 39 | pass 40 | 41 | 42 | @main.command(context_settings=CONTEXT_SETTINGS) 43 | @add_shared_options 44 | def list(host, port, secure): 45 | """list all the available targets/tabs""" 46 | url = "%s://%s:%s" % ("https" if secure else "http", host, port) 47 | try: 48 | browser = pychrome.Browser(url) 49 | click.echo(json.dumps(browser.list_tab(), cls=JSONTabEncoder, indent=4)) 50 | except Exception as e: 51 | click.echo(e) 52 | 53 | 54 | @main.command(context_settings=CONTEXT_SETTINGS) 55 | @click.argument("url", required=False) 56 | @add_shared_options 57 | def new(host, port, secure, url="about:blank"): 58 | """create a new target/tab""" 59 | _url = "%s://%s:%s" % ("https" if secure else "http", host, port) 60 | 61 | try: 62 | browser = pychrome.Browser(_url) 63 | click.echo(json.dumps(browser.new_tab(url), cls=JSONTabEncoder, indent=4)) 64 | except Exception as e: 65 | click.echo(e) 66 | 67 | 68 | @main.command(context_settings=CONTEXT_SETTINGS) 69 | @click.argument("id") 70 | @add_shared_options 71 | def activate(host, port, secure, id): 72 | """activate a target/tab by id""" 73 | url = "%s://%s:%s" % ("https" if secure else "http", host, port) 74 | 75 | try: 76 | browser = pychrome.Browser(url) 77 | click.echo(browser.activate_tab(id)) 78 | except Exception as e: 79 | click.echo(e) 80 | 81 | 82 | @main.command(context_settings=CONTEXT_SETTINGS) 83 | @click.argument("id") 84 | @add_shared_options 85 | def close(host, port, secure, id): 86 | """close a target/tab by id""" 87 | url = "%s://%s:%s" % ("https" if secure else "http", host, port) 88 | 89 | try: 90 | browser = pychrome.Browser(url) 91 | click.echo(browser.close_tab(id)) 92 | except Exception as e: 93 | click.echo(e) 94 | 95 | 96 | @main.command(context_settings=CONTEXT_SETTINGS) 97 | @add_shared_options 98 | def version(host, port, secure): 99 | """show the browser version""" 100 | url = "%s://%s:%s" % ("https" if secure else "http", host, port) 101 | 102 | try: 103 | browser = pychrome.Browser(url) 104 | click.echo(json.dumps(browser.version(), indent=4)) 105 | except Exception as e: 106 | click.echo(e) 107 | 108 | 109 | if __name__ == '__main__': 110 | main() 111 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # pychrome 2 | 3 | [![Build Status](https://travis-ci.org/fate0/pychrome.svg?branch=master)](https://travis-ci.org/fate0/pychrome) 4 | [![Codecov](https://img.shields.io/codecov/c/github/fate0/pychrome.svg)](https://codecov.io/gh/fate0/pychrome) 5 | [![Updates](https://pyup.io/repos/github/fate0/pychrome/shield.svg)](https://pyup.io/repos/github/fate0/pychrome/) 6 | [![PyPI](https://img.shields.io/pypi/v/pychrome.svg)](https://pypi.python.org/pypi/pychrome) 7 | [![PyPI](https://img.shields.io/pypi/pyversions/pychrome.svg)](https://github.com/fate0/pychrome) 8 | 9 | A Python Package for the Google Chrome Dev Protocol, [more document](https://fate0.github.io/pychrome/) 10 | 11 | 12 | ## Installation 13 | 14 | To install pychrome, simply: 15 | 16 | ``` 17 | $ pip install -U pychrome 18 | ``` 19 | 20 | or from GitHub: 21 | 22 | ``` 23 | $ pip install -U git+https://github.com/fate0/pychrome.git 24 | ``` 25 | 26 | or from source: 27 | 28 | ``` 29 | $ python setup.py install 30 | ``` 31 | 32 | ## Setup Chrome 33 | 34 | simply: 35 | 36 | ``` 37 | $ google-chrome --remote-debugging-port=9222 38 | ``` 39 | 40 | or headless mode (chrome version >= 59): 41 | 42 | ``` 43 | $ google-chrome --headless --disable-gpu --remote-debugging-port=9222 44 | ``` 45 | 46 | or use docker: 47 | 48 | ``` 49 | $ docker pull fate0/headless-chrome 50 | $ docker run -it --rm --cap-add=SYS_ADMIN -p9222:9222 fate0/headless-chrome 51 | ``` 52 | 53 | ## Getting Started 54 | 55 | ``` python 56 | # create a browser instance 57 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 58 | 59 | # list all tabs (default has a blank tab) 60 | tabs = browser.list_tab() 61 | 62 | if not tabs: 63 | tab = browser.new_tab() 64 | else: 65 | tab = tabs[0] 66 | 67 | 68 | # register callback if you want 69 | def request_will_be_sent(**kwargs): 70 | print("loading: %s" % kwargs.get('request').get('url')) 71 | 72 | tab.Network.requestWillBeSent = request_will_be_sent 73 | 74 | # start the tab 75 | tab.start() 76 | 77 | # call method 78 | tab.Network.enable() 79 | # call method with timeout 80 | tab.Page.navigate(url="https://github.com/fate0/pychrome", _timeout=5) 81 | 82 | # wait for loading 83 | tab.wait(5) 84 | 85 | # stop the tab (stop handle events and stop recv message from chrome) 86 | tab.stop() 87 | 88 | # close tab 89 | browser.close_tab(tab) 90 | 91 | ``` 92 | 93 | or (alternate syntax) 94 | 95 | ``` python 96 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 97 | 98 | tabs = browser.list_tab() 99 | if not tabs: 100 | tab = browser.new_tab() 101 | else: 102 | tab = tabs[0] 103 | 104 | 105 | def request_will_be_sent(**kwargs): 106 | print("loading: %s" % kwargs.get('request').get('url')) 107 | 108 | 109 | tab.set_listener("Network.requestWillBeSent", request_will_be_sent) 110 | 111 | tab.start() 112 | tab.call_method("Network.enable") 113 | tab.call_method("Page.navigate", url="https://github.com/fate0/pychrome", _timeout=5) 114 | 115 | tab.wait(5) 116 | tab.stop() 117 | 118 | browser.close_tab(tab) 119 | ``` 120 | 121 | more methods or events could be found in 122 | [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 123 | 124 | 125 | ## Debug 126 | 127 | set DEBUG env variable: 128 | 129 | ![pychrome_with_debug_env](https://raw.githubusercontent.com/fate0/pychrome/master/docs/images/pychrome_with_debug_env.png) 130 | 131 | 132 | ## Tab management 133 | 134 | run `pychrome -h` for more info 135 | 136 | example: 137 | 138 | ![pychrome_tab_management](https://raw.githubusercontent.com/fate0/pychrome/master/docs/images/pychrome_tab_management.png) 139 | 140 | 141 | ## Examples 142 | 143 | please see the [examples](http://github.com/fate0/pychrome/blob/master/examples) directory for more examples 144 | 145 | 146 | ## Ref 147 | 148 | * [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface/) 149 | * [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pychrome 2 | 3 | [![Build Status](https://travis-ci.org/fate0/pychrome.svg?branch=master)](https://travis-ci.org/fate0/pychrome) 4 | [![Codecov](https://img.shields.io/codecov/c/github/fate0/pychrome.svg)](https://codecov.io/gh/fate0/pychrome) 5 | [![Updates](https://pyup.io/repos/github/fate0/pychrome/shield.svg)](https://pyup.io/repos/github/fate0/pychrome/) 6 | [![PyPI](https://img.shields.io/pypi/v/pychrome.svg)](https://pypi.python.org/pypi/pychrome) 7 | [![PyPI](https://img.shields.io/pypi/pyversions/pychrome.svg)](https://github.com/fate0/pychrome) 8 | 9 | A Python Package for the Google Chrome Dev Protocol, [more document](https://fate0.github.io/pychrome/) 10 | 11 | ## Table of Contents 12 | 13 | * [Installation](#installation) 14 | * [Setup Chrome](#setup-chrome) 15 | * [Getting Started](#getting-started) 16 | * [Tab management](#tab-management) 17 | * [Debug](#debug) 18 | * [Examples](#examples) 19 | * [Ref](#ref) 20 | 21 | 22 | ## Installation 23 | 24 | To install pychrome, simply: 25 | 26 | ``` 27 | $ pip install -U pychrome 28 | ``` 29 | 30 | or from GitHub: 31 | 32 | ``` 33 | $ pip install -U git+https://github.com/fate0/pychrome.git 34 | ``` 35 | 36 | or from source: 37 | 38 | ``` 39 | $ python setup.py install 40 | ``` 41 | 42 | ## Setup Chrome 43 | 44 | simply: 45 | 46 | ``` 47 | $ google-chrome --remote-debugging-port=9222 48 | ``` 49 | 50 | or headless mode (chrome version >= 59): 51 | 52 | ``` 53 | $ google-chrome --headless --disable-gpu --remote-debugging-port=9222 54 | ``` 55 | 56 | or use docker: 57 | 58 | ``` 59 | $ docker pull fate0/headless-chrome 60 | $ docker run -it --rm --cap-add=SYS_ADMIN -p9222:9222 fate0/headless-chrome 61 | ``` 62 | 63 | ## Getting Started 64 | 65 | ``` python 66 | import pychrome 67 | 68 | # create a browser instance 69 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 70 | 71 | # create a tab 72 | tab = browser.new_tab() 73 | 74 | # register callback if you want 75 | def request_will_be_sent(**kwargs): 76 | print("loading: %s" % kwargs.get('request').get('url')) 77 | 78 | tab.Network.requestWillBeSent = request_will_be_sent 79 | 80 | # start the tab 81 | tab.start() 82 | 83 | # call method 84 | tab.Network.enable() 85 | # call method with timeout 86 | tab.Page.navigate(url="https://github.com/fate0/pychrome", _timeout=5) 87 | 88 | # wait for loading 89 | tab.wait(5) 90 | 91 | # stop the tab (stop handle events and stop recv message from chrome) 92 | tab.stop() 93 | 94 | # close tab 95 | browser.close_tab(tab) 96 | 97 | ``` 98 | 99 | or (alternate syntax) 100 | 101 | ``` python 102 | import pychrome 103 | 104 | browser = pychrome.Browser(url="http://127.0.0.1:9222") 105 | tab = browser.new_tab() 106 | 107 | def request_will_be_sent(**kwargs): 108 | print("loading: %s" % kwargs.get('request').get('url')) 109 | 110 | 111 | tab.set_listener("Network.requestWillBeSent", request_will_be_sent) 112 | 113 | tab.start() 114 | tab.call_method("Network.enable") 115 | tab.call_method("Page.navigate", url="https://github.com/fate0/pychrome", _timeout=5) 116 | 117 | tab.wait(5) 118 | tab.stop() 119 | 120 | browser.close_tab(tab) 121 | ``` 122 | 123 | more methods or events could be found in 124 | [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 125 | 126 | 127 | ## Debug 128 | 129 | set DEBUG env variable: 130 | 131 | ![pychrome_with_debug_env](https://raw.githubusercontent.com/fate0/pychrome/master/docs/images/pychrome_with_debug_env.png) 132 | 133 | 134 | ## Tab management 135 | 136 | run `pychrome -h` for more info 137 | 138 | example: 139 | 140 | ![pychrome_tab_management](https://raw.githubusercontent.com/fate0/pychrome/master/docs/images/pychrome_tab_management.png) 141 | 142 | 143 | ## Examples 144 | 145 | please see the [examples](http://github.com/fate0/pychrome/blob/master/examples) directory for more examples 146 | 147 | 148 | ## Ref 149 | 150 | * [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface/) 151 | * [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/tot/) 152 | -------------------------------------------------------------------------------- /pychrome/tab.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | import json 8 | import logging 9 | import warnings 10 | import threading 11 | import functools 12 | 13 | import websocket 14 | 15 | from .exceptions import * 16 | 17 | try: 18 | import Queue as queue 19 | except ImportError: 20 | import queue 21 | 22 | 23 | __all__ = ["Tab"] 24 | 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class GenericAttr(object): 30 | def __init__(self, name, tab): 31 | self.__dict__['name'] = name 32 | self.__dict__['tab'] = tab 33 | 34 | def __getattr__(self, item): 35 | method_name = "%s.%s" % (self.name, item) 36 | event_listener = self.tab.get_listener(method_name) 37 | 38 | if event_listener: 39 | return event_listener 40 | 41 | return functools.partial(self.tab.call_method, method_name) 42 | 43 | def __setattr__(self, key, value): 44 | self.tab.set_listener("%s.%s" % (self.name, key), value) 45 | 46 | 47 | class Tab(object): 48 | status_initial = 'initial' 49 | status_started = 'started' 50 | status_stopped = 'stopped' 51 | 52 | def __init__(self, **kwargs): 53 | self.id = kwargs.get("id") 54 | self.type = kwargs.get("type") 55 | self.debug = os.getenv("DEBUG", False) 56 | 57 | self._websocket_url = kwargs.get("webSocketDebuggerUrl") 58 | self._kwargs = kwargs 59 | 60 | self._cur_id = 1000 61 | 62 | self._ws = None 63 | 64 | self._recv_th = threading.Thread(target=self._recv_loop) 65 | self._recv_th.daemon = True 66 | self._handle_event_th = threading.Thread(target=self._handle_event_loop) 67 | self._handle_event_th.daemon = True 68 | 69 | self._stopped = threading.Event() 70 | self._started = False 71 | self.status = self.status_initial 72 | 73 | self.event_handlers = {} 74 | self.method_results = {} 75 | self.event_queue = queue.Queue() 76 | 77 | def _send(self, message, timeout=None): 78 | if 'id' not in message: 79 | self._cur_id += 1 80 | message['id'] = self._cur_id 81 | 82 | message_json = json.dumps(message) 83 | 84 | if self.debug: # pragma: no cover 85 | print("SEND > %s" % message_json) 86 | 87 | if not isinstance(timeout, (int, float)) or timeout > 1: 88 | q_timeout = 1 89 | else: 90 | q_timeout = timeout / 2.0 91 | 92 | try: 93 | self.method_results[message['id']] = queue.Queue() 94 | 95 | # just raise the exception to user 96 | self._ws.send(message_json) 97 | 98 | while not self._stopped.is_set(): 99 | try: 100 | if isinstance(timeout, (int, float)): 101 | if timeout < q_timeout: 102 | q_timeout = timeout 103 | 104 | timeout -= q_timeout 105 | 106 | return self.method_results[message['id']].get(timeout=q_timeout) 107 | except queue.Empty: 108 | if isinstance(timeout, (int, float)) and timeout <= 0: 109 | raise TimeoutException("Calling %s timeout" % message['method']) 110 | 111 | continue 112 | 113 | raise UserAbortException("User abort, call stop() when calling %s" % message['method']) 114 | finally: 115 | self.method_results.pop(message['id'], None) 116 | 117 | def _recv_loop(self): 118 | while not self._stopped.is_set(): 119 | try: 120 | self._ws.settimeout(1) 121 | message_json = self._ws.recv() 122 | message = json.loads(message_json) 123 | except websocket.WebSocketTimeoutException: 124 | continue 125 | except (websocket.WebSocketException, OSError): 126 | if not self._stopped.is_set(): 127 | logger.error("websocket exception", exc_info=True) 128 | self._stopped.set() 129 | return 130 | 131 | if self.debug: # pragma: no cover 132 | print('< RECV %s' % message_json) 133 | 134 | if "method" in message: 135 | self.event_queue.put(message) 136 | 137 | elif "id" in message: 138 | if message["id"] in self.method_results: 139 | self.method_results[message['id']].put(message) 140 | else: # pragma: no cover 141 | warnings.warn("unknown message: %s" % message) 142 | 143 | def _handle_event_loop(self): 144 | while not self._stopped.is_set(): 145 | try: 146 | event = self.event_queue.get(timeout=1) 147 | except queue.Empty: 148 | continue 149 | 150 | if event['method'] in self.event_handlers: 151 | try: 152 | self.event_handlers[event['method']](**event['params']) 153 | except Exception as e: 154 | logger.error("callback %s exception" % event['method'], exc_info=True) 155 | 156 | self.event_queue.task_done() 157 | 158 | def __getattr__(self, item): 159 | attr = GenericAttr(item, self) 160 | setattr(self, item, attr) 161 | return attr 162 | 163 | def call_method(self, _method, *args, **kwargs): 164 | if not self._started: 165 | raise RuntimeException("Cannot call method before it is started") 166 | 167 | if args: 168 | raise CallMethodException("the params should be key=value format") 169 | 170 | if self._stopped.is_set(): 171 | raise RuntimeException("Tab has been stopped") 172 | 173 | timeout = kwargs.pop("_timeout", None) 174 | result = self._send({"method": _method, "params": kwargs}, timeout=timeout) 175 | if 'result' not in result and 'error' in result: 176 | warnings.warn("%s error: %s" % (_method, result['error']['message'])) 177 | raise CallMethodException("calling method: %s error: %s" % (_method, result['error']['message'])) 178 | 179 | return result['result'] 180 | 181 | def set_listener(self, event, callback): 182 | if not callback: 183 | return self.event_handlers.pop(event, None) 184 | 185 | if not callable(callback): 186 | raise RuntimeException("callback should be callable") 187 | 188 | self.event_handlers[event] = callback 189 | return True 190 | 191 | def get_listener(self, event): 192 | return self.event_handlers.get(event, None) 193 | 194 | def del_all_listeners(self): 195 | self.event_handlers = {} 196 | return True 197 | 198 | def start(self): 199 | if self._started: 200 | return False 201 | 202 | if not self._websocket_url: 203 | raise RuntimeException("Already has another client connect to this tab") 204 | 205 | self._started = True 206 | self.status = self.status_started 207 | self._stopped.clear() 208 | self._ws = websocket.create_connection(self._websocket_url, enable_multithread=True, suppress_origin=True) 209 | self._recv_th.start() 210 | self._handle_event_th.start() 211 | return True 212 | 213 | def stop(self): 214 | if self._stopped.is_set(): 215 | return False 216 | 217 | if not self._started: 218 | raise RuntimeException("Tab is not running") 219 | 220 | self.status = self.status_stopped 221 | self._stopped.set() 222 | if self._ws: 223 | self._ws.close() 224 | return True 225 | 226 | def wait(self, timeout=None): 227 | if not self._started: 228 | raise RuntimeException("Tab is not running") 229 | 230 | if timeout: 231 | return self._stopped.wait(timeout) 232 | 233 | self._recv_th.join() 234 | self._handle_event_th.join() 235 | return True 236 | 237 | def __str__(self): 238 | return "" % self.id 239 | 240 | __repr__ = __str__ 241 | -------------------------------------------------------------------------------- /tests/test_single_tab.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import logging 5 | import pychrome 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | 10 | def close_all_tabs(browser): 11 | if len(browser.list_tab()) == 0: 12 | return 13 | 14 | logging.debug("[*] recycle") 15 | for tab in browser.list_tab(): 16 | browser.close_tab(tab) 17 | 18 | time.sleep(1) 19 | assert len(browser.list_tab()) == 0 20 | 21 | 22 | def setup_function(function): 23 | browser = pychrome.Browser() 24 | close_all_tabs(browser) 25 | 26 | 27 | def teardown_function(function): 28 | browser = pychrome.Browser() 29 | close_all_tabs(browser) 30 | 31 | 32 | def test_normal_callmethod(): 33 | browser = pychrome.Browser() 34 | tab = browser.new_tab() 35 | 36 | tab.start() 37 | result = tab.Page.navigate(url="http://www.fatezero.org") 38 | assert result['frameId'] 39 | 40 | time.sleep(1) 41 | result = tab.Runtime.evaluate(expression="document.domain") 42 | 43 | assert result['result']['type'] == 'string' 44 | assert result['result']['value'] == 'www.fatezero.org' 45 | tab.stop() 46 | 47 | 48 | def test_invalid_method(): 49 | browser = pychrome.Browser() 50 | tab = browser.new_tab() 51 | 52 | tab.start() 53 | try: 54 | tab.Page.NotExistMethod() 55 | assert False, "never get here" 56 | except pychrome.CallMethodException: 57 | pass 58 | tab.stop() 59 | 60 | 61 | def test_invalid_params(): 62 | browser = pychrome.Browser() 63 | tab = browser.new_tab() 64 | 65 | tab.start() 66 | try: 67 | tab.Page.navigate() 68 | assert False, "never get here" 69 | except pychrome.CallMethodException: 70 | pass 71 | 72 | try: 73 | tab.Page.navigate("http://www.fatezero.org") 74 | assert False, "never get here" 75 | except pychrome.CallMethodException: 76 | pass 77 | 78 | try: 79 | tab.Page.navigate(invalid_params="http://www.fatezero.org") 80 | assert False, "never get here" 81 | except pychrome.CallMethodException: 82 | pass 83 | 84 | try: 85 | tab.Page.navigate(url="http://www.fatezero.org", invalid_params=123) 86 | except pychrome.CallMethodException: 87 | assert False, "never get here" 88 | 89 | tab.stop() 90 | 91 | 92 | def test_set_event_listener(): 93 | browser = pychrome.Browser() 94 | tab = browser.new_tab() 95 | 96 | def request_will_be_sent(**kwargs): 97 | tab.stop() 98 | 99 | tab.start() 100 | tab.Network.requestWillBeSent = request_will_be_sent 101 | tab.Network.enable() 102 | 103 | try: 104 | tab.Page.navigate(url="chrome://newtab/") 105 | except pychrome.UserAbortException: 106 | pass 107 | 108 | if not tab.wait(timeout=5): 109 | assert False, "never get here" 110 | 111 | 112 | def test_set_wrong_listener(): 113 | browser = pychrome.Browser() 114 | tab = browser.new_tab() 115 | 116 | tab.start() 117 | try: 118 | tab.Network.requestWillBeSent = "test" 119 | assert False, "never get here" 120 | except pychrome.RuntimeException: 121 | pass 122 | tab.stop() 123 | 124 | 125 | def test_get_event_listener(): 126 | browser = pychrome.Browser() 127 | tab = browser.new_tab() 128 | 129 | def request_will_be_sent(**kwargs): 130 | tab.stop() 131 | 132 | tab.start() 133 | tab.Network.requestWillBeSent = request_will_be_sent 134 | tab.Network.enable() 135 | try: 136 | tab.Page.navigate(url="chrome://newtab/") 137 | except pychrome.UserAbortException: 138 | pass 139 | 140 | if not tab.wait(timeout=5): 141 | assert False, "never get here" 142 | 143 | assert tab.Network.requestWillBeSent == request_will_be_sent 144 | tab.Network.requestWillBeSent = None 145 | 146 | assert not tab.get_listener("Network.requestWillBeSent") 147 | # notice this 148 | assert tab.Network.requestWillBeSent != tab.get_listener("Network.requestWillBeSent") 149 | 150 | tab.stop() 151 | 152 | 153 | def test_reuse_tab_error(): 154 | browser = pychrome.Browser() 155 | tab = browser.new_tab() 156 | 157 | def request_will_be_sent(**kwargs): 158 | tab.stop() 159 | 160 | tab.start() 161 | tab.Network.requestWillBeSent = request_will_be_sent 162 | tab.Network.enable() 163 | try: 164 | tab.Page.navigate(url="chrome://newtab/") 165 | except pychrome.UserAbortException: 166 | pass 167 | 168 | if not tab.wait(timeout=5): 169 | assert False, "never get here" 170 | 171 | try: 172 | tab.Page.navigate(url="http://www.fatezero.org") 173 | assert False, "never get here" 174 | except pychrome.RuntimeException: 175 | pass 176 | tab.stop() 177 | 178 | 179 | def test_del_event_listener(): 180 | browser = pychrome.Browser() 181 | tab = browser.new_tab() 182 | test_list = [] 183 | 184 | def request_will_be_sent(**kwargs): 185 | test_list.append(1) 186 | tab.Network.requestWillBeSent = None 187 | 188 | tab.start() 189 | tab.Network.requestWillBeSent = request_will_be_sent 190 | tab.Network.enable() 191 | tab.Page.navigate(url="chrome://newtab/") 192 | tab.Page.navigate(url="http://www.fatezero.org") 193 | 194 | if tab.wait(timeout=5): 195 | assert False, "never get here" 196 | 197 | assert len(test_list) == 1 198 | tab.stop() 199 | 200 | 201 | def test_del_all_event_listener(): 202 | browser = pychrome.Browser() 203 | tab = browser.new_tab() 204 | test_list = [] 205 | 206 | def request_will_be_sent(**kwargs): 207 | test_list.append(1) 208 | tab.del_all_listeners() 209 | 210 | tab.start() 211 | tab.Network.requestWillBeSent = request_will_be_sent 212 | tab.Network.enable() 213 | tab.Page.navigate(url="chrome://newtab/") 214 | 215 | if tab.wait(timeout=5): 216 | assert False, "never get here" 217 | 218 | assert len(test_list) == 1 219 | tab.stop() 220 | 221 | 222 | class CallableClass(object): 223 | def __init__(self, tab): 224 | self.tab = tab 225 | 226 | def __call__(self, *args, **kwargs): 227 | self.tab.stop() 228 | 229 | 230 | def test_use_callable_class_event_listener(): 231 | browser = pychrome.Browser() 232 | tab = browser.new_tab() 233 | 234 | tab.start() 235 | tab.Network.requestWillBeSent = CallableClass(tab) 236 | tab.Network.enable() 237 | try: 238 | tab.Page.navigate(url="chrome://newtab/") 239 | except pychrome.UserAbortException: 240 | pass 241 | 242 | if not tab.wait(timeout=5): 243 | assert False, "never get here" 244 | 245 | tab.stop() 246 | 247 | 248 | def test_status(): 249 | browser = pychrome.Browser() 250 | tab = browser.new_tab() 251 | 252 | assert tab.status == pychrome.Tab.status_initial 253 | 254 | def request_will_be_sent(**kwargs): 255 | tab.stop() 256 | 257 | tab.Network.requestWillBeSent = request_will_be_sent 258 | 259 | assert tab.status == pychrome.Tab.status_initial 260 | 261 | tab.start() 262 | tab.Network.enable() 263 | assert tab.status == pychrome.Tab.status_started 264 | 265 | try: 266 | tab.Page.navigate(url="chrome://newtab/") 267 | except pychrome.UserAbortException: 268 | pass 269 | 270 | if not tab.wait(timeout=5): 271 | assert False, "never get here" 272 | 273 | tab.stop() 274 | assert tab.status == pychrome.Tab.status_stopped 275 | 276 | 277 | def test_call_method_timeout(): 278 | browser = pychrome.Browser() 279 | tab = browser.new_tab() 280 | 281 | tab.start() 282 | tab.Page.navigate(url="chrome://newtab/", _timeout=5) 283 | 284 | try: 285 | tab.Page.navigate(url="http://www.fatezero.org", _timeout=0.8) 286 | except pychrome.TimeoutException: 287 | pass 288 | 289 | try: 290 | tab.Page.navigate(url="http://www.fatezero.org", _timeout=0.005) 291 | except pychrome.TimeoutException: 292 | pass 293 | 294 | tab.stop() 295 | 296 | 297 | def test_callback_exception(): 298 | browser = pychrome.Browser() 299 | tab = browser.new_tab() 300 | 301 | def request_will_be_sent(**kwargs): 302 | raise Exception("test callback exception") 303 | 304 | tab.start() 305 | tab.Network.requestWillBeSent = request_will_be_sent 306 | tab.Network.enable() 307 | tab.Page.navigate(url="chrome://newtab/") 308 | 309 | if tab.wait(timeout=3): 310 | assert False, "never get here" 311 | 312 | tab.stop() 313 | --------------------------------------------------------------------------------