先说结论,再说为什么。
Python 读取 JSON 文件,核心代码其实只有两行:
import json
with open("data.json", "r", encoding="utf-8") as file:
data = json.load(file)真正让初学者卡住的,往往不是“怎么读取”,而是读取之后不知道 data 到底是什么,也不知道该用键名、下标还是循环取值。
这事儿其实没那么玄。先把 JSON 和 Python 数据类型的对应关系弄清楚,后面的代码就顺了。
新建一个 user.json 文件:
{
"name": "沈砚",
"age": 35,
"active": true,
"skills": ["Python", "Scrapy", "Playwright"],
"profile": {
"city": "杭州",
"job": "数据工程师"
},
"projects": [
{
"name": "网页采集",
"status": "running"
},
{
"name": "语料清洗",
"status": "finished"
}
]
}读取代码如下:
import json
with open("user.json", "r", encoding="utf-8") as file:
data = json.load(file)
print(data)
print(type(data))输出的类型是:
<class 'dict'>因为这份 JSON 最外层是 {},所以读进 Python 后是字典 dict。如果最外层是 [],得到的就是列表 list。
我刚开始带新人写采集脚本时,会让他先打印一次 type(data)。这一步看起来有点笨,却能避开一半的提取错误——连顶层结构都没看清,就急着写 data["name"],报错并不奇怪。
字典可以直接通过键名取值:
name = data["name"]
age = data["age"]
print(name)
print(age)输出:
沈砚
35这里有个前提:你确定字段存在。
如果执行下面这行代码:
phone = data["phone"]Python 会报错:
KeyError: 'phone'字段可能缺失时,我更习惯用 get():
phone = data.get("phone", "未填写")
print(phone)找不到 phone,程序会返回默认值“未填写”,不会直接停掉。
说白了,方括号适合必填字段,get() 更适合接口数据、采集结果和用户配置——这些数据从来没有看上去那么规整。
现在要获取 profile 里的城市,可以连续写两个键名:
city = data["profile"]["city"]
print(city)输出:
杭州如果中间字段不一定存在,可以这样写:
profile = data.get("profile") or {}
city = profile.get("city", "未知城市")
print(city)为什么不直接写 data.get("profile", {})?
因为当 profile 不存在时,它确实会返回空字典;只不过,当 profile 存在但值为 null 时,Python 得到的是 None,继续调用 .get() 仍然会报错。加上 or {},这一层才算兜住了。
这地方很多人踩坑。
skills 在 JSON 中是数组,读进 Python 后会变成列表:
skills = data.get("skills", [])
print(skills)
print(skills[0])输出:
['Python', 'Scrapy', 'Playwright']
Python提取全部技能,可以直接遍历:
for skill in skills:
print(skill)如果只想取第一个元素,最好先判断列表是否为空:
if skills:
print(skills[0])否则空列表访问 skills[0],会触发 IndexError。
采集接口里更常见的是这种结构:
"projects": [
{
"name": "网页采集",
"status": "running"
},
{
"name": "语料清洗",
"status": "finished"
}
]可以先遍历列表,再从每个字典里取字段:
projects = data.get("projects", [])
for project in projects:
name = project.get("name", "未命名")
status = project.get("status", "unknown")
print(name, status)如果只想保留状态为 running 的项目:
running_projects = [
project
for project in projects
if project.get("status") == "running"
]
print(running_projects)往下拆一层看,这段代码做了三件事:遍历项目、检查状态、把符合条件的完整对象放进新列表。数据量不大时,列表推导式很省事;逻辑一旦超过两三个条件,我还是建议老老实实写普通循环,可读性比少写几行重要。
json.load() 接收文件对象:
with open("user.json", "r", encoding="utf-8") as file:
data = json.load(file)json.loads() 接收 JSON 字符串:
json_text = '{"name": "沈砚", "age": 35}'
data = json.loads(json_text)简单记:
json.load()。json.loads()。多出来的那个 s,可以理解成 string。
最常见的不是 Python 有问题,而是文件不符合标准 JSON:
// 或 # 注释。true、false、null 写成了 Python 的 True、False、None。遇到 Unexpected UTF-8 BOM,可以试一下:
with open("user.json", "r", encoding="utf-8-sig") as file:
data = json.load(file)Python 读取 JSON 不难。我的习惯是先确认顶层类型,再判断字段是必填还是可选,碰到嵌套对象就一层一层取,不追求把所有逻辑挤进一行。
代码写短不等于写稳。尤其是第三方数据,别对它太有信心。
这事儿我有发言权。
Selenium 我从读研时就开始用。后来做新闻源采集、视频元数据,再到现在跑跨境电商页面,前前后后写过不少浏览器自动化脚本。它给我的感觉一直很矛盾:入门特别快,写稳特别难。
打开网页,输入关键词,点击按钮。半小时就会了。
结果呢?同一段代码,上午跑得好好的,下午突然提示元素不存在。你加了三秒等待,它下一次偏偏第四秒才出来。这能不烦?
Selenium 是一套浏览器自动化工具。它不是在后台简单下载网页源码,而是在控制一个真实浏览器。你平时能在 Chrome 里完成的动作,它基本都能模拟,比如打开页面、填写表单、点击按钮、切换窗口、上传文件、滚动页面和保存截图。
这也是 Selenium 和普通 HTTP 请求最大的差别。
如果目标页面的内容直接存在于 HTML 中,或者网站提供了稳定的公开 API,我一般不会先用 Selenium。启动一个浏览器要占内存,页面渲染还要花时间。为了读取一段静态文字,把整个 Chrome 拉起来,我图啥呢?
Selenium 真正适合的是动态页面。比如内容需要执行 JavaScript 才出现,搜索结果必须点击后加载,或者任务要在登录状态下完成一整套操作。到了这种场景,直接请求处理起来可能更绕,浏览器自动化反而省事。
Python 项目建议放在独立虚拟环境里。安装过程很简单:
python -m venv .venvWindows 激活环境:
.venv\Scripts\activatemacOS 或 Linux 激活环境:
source .venv/bin/activate接着安装 Selenium:
pip install -U selenium现代版本通常可以通过 Selenium Manager 处理常见浏览器驱动。以前那种先查 Chrome 版本,再找对应 ChromeDriver,下载完还要配置路径的流程,现在大多数情况下不用手动做了。
先跑一段最小代码:
from selenium import webdriver
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
print(driver.title)
finally:
driver.quit()浏览器能打开,终端能输出网页标题,基础环境就没问题。
这里的 finally 别省。脚本中途报错时,它仍然会关闭浏览器。否则调试十几次,后台留下一排 Chrome 进程,内存一路往上走。那场面我见过。
离谱。
调试阶段我一般保留浏览器界面。至少脚本停住时,我能直接看到是按钮没出现,还是弹窗把页面挡住了。等流程跑稳以后,再切到无头模式:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)无头模式也要设置窗口尺寸。很多网站用了响应式布局,窗口太窄时,桌面导航会折叠成手机菜单。页面布局一变,原来的按钮可能直接消失。代码本身没改,定位却失效了,这就很尴尬。
浏览器打开以后,Selenium 得先找到页面上的元素,才能点击或者输入。
常用定位方式没几种:
from selenium.webdriver.common.by import By
driver.find_element(By.ID, "username")
driver.find_element(By.NAME, "keyword")
driver.find_element(By.CSS_SELECTOR, "button.submit")
driver.find_element(By.XPATH, "//button[contains(text(), '查询')]")我自己最常用的是 id 和 CSS Selector。如果前端提供了 data-testid 之类的测试属性,会更省心:
search_input = driver.find_element(
By.CSS_SELECTOR,
'input[data-testid="search-input"]'
)
search_input.send_keys("Selenium")XPath 当然可以用。需要按文字、相邻关系或者复杂层级找元素时,它很方便。问题是,很多人会直接从浏览器开发者工具里复制完整 XPath:
/html/body/div[2]/div/div[3]/form/input看上去非常精确,对吧?
前端只要多套一层 div,整条路径就废了。
我做跨境页面时尤其不爱依赖显示文字。同一个按钮,在英文环境叫 Search,切到其他语言以后文案就变了。脚本如果按按钮文字定位,地区一换就集体罢工。关键还是找稳定的业务属性,而不是赌页面永远不改。
9 月 17 日早上 9 点 12 分,我刚把工位上的黑咖啡打开,组里同事发来一段代码:
driver.get(url)
time.sleep(5)
button = driver.find_element(By.ID, "submit")
button.click()他说已经等了五秒,为什么偶尔还是找不到按钮?
答案很简单。因为时间过去五秒,不等于按钮已经可以点击。
页面一秒加载完,脚本白等四秒。网络慢一点,按钮第六秒才出现,五秒结束后照样报错。把等待时间改成十秒,只是让脚本在大多数时候多等五秒,并没有解决判断条件的问题。
更合适的做法是显式等待:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
button = wait.until(
EC.element_to_be_clickable(
(By.ID, "submit")
)
)
button.click()这段代码等的不是固定时长,而是“按钮已经可以点击”。按钮两秒后准备好,脚本两秒后继续;按钮八秒后准备好,脚本就等八秒。
如果只是要求元素进入页面结构,可以使用 presence_of_element_located。如果必须看得见,再用 visibility_of_element_located。遇到加载遮罩或者弹窗,也可以等它消失:
wait.until(
EC.invisibility_of_element_located(
(By.CSS_SELECTOR, ".loading-mask")
)
)time.sleep() 不是完全不能出现。调试动画、观察执行过程或者控制操作间隔时,偶尔用一下没问题。真正坑的是把它当成页面状态判断。
页面有没有准备好,应该问页面。
问时钟干什么?
下面这段示例会打开测试表单,填写文字和密码,选择下拉项,勾选复选框,提交以后再验证结果:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1440,1000")
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 10)
try:
driver.get(
"https://www.selenium.dev/selenium/web/web-form.html"
)
text_input = wait.until(
EC.visibility_of_element_located(
(By.NAME, "my-text")
)
)
text_input.send_keys("Selenium 自动化测试")
driver.find_element(
By.NAME,
"my-password"
).send_keys("demo-password")
select_element = driver.find_element(
By.NAME,
"my-select"
)
Select(select_element).select_by_visible_text("Two")
checkbox = driver.find_element(
By.ID,
"my-check-1"
)
if not checkbox.is_selected():
checkbox.click()
driver.find_element(
By.CSS_SELECTOR,
"button"
).click()
message = wait.until(
EC.visibility_of_element_located(
(By.ID, "message")
)
)
assert message.text == "Received!"
driver.save_screenshot("form-result.png")
finally:
driver.quit()这已经是一段完整的浏览器自动化流程。浏览器启动后先进入页面,等输入框可见,再完成填写和提交。提交以后不盲等,而是等结果消息出现。确认文案符合预期后截图,无论中途是否报错,最终都关闭浏览器。
刚入门不用急着引入很多框架。先把这段代码看明白,再换成自己的页面。学习速度反而更快。
这种情况先别急着换选择器。
如果元素位于 iframe 中,Selenium 默认在主页面里查找,当然找不到。需要先切进去:
wait.until(
EC.frame_to_be_available_and_switch_to_it(
(By.CSS_SELECTOR, "iframe.payment-frame")
)
)
driver.find_element(
By.ID,
"card-number"
).send_keys("1234")操作完成后,再回到主页面:
driver.switch_to.default_content()新标签页也是同一个道理。点击链接以后,浏览器虽然打开了新页面,Selenium 当前控制的却可能还是原窗口:
original_window = driver.current_window_handle
old_windows = set(driver.window_handles)
driver.find_element(
By.LINK_TEXT,
"查看详情"
).click()
wait.until(
lambda d: len(d.window_handles) > len(old_windows)
)
new_window = (
set(driver.window_handles) - old_windows
).pop()
driver.switch_to.window(new_window)
print(driver.title)
driver.close()
driver.switch_to.window(original_window)很多所谓的“元素定位失败”,根本不是定位表达式写错了,而是查找上下文错了。人已经去了新页面,脚本还留在旧页面,这能找到才奇怪。
动态页面局部刷新后,原来保存的元素对象可能会失效。这时常见的报错是:
StaleElementReferenceException我第一次遇到时,对着 CSS Selector 改了半天。后来才反应过来,选择器没问题,是页面已经变了,我还在操作更新前的对象。
例如先获取一批卡片,再逐个点进去:
cards = driver.find_elements(
By.CSS_SELECTOR,
".result-card"
)进入详情页再返回后,列表可能已经重新渲染。原来的 cards 不一定还能继续用。更稳的做法是每轮重新定位:
cards_locator = (By.CSS_SELECTOR, ".result-card")
count = len(
wait.until(
EC.presence_of_all_elements_located(cards_locator)
)
)
for index in range(count):
cards = wait.until(
EC.presence_of_all_elements_located(cards_locator)
)
cards[index].click()
# 在这里处理详情页
print(driver.title)
driver.back()
wait.until(
EC.presence_of_all_elements_located(cards_locator)
)页面状态变了,就重新找元素。
别跟旧对象谈感情。
脚本只有几十行时,把定位、操作和断言放在一起没什么问题。页面一多,同一个按钮选择器可能散落在十几个文件中。前端改一次页面,测试代码跟着全线报错。
这时可以引入 Page Object,把页面细节封装起来:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class LoginPage:
URL = "https://example.com/login"
USERNAME = (By.ID, "username")
PASSWORD = (By.ID, "password")
SUBMIT = (
By.CSS_SELECTOR,
"button[type='submit']"
)
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def open(self):
self.driver.get(self.URL)
return self
def login(self, username, password):
self.wait.until(
EC.visibility_of_element_located(
self.USERNAME
)
).send_keys(username)
self.driver.find_element(
*self.PASSWORD
).send_keys(password)
self.driver.find_element(
*self.SUBMIT
).click()调用时只描述业务动作:
page = LoginPage(driver).open()
page.login("demo-user", "demo-password")这样一来,页面改了选择器,只需要修改 LoginPage。测试用例不必知道按钮用了什么 CSS,也不用到处复制等待代码。
不过别刚写三十行脚本,就拆出十二个类。先跑通,再把重复部分整理出来。为了看起来专业而搭一大堆空架子,也挺折磨人的。
可以。特别是页面必须执行 JavaScript、经过点击或者登录后才显示内容时,Selenium 很实用。
问题是,工具能操作浏览器,不代表所有内容都可以随便获取。
我们组在自动化任务上线前,会先确认目标数据是否允许访问,操作会不会触发订单、评论或其他写入行为,是否涉及个人敏感信息,以及访问频率是否合理。验证码、登录保护和平台风控是安全边界,不是拿来练手的题目。
如果只是正常测试自己的系统,这些问题比较好控制。涉及第三方页面,就要先看平台规则和授权范围。脚本跑通只是技术问题,能不能跑则是另一回事。
Selenium 的方法名随用随查就行。
真正决定脚本稳不稳的,是你能不能看懂页面状态。按钮还没出现,就等它出现;按钮被遮住,就等遮挡层消失;页面开了新窗口,就切换过去;页面重新渲染,就重新定位元素。
9 月 18 日晚上 10 点 40 分,我把初稿发给我家那位看。他扫完后问:“你是不是漏了一句最重要的?”
我问哪句。
他说:“成功一次,不叫稳定。”
行吧。
后端说这话,确实没毛病。一段 Selenium 脚本连续运行几十次,遇到加载波动和页面更新也能正确处理,才有资格交给定时任务。
能打开浏览器,只是开始。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。