├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── requirements.txt ├── setup.py ├── snapshot_pyppeteer ├── __init__.py ├── _version.py └── snapshot.py ├── test.sh └── test ├── render.html ├── requirements.txt └── test_snapshot.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=crlf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .idea 9 | # Distribution / packaging 10 | .Python 11 | test/*.html 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | *~ 92 | 93 | # for mac 94 | .DS_store 95 | 96 | # for vscode 97 | .vscode/ 98 | 99 | # example 100 | example/*.png 101 | example/*.html 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | notifications: 4 | email: 5 | recipients: 6 | - 379978424@qq.com 7 | on_success: always # default: change 8 | on_failure: always # default: always 9 | python: 10 | - "3.7-dev" 11 | - "3.6" 12 | before_install: 13 | - pip install -r test/requirements.txt 14 | script: 15 | - python setup.py install 16 | - bash test.sh 17 | after_success: 18 | cd test && codecov -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 sunhailin-Leo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

snapshot-pyppeteer

2 |

3 | Render pyecharts as image via pyppeteer 4 |

5 |

6 | 7 | Author 8 | 9 |

10 |

11 | 12 | Travis Build Status 13 | 14 | 15 | Appveyor Build Status 16 | 17 | 18 | Codecov 19 | 20 | 21 | PyPI version 22 | 23 | 24 | PyPI - Python Version 25 | 26 |

27 |

28 | 29 | PyPI - Format 30 | 31 | 32 | Contributions welcome 33 | 34 | 35 | License 36 | 37 |

38 | 39 | ## 🔰 安装 40 | 41 | **pip 安装** 42 | ```shell 43 | # 安装 44 | $ pip install snapshot-pyppeteer 45 | 46 | # 安装完后建议执行 chromium 安装命令 47 | pyppeteer-install 48 | ``` 49 | 50 | ## 📊 生成图片 51 | 52 | * Pycharm 环境下 53 | ```python 54 | from snapshot_pyppeteer import snapshot 55 | 56 | from pyecharts.charts import Bar 57 | from pyecharts.faker import Faker 58 | from pyecharts import options as opts 59 | from pyecharts.render import make_snapshot 60 | 61 | 62 | def bar_base() -> Bar: 63 | c = ( 64 | Bar() 65 | .add_xaxis(Faker.choose()) 66 | .add_yaxis("商家A", Faker.values()) 67 | .add_yaxis("商家B", Faker.values()) 68 | .set_global_opts(title_opts=opts.TitleOpts(title="Bar-基本示例", subtitle="我是副标题")) 69 | ) 70 | make_snapshot(snapshot, c.render(), "bar.png") 71 | 72 | 73 | if __name__ == '__main__': 74 | bar_base() 75 | ``` 76 |

77 | 78 |

79 | 80 | * Notebook 环境下 81 | ```jupyterpython 82 | #%% 83 | 84 | from snapshot_pyppeteer import snapshot 85 | 86 | from pyecharts.charts import Bar 87 | from pyecharts.faker import Faker 88 | from pyecharts import options as opts 89 | from pyecharts.render import make_snapshot 90 | 91 | #%% 92 | 93 | c = ( 94 | Bar() 95 | .add_xaxis(Faker.choose()) 96 | .add_yaxis("商家A", Faker.values()) 97 | .add_yaxis("商家B", Faker.values()) 98 | .set_global_opts(title_opts=opts.TitleOpts(title="Bar-基本示例", subtitle="我是副标题")) 99 | ) 100 | make_snapshot(snapshot, c.render(), "bar.png", notebook=True) 101 | 102 | #%% 103 | ``` 104 |

105 | 106 |

