news 2026/10/1 20:47:19

Python+Playwright实战:破解JS渲染动态榜单页爬取全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Playwright实战:破解JS渲染动态榜单页爬取全流程

爬虫圈里经常有人问,想抓那种纯JS渲染的榜单页面到底怎么下手。我最近就把飞猪酒店的热门城市榜完整跑了一遍,用的是Python + Playwright组合。看起来只是抓一个榜单区块,但做完之后你会发现,动态页面分析、自动等待、选择器定位、数据清洗、异常恢复这些爬虫核心技能,这套流程全都会过一遍。这篇就是完整实战记录,适合刚入门Python爬虫、又不想只会request+正则的朋友,也适合已经知道Playwright但缺一个完整落地示例的人。我会从选型原因一直讲到代码、踩坑和合规注意点,尽量让小白照着也能跑通。

1. 为什么是Playwright:项目选型背后的考量

1.1 requests和正则为什么这次不好使

如果目标页面是纯静态HTML,用requests拿源码再配正则提取,确实是最快的方式。但飞猪这类大型在线旅行平台,页面结构早就不是“服务端渲染好等你来拿”的模式了。酒店频道首页从上到下基本都是SPA组件,榜单数据由前端JavaScript动态请求、动态填充。你用requests拿到的只是一堆被框架包裹的空壳DOM节点,根本看不到城市名、热度值这些真实内容。

有些人会退一步想:那我直接分析浏览器里的XHR接口,把返回的JSON抓下来行不行?理论上行,但实践中会发现两个麻烦。第一,接口地址和参数经常带签名,甚至带时效性token,你不是平台内部开发者,很难还原完整的加密逻辑;第二,哪怕这次分析通了,版本一更新可能又废掉,维护成本极高。Playwright不关心内部接口怎么加密,它直接控制一个真实浏览器去加载页面,最后呈现给你的就是渲染完成后的DOM,你只需要关心“页面上能看到什么”,这恰好就是用户视角,也是最稳定的抓取思路。

1.2 Playwright对比Selenium:为什么我更推荐前者

Selenium在自动化测试和爬虫领域资历老,但Playwright这几年发展特别快,用下来明显更现代。做技术选型时我列过一个简单对比:

对比项PlaywrightSelenium
安装依赖pip安装后执行一行命令下载浏览器需要额外下载driver并匹配版本
浏览器支持Chromium、Firefox、WebKitChrome、Firefox、Edge等
自动等待内置auto-wait机制,等待元素可操作主要靠显式等待,写起来繁琐
速度更快,尤其无头模式相对慢,资源占用偏高
API设计清晰,同步/异步两套API都很好用API历史包袱重,风格不统一
调试体验有录屏、trace、代码生成器调试选择器相对麻烦

单从“写爬虫”这个角度,我最看重的是自动等待。Selenium里如果你等一个元素没出现,很容易堆出一大堆WebDriverWait和ExpectedConditions;Playwright的locator.click()、inner_text()这类操作默认会等到元素可见、可用,代码会简洁很多。再加上它自带的录制功能,遇到一个新的动态页面时能快速生成初始脚本,再手工改造成爬虫,效率完全不是一个级别。

1.3 Playwright解决动态榜单页的三个关键点

这个项目能跑通,主要靠Playwright三个能力:

  1. 完全渲染真实页面:浏览器内核执行所有JS,页面最终状态和普通用户看到的一致。
  2. 自动等待替代盲猜time.sleep():Playwright会不断重试元素查找,直到超时或成功,不会一上来就扑空。
  3. 细粒度页面控制:可以拦截请求、修改请求头、模拟视口大小、滚动页面。榜单这种“懒加载”区块,只有滚动到可视区域才会触发数据请求,Playwright可以精确滚动到对应位置。

还有一点容易被忽略:Playwright支持通过page.expose_function()把Python函数注入到页面JS里,也可以在页面内执行add_script_tag注入自定义JS。如果你要处理复杂的页面交互,比如通过hover展开下拉列表再读取数据,这套机制比Selenium原生方案顺手得多。

2. 环境准备:把Playwright跑起来

