1. 使用 tracemalloc 分析 python 内存使用情况

    使用 tracemalloc 分析 python 内存使用情况 工作中,遇到 tornado 启动的服务器,启动成功后单进程内存过大的问题。 尝试使用内存分析工具,分析代码中那部分占用的内存过多。 在 python3环境下, python自带的工具tracemalloc,可以分析内存的使用情况。 实例 建立如下的 tornado 服务,期望tracemalloc找到内存占用最大的代码块: import tracemalloc logs = [] tracemalloc.start(25) import tornado.ioloop import tornado.web class BadHandler(tornado.web.RequestHandler): memory_list = [i for i in range(1000000)] def get(self): global logs self.write("<br><br><br><br>".join(logs)) app = tornado.web.Application([ (r'/bad', BadHandler), ]) snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('traceback') logs.append("[ Top Biggest 10 block ]") for stat in top_stats[:10]: _msg = ["%s memory blocks: %.1f KiB" % (stat.count, stat.size / 1024)] for line in stat.traceback.format(): _msg.append(line.strip()) logs.append("<br>".join(_msg)) print("\n\n\n\n".join(logs).replace("<br>", "\n")) if __name__ == '__main__': app.listen(8080) tornado.ioloop.IOLoop.instance().start() 启动服务后, 结果可以通过查看控制台或访问 http://localhost:8080/bad得到。 输出结果如下: [ Top Biggest 10 block ] 999744 memory blocks: 35830.3 KiB File "/Users/rkfeng/code/streamit_demo/demo_memory/server.py", line 11 memory_list = [i for i in range(1000000)] File "/Users/rkfeng/code/streamit_demo/demo_memory/server.py", line 11 memory_list = [i for i in range(1000000)] File "/Users/rkfeng/code/streamit_demo/demo_memory/server.py", line 10 class BadHandler(tornado.web.RequestHandler): 2147 memory blocks: 784.9 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/Users/rkfeng/code/streamit_demo/demo_memory/server.py", line 7 import tornado.web 3986 memory blocks: 243.9 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/html/__init__.py", line 6 from html.entities import html5 as _html5 File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "", line 219 File "", line 941 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/escape.py", line 31 import html.entities as htmlentitydefs File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/log.py", line 36 from tornado.escape import _unicode File "", line 219 File "", line 678 File "", line 665 1758 memory blocks: 232.1 KiB File "", line 219 File "", line 734 File "", line 571 File "", line 658 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ast.py", line 27 from _ast import * File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/inspect.py", line 35 import ast File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/util.py", line 30 from inspect import getfullargspec as getargspec File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/escape.py", line 27 from tornado.util import PY3, unicode_type, basestring_type 1568 memory blocks: 153.5 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/util.py", line 30 from inspect import getfullargspec as getargspec File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/escape.py", line 27 from tornado.util import PY3, unicode_type, basestring_type File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/log.py", line 36 from tornado.escape import _unicode File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/concurrent.py", line 38 from tornado.log import app_log 1670 memory blocks: 142.8 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/util.py", line 48 import typing # noqa File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/escape.py", line 27 from tornado.util import PY3, unicode_type, basestring_type File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/log.py", line 36 from tornado.escape import _unicode File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/concurrent.py", line 38 from tornado.log import app_log 1255 memory blocks: 132.2 KiB File "", line 219 File "", line 922 File "", line 571 File "", line 658 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 101 import _ssl # if we can't import it, let the error propagate File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/selector_events.py", line 16 import ssl File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "", line 219 File "", line 1023 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/unix_events.py", line 21 from . import selector_events File "", line 219 File "", line 678 File "", line 665 File "", line 955 1303 memory blocks: 122.3 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/ssl.py", line 93 import ipaddress File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/selector_events.py", line 16 import ssl File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "", line 219 File "", line 1023 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/unix_events.py", line 21 from . import selector_events File "", line 219 File "", line 678 File "", line 665 File "", line 955 1179 memory blocks: 112.9 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/ioloop.py", line 41 import logging File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Users/rkfeng/code/streamit_demo/demo_memory/server.py", line 6 import tornado.ioloop 1675 memory blocks: 100.1 KiB File "", line 487 File "", line 779 File "", line 674 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/calendar.py", line 10 import locale as _locale File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/email/_parseaddr.py", line 16 import time, calendar File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/email/utils.py", line 33 from email._parseaddr import quote File "", line 219 File "", line 678 File "", line 665 File "", line 955 File "", line 971 File "/opt/streamit_demo/lib/python3.6/site-packages/tornado/web.py", line 65 import email.utils 通过输出结果可以找到内存占用最大的代码段为: class BadHandler(tornado.web.RequestHandler): memory_list = [i for i in range(1000000)] 进阶 参考官方文档, 常见的应用: 展示分配内存最多的前 n 个 py 文件(Display the 10 files allocating the most memory) 比较两次快照之间的内存差别(Compute differences: Take two snapshots and display the differences) 显示最大内存块回溯的代码(Get the traceback of a memory block)

    2019/10/19 技术

  2. spark 集群试用

    spark 集群使用 利用 docker 可以方便地搭建 spark 集群。 网上相关资源较少, 本文将分享作者的经验。 1. docker 搭建 spark集群 github 项目mvillarrealb/docker-spark-cluster, 提供了一个基于 docker 的 spark 集群搭建方案。 但有两个问题: 仅提供 Dockerfile 文件,国内环境创建 docker 时,速度非常慢 没有提供新手友好的入门实例,尤其是基于 python 的实例 本人在该项目基础上,完善入门实例,并将 docker 镜像发布到 docker hub 上。国内用户适用 docker 加速器,便可以很方便地将该项目运行起来。项目源码见github: frkhit/docker-spark-cluster。 环境搭建教程: 克隆项目: git clone git@github.com:frkhit/docker-spark-cluster.git 进入项目目录,使用前, 请设置 docker 加速器, 具体可以参考Docker Hub 镜像加速器 启动集群: docker-compose down && docker-compose up -d, 如果 docker-compose 没安装,可参考docker-compose 安装方法 访问 http://localhost:8080/ 即可访问 spark 集群。 更详细的教程可以参考 README.md 文件。 2. 向 docker 集群提交 python 代码任务 项目中提供一个 python 任务样例 data/spark-apps/test.py, 具体代码如下: # coding:utf-8 __author__ = 'rk.feng' from pyspark import SparkContext, SparkConf conf = SparkConf().set("spark.worker.cleanup.enabled", False) sc = SparkContext( master="spark://spark-master:7077", appName="WordCount", environment={"PYSPARK_PYTHON": "python3"}, conf=conf ) lines = sc.textFile("/spark/README.md") print("count of text is {}".format(lines.count())) result = lines.flatMap(lambda x: x.split(" ")).countByValue() for key, value in result.items(): print("%s %i" % (key, value)) 该任务用于统计spark 自带的/spark/README.md 文件的各个单词出现的次数。 任务提交的方法为, 在项目根目录下, 执行 ./crimes-app.sh。 访问 http://localhost:8080/ 可以看到执行情况。 3. 示例: 在 master 中收集各个 worker 的执行日志 在 data/spark-apps/collect_log.py 中 写入如下代码: # coding:utf-8 __author__ = 'rk.feng' from pyspark import SparkContext, SparkConf import time def do_some_job(_line): # do some thine time.sleep(2) # create logger log_info = "I Got line: {}!".format(_line) print("Cannot show: {}".format(log_info)) return log_info conf = SparkConf().set("spark.worker.cleanup.enabled", False) sc = SparkContext( master="spark://spark-master:7077", appName="CollectLogTest", environment={"PYSPARK_PYTHON": "python3"}, conf=conf ) line_list = [ "LINE {}".format(i) for i in range(20)] new_pipe_rdd = sc.parallelize(line_list, len(line_list)) result_rdd = new_pipe_rdd.map(lambda v:do_some_job(v)) # do job result_list = result_rdd.collect() # print result print("Result of spark is:\n{}".format("\n".join(result_list))) 执行 ./crimes-app-collect-log.sh 命令, 即可看到 master 中输入各个 worker 返回的日志。

    2019/10/18 技术

  3. openresty使用笔记

    openresty使用笔记 1. 配置 openresty 环境 通过 docker-compose 安装 openresty docker-compose.yml 配置如下: version: '3.7' services: nginx: image: openresty/openresty container_name: nginx restart: always ports: - "80:80" - "443:443" volumes: - "./openresty_setting.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro" - "/var/log:/var/log" environment: TZ: Asia/Shanghai networks: - nginx-web networks: nginx-web: driver: bridge openresty 配置实例, openresty_setting.conf文件如下: # nginx.conf -- docker-openresty # # This file is installed to: # `/usr/local/openresty/nginx/conf/nginx.conf` # and is the file loaded by nginx at startup, # unless the user specifies otherwise. # # It tracks the upstream OpenResty's `nginx.conf`, but removes the `server` # section and adds this directive: # `include /etc/nginx/conf.d/*.conf;` # # The `docker-openresty` file `nginx.vh.default.conf` is copied to # `/etc/nginx/conf.d/default.conf`. It contains the `server section # of the upstream `nginx.conf`. # # See https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files # user root; worker_processes auto; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; # include /etc/nginx/mime.types; include /usr/local/openresty/nginx/conf/mime.types; default_type application/octet-stream; ## # Logging Settings ## access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; ## # Gzip Settings ## gzip on; gzip_static on; # gzip_vary on; gzip_proxied any; gzip_comp_level 4; gzip_buffers 16 8k; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; ## # Virtual Host Configs ## ################################## # demo.com ################################## server { listen 443 ssl http2; server_name demo.com; location / { proxy_pass http://django:6000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_send_timeout 600; proxy_connect_timeout 600; proxy_read_timeout 600; } } server { listen 80; server_name demo.com; rewrite ^(.*)$ https://$host$1 permanent; } } 2. 使用 lua 在请求 header 上写入请求到达时的时间戳 http { server { listen 80; location / { rewrite_by_lua ' ngx.req.set_header("RTIME", ngx.req.start_time()*1000) '; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://127.0.0.1:8888; } } }

    2019/10/17 技术

  4. mac下 python 报错 CERTIFICATE_VERIFY_FAILED

    mac下 python 报错 CERTIFICATE_VERIFY_FAILED python 下载时, 报错如下: ERROR: Unable to download webpage: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581)> (caused by URLError(SSLError(1, u'[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581)'),)) 多方搜索,得到错误原因urllib and “SSL: CERTIFICATE_VERIFY_FAILED” Error 根据 Craig Glennie分析如下: This isn't a solution to your specific problem, but I'm putting it here because this thread is the top Google result for "SSL: CERTIFICATE_VERIFY_FAILED", and it lead me on a wild goose chase. If you have installed Python 3.6 on OSX and are getting the "SSL: CERTIFICATE_VERIFY_FAILED" error when trying to connect to an https:// site, it's probably because Python 3.6 on OSX has no certificates at all, and can't validate any SSL connections. This is a change for 3.6 on OSX, and requires a post-install step, which installs the certifi package of certificates. This is documented in the ReadMe, which you should find at /Applications/Python\ 3.6/ReadMe.rtf The ReadMe will have you run this post-install script, which just installs certifi: /Applications/Python\ 3.6/Install\ Certificates.command Release notes have some more info: https://www.python.org/downloads/release/python-360/ 解决方法: 步骤 1: 重新安装 certifi: pip3 install certifi 步骤 2: 执行 /Applications/Python\ 3.6/Install\ Certificates.command

    2019/10/15 技术

  5. docker-compose 安装方法

    docker-compose 安装方法 本文提供一种简单的docker-compose安装方法。 compose项目提供了一种通过封装 docker 中 compose 容器的方式,执行 compose 命令的方案。 具体步骤是: 下载 compose 项目中releases提供的run.sh文件, 链接为https://github.com/docker/compose/releases/download/1.25.0-rc2/run.sh 赋予执行权限 sudo chmod +x run.sh 移动到系统目录下并改名为 docker-compose: sudo mv run.sh /usr/local/bin/docker-compose 执行 docker-compose命令: docker-compose ps

    2019/10/14 技术

  6. 系统代理

    系统代理 1. mac 系统 1.1 terminal 设置代理 在 ~/.bash_profile 中添加设置 alias proxytest='curl http://pv.sohu.com/cityjson?ie=utf-8' deproxy () { unset https_proxy unset http_proxy echo "clear proxy!" } acproxy (){ _proxy="http://localhost:8118" export https_proxy=$_proxy export http_proxy=$_proxy echo "using proxy: $_proxy!" } 使用示例: # 设置前 proxytest # 设置代理 acproxy proxytest # 取消代理 deproxy proxytest 1.2 全局设置代理 在 ~/.bash_profile 中添加设置 # global proxy acglobalproxy () { sudo networksetup -setwebproxy "Wi-Fi" 127.0.0.1 8118 sudo networksetup -setsecurewebproxy "Wi-Fi" 127.0.0.1 8118 sudo networksetup -setsocksfirewallproxy "Wi-Fi" 127.0.0.1 1080 } deglobalproxy () { sudo networksetup -setwebproxy "Wi-Fi" "" "" sudo networksetup -setwebproxystate "Wi-Fi" off sudo networksetup -setsecurewebproxy "Wi-Fi" "" "" sudo networksetup -setsecurewebproxystate "Wi-Fi" off sudo networksetup -setsocksfirewallproxy "Wi-Fi" "" "" sudo networksetup -setsocksfirewallproxystate "Wi-Fi" off } 使用示例: # 设置前 proxytest # 设置代理 acglobalproxy proxytest # 取消代理 deglobalproxy proxytest

    2019/10/08 技术

  7. mac 下安装 adb

    mac 下安装 adb 原理, 使用 python 包 airtest自带的 adb, 实现 adb 安装。 步骤如下: 安装Airtest, 具体为 pip install -U airtest 为 adb 增加执行权限, chmod +x {your_python_path}/site-packages/airtest/core/android/static/adb/mac/adb, 如, {your_python_path}为 /opt/py35/lib/python3.5 设置全局配置: 在~/.bash_profile 中, 添加 export PATH=$PATH:/opt/py35/lib/python3.5/site-packages/airtest/core/android/static/adb/mac adb 使用: source ~/.bash_profile && adb devices 注意,刚方法适用于 linux, 区别在于, linux 下的 adb 位于 {your_python_path}/site-packages/airtest/core/android/static/adb/linux目录下

    2019/10/07 技术

  8. scrapy项目作为工具库使用

    scrapy项目作为工具库使用 1. 目录树 pysrc - main.py - items.py - ScrapyDemo - scrapy.cfg - ScrapyDemo - spiders - __init__.py - DemoCrawler.py - __init__.py - items.py - pipelines.py - settings.py - utils.py - demo_api.py 2. scrapy项目提供对外接口 在 ScrapyDemo 中 新建文件 demo_api.py, 提供对外访问接口 # coding:utf-8 import json import os import sys root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if root_dir not in sys.path: sys.path.append(root_dir) from scrapy.crawler import CrawlerProcess from ScrapyDemo.spiders.DemoCrawler import DemoSpider, logger from ScrapyDemo.utils import get_settings, reset_settings def run_demo_spider(query: str, proxy: dict, output_file: str, lang='', max_page: int = None): """ :param proxy: :param output_file: :param query: :param lang: :param max_page: :return: """ # 记录旧的环境变量 old_environ_dict = {} # proxy for key, value in proxy.items(): old_environ_dict[key] = os.environ.get(key) os.environ[key] = value if proxy and ("https_proxy" not in proxy and "HTTPS_PROXY" not in proxy): logger.warning("https_proxy not found in proxy!") try: settings = get_settings() # 修改 settings settings.set("XXX", output_file, priority="project") process = CrawlerProcess(settings=settings) process.crawl(DemoSpider, query=query, lang=lang, max_page=max_page) process.start() finally: # 恢复环境变量 for key, value in old_environ_dict.items(): if value is None: if key in os.environ: del os.environ[key] else: os.environ[key] = value reset_settings() def run_scrapy(cmd_json: str): """ 执行命令 """ # parse args cmd_kwargs = json.loads(cmd_json) # run spider run_demo_spider(**cmd_kwargs) if __name__ == '__main__': run_scrapy(sys.argv[1]) 3. 主项目通过subprocess调用 scrapy 在主项目main.py中, 通过subprocess调用 scrapy 项目 # coding:utf-8 import json import logging import os import subprocess import sys import arrow import shortuuid from items import DemoItem _cur_dir = os.path.dirname(__file__) def _run_scrapy(**kwargs): """ 执行 scrapy 任务""" cur_dir = os.getcwd() try: os.chdir(os.path.join(_cur_dir, "ScrapyDemo")) subprocess.run([sys.executable, "ScrapyDemo/demo_api.py", json.dumps(kwargs)]) finally: os.chdir(cur_dir) def search(query: str, proxy: dict, lang='', max_page: int = None, logger: logging.Logger = None) -> [DemoItem]: """ :param proxy: :param query: :param lang: :param max_page: :param logger: :return: """ logger = logger or logging.getLogger("xxx") # 数据文件 data_dir = "/tmp/data" if not os.path.exists(data_dir): os.makedirs(data_dir) uid = arrow.get().format("YYYYMMDD") + "_" + shortuuid.ShortUUID().random(length=8) output_file = os.path.join(data_dir, "{}.json".format(uid)) # 启动任务 if proxy and ("https_proxy" not in proxy and "HTTPS_PROXY" not in proxy): logger.warning("https_proxy not found in proxy!") _run_scrapy(**dict( query=query, proxy=proxy, lang=lang, max_page=max_page, output_file=output_file, )) # 解析结果 result_list = [] if not os.path.exists(output_file): logger.error("{} not found!".format(output_file)) return result_list result_dict = {} with open(output_file, "r", encoding="utf-8") as f: for line in f: if len(line) > 2: try: raw_item_dict = json.loads(line.strip()) _index = raw_item_dict.pop("index") if _index in result_dict: logger.warning("item index {} exists before!".format(_index)) demo_item = DemoItem() demo_item["id"] = raw_item_dict.get("ID") result_dict[_index] = demo_item except Exception as e: logger.error(e) # 按照 index 排序 logger.info("index list {}".format([_index for _index in sorted(result_dict.keys())])) for _index in sorted(result_dict.keys()): result_list.append(result_dict[_index]) # 删除临时文件 return result_list

    2019/10/06 技术

  9. charles over proxy

    charles over proxy 使用 charles 对安卓应用进行抓包时,会遇到部分应用必须使用代理才能上网的问题。 解决思路是, charles外接代理。原理如下所示: Android App --> charles --> other proxy --> internet 具体操作如下: 安卓中设置 charles 提供的代理,以便抓包 charles 中设置外部代理,设置方法为依次展开Proxy --> External Proxy Settings..., 填写外部代理即可。

    2019/10/05 技术

  10. 使用 markdown 制作 ppt

    使用 markdown 制作 ppt reveal.js提供了一种利用 markdown 生成 ppt 的方法。 可以使用vscode及vscode-reveal插件,搭建书写环境。 1. 环境配置 安装过程: 安装 vscode 打开vscode, 安装插件 vscode-reveal 2. 示例 步骤一、在 vscode 中新建 sample.md, 并写入如下内容: --- theme : "night" transition: "slide" highlightTheme: "monokai" logoImg: "logo.png" slideNumber: false title: "XXX调研报告" --- <style type="text/css"> .reveal p { text-align: left; } .reveal ul { display: block; } .reveal ol { display: block; } </style> # XXX调研报告 --- ## 1. 概述 --- ## 2. 行业现状 示例要点: ppt 基本配置 ppt 样式写在 style 中 步骤二、通过快键键Command + Shift + P 选择 Revealjs: Open presentation in browser, 即可在浏览器中预览ppt。也可以通过选择 Revealjs: Export in PDF, 导出 pdf 文件。

    2019/10/04 技术