看懂 HTML,会用开发者工具
完整讲解 · 9 段教学·配 3 道练习题·预计 45 分钟
本页是本章的通读版,可直接读完全部讲解。想动手写代码、跑判分,去 闯关模式。
前言
前两章,咱们已经能把网页取回来,也知道怎么判断取得对不对了。可打印 response.text 一看,满屏都是尖括号,书名和价格就夹在里面,眼睛都找花了。
怎么从这一大段文字里,把想要的数据挑出来呢?这就是爬虫的第二步:解析。要从网页里挑出数据,先得看懂网页是怎么写的。
网页是用 HTML 写的。各位童鞋不用把 HTML 学成网页设计师,只要能看懂它的结构,知道「书名在哪个标签里、价格在哪个标签里」,就够写爬虫了。
那么,怎么看清网页的结构呢?最好的帮手,是浏览器自带的开发者工具。它能把网页拆成一层一层的标签,鼠标指到哪儿,就告诉你那块内容对应的是哪段 HTML。
结构看懂了,还得动手把数据取出来。这一章会先用最笨的办法,拿字符串的 find() 和切片,从网页里抠出一个书名。抠完你就会明白,为什么下一章要请出专门的解析工具。
学这一章之前,需要先学完 爬虫课第 2 章 ,会取回网页、检查状态码;还要熟悉基础课第 3 章讲的切片。用到的 find() 、split() 这两个字符串方法,第五节会边用边讲。
目录
这一章会学到:
- 一、HTML 长什么样——标签、属性和文字
- 二、标签套标签——一棵树
- 三、浏览器的开发者工具——鼠标指到哪儿,就看到哪段 HTML
- 四、用 CSS 选择器描述位置——给每块内容一个「地址」
- 五、用字符串方法硬抠一个书名——体会一下有多麻烦
- 六、常见坑
- 七、自己查一次文档——详情页的表格是怎么写的
- 八、小结与回看
学完这一章,你能看懂网页的结构,用开发者工具找到数据所在的位置,并用选择器把位置描述出来。
一、HTML 长什么样
要看懂网页,先得看看它到底长什么样。整个网页太长,咱们先从两点书店的列表页里,截出一本书的 HTML 来看看:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/').text
start = html.find('<article')
end = html.find('</article>') + len('</article>')
print(html[start:end])输出的结果:
<article class="book-card" data-id="1">
<h3 class="title"><a href="/lab/bookstore/books/1/">轻松玩转自动化</a></h3>
<p class="author">陈星河</p>
<p class="category">编程</p>
<p class="price">¥104.40</p>
<p class="rating">★★★★★</p>
<p class="stock">有货</p>
</article>截取用的是字符串的 find() :先找到开头和结尾的位置,再用切片截出来。这个办法第五节会细说,这里先看截出来的结果。
这就是一本书在网页里的样子。乍一看有点乱,其实 HTML 只由三样东西组成:
- 标签:尖括号括起来的部分,比如
<p>和</p>。大多数标签成对出现,<p>是开始,</p>是结束,中间夹着的就是这个标签的内容。标签的名字说明它是什么:p是段落,h3是三级标题,a是链接,article是一块独立的内容。 - 属性:写在开始标签里的
名字="值",比如class="price"、href="/lab/bookstore/books/1/"。class给标签分类,网页靠它决定样子;href是链接指向的地址;data-id是网站自己加的,记着这本书的编号。 - 文字:夹在标签中间的内容,比如
轻松玩转自动化、¥104.40。咱们要的数据,大多就在这里。
这三样东西里,对写爬虫最有用的,是属性里的 class 。为什么这么说呢?因为网站为了让每本书的价格都显示成红色,会给它们加上同一个 class="price" 。对爬虫来说,这就像一个现成的路标:找到所有 class="price" 的标签,就找到了所有的价格。
下面先做一道练习,借着这样的路标,数一数一页里有几本书、其中几本缺货。
取回两点书店的第 2 页,用字符串的 count() 数一数:这一页有几本书(每本书是一个 <article class="book-card" ),其中几本缺货(缺货的库存标签写成 class="stock out" )。
运行后的输出应该是:
20 本书,其中 2 本缺货💡 提示(3 条)
字符串的 count(子串) 返回子串出现的次数
注意子串里有双引号,外面用单引号把它括起来:'<article class="book-card"'
完整代码:
import requests html = requests.get('https://learn-py.org/lab/bookstore/page/2/').text total = html.count('<article class="book-card"') sold_out = html.count('class="stock out"') print(f'{total} 本书,其中 {sold_out} 本缺货')
二、标签套标签
第一节截出来的那本书,各位童鞋有没有注意到它的层次?咱们再看一眼:<article> 里面装着 <h3> 和好几个 <p> ,而 <h3> 里面又装着一个 <a> 。
像这样标签里面套标签,一层套一层,画出来就像一棵倒着长的树:
article.book-card
├── h3.title
│ └── a → 轻松玩转自动化
├── p.author → 陈星河
├── p.category → 编程
├── p.price → ¥104.40
├── p.rating → ★★★★★
└── p.stock → 有货图里每个节点的写法,是「标签名.class 名」。比如 article.book-card ,说的就是 class 为 book-card 的那个 article 标签。这种写法,第四节讲选择器时马上会用到。
树画出来了。要说清楚树里谁和谁是什么关系,得先认识几个词:
- 父元素、子元素:
article直接装着h3,所以article是h3的父元素,h3是article的子元素。 - 后代:
a在h3里,h3又在article里,所以a是article的后代。不管隔了几层,都算后代。 - 兄弟:同一个父元素下面的
h3和那几个p,互相是兄弟。
有的童鞋可能会问:认识这些词有什么用?因为一个页面上有 20 本书,就有 20 个 class="price" 。要知道哪个价格属于哪本书,就得顺着树往上找:这个价格的父元素是哪个 article ,它的 data-id 是多少。第 5 章抓整个列表页时,靠的就是这层关系。
一本书的结构,画出来就这么几行。可真实的网页要长得多,想在里面找到某一块内容,总不能每次都写代码截出来看吧?下一节就来解决这个问题。
三、浏览器的开发者工具
整个网页有几百行 HTML,像第一节那样一段一段截出来看,太慢了。好在浏览器自带的开发者工具,能帮你一眼找到数据在哪儿。
具体怎么用呢?先在新标签页里打开 两点书店 ,然后跟着下面几步做:
- 在一本书的价格上点鼠标右键,选「检查」(英文版叫 Inspect)。也可以按
F12,Mac 上按Cmd + Option + I。 - 窗口一侧会弹出开发者工具,「元素」面板(Elements)里,价格对应的那一行
<p class="price">¥104.40</p>已经高亮选中了。 - 在「元素」面板里上下移动鼠标,网页上对应的那块内容会跟着高亮。指到
<article class="book-card" ...>那一行,整本书的卡片都亮了。 - 点开标签前面的小三角,可以展开、收起它里面的子元素。
这样来回指一指,书名在哪个标签,价格在哪个标签,它们的父元素又是谁,就都清楚了。
开发者工具里还有一个很有用的功能。在「元素」面板里选中一行,点右键,选「复制」→「复制 selector」(Copy selector),浏览器就会给出这块内容的选择器。选择器是一串描述位置的文字,它怎么写、怎么读,下一节就讲。
在讲选择器之前,还有一点要知道。「元素」面板里看到的,是浏览器运行完网页里的 JavaScript 之后的样子;而 requests 拿到的,是网站最初发回来的 HTML,没有运行过 JavaScript。大多数时候两者是一样的,但也有不一样的时候。
那怎么看到和 requests 一样的内容呢?在网页上点右键,选「查看网页源代码」,看到的才和 requests 拿到的一致。第 7 章会遇到一个两者不一样的页面。
四、用 CSS 选择器描述位置
上一节说到,开发者工具能复制出一串叫选择器的文字。为什么需要这样一串文字呢?
因为找到了数据在哪儿,还得能告诉程序。可怎么跟程序说「我要每本书的价格」呢?总不能说「第 37 行那个」吧,网页稍微改一点,行号就变了。
其实,网站自己给内容定样子的时候,也要解决同样的问题,它用的是 CSS 选择器,也就是一种描述「哪些标签」的写法。写爬虫的时候,借用同一套写法,就能告诉程序去哪儿找数据。最常用的有下面几种:
| 选择器 | 意思 | 在两点书店里选中的 |
|---|---|---|
p | 所有 p 标签 | 作者、分类、价格、评分、库存这些段落 |
.price | 所有 class="price" 的标签 | 每本书的价格 |
p.price | class="price" 的 p 标签 | 同上,写得更具体 |
#new-books | id="new-books" 的标签 | 新书速递页上的那个列表 |
article.book-card .price | book-card 里面的 .price ,中间的空格表示「后代」 | 每张书卡里的价格 |
h3.title > a | h3.title 的直接子元素 a | 每本书书名上的链接 |
a[href] | 带 href 属性的 a 标签 | 页面上所有的链接 |
表格有点长,不用死记,看懂下面这几条规律就够了:
- 只写名字,是标签;前面加
.,是 class;加#,是 id。 - 中间用空格隔开,表示后面那个在前面那个里面,隔几层都行;用
>隔开,表示必须是直接的子元素。 - 方括号
[ ]里写属性,表示要带着这个属性。
那么,class 和 id 有什么区别呢?class 可以很多标签共用,比如一个页面上有 20 个 .price 。而 id 在一个页面上只该出现一次,用来标出独一无二的那块内容。
选择器又该写得多具体呢?写得越具体,越不容易选错;可写得太死,网站稍微改一下就失效了。像 article.book-card .price 这样,用 class 把位置说清楚,不依赖第几个、第几层,是比较稳妥的写法。
不过,选择器现在还不能在 Python 里直接用,下一章的 BeautifulSoup 会教。眼下可以先在开发者工具里练练眼力:打开两点书店,按 Ctrl + F (Mac 上是 Cmd + F ),在「元素」面板的搜索框里输入 .price ,看看能找到几个。
五、用字符串方法硬抠一个书名
上一节说,选择器要等到下一章,才能在 Python 里用。那么,不用任何解析工具,只靠字符串自带的方法,能不能从详情页里取出书名呢?咱们来硬抠一次试试。
要抠书名,先得知道书名在 HTML 里是怎么写的。下面先找到书名的位置,再从那儿往后多看一段:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/books/12/').text
start = html.find('<h2 class="title">')
print(html[start:start + 40])输出的结果:
<h2 class="title">色彩的力量</h2>
<table clas这段代码用到了两样东西,后面会一直用,所以先说清楚:
html.find(一段文字)返回这段文字第一次出现的位置,是一个数字。找不到,就返回-1。html[start:start + 40]从这个位置开始,往后切 40 个字符。字符串的切片,和基础课第 3 章列表的切片是一样的。
从输出可以看出,书名夹在 <h2 class="title"> 和 </h2> 中间。那就分别找到这两段的位置,把中间的部分切出来:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/books/12/').text
begin = '<h2 class="title">'
start = html.find(begin) + len(begin)
end = html.find('</h2>', start)
print(html[start:end])输出的结果:
色彩的力量书名取到了。这里要注意 find('</h2>', start) 的第二个参数 start :它让 find() 从书名开始的地方往后找,免得找到前面别的 </h2> 。
书名能抠出来,价格呢?价格放在表格的一个格子里,这回换个办法来取:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/books/12/').text
price = html.split('<td class="price">')[1].split('</td>')[0]
print(price)
print(float(price.replace('¥', '')))输出的结果:
¥64.50
64.5这回用的是 split() ,分两刀来切:
- 先按
<td class="price">切开,取后半段,价格就排到了最前面。 - 再按
</td>切开,取前半段,剩下的就只有价格了。
最后去掉 ¥ ,就能转成数字。
各位童鞋是不是觉得有点费劲?这还只是一个书名、一个价格。更麻烦的是,网页稍微一变,这些写法就不灵了。比如:
- 网站把
class="title"改成了class="book-title"。 - 标签里多了个属性,变成了
<h2 class="title" id="t">。 - 一个页面上有 20 本书,要一本一本地对上号……
碰到这些情况,字符串的写法都得跟着改,一不小心就切错了位置。
好在这些麻烦,专门的 HTML 解析工具都替我们处理好了。下一章就用 BeautifulSoup 和上一节的选择器,几行代码取出整页的数据。
在那之前,先做一道练习,用 find() 和切片自己抠两个书名,体会一下这种写法。
取回编号 30 和编号 3 这两本书的详情页(https://learn-py.org/lab/bookstore/books/编号/ ),用 find() 和切片取出 <h2 class="title"> 和 </h2> 中间的书名,一行打印一本。
运行后的输出应该是:
图标的秘密
早起养花💡 提示(3 条)
开始位置是 html.find(begin) + len(begin),跳过 <h2 class="title"> 这一段
结束位置从开始位置往后找:html.find('</h2>', start)
完整代码:
import requests begin = '<h2 class="title">' for book_id in [30, 3]: html = requests.get(f'https://learn-py.org/lab/bookstore/books/{book_id}/').text start = html.find(begin) + len(begin) end = html.find('</h2>', start) print(html[start:end])
六、常见坑
看网页、找数据的时候,有几个坑很容易踩,下面一个一个来看。
1、开发者工具里有,requests 里没有
第三节说过,开发者工具的「元素」面板,显示的是运行完 JavaScript 之后的网页。这会带来什么问题呢?两点书店的「新书速递」页就是一个例子:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/new/').text
start = html.find('<ul id="new-books">')
print(html[start:html.find('</ul>', start) + 5])输出的结果:
<ul id="new-books"><li class="loading">正在加载……</li></ul>在浏览器里打开这一页,列表里有 8 本新书;可 requests 拿到的,只有一句「正在加载……」。为什么会这样?因为书单是网页里的 JavaScript 后来才填进去的,而 requests 拿到的 HTML 没有运行过 JavaScript。
那怎么提前知道 requests 能不能拿到呢?方法是在网页上点右键,选「查看网页源代码」,在里面搜你要的数据。源代码里有,requests 就能拿到;源代码里没有,就要另想办法,第 7 章会讲。
2、只按 class 找,找到了别处的同名标签
class="title" 不只书名在用,网站的其他地方也可能用。所以在详情页里,find('class="title"') 找到的第一个,未必是你想要的那个。怎么避免呢?选择器要把位置说具体,比如 article.book-detail .title ,先限定在书的详情里,再去找标题。
3、文字前后的空白和换行
HTML 里的文字,经常带着换行和缩进,取出来就成了 '\n 色彩的力量\n' 这样。所以用之前,记得 strip() 一下,把前后的空白去掉,实战 1 讲过它。
4、HTML 里的特殊字符
< 、> 、& 这几个字符,在 HTML 里有特殊含义。所以网页里要显示它们的时候,会写成 < 、> 、& 。用字符串方法硬抠出来的文字里,要是看到 & ,别奇怪,它就是 & 。下一章的 BeautifulSoup 会自动把它们转回来。
七、自己查一次文档
第五节取价格的时候说过,价格放在表格的一个格子里。其实详情页里,作者、分类、价格这些信息,都放在同一个表格里。先把整个表格截出来看看:
import requests
html = requests.get('https://learn-py.org/lab/bookstore/books/12/').text
start = html.find('<table')
end = html.find('</table>') + len('</table>')
print(html[start:end])输出的结果:
<table class="info">
<tr><th>作者</th><td class="author">唐知秋</td></tr>
<tr><th>分类</th><td class="category">设计</td></tr>
<tr><th>价格</th><td class="price">¥64.50</td></tr>
<tr><th>评分</th><td class="rating">4.9(7 条评价)</td></tr>
<tr><th>库存</th><td class="stock">6 本</td></tr>
<tr><th>页数</th><td class="pages">599</td></tr>
<tr><th>出版年份</th><td class="year">2008</td></tr>
</table>这回出现了几个新标签:table 、tr 、th 、td 。它们各是什么意思呢?正好拿来练一练查文档。
查 HTML 标签,最常用的参考资料是 MDN。去看看它的中文版:<table>:表格元素 。
页面开头就有一句说明:<table> 表示表格数据,也就是由行和列组成的二维表格。再顺着页面里的链接,把另外几个也点开看看:
<tr>:表格里的一行。<th>:表头单元格,这一行的「标题」,比如「作者」。<td>:数据单元格,这一行的「内容」,比如「唐知秋」。
这样一对照,详情页的结构就清楚了:每个 tr 是一项信息,左边的 th 写着这是什么,右边的 td 写着具体的值。想取「页数」,就找 th 文字是「页数」的那一行,取它旁边的 td 。
MDN 是写网页的人天天在查的资料,HTML 标签、CSS 选择器、浏览器的各种功能,都能在上面找到。以后在网页里遇到不认识的标签,就去 MDN 搜它的名字。
表格的结构弄清楚了,下面先做一道练习,从详情页的表格里取出任意一项。
详情页的表格里,每一项的值都写成 <td class="名字">值</td> 。请写一个函数 get_field(html, name) ,用 split() 取出 class 为 name 的那个格子里的文字。
编辑器下面已经写好了调用,取的是编号 12 这本书的页数、出版年份和评分。运行后的输出应该是:
599 页,2008 年出版
4.9(7 条评价)💡 提示(3 条)
用 f-string 拼出要切的那一段:f'<td class="{name}">'
html.split(那一段)[1] 是后半段,再 .split('</td>')[0] 取前半段,return 出去
完整代码:
import requests def get_field(html, name): return html.split(f'<td class="{name}">')[1].split('</td>')[0] html = requests.get('https://learn-py.org/lab/bookstore/books/12/').text print(f"{get_field(html, 'pages')} 页,{get_field(html, 'year')} 年出版") print(get_field(html, 'rating'))
八、小结与回看
这一章,咱们学会了看网页,还用最笨的办法抠出了书名和价格。回头梳理一下,学到了这几件事:
- HTML 由标签、属性和文字组成。数据大多在文字里,
class、id这些属性是找数据的路标。 - 标签套标签,组成一棵树。父元素、子元素、后代、兄弟,用来描述它们的关系。
- 开发者工具的「元素」面板:右键「检查」,就能找到一块内容对应的 HTML;「查看网页源代码」看到的才和
requests拿到的一致。 - CSS 选择器描述位置:
p是标签,.price是 class,#new-books是 id,空格表示后代,>表示直接子元素,[href]表示带某个属性。 - 只靠
find()、切片和split()也能抠出数据,可网页稍一变就会出错。 - 不认识的 HTML 标签,去 MDN 查。
哪里卡住了,就回去看一看:
- 切片:第 3 章;
strip():实战 1 的「五、常见坑」 - 一次请求和响应:爬虫课第 2 章
到这里,咱们已经知道数据藏在哪儿,也会用选择器描述它的位置了。剩下的问题是:怎么让程序照着选择器去取数据,不用再一刀一刀地切字符串?下一章,就请出 BeautifulSoup:把 HTML 交给它,用选择器一指,书名、价格、链接就都出来了。
读完了?动手练一遍才算真会。
去闯关模式练习 →