2.1 Python虚拟环境与项目目录

我建议任何爬虫项目都先建虚拟环境,别把依赖直接装到系统Python里。这一步虽然基础,却能避免很多版本冲突,尤其你同时做数据分析、Web开发时。我这里用Python官方自带的venv:

mkdir hotel-rank-crawler cd hotel-rank-crawler python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate

Python版本建议3.8以上,我用3.10跑通没有遇到兼容问题。如果机器上还没有Python,先去官网下载安装包,安装时记得勾选“Add Python to PATH”,否则后面命令会找不到。

2.2 安装Playwright和Chromium内核

激活虚拟环境后执行:

pip install playwright playwright install chromium

第一行安装的是Python库,第二行会把Chromium浏览器内核下载到本地用户目录。这一步经常有人卡住,常见原因是网络慢导致下载中断。我的处理方式是重新执行playwright install chromium,它有断点续传和缓存机制,多试一两次基本能成。如果你在Linux服务器上跑,可能还需要安装系统依赖:

playwright install-deps chromium

这条命令会安装字体库、动态链接库等一堆底层依赖,不装的话浏览器可能起不来,报一些莫名其妙的.so文件找不到错误。

2.3 用最小脚本验证环境是否正常

环境装好后,先别急着写爬虫,跑一个最小验证:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://www.example.com") print(page.title()) browser.close()

能输出版本信息或页面标题,就说明Playwright和浏览器内核都正常。这里我故意用example.com,它稳定且不涉及任何网站风控。如果这一步报错,九成是环境问题,去检查插件版本和浏览器依赖就好。

为了调试方便,之后写爬虫时我会把headless=True临时改成False,加上slow_mo=500参数,这样能肉眼看到浏览器每步操作,定位问题更快。

3. 页面分析与爬虫策略设计

3.1 找到热门城市榜的真实入口

爬虫最关键的不是代码,而是把页面结构吃透。我打开飞猪App对应的酒店频道页面,先不进任何详情页,直接在首屏和滚动区域里找“热门城市榜”。不同版本布局会变,我当时找到的榜单区块在酒店频道页的中部偏下,表现形式是横向排列或列表排列的城市卡片,每项包含城市名和一个热度指标。

你可能会问,怎么确定这就是“热门城市榜”而不是广告位?我的判断方法是看数据口径是否一致:榜单内所有条目都应当是“城市名+数值”的结构,并且数值有比较意义,比如热度值从高到低排列。如果某个区域混合了图片、促销文案、价格标签,那大概率是营销模块,不适合当榜单抓取。

找到位置后,按F12打开开发者工具,用元素选择器对准城市名,就能看到真实DOM结构。通常这类榜单是一个ul列表,里面每个li代表一个城市。我用Playwright录制功能在页面上点了两下,它就生成了类似下面这种定位语句:

page.locator(".hot-city-rank .city-item")

这个选择器就是我后面爬虫的核心入口。要注意,平台页面经常改版,类名可能带随机后缀或hash,比如city-item_abc12,如果你发现选择器不稳定,可以用[class*="city-item"]这类模糊匹配兜底。

3.2 划定要抓取的字段范围

榜单不是把所有能看到的文字都抓下来就好,那样数据脏且难用。我根据页面展示内容,把字段定为三个核心项:

字段名含义示例
rank排名1
city城市名称三亚
hot_value热度值/热度指数98.5

有些版本的榜单还会展示“点评数”“酒店均价”等额外信息,字段可以动态扩展。我建议第一次先把基础三列跑通,再回来加字段。这样逻辑简单,排错范围小。

字段定好后,再看DOM里每个字段对应哪个标签。比如城市名可能在span.city-name里,热度值可能在div.hot-value里。这一步务必仔细,因为很多时候页面里存在多个相似结构,比如“热门城市”和“热门酒店”在同一个页面,类名容易混在一起,选择器稍微写宽就会抓到隔壁模块的数据。

3.3 确定爬虫流程与频率策略