107 | 108 | ## ☁️ 扩展参数 109 | * 在 `pyecharts` 的 `make_snapshot` 中允许传递 `kwargs` 类型的参数: 110 | 111 | 参数名称 | 参数类型 | 参数默认值 | 参数说明 112 | -|-|-|- 113 | notebook | bool | False | 判断渲染环境是否处于 notebook 114 | remoteAddress | str | 空字符串 | 用于 docker browserless 的地址配置 115 | 116 | ## 🚀Docker browserless 的使用说明 117 | * 文档:[browserless 文档地址](https://docs.browserless.io/) 118 | * 使用步骤: 119 | * 第一步:省略 docker 环境构建 120 | * 第二步:拉取镜像 & 启动容器 121 | ```shell script 122 | $ docker pull browserless/chrome:latest 123 | $ docker run -d -p 3000:3000 --shm-size 2gb --name browserless --restart always -e "DEBUG=browserless/chrome" -e "MAX_CONCURRENT_SESSIONS=10" browserless/chrome:latest 124 | ``` 125 | * 第三步:渲染参数 `remoteAddress` 按此方式填入:`ws://<容器服务的IP>:3000` 126 | 127 | ## 📃 License 128 | 129 | MIT [©sunhailin-Leo](https://github.com/sunhailin-Leo) 130 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | 3 | matrix: 4 | - PYTHON: "C:\\Python36-x64" 5 | PYTHON_VERSION: "3.6" 6 | - PYTHON: "C:\\Python37-x64" 7 | PYTHON_VERSION: "3.7" 8 | 9 | install: 10 | - "%PYTHON%\\python.exe -m pip install -r requirements.txt" 11 | - cd test 12 | - "%PYTHON%\\python.exe -m pip install -r requirements.txt" 13 | build: off 14 | 15 | test_script: 16 | - "%PYTHON%/Scripts/coverage run -m unittest && cd .. && %PYTHON%/Scripts/flake8 --exclude build --max-line-length 89 --ignore=F401" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyppeteer 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from shutil import rmtree 4 | 5 | from setuptools import Command, find_packages, setup 6 | 7 | # RELEASE STEPS 8 | # $ python setup.py upload 9 | 10 | __title__ = "snapshot-pyppeteer" 11 | __description__ = "Render pyecharts as image via pyppeteer" 12 | __url__ = "https://github.com/pyecharts/snapshot-pyppeteer" 13 | __author_email__ = "379978424@qq.com" 14 | __license__ = "MIT" 15 | __requires__ = ["pyppeteer", "nest_asyncio"] 16 | __keywords__ = ["snapshot-pyppeteer", "snapshot", "pyppeteer"] 17 | here = os.path.abspath(os.path.dirname(__file__)) 18 | 19 | __version__ = "0.0.2" 20 | 21 | 22 | class UploadCommand(Command): 23 | description = "Build and publish the package." 24 | user_options = [] 25 | 26 | @staticmethod 27 | def status(s): 28 | print("✨✨ {0}".format(s)) 29 | 30 | def initialize_options(self): 31 | pass 32 | 33 | def finalize_options(self): 34 | pass 35 | 36 | def run(self): 37 | try: 38 | self.status("Removing previous builds…") 39 | rmtree(os.path.join(here, "dist")) 40 | rmtree(os.path.join(here, "build")) 41 | rmtree(os.path.join(here, "{0}.egg-info".format(__title__))) 42 | except OSError: 43 | pass 44 | 45 | self.status("Building Source and Wheel distribution…") 46 | os.system("{0} setup.py bdist_wheel".format(sys.executable)) 47 | 48 | self.status("Uploading the package to PyPI via Twine…") 49 | os.system("twine upload dist/*") 50 | 51 | self.status("Pushing git tags…") 52 | os.system('git tag -a v{0} -m "release version v{0}"'.format(__version__)) 53 | os.system("git push origin v{0}".format(__version__)) 54 | 55 | sys.exit() 56 | 57 | 58 | setup( 59 | name=__title__, 60 | version=__version__, 61 | description=__description__, 62 | url=__url__, 63 | author="sunhailin-Leo", 64 | author_email=__author_email__, 65 | license=__license__, 66 | packages=find_packages(exclude=("test",)), 67 | keywords=__keywords__, 68 | install_requires=__requires__, 69 | zip_safe=False, 70 | include_package_data=True, 71 | classifiers=[ 72 | "Development Status :: 4 - Beta", 73 | "Environment :: Console", 74 | "Intended Audience :: Developers", 75 | "License :: OSI Approved :: MIT License", 76 | "Operating System :: OS Independent", 77 | "Programming Language :: Python", 78 | "Programming Language :: Python :: 3.6", 79 | "Programming Language :: Python :: 3.7", 80 | "Topic :: Software Development :: Libraries", 81 | ], 82 | cmdclass={"upload": UploadCommand}, 83 | ) 84 | -------------------------------------------------------------------------------- /snapshot_pyppeteer/__init__.py: -------------------------------------------------------------------------------- 1 | from snapshot_pyppeteer._version import __author__, __version__ 2 | -------------------------------------------------------------------------------- /snapshot_pyppeteer/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.2" 2 | __author__ = "sunhailin-Leo" 3 | -------------------------------------------------------------------------------- /snapshot_pyppeteer/snapshot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | from typing import Any 4 | 5 | from pyppeteer import launch, connect 6 | 7 | SNAPSHOT_JS = ( 8 | "echarts.getInstanceByDom(document.querySelector('div[_echarts_instance_]'))." 9 | "getDataURL({type: '%s', pixelRatio: %s, excludeComponents: ['toolbox']})" 10 | ) 11 | 12 | SNAPSHOT_SVG_JS = "document.querySelector('div[_echarts_instance_] div').innerHTML" 13 | 14 | 15 | async def run_snapshot( 16 | html_path: str, 17 | file_type: str, 18 | pixel_ratio: int = 2, 19 | delay: int = 2, 20 | **kwargs 21 | ) -> Any: 22 | # You can load remote html file or local file 23 | if not html_path.startswith("http"): 24 | html_path = "file://" + os.path.abspath(html_path) 25 | 26 | # You can use browserless by docker(chrome browser) 27 | """ 28 | $ docker pull browserless/chrome:latest 29 | $ docker run -d -p 3000:3000 --shm-size 2gb --name browserless --restart always \ 30 | -e "DEBUG=browserless/chrome" -e "MAX_CONCURRENT_SESSIONS=10" \ 31 | browserless/chrome:latest 32 | # the args `remoteAddress` is "ws://:3000" 33 | """ 34 | remote_address = kwargs.get("remoteAddress") 35 | if remote_address is not None: 36 | browser = await connect({"browserWSEndpoint": kwargs.get("remoteAddress")}) 37 | else: 38 | browser = await launch({"headless": True}) 39 | 40 | # Init and config code 41 | page = await browser.newPage() 42 | await page.setJavaScriptEnabled(enabled=True) 43 | await page.goto(html_path) 44 | await asyncio.sleep(delay) 45 | 46 | # Generate js function 47 | if file_type == "svg": 48 | snapshot_js = SNAPSHOT_SVG_JS 49 | else: 50 | snapshot_js = SNAPSHOT_JS % (file_type, pixel_ratio) 51 | 52 | # execute js function 53 | execute_js_result = await page.evaluate(snapshot_js) 54 | 55 | # disconnect or close the browser session 56 | if remote_address is not None: 57 | await browser.disconnect() 58 | else: 59 | await browser.close() 60 | return execute_js_result 61 | 62 | 63 | def make_snapshot( 64 | html_path: str, file_type: str, pixel_ratio: int = 2, delay: int = 2, **kwargs 65 | ) -> Any: 66 | # For notebook environment 67 | is_notebook = kwargs.get("notebook", False) 68 | if is_notebook: 69 | import nest_asyncio 70 | nest_asyncio.apply() 71 | 72 | snapshot_result = asyncio.get_event_loop().run_until_complete( 73 | run_snapshot( 74 | html_path=html_path, 75 | file_type=file_type, 76 | pixel_ratio=pixel_ratio, 77 | delay=delay, 78 | **kwargs 79 | ) 80 | ) 81 | return snapshot_result 82 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | cd test 2 | coverage run -m unittest && cd .. && flake8 --exclude build --max-line-length 89 --ignore=F401 -------------------------------------------------------------------------------- /test/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Awesome-pyecharts 6 | 7 | 8 | 9 | 10 |
11 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | nose 2 | codecov 3 | coverage 4 | flake8 5 | pyppeteer -------------------------------------------------------------------------------- /test/test_snapshot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import unittest 4 | 5 | from pyppeteer import launch 6 | 7 | 8 | class TestBrowser(unittest.TestCase): 9 | SNAPSHOT_JS = ( 10 | "echarts.getInstanceByDom(document.querySelector('div[_echarts_instance_]'))." 11 | "getDataURL({type: '%s', pixelRatio: %s, excludeComponents: ['toolbox']})" 12 | ) 13 | 14 | def setUp(self) -> None: 15 | super().setUp() 16 | self.loop = asyncio.get_event_loop() 17 | 18 | async def _make_snapshot(self): 19 | html_path = "file://" + os.path.abspath("render.html") 20 | browser = await launch({"headless": True}) 21 | page = await browser.newPage() 22 | await page.setJavaScriptEnabled(enabled=True) 23 | await page.goto(html_path) 24 | await asyncio.sleep(2) 25 | snapshot_js = self.SNAPSHOT_JS % ("png", 2) 26 | execute_js_result = await page.evaluate(snapshot_js) 27 | self.assertNotEqual(execute_js_result, "{}") 28 | await browser.close() 29 | 30 | async def test_snapshot_base(self): 31 | self.loop.run_until_complete(self._make_snapshot()) 32 | --------------------------------------------------------------------------------