├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
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 |
--------------------------------------------------------------------------------