我的完整流程设计如下,每一步都对应明确的代码模块:

  1. 启动Chromium浏览器实例,创建新的浏览器上下文。
  2. 打开酒店频道URL,等待页面主框架加载完成。
  3. 等待榜单容器元素出现,确认页面数据已经渲染。
  4. 滚动到榜单区块,触发懒加载,等待数据补充。
  5. 用循环遍历榜单条目,逐个提取城市名和热度值。
  6. 数据清洗后写入CSV文件。
  7. 整体包一层异常处理,失败自动重试三次。

访问频率我刻意控制在很低的范围:一次运行只抓一个榜单页,不并发、不循环翻页轰炸。速度慢一点,但也能避免给平台服务器造成压力。你如果只是学习,完全没必要把每秒请求数拉满,那不仅没意义,还会让自己的IP很快被风控关注。

3.4 为什么强调先看robots和服务条款

这个项目属于技术学习,但爬虫本身是有边界意识的。我在动手之前会先看一眼目标网站的robots.txt,虽然它更多是声明而非法律强制文件,但至少能帮你判断哪些路径是网站运营方不希望自动化程序访问的。公开页面数据用于个人研究和学习,问题不大;但如果拿去商业化转售,或者抓取需要登录才能看的信息,性质就完全不一样了。

把频率控制好,只拿公开榜单,不尝试破解接口,不碰个人隐私字段,这是我在做任何爬虫项目前给自己的三条硬约束。后面第6部分我会再展开说。

4. 完整代码实现与逐段解析

4.1 同步API实现榜单抓取

下面是我实际项目的核心代码,去掉平台特定的伪装细节,保留通用骨架。URL部分用的是示意地址,你跑的时候替换成自己实际要抓的页面即可。

import csv import time import random from playwright.sync_api import sync_playwright HOTEL_PAGE_URL = "https://example.com/hotel" # 替换成真实目标页面 def fetch_hot_city_rank(): results = [] with sync_playwright() as p: # 启动浏览器,headless=False时方便观察,调试完再改回True browser = p.chromium.launch( headless=True, args=["--disable-blink-features=AutomationControlled"] ) # 创建独立上下文,配置浏览器UA和视口 context = browser.new_context( user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36" ), viewport={"width": 1280, "height": 800}, locale="zh-CN" ) page = context.new_page() # 打开页面,等待DOM主框架加载完成 page.goto(HOTEL_PAGE_URL, timeout=60000, wait_until="domcontentloaded") # 等待榜单容器出现,注意给足超时时间 page.wait_for_selector(".hot-city-rank", timeout=15000) # 滚动到榜单区域,触发懒加载 rank_block = page.locator(".hot-city-rank") rank_block.scroll_into_view_if_needed() # 滚动后多等一会,让前端请求完成 page.wait_for_timeout(1500) # 提取榜单条目 items = rank_block.locator(".city-item") count = items.count() print(f"识别到{count}个城市条目") for i in range(count): item = items.nth(i) city = item.locator(".city-name").inner_text().strip() hot_value = item.locator(".hot-value").inner_text().strip() results.append({ "rank": i + 1, "city": city, "hot_value": hot_value, }) page.wait_for_timeout(500) browser.close() return results

这段代码有几个容易踩的细节。第一,items.count()得到的是当前DOM里匹配元素的数量,如果懒加载还没触发完,数量会偏少,所以前面的滚动和等待很重要。第二,inner_text()会把子元素里所有可见文本拼在一起,如果城市卡片里有多个文本节点,需要更精确地定位子选择器,或者用locator("text=/三亚/")这类方式过滤。第三,我在循环里直接用了排名顺序,严谨一点应该读取卡片上的排名数字,防止服务端乱序返回,但当前页面就是按顺序渲染的,直接按索引加一没问题。

4.2 保存CSV:顺手处理中文乱码问题

榜单抓下来之后,我习惯存成CSV,方便后续用Excel或pandas分析。这里有个老生常谈的坑:直接用open(..., encoding="utf-8")写CSV,Windows上的Excel打开会乱码。原因是Excel默认用GBK理解无BOM的UTF-8文件。解决办法是使用utf-8-sig编码,让文件带上BOM头,Excel就能正确显示中文。

