requests 拿回来的是服务器发的那段文本。
如果内容要靠 JavaScript 跑完才填进去,你拿到的只是一个空壳。
Selenium 的思路反过来:真开一个浏览器,让它把页面跑完,再问它「现在长什么样」。
慢,但什么都能看。
八种定位方式里,CSS 选择器和 XPath 覆盖九成场景,其余六种只是让常见情况好读一点。
上一篇学会了在树上找节点,这一篇换一种「树」:浏览器里那份活的。 教材第 6 章中间两节是【认识浏览器操控工具】和【通过 Chrome 浏览器和 Selenium 选定网页元素】。 本沙箱没有 Chrome,也没有浏览器驱动,所以 Selenium 那部分标了「平台原样 · 本沙箱不执行」。 但「按选择器找元素」这件事本身能真跑——本篇用一个自己写的查找函数,在上一篇那棵树上跑, 语义和 Selenium 一致。等待那一节的时间也是算出来的,不是实测,演示里会明说。
一、为什么光有 requests 不够
早期的网页是「服务器把整页 HTML 发给你,浏览器只负责画」。
这种页面,requests.get() 拿回来的文本里就有全部内容。
现在的网页不是这样。服务器发过来的 HTML 只有骨架,内容靠 JavaScript 跑完再填进去。
购物车、无限滚动、点「加载更多」才出来的列表,都属于这一类。
用 requests 抓这种页面,抓到的是骨架,看不到数据。 而且它不报错,你只会发现「明明页面上有,
怎么抓出来是空的」。
Selenium 的思路反过来:真的开一个浏览器,让它把页面跑完,再问浏览器「现在长什么样」。
慢,但什么都能看。
代价也明显:要装浏览器、要装驱动、跑起来占几百兆内存、比发个请求慢几十倍。
所以能不用就不用——先试 requests,抓不到再换。
二、八种找元素的方式
| 方式 | 写法(Selenium 4) | 什么时候用 |
|---|---|---|
| id | By.ID, 'main' |
最稳,页面唯一 |
| name | By.NAME, 'q' |
表单字段常用 |
| class | By.CLASS_NAME, 'item' |
一类元素批量取 |
| tag | By.TAG_NAME, 'a' |
捞全部链接 |
| 链接文字 | By.LINK_TEXT, '下一页' |
翻页按钮 |
| 部分文字 | By.PARTIAL_LINK_TEXT, '下一' |
文字有变动时 |
| CSS 选择器 | By.CSS_SELECTOR, 'div.item a' |
够用且快 |
| XPath | By.XPATH, '//div[@class="item"]' |
最灵活,能按文字找 |
前六种是后两种的特例。 By.ID, 'main' 等价于 By.CSS_SELECTOR, '#main',
By.CLASS_NAME, 'item' 等价于 By.CSS_SELECTOR, '.item'。
那为什么还留着前六种?因为它们更短、更好读,而且写错了不容易错得莫名其妙。
真写起来,CSS 选择器和 XPath 覆盖了九成场景。
区别是:CSS 选择器快、语法简单,但不能按文字内容找;XPath 能按文字找,也能往上找父节点。
所以规律是:按结构找用 CSS,按文字找用 XPath。下一篇专门讲 XPath。
三、这几种定位在本机 HTML 上是什么效果
演示第三段用一个自己写的查找函数在上一篇那棵树上跑,语义跟 Selenium 一致:id 找唯一, class 找一批,标签名找全部。
| 定位方式 | 命中 | 前几个的内容 |
|---|---|---|
id=main |
1 个 | 六角螺栓 M8 |
class=item |
3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
class=price |
3 个 | 1.50、2.30、0.40 |
tag=a |
3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
tag=img |
1 个 | <img> |
div.item |
3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
这一段是本课自己写的,不是真 Selenium。 区别只在「由谁来找」:这里是在 Python 建的那棵树上找,
真机上是浏览器在它自己那份 DOM 上找。
真机上换成 driver.find_elements(By.ID, 'main') 就行。
元素多的时候用 find_element(单数,只取第一个),要一批用 find_elements(复数)。
单数那个找不到会抛异常,复数那个找不到返回空列表。 这一个小差别,
决定了你是要写 try 还是要写 if。
四、等,是这件事最大的坑
页面加载要时间。你 driver.get() 完立刻找元素,很可能那个元素还没渲染出来。
最笨的办法是 time.sleep(5)。问题是:网快的时候白等五秒,网慢的时候五秒不够。
写死的时间永远在「太短」和「白等」之间二选一。
正确的做法叫显式等待:条件成立就往下走,最多等 N 秒。
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.item')))——
意思是「等到 div.item 出现,最多等 10 秒」。出现了立刻返回,不白等。
还有一个隐式等待:driver.implicitly_wait(10)。
它是全局的,设一次影响之后所有查找,每次找不到就先等一会儿,最多等 10 秒。
两种别混着用。 混起来等待时间会叠:隐式等 10 秒、显式又等 10 秒,实际可能等到 20 秒,
而你以为设的是 10 秒。
演示第四段跑的是一个模拟轮询:间隔 20、50、100、200 毫秒重试,第 4 次拿到元素,累计 0.37 秒。 这个 0.37 是把间隔加起来算的,不是实测——本沙箱没有浏览器,测不出真实页面的加载时间。 要说的是这个结构:重试次数不确定,但最坏情况有上限。这就是等待该有的样子。
五、真机上长这样
完整的流程是五步:开浏览器 → 打开网址 → 等到关键元素出现 → 取数据 → 关浏览器。
from selenium import webdriver,driver = webdriver.Chrome(),driver.get(url),
然后 find_elements,最后 driver.quit()。
driver.quit() 一定要写。 不写的话浏览器进程会一直留着,跑几次就满屏都是窗口,而且吃内存。
用完 quit() 而不是 close():close() 只关当前标签页,quit() 才关整个浏览器并结束驱动进程。
Selenium 4 之后 Chrome 驱动不用自己下了,webdriver-manager 会处理。
但「浏览器版本和驱动版本对不上」依然是这个工具最常见的报错来源。
这一段在演示里标了「平台原样 · 本沙箱不执行」——沙箱里没有 Chrome。
代码是从教材里原样抄下来的,你可以照贴到本机跑。
六、动手 Demo:Selenium:让浏览器替你点
先说清静态页和动态页的差别,再列八种定位方式。 然后用本课自己写的查找函数,在上一个 Demo 那棵树上跑一遍各种定位。 Selenium 的部分标「平台原样」,等待那一节的时间是算出来的,不是实测。
from html.parser import HTMLParser
HTML = ('<html><body><div id="main" class="list"><h1>今日材料报价</h1>'
'<div class="item" data-sku="A-01"><h3><a href="/p/1">六角螺栓 M8</a></h3>'
'<span class="price">1.50</span></div>'
'<div class="item" data-sku="B-02"><h3><a href="/p/2">膨胀螺栓 10mm</a></h3>'
'<span class="price">2.30</span></div>'
'<div class="item" data-sku="C-03"><h3><a href="/p/3">不锈钢垫片</a></h3>'
'<span class="price">0.40</span></div>'
'<img src="/img/logo.png" alt="logo"></div></body></html>')
class Node:
def __init__(self, tag, attrs, parent=None):
self.tag, self.attrs, self.parent = tag, dict(attrs), parent
self.kids, self.text = [], ''
class B(HTMLParser):
def __init__(self):
super().__init__()
self.root = Node('#root', [])
self.stack = [self.root]
def handle_starttag(self, tag, attrs):
n = Node(tag, attrs, self.stack[-1])
self.stack[-1].kids.append(n)
if tag not in ('img', 'br', 'meta', 'link', 'input', 'hr'):
self.stack.append(n)
def handle_endtag(self, tag):
if len(self.stack) > 1 and self.stack[-1].tag == tag:
self.stack.pop()
def handle_data(self, data):
if data.strip():
self.stack[-1].text += data.strip()
b = B()
b.feed(HTML)
def walk(n):
yield n
for k in n.kids:
yield from walk(k)
def find(by, value):
all_ = list(walk(b.root))
if by == 'id':
return [n for n in all_ if n.attrs.get('id') == value]
if by == 'class':
return [n for n in all_ if value in n.attrs.get('class', '').split()]
if by == 'tag':
return [n for n in all_ if n.tag == value]
if by == 'css':
tag, _, cls = value.partition('.')
return [n for n in all_ if n.tag == tag and cls in n.attrs.get('class', '').split()]
return []
print('① 为什么光有 requests 不够')
print(' 静态页:服务器返回的 HTML 里就有内容')
print(' 动态页:HTML 只有骨架,内容靠 JavaScript 跑完再填进去')
print('② 八种找元素的方式')
print('| 方式 | 写法(Selenium 4) | 什么时候用 |')
print('|---|---|---|')
for r in (('id', "By.ID, 'main'", '最稳,页面唯一'),
('name', "By.NAME, 'q'", '表单字段常用'),
('class', "By.CLASS_NAME, 'item'", '一类元素批量取'),
('tag', "By.TAG_NAME, 'a'", '捞全部链接'),
('链接文字', "By.LINK_TEXT, '下一页'", '翻页按钮'),
('部分文字', "By.PARTIAL_LINK_TEXT, '下一'", '文字有变动时'),
('CSS 选择器', "By.CSS_SELECTOR, 'div.item a'", '够用且快'),
('XPath', 'By.XPATH, //div[@class="item"]', '最灵活,能按文字找')):
print('| %s | %s | %s |' % r)
print('③ 这几种定位在本机 HTML 上是什么效果')
print('| 定位方式 | 命中 | 前几个的内容 |')
print('|---|---|---|')
for tag, by, val in (('id=main', 'id', 'main'), ('class=item', 'class', 'item'),
('class=price', 'class', 'price'), ('tag=a', 'tag', 'a'),
('tag=img', 'tag', 'img'), ('div.item', 'css', 'div.item')):
hit = find(by, val)
names = [n.text for n in hit if n.text][:3] or ['<img>']
print('| %s | %d 个 | %s |' % (tag, len(hit), '、'.join(names)))
print('④ 等,是这件事最大的坑')
print(' 隐式等待:driver.implicitly_wait(10)')
print(' → 每次找元素,找不到就先等,最多等 10 秒')
print(' 显式等待:WebDriverWait(driver, 10).until(...)')
print(' → 等到某个具体条件成立,比如「这个元素出现了」')
import time
delays = [0.02, 0.05, 0.1, 0.2]
total = 0.0
for i, d in enumerate(delays, 1):
total += d
print(' 模拟轮询:按 20、50、100、200 毫秒的间隔重试')
print(' 第 %d 次才拿到元素,累计 %.2f 秒' % (len(delays), total))
print(' 这里的时间是上面那组间隔加起来算的,不是实测——'
'本沙箱没有浏览器,测不出真实页面的加载时间。')
print('⑤ 真机上长这样')
print(' [平台原样 · 本沙箱不执行]')
print(' | # 本书第 6-4 节(平台原样,本沙箱不执行:没有 Chrome 与驱动)')
print(' | from selenium import webdriver')
print(' | from selenium.webdriver.common.by import By')
print(' | from selenium.webdriver.support.ui import WebDriverWait')
print(' | from selenium.webdriver.support import expected_conditions as EC')
print(' |')
print(' | driver = webdriver.Chrome()')
print(" | driver.get('https://example.com/list')")
print(' | WebDriverWait(driver, 10).until(')
print(" | EC.presence_of_element_located((By.CSS_SELECTOR, 'div.item')))")
print(" | for el in driver.find_elements(By.CSS_SELECTOR, 'div.item a'):")
print(" | print(el.text, el.get_attribute('href'))")
print(' | driver.quit()')
print(' driver.quit() 一定要写,不然浏览器进程会一直留着')
实跑输出:
--------------------------------------------------------------------
Selenium:让浏览器替你点
--------------------------------------------------------------------
① 为什么光有 requests 不够
--------------------------------------------------------------------
静态页:服务器返回的 HTML 里就有内容
动态页:HTML 只有骨架,内容靠 JavaScript 跑完再填进去
购物车、无限滚动、点「加载更多」才出来的列表,都属于第二种。
用 requests 抓回来的是骨架,看不到数据。
Selenium 的思路是反过来的:真的开一个浏览器,让它把页面跑完,再问浏览器「现在长什么样」。
慢,但什么都能看。
② 八种找元素的方式
--------------------------------------------------------------------
| 方式 | 写法(Selenium 4) | 什么时候用 |
|---|---|---|
| id | By.ID, 'main' | 最稳,页面唯一 |
| name | By.NAME, 'q' | 表单字段常用 |
| class | By.CLASS_NAME, 'item' | 一类元素批量取 |
| tag | By.TAG_NAME, 'a' | 捞全部链接 |
| 链接文字 | By.LINK_TEXT, '下一页' | 翻页按钮 |
| 部分文字 | By.PARTIAL_LINK_TEXT, '下一' | 文字有变动时 |
| CSS 选择器 | By.CSS_SELECTOR, 'div.item a' | 够用且快 |
| XPath | By.XPATH, '//div[@class="item"]' | 最灵活,能按文字找 |
前六种是后两种的特例。
真写起来,CSS 选择器和 XPath 覆盖了九成场景,其余六种只是让常见情况好读一点。
③ 这几种定位在本机 HTML 上是什么效果
--------------------------------------------------------------------
| 定位方式 | 命中 | 前几个的内容 |
|---|---|---|
| id=main | 1 个 | 六角螺栓 M8 |
| class=item | 3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
| class=price | 3 个 | 1.50、2.30、0.40 |
| tag=a | 3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
| tag=img | 1 个 | <img> |
| div.item | 3 个 | 六角螺栓 M8、膨胀螺栓 10mm、不锈钢垫片 |
这段是本课自己写的查找函数,跑在刚才那棵树上,语义和 Selenium 一致:id 找唯一,class 找一批,
标签名找全部。
真机上换成 driver.find_elements(By.ID, 'main') 就行,只是查找由浏览器在它自己那份 DOM 上做。
④ 等,是这件事最大的坑
--------------------------------------------------------------------
隐式等待:driver.implicitly_wait(10)
→ 每次找元素,找不到就先等,最多等 10 秒
显式等待:WebDriverWait(driver, 10).until(...)
→ 等到某个具体条件成立,比如「这个元素出现了」
模拟轮询:按 20、50、100、200 毫秒的间隔重试
第 4 次才拿到元素,累计 0.37 秒
这里的时间是上面那组间隔加起来算的,不是实测——本沙箱没有浏览器,测不出真实页面的加载时间。
要说的是这个结构:重试次数不确定,但最坏情况有个上限。
页面加载快慢不确定,写死 sleep(5) 要么太短要么白等。
正确做法是「条件成立就往下走,最多等 N 秒」——这就是显式等待。
隐式等待是全局的,设一次影响所有查找;显式等待只对那一句生效。
混着用容易把等待时间叠起来,实际等了 20 秒还以为设的是 10 秒。
⑤ 真机上长这样
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| # 本书第 6-4 节(平台原样,本沙箱不执行:没有 Chrome 与驱动)
| 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
|
| driver = webdriver.Chrome()
| driver.get('https://example.com/list')
| WebDriverWait(driver, 10).until(
| EC.presence_of_element_located((By.CSS_SELECTOR, 'div.item')))
| for el in driver.find_elements(By.CSS_SELECTOR, 'div.item a'):
| print(el.text, el.get_attribute('href'))
| driver.quit()
driver.quit() 一定要写,不然浏览器进程会一直留着
Selenium 4 之后 Chrome 驱动不用自己下了,webdriver-manager 会处理。
但「浏览器版本和驱动版本对不上」依然是这个工具最常见的报错来源。
第一段那句「动态页:HTML 只有骨架」是本篇的动机。 用 requests 抓这种页面不报错,只是抓出来是空的——这才是最麻烦的地方。 第三段那张表是本文自己写的 find 函数跑出来的,不是真 Selenium。 区别只在「由谁来找」:这里在 Python 建的树上找,真机上是浏览器在它那份 DOM 上找。 第四段那个 0.37 秒是把 20、50、100、200 毫秒四个间隔加起来算的,不是实测。 要说的是结构:重试次数不确定,但最坏情况有上限。 第五段最后那句 quit() 是个真坑。不写它,浏览器进程会一直留着,跑几次就满屏窗口,还吃内存。 注意是 quit 不是 close——close 只关标签页。
30 秒实验
- 把演示第三段的
('tag=img', 'tag', 'img')改成('tag=span', 'tag', 'span'),看命中几个。 - 在本机装一次 Selenium 和 Chrome,把第五段那段代码贴出来跑一遍,看能不能拿到真实的三个链接。
- 把演示第四段的
delays改成[0.1, 0.5, 1, 2],看累计时间变成多少, 再想想为什么显式等待比「写死 sleep」好。
常见误区
- 「requests 抓不到内容,一定是网站反爬了。」也可能内容本来就是 JavaScript 填的。 先看抓回来的 HTML 里有没有你要的数据,再判断是不是反爬。
- 「等待就写
time.sleep(5)。」写死的时间永远在「太短」和「白等」之间二选一。 显式等待是「条件成立就走,最多等 N 秒」。 - 「
find_element和find_elements只差一个字母。 」差别很大:单数找不到抛异常,复数找不到返回空列表。 选错了代码的写法就完全不一样。
自测三问(点开对答案)
1. 同一个网址,浏览器里能看到内容,`requests` 抓回来却是空的,最可能是什么原因? —— 内容是在页面跑起来之后由 JavaScript 填进去的,服务器发过来的 HTML 里本来就没有。 `requests` 只拿服务器发的那段文本,不会执行 JavaScript。 解决办法是换 Selenium 这类真开浏览器的方式。 2. 为什么推荐显式等待,而不是 `time.sleep(5)`? —— `sleep(5)` 是写死的:网快时白等五秒,网慢时五秒不够。 显式等待是「条件成立立刻往下走,最多等 N 秒」,两种情况都合适,而且不用猜时间。 3. 隐式等待和显式等待混着用,会出什么问题? —— 等待时间会叠加。隐式等 10 秒、显式又等 10 秒,实际可能等到 20 秒,而你以为上限是 10 秒。 调试时会觉得「怎么这么慢」,还找不到原因。下一篇讲 XPath——那个「能按文字找、还能往上找」的定位方式,还有网址里那一串参数。
- ☐ (本篇完,下一篇 6.3 XPath 与查询字符串)