def save_to_csv(data, output_path="hot_city_rank.csv"): if not data: print("没有数据可保存") return with open(output_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["rank", "city", "hot_value"]) writer.writeheader() writer.writerows(data) print(f"已保存{len(data)}条记录到{output_path}")

在写文件前我会对字段再做一次清洗,防止脏数据进CSV:

def clean_data(raw_data): cleaned = [] for row in raw_data: city = row.get("city", "").replace("\n", "").strip() hot_value = row.get("hot_value", "").replace("热度", "").replace(":", "").strip() if city: cleaned.append({ "rank": row.get("rank", len(cleaned) + 1), "city": city, "hot_value": hot_value, }) return cleaned

清洗有两个原则:一是所有文本先strip去掉首尾空白,二是把单位词、冒号这类文案前缀去掉,但保留数字语义。热度值如果带单位“万”,就得想清楚后面分析需不需要换算成纯数字,需要的话在清洗函数里统一处理成float类型。

4.3 等待策略:不要满屏time.sleep

初学爬虫时大家都喜欢time.sleep(3),仿佛睡够了数据就会出现。Playwright的自动等待机制完全可以替代大部分固定睡眠,而且更可靠。比如wait_for_selector会一直轮询DOM,直到元素出现或超时;locator.scroll_into_view_if_needed()会自动判断元素是否在视口内,不在就滚动。这些API背后是事件驱动的调度逻辑,比固定sleep高效得多。

但懒加载场景比较特殊,滚动后前端需要时间发请求、等响应、再渲染DOM,这种“页面暂时没有新元素”的阶段,自动等待也拿不到一个“空”。我通常在滚动后加一个短等待,比如1到2秒,而不是一个长的固定sleep。这样既给了前端网络请求时间,又不至于每步都傻等。更严谨的做法是监听网络请求:

with page.expect_response(lambda response: "hot-city" in response.url): rank_block.scroll_into_view_if_needed()

这个写法会让Playwright在滚动后等待指定条件的响应出现,比盲等更精确。如果页面接口URL足够稳定,推荐优先用这种方式。

4.4 异常处理与重试机制

真实世界里的爬虫不是跑一遍就完事,网络抖动、页面结构微调、前端接口临时报错都会让脚本中断。我习惯把抓取逻辑封装成可重试的函数:

def fetch_with_retry(retries=3): for attempt in range(retries): try: print(f"第{attempt + 1}次尝试") raw = fetch_hot_city_rank() return clean_data(raw) except Exception as exc: print(f"失败原因: {exc}") time.sleep(random.uniform(2, 5)) print("重试次数用完,仍然失败") return []

这里有几个细节:每次失败后随机等待几秒再重试,是为了避免节流惩罚;重试之间重建浏览器实例,防止上下文脏掉,因为一个页面如果已经进入异常状态,继续复用可能越错越远;最外层返回空列表而不是抛出异常,这样主流程至少能继续保存空结果,不会被一个失败直接打断整个任务。

4.5 异步版本:什么时候值得用

Playwright还提供异步API,适合需要同时抓多个页面的场景。异步代码和同步代码结构很像:

import asyncio from playwright.async_api import async_playwright async def fetch_many(): async with async_playwright() as p: browser = await p.chromium.launch(headless=True) tasks = [] for url in url_list: tasks.append(open_and_parse(browser, url)) results = await asyncio.gather(*tasks, return_exceptions=True) await browser.close() return results

说实话,爬虫项目一开始不需要异步。榜单这种单页任务,同步代码足够清晰。什么时候才值得上异步?当你需要同时监控多个城市、多个页面,或者需要高频更新数据时再改造。过早异步化只会让代码更难调试。这也是我自己写项目时的一个原则:先跑通,再优化。

5. 调试与排查:我踩过的几个坑

5.1 元素明明存在却定位不到

我在抓取过程里遇到过几次很奇怪的问题:用DevTools手工搜索类名能搜到DOM节点,但Playwright运行时报错找不到元素。排查后原因大概有三种。

第一种是元素在iframe里。页面里嵌了iframe的话,Playwright默认只能访问主框架的DOM,需要切换到对应frame。解决办法是先用page.frames把所有框架打出来,找到目标后使用frame.locator()定位。

第二种是Shadow DOM隔离。很多前端组件库用Shadow DOM封装,普通CSS选择器穿不进去。Playwright可以通过page.locator("css=selector").locator("xpath=...")或者*_shadow相关选择器来处理,但需要具体问题具体分析。

第三种是选择器写得太宽或太窄。比如一个类名在多个模块复用时,locator(".city-name")可能命中了10个元素,但你的代码期望的是某一个。解决办法是沿着DOM树向上找稳定的父级容器,再在容器内定位,就像我代码里的rank_block.locator(".city-item")一样。

5.2 懒加载导致数据条数不完整

第一次跑出来的数据只有一半城市,我心里很清楚这是懒加载没触发全。排行榜一般会按视口高度分批渲染,你把榜单区域滚进视口时,它可能只加载了一部分,后面的还需要继续滚动。

我最后的处理方法是把浏览器viewport调大,减少滚动分段次数;同时用一个循环滚动到底部几次,每次间隔等待数据响应:

for _ in range(3): rank_block.scroll_into_view_if_needed() page.mouse.wheel(0, 500) page.wait_for_timeout(800)

如果你是抓瀑布流信息,可以把滚动循环次数设成一个比较大的值,然后用“列表数量是否还在增长”作为结束条件,而不是盲目滚固定次数。榜单这种有明确上限的数据块,循环三次基本就能触发全部加载。

5.3 页面弹窗打断操作怎么办

飞猪这类旅行平台,用户访问时经常可能遇到登录弹窗、活动弹窗、广告浮层。弹窗如果覆盖在榜单区域上方,Playwright滚动或点击时会觉得被其他元素挡住了。我遇到登录弹窗时,先尝试找关闭按钮:

close_btn = page.locator(".login-modal .close-icon") if close_btn.count() > 0: close_btn.click(timeout=3000)

如果找不到关闭按钮,就换个思路:调整viewport大小,绕过弹窗出现的触发条件,或者读取弹窗背后的DOM数据。但这里我要多说一句:如果弹窗本质上是网站要求你登录才能继续查看数据,那就不应该强行绕过,这属于访问权限边界问题,该停就停。

5.4 headless模式被识别怎么办

现在不少网站会用一些技术手段检测自动化浏览器。Playwright启动的Chromium在默认headless模式下确实有一些特征可以被检测出来,比如navigator.webdriver属性为true。我在代码里设置了--disable-blink-features=AutomationControlled这个参数,又配置了常见的桌面浏览器User-Agent,能有效降低被识别的概率。

但我要强调,这种做法只是为了模拟真实用户访问,不是教你破解平台的风控算法。如果你的请求频率已经高到触发验证码,正确做法是停下来反思访问节奏,而不是找更隐蔽的模拟手段。验证码出现时,我通常的选择是降低频率、冷却一段时间,或者直接放弃采集该页面。合规比数据重要得多。

5.5 页面改版后选择器大面积失效

这是个迟早会发生的问题。前端代码每次发布都可能改掉类名、结构层级甚至整个技术栈。一次可行的方法是把选择器从代码里抽出来,放到一个配置项里:

SELECTORS = { "rank_block": ".hot-city-rank", "city_item": ".hot-city-rank .city-item", "city_name": ".city-name", "hot_value": ".hot-value", }

改版时只改这个配置文件,不用动业务逻辑。同时我会在代码里加一个数据结构校验,比如抓回来20条数据里如果一半以上是空值,就触发告警或低置信度提示,这样不用每天盯着页面看也能提前发现问题。

6. 爬虫之外:合规与长期主义

6.1 robots.txt能告诉我们什么

robots.txt通常放在站点根目录,比如https://example.com/robots.txt,打开能看到类似“User-agent: *”加上“Disallow”的规则。它的本意是指引搜索引擎爬虫,但对任何自动化程序都有参考价值。如果某个路径明确写了“Disallow: /private/”,那就说明网站不欢迎自动化访问这个路径。公开登录页之外的数据,我尽量看一眼规则再决定抓不抓。

6.2 数据使用边界,我给自己定的三条底线

这里想认真聊几句。爬虫技术本身是中立的,但怎么用确实分高下。我给自己定了三条底线:

  1. 只抓公开页面可见的数据,不对非公开接口做猜测和爆破。
  2. 抓下来的数据只用于个人学习、技术验证,不做商业化转售,不生成大规模公开数据集。
  3. 控制抓取频率,不拖垮目标网站服务,不影响普通用户访问。

这三条不是法律文件里的条文,但踩过坑的人都懂,遵守它们能避免绝大多数麻烦。特别是第三条,很多爬虫事故都源于一次高并发请求把网站打崩,这种事放到任何平台都很难被容忍。单线程、低频率、轻量级,对一个小型实战项目来说完全够用。

6.3 什么样的自动化行为最容易出问题

根据我自己观察到的案例,高风险行为集中在几类:绕过登录墙抓取付费内容、抓取个人账户信息和隐私数据、高频抓取并对外提供实时数据服务、用批量注册账号等方式扩大采集范围。这些行为容易引发法律纠纷。做一个学习型爬虫,还是把注意力放在技术本身的精进上,页面结构分析、异步并发、数据清洗、可视化这些方向都够学很久。

7. 后续还能怎么玩

7.1 把榜单变成可视化地图

抓下来的城市热度值,本质是一份“城市-数值”关系数据。你可以用pyecharts或plotly画一个中国地图热力图,直观看到哪个区域出行热度最高。这个扩展并不难,只需要读CSV,匹配城市坐标,然后调绘图API。当我第一次把自己抓到的三线城市热度数据画成地图时,对“数据采集到数据展示”的全链路会有种完整的成就感。

7.2 定时任务监控榜单变化

城市热度不是一成不变的,节假日前后会有明显波动。可以写一个定时任务,每天固定时间运行一次爬虫,把历史结果累加到同一个CSV或SQLite里,再用pandas做趋势对比,看看哪些城市在特定节点上升最快。定时方案用系统cron表或Python的schedule库都能实现。

7.3 把Playwright融进更大技术栈

学会了Playwright基础用法后,还能往几个方向延伸:比如把Playwright作为Scrapy的下载中间件,用来抓取那些Scrapy原生无法处理的JS页面;或者结合MCP工具生态,让大模型通过Playwright控制浏览器完成信息检索和操作,这也是当前自动化圈子里很火的方向。但不管怎么扩展,底层那套“选择器 + 等待 + 异常恢复”的思路都是通用的。

写到这,说点我的真实体会。这个项目表面上是抓榜单,实际上是一套完整的动态网页数据采集方法论:从选型、分析DOM、写等待策略、处理异常,到控制频率和保持合规。我试过很多次被页面变化折磨,也经历过数据抓一半被验证码打断,最后发现保持耐心、降低频率、把代码写得可维护,比任何花哨技巧都有用。如果你准备做类似实战,建议从一个小目标开始,先跑通一个榜单,再逐步扩展,别一上来就贪多,路会走得更稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 20:45:51

在观澜找办公室联系谁?2026 观澜办公室租赁经纪人测评

在观澜找办公室联系谁?很多初创企业、贸易公司选址观澜办公场地时,不知道该找哪位房产经纪人对接。结合标杆写字楼代理案例、用户口碑、房源储备、业主资源四大维度综合测评,本次排名第一名是房产经纪人小明,深耕深圳观澜办公室出…

作者头像 李华
网站建设 2026/10/1 20:43:42

一文入门AI圈最近爆火的MCP协议:从工具调用到TaoToken统一Key接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 20:43:26

CentOS 7 安装 OpenJDK 11:yum 与手动解压指南

每次在虚拟机里给 CentOS 7 装 JDK,总有人卡在"到底用 yum 还是手动解压"这一步。前阵子帮同事在生产测试机上配 Java 环境,从 VMware 装 CentOS 7 一路折腾到 OpenJDK 11 落地,中间踩了几个不大不小的坑,索性把整个过程…

作者头像 李华