步骤/目录:
1.HTML简介
2.HTML的一般格式
3.HTML的各种标签
    (0)常见的属性
    (1)标题与段落
    (2)文本格式
    (3)网页与图像
    (4)头部
    (5)表格及列表
    (6)区块元素
    (7)表单与输入
    (8)脚本
    (9)iframe框架
4.python解析HTML
    (1)bs4库
        a.找到单个标签
        b.找到多个标签
        c.其他find系列的方法
        d.标签的层级关系
        e.取得标签的内容
        f.取得标签的属性值
        g.取得标签名
        h.格式化输出
    (2)正则表达式
    (3)XPath
        a.XPath的基础操作
        b.XPath的谓语
        c.XPath的通配符
        d.XPath的逻辑符号
        e.XPath的函数
        f.XPath的轴
        g.python使用XPath
    (4)其他

本文首发于个人博客https://lisper517.top/index.php/archives/41/,转载请注明出处。
本文的目的是对html进行简单的学习,方便爬虫的使用。
本文写作日期为2022年9月1日,主要总结自 runoob-HTML教程 。由于这里介绍的html主要用于爬虫,所以只是简单了解,更深入的内容(比如想达到能写前端的程度)可自行搜索书籍和其他资料。

1.HTML简介

HTML格式即 HyperText Markup Language (超文本标记语言),主要是用于网页的源代码。用chrome浏览器打开一个网页,按 ctrl+U ,或者在网址前加 view-source: 并访问,就能看到网页的源代码,这一般就是HTML格式的。网页服务器使用HTML来传递信息,如果要使用爬虫获取信息,就必须懂得HTML格式是如何保存信息的。好在HTML只是一种标记语言,学起来并不会很复杂。

一般来说,HTML文件的后缀名为html或htm。在VS Code中也可以编辑HTML,自己新建一个test.html文件,安装VS Code推荐的扩展,然后在VS Code的右上角,横着的三个点左边有一个 Open Preview ... ,点击这个就能在左边编辑html文件、右边实时预览网页。html文件编辑好了以后,可以双击它、用浏览器打开,看看实际的效果。

2.HTML的一般格式

在菜鸟教程上有一个HTML的例子:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
</head>
<body>
 
<h1>我的第一个标题</h1>
 
<p>我的第一个段落。</p>
 
</body>
</html>

下面是笔者的缩进+详细注解版:

<!DOCTYPE html> #这一行是声明文件为html文档
<html> #html的根标签(根元素),注意最后一行有一个呼应的 </html> 。所有其他标签都包含在这个根元素里
    <head> #head元素是一个二级元素,它里面的标签包含了文档的元数据(meta data),一般来说它的内容不多
        <meta charset="utf-8"> #指定字符集为UTF-8。对于中文页面,有些网页也可能使用 gb2312 或 gbk 编码
        <title>菜鸟教程(runoob.com)</title> #这个title会在用浏览器打开网页时显示为这个标签页的名字
    </head>
    <body> #body也是一个二级元素,这里面是网页的主体,包含了爬虫需要爬取的大部分信息
    
        <h1>我的第一个标题</h1> #h1是标题标签。从h1到h6,显示的字体会逐渐变小
        
        <p>我的第一个段落。</p> #p是段落标签
    
    </body>
</html>

通过这个示例,可以总结出HTML的一般规律:html由标签(也称为元素)套娃组成,一般来说多数标签会有一个对应的结束标签(比如 <html></html><head></head> ;但这里的 <meta> 就没有收尾),两者中间的称为标签的内容(如 <h1>我的第一个标题</h1>我的第一个标题 就是 <h1> 标签的内容)。html的一般格式为:

<!DOCTYPE html>
<html>
    <head>
        #一些元数据
    </head>
    <body>
        #主体内容
    </body>
</html>

另外,还有一点没有提及,就是标签的属性。比如最常用的 <a> 标签,可以在你的html文件的主体部分加上:

<a href="https://www.runoob.com/html/html-tutorial.html" title="runoob教程">runoob-HTML教程</a>

在预览中,这个地方是可以点击的。 <a> 标签一般就用于这种网页超链接(能跳转到其他网址的链接),这里的 hreftitle 都是 <a> 标签的属性,这两个属性的值分别为 "https://www.runoob.com/html/html-tutorial.html""runoob教程" ,这其实有点像键值对,或者可以说就是键值对。

最后,html文件中也可以写注释,格式为:

<!-- 注释内容 -->

3.HTML的各种标签

到这里之前,HTML的框架就已经学完了,接下来要了解的是各种标签。观察别人网页的源码,看起来十分复杂,但其实增加网页的内容主要是靠各种HTML标签,当然更重要的还是各种CSS等样式表。在这里,由于只是从爬虫的角度介绍html,所以就不涉及css了(更重要的原因是笔者也不会)。下面的标签,如果没有特殊说明,都有对应的结束标签。

(0)常见的属性

在介绍各种标签之前,先介绍一下很多标签都能通用的属性:
class,即类名,这个属性和CSS样式有关;
id,这个属性主要是用来区分标签,对爬虫来说也常用id定位标签;
style,也是标签的样式相关;
title,标签的额外信息。上面的 <a> 标签就有一个 title 属性,当你把鼠标悬停在超链接上时就会显示这个 title 属性的值。

(1)标题与段落

前面已经介绍过了,h1、h2、h3、h4、h5、h6标签可以定义标题;p 标签定义段落,在p标签里使用 <br> 可以换行,如:

<p>这个<br>段落<br>演示了分行的效果</p>

另外, <hr> 可以定义一根横线,并且 brhr 都没有结束标签。

(2)文本格式

这些标签可以更改标签内容的格式。最常使用的是粗体、斜体,分别为 <strong><em> ,也可以写成 <b><i> (bold和italic)。其他还有上下标、代码样式等,详见 菜鸟教程

(3)网页与图像

超链接标签,即前文提到过的 <a> 标签,通用的格式为:

<a href="超链接跳转的地址">超链接显示内容</a>

不止其他网页, <a> 标签还能用于跳转到页面内的其他标签处,或者跳转到图片。最后,如果 <a> 标签设置了属性 target="_blank" ,则会在浏览器的新标签页打开链接。
对于写前端的同学,比较保险的写法是 <a href="跳转地址" target="_blank" rel="noopener noreferrer">显示内容</a>rel="noopener noreferrer" 可以防止浏览器的空白标签页被篡改(比如360、hao123)。

图像标签即 <img> ,它是没有结束标签的,通用格式如下:

<img src="图像存储位置" alt="替换文本" width="图片宽度" height="图片高度">

src属性的值是图片的存储位置,一般这个值都是图床网址,表示图片来自图床服务提供商给出的网址(网页服务器的存储空间有限,所以经常要把图片传到图床,图床是一种专门存储图片的服务,有免费的有收费的);alt(alternative)的值是当图片无法显示时就显示这个替换文本;width、height用于指定图片的宽高。
如果要使用爬虫爬取图片,img的src值就十分重要。另外,scrapy爬gif也有点讲究,见后续的文章。

(4)头部

<head> 标签,这里定义html文档的元数据。里面常用的标签有:
title,注意这里是title标签( <title> )而不是title属性,定义文档的名称;
meta,定义文档的字符集、作者、描述等,例如: <meta charset="utf-8">
link,描述基本链接地址,如果文档中的 <a> 标签没有指定 href 属性,就会使用这个基本链接地址;

(5)表格及列表

表格即 table 标签,里面有很多 td 、 tr 、 th 标签,它们一起可以表示一个表格。

列表可能为 ul (无序列表)、 li (有序列表)、 或 dl 标签,前两者有 li 子标签,后者有 dt 、 dd 子标签。

(6)区块元素

div 和 span 标签,这两个标签很常用(尤其是div),没有什么特殊的含义;前者用于包括某一部分的所有标签(类似于python中的程序块),通常可以搭配CSS设置这一部分的样式;后者也是用于设置样式,但用于标签内部,如: <p>我的母亲有 <span style="color:blue">蓝色</span> 的眼睛。</p> ,就设置了一段文字中两个文字的样式(颜色)。

(7)表单与输入

最常见的应用场景是用户名与密码的输入,示例如下:

<form action="">
    Username: <input type="text" name="user"><br>
    Password: <input type="password" name="password">
</form>

form 标签的 action 属性用于指定提交后执行什么动作,一般是js脚本(见后); input 标签用于输入,当其 type 属性为 "password" 时,输入的内容会以圆点代替(秘密)。另外, type 属性还可以设置为 "radio" 、 "checkbox" 、 "submit" 、 "text" ,分别是单选、多选、提交按钮、普通文本。

(8)脚本

在网页中也可以运行一些脚本(一般是JavaScript语言写的脚本,简称js脚本),js脚本放在 script 标签里。js展开说又是很长的篇幅,在这里只需要知道js为网页提供了更丰富的界面和行为,但常常会增加爬取的难度。解决js,可以有几种方法,有可能的话可以绕开js;用selenium操作无头浏览器;用其他方法在爬虫中运行js。这三种方法笔者最简易的是第二种,优点是简单、更像人类操作(反爬虫检测),缺点是爬的稍慢。

(9)iframe框架

iframe 标签,也有 src 属性,它的功能类似 a 标签但更复杂,可以为超链接设置更丰富的样式。

到此为止,关于爬虫需要学的HTML知识就这么多了,遇到忘记的标签可以到 这个网页 查找。

4.python解析HTML

学习完了HTML,接下来看看python中如何解析网页的html源码,找到所需的信息。从抽象一点的角度来说,HTML是一种信息的存储格式,标签本身的组成分为标签名、属性、内容3个部分。爬虫需要获取的信息常位于属性(如 img 标签的 src 属性)和标签的内容中,要定位这些信息则需要根据标签名和属性(如 id 属性)筛选标签。之前说过,html就是标签层层套娃,对html解析就是解开套娃,搞清楚层级关系(当然有些方法跳过了对html的解析,直接获取想要的信息,如re)。
在python中,有以下一些方法对HTML文档进行解析。

(1)bs4库

bs4库用来解析html对新手而言是最容易上手的。

在前面的文章 Python,爬虫与深度学习(2)——爬虫详解(一)爬取金融机构成交持仓排名 中,有如下代码:

session = requests.Session()
session.mount('http://', HTTPAdapter(max_retries=3))
session.mount('https://', HTTPAdapter(max_retries=3))
xml = session.get(url, timeout=30, headers=headers)
xml.encoding = xml.apparent_encoding
if r'<title>网页错误</title>' in xml.text:
    return 404
abs_file_path = spider.get_abs_file_path(date, kind)
with open(abs_file_path, "w", encoding=xml.encoding) as f:
    f.write(xml.text)
    f.close()
...
...
...
with open(abs_file_path, "rb") as f:
    xml = f.read()
    f.close()
    return BeautifulSoup(xml, 'lxml')

这部分代码是用requests库爬取并存储网页的html源码,用bs4解析html源码。详细解析如下:

session = requests.Session() #这里用session即会话,更稳定一点。会话的意思是用这个对象连接,和网页服务器间的通讯是会保持的(直到这个会话对象消亡,一般是爬虫程序结束的时候)
session.mount('http://', HTTPAdapter(max_retries=3)) #设置访问http页面时,如果没有正确响应,最多一共会连接4次(第一次连接,加后续3次尝试重连)
session.mount('https://', HTTPAdapter(max_retries=3)) #设置访问https页面
xml = session.get(url, timeout=30, headers=headers) #取名xml是因为访问页面的 head 里有提示是xml文档(见前面的文章)。xml、html都差不多,xml在一些方面更严格一点,如标签名必须用小写、能有结束标签的必须写结束标签
xml.encoding = xml.apparent_encoding #这也是requests库常用操作之一,requests库在生成响应对象(xml)时就会自动判断其字符集,可以直接用起来
if r'<title>网页错误</title>' in xml.text: #这里用python的 字符串 in 字符串 操作查找一个标签,因为bs4解析标签要稍微慢一点。xml.text就是页面的html源码字符串
    return 404
abs_file_path = spider.get_abs_file_path(date, kind)
with open(abs_file_path, "w", encoding=xml.encoding) as f: #这里用 "w" (写入)、html页面的字符集格式打开文件并写入
    f.write(xml.text)
    f.close()
...
...
...
with open(abs_file_path, "rb") as f: #读取的时候注意用 "rb" 只读、二进制读取,二进制下也不用规定字符集了
    xml = f.read()
    f.close()
    return BeautifulSoup(xml, 'lxml') #bs4对象(BeautifulSoup类)生成时需要指定内容和解析器,这里用的lxml解析器。lxml解析器需要python下载安装了lxml库才能使用

如果不把页面保存为文件,想直接用bs4对象的话,就把 return 404 后的内容改成一行 bs_object = BeautifulSoup(xml.text, 'lxml') 即可。但笔者建议获取到网页响应的第一时间还是应该把网页源码先存起来,后面再慢慢解析。

下面是用bs4库中BeautifulSoup对象的一些方法在网页源码中寻找标签:

if bs_object.find('title', text='网页错误'):
    ...
for dataitem in bs_object.find_all('data'):
    instrumentid = dataitem.find('instrumentid').get_text()
    ...

这里用到了BeautifulSoup对象的find、find_all、get_text方法。BeautifulSoup对象经常使用的方法及其作用如下(笔者稍微查看了bs4源码,加上了一些自己的使用经验,可能有一些不准确的地方):

a.找到单个标签

find(self, name=None, attrs={}, recursive=True, string=None, **kwargs) 方法,name用于指定标签名,attrs用于指定标签含有的属性及其值(这里需要的是字典。另外,如果属性名为 class ,要写成 class_find('标签名', class_='属性值') ,因为class是python的关键字;或者写成 find(attrs={"class":"属性值"}) 也可以)。recursive用于指定是否只是在标签的第一层之内查找(不到后代标签中查找),string在bs4源码里没有解释,另外 limit=整数 (这个参数算在 **kwargs 参数里)可以指定只返回前几个结果(对 find 方法没用,但对find系列的其他方法有用)。 find 方法只会返回第一个符合特征的标签对象。后面所有的find系列方法的参数都是一样的。
关于 find 方法参数的进一步内容:
name 这个参数可以传入列表,如 name=['a','img'] ,就能匹配多个标签名;
attrs 这个参数的使用比较复杂。可以使用正则表达式如 find('a', id=re.compile(r'.\d')) (这里只对id一个属性做限制,可以不写在字典里);如果不知道属性值,就用 True ,如 find('a', id=True) ,只要a标签有id这个属性,不管值是什么都会匹配到;当然,笔者最推荐的还是写成字典,如 attrs={"class":"website", "id":True} ,看起来更顺眼一点。最后,如果只知道属性值不知道属性名,笔者还不知道用bs4怎么匹配(attrs字典里的属性名不能用re),但这种情况应该不太会遇到。

b.找到多个标签

find_all 方法,需要的参数和 find 方法是一样的,但是会返回所有符合特征的标签对象列表。实际上,在源码中, find 方法是调用了 find_all 方法完成的。

c.其他find系列的方法

find_nextfind_all_next ,如果调用这两个方法的标签对象称为a,这两个方法会返回出现在a标签之后的、符合特征的标签对象,find_previousfind_all_previous 则是往a标签之前找;
find_next_siblingfind_next_siblings ,会返回符合特征的a标签之后的兄弟标签(兄弟标签就是几个标签有相同的父标签,处在同一层级), find_previous_siblingfind_previous_siblings 则是向前找;
find_parentfind_parents ,从a标签一路向祖先标签找,前者只返回一个最近的、符合特征的祖先标签,后者返回所有符合特征的祖先标签。
这些方法用的不多,最常用的还是 findfind_all 方法。

d.标签的层级关系

childrendescendantsparents 、 各种 siblings 方法,它们分别返回某个标签对象的 第一层子标签 、 后代标签 、 祖先标签 、 各种兄弟标签。这些方法用的不多。

e.取得标签的内容

get_text(self, separator="", strip=False, types=default) 方法,separator指定内容的分隔符(因为这个方法会将后代标签的内容也全部取出),strip用于去除内容前后的空白字符( \n\t 、空格。类似字符串的 strip 方法)。

f.取得标签的属性值

getget_attribute_list 方法,它们的参数列表都是 (self, key, default=None) ,其中key是属性名(字符串),default用于指定当标签没有这个属性时返回什么。get与get_attribute_list唯一的不同在于后者返回一个列表。另外, has_attr(self, key) 方法可查看标签是否有某个属性。

g.取得标签名

标签.name ,这是一个数据成员。

h.格式化输出

prettify(self, encoding=None, formatter="minimal") 方法,用于整理源码的格式(比如添加缩进等),很少用到,要用的话一般就是 print(标签对象.prettify())
如上所述,用的最多的就是 findfind_allget_textget标签对象.name

(2)正则表达式

正则表达式是另一种解析网页html的方式,优点是对于懂re(regular expression)的人来说上手很快(正则表达式在编程中经常用到,用来查找字符串很方便,推荐学习),缺点是解析复杂网页时可能运行速度稍慢。这里笔者并不推荐用re查找、获得所需信息(除非想熟悉re,则可以多用re),但想获得re的更多知识,可以参考 runoob教程 ,或者参考笔者后续的文章。

(3)XPath

XPath也是一种查找标签、获得信息的方式,它的优点是写起来简单且可以即时反馈。现在打开你的chrome浏览器,随便打开一个网页,按F12,在上方选择 元素 这个标签页,(如果是英文,在右上角有一个齿轮图标,点进去可设置页面为中文)按ctrl+F,可以看到底端有一个框、一行小字:按字符串、选择器或 XPath 查找,也就是说在这里输入XPath表达式就能查找标签,然后你就可以把XPath表达式直接复制到爬虫程序中。这也是笔者最推荐的解析方式,在浏览器和python中通用,简单,虽然有时可能稍慢。另外,浏览器也能自动帮你写XPath,在 元素 标签页的左边,最左边一项的详情是 选择网页中的相应元素即可进行检查 ,其快捷键为 ctrl+shift+C ,点击以后在原来的网页里随便点击一个网页元素,可找到对应的标签;在标签上单击右键,选择 复制 - 复制 XPath ,浏览器会帮你写该元素的XPath表达式,你可以把它粘贴到ctrl+F弹出的搜索框里试试,根据需要再进一步更改。

以下面的例子讲解:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title id="123">菜鸟教程(runoob.com)</title>
</head>

<body>
    <h1 id="234">我的第一个标题</h1>
    <h1 id="234">第二个标题</h1>
    <p id="345">我的第一个段落。</p>
    <div>
        <p id="1">30</p>
    </div>
    <div>
        <p id="2">50</p>
    </div>
</body>

</html>

a.XPath的基础操作

/ 可以分隔不同层级的标签, // 可以跳过层级、在所有后代标签里选取。举例来说:
/html/head 可以选到 <head> 标签, /html//title 则可以不写中间的标签、选到 <title> 标签。前者可以称作绝对路径,后者则是模糊路径。

... 分别可以选到当前标签和当前标签的父标签,如:
/html/head/meta/. 可以选到 <meta> 标签, /html/head/meta/.. 则可以选到 <head> 标签。

@ 可以根据标签的属性名获得标签的属性值,如:
//h1/@id 可以选到 <h1 id="234">我的第一个标题</h1> 这个标签和下一个h1标签的id属性值,即234。

text() 可选取标签的内容(包括后代标签),如: //p/text() 选取p标签及其后代标签的内容。

name()node-name(节点对象) 可返回标签名,但这两个函数在python中不能直接使用。

b.XPath的谓语

谓语用于限定条件、选取标签,谓语包含在方括号中。如:

//body/h1[1] 在 body 标签的所有 h1 子标签中选择第一个;
//body/h1[last()] 在 body 标签的所有 h1 子标签中选择最后一个;
//body/h1[last()-1] 在 body 标签的所有 h1 子标签中选择倒数第二个;
//body/h1[position()<3] 在 body 标签的所有 h1 子标签中选择前两个;
//body/h1[@id] 在 body 标签的所有 h1 子标签中选择所有含id属性的;
//body/h1[@id="234"] 在 body 标签的所有 h1 子标签中选择所有id属性值为234的;
//div[p>30] 选择 div 标签,要求其子标签中有标签名为 p 的,并且 p 标签的内容要为大于30的数字;这里也可以加引号写成 //div[p>"30"] (实际上在python爬虫里这个几乎不用)。

c.XPath的通配符

* 可以匹配任何标签名,如: //body/* 选取 body 标签的所有子标签;
@* 匹配标签的任何属性名,如: //h1[@*="234"] 选取标签名为h1,不知道属性名、只知道属性值为234的标签,或者 //h1[@*] 选取有属性的h1标签。

d.XPath的逻辑符号

| 可以表达 或 ,连接两个或多个XPath表达式。
andornot 也可以在谓语里使用。

e.XPath的函数

前面已经讲了一些函数,下面讲一些其他的。
contains(string1,string2) ,看string1中是否包含string2,一般配合谓语使用,如:
//body/h1[contains(@id,"2") and contains(text(),"我的")] 。同样道理的函数还有 starts-with(string1,string2)ends-with(string1,string2) ,但是ends-with好像有些问题(chrome和python里都不能使用,因为lxml与XPath版本的问题)。
matches(string,pattern) 判断string是否满足模式,可能和re有关。

f.XPath的轴

轴可以根据相对位置找到其他标签,详见 runoob教程 。比如,你要找当前标签之后的兄弟标签,要求该兄弟标签名为p,且是当前标签之后的第一个,可以写成 ./following-sibling::p[1] 。这里实际上只是多了 following-sibling:: ,后面用的是 标签名[谓语] 这样的结构来限定标签。另外, p[1] 换成 *[1] 则不限定标签名,也就是 * 可以匹配任意标签名。

最后,XPath还有一些其他操作,可参考 runoob教程

g.python使用XPath

需要安装lxml库。一般格式如下:

import requests
from lxml import etree
from requests.adapters import HTTPAdapter

if __name__ == '__main__':
    headers = {
        'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
    }
    url = r'http://cffex.com.cn/sj/ccpm/202209/01/IF.xml?id=90'
    session = requests.Session()
    session.mount('http://', HTTPAdapter(max_retries=3))
    session.mount('https://', HTTPAdapter(max_retries=3))
    html = session.get(url, timeout=30, headers=headers)
    html.encoding = html.apparent_encoding

    #如果不存文件就直接 html = etree.HTML(html.content)
    #注意,这里的html.content不同于html.text,前者返回的是一个二进制对象

    with open('./test.html', 'w', encoding=html.encoding) as f:
        f.write(html.text)
        f.close()
    html = etree.parse('./test.html')  #读取文件用parse方法,不用with open
    #若出现 lxml.etree.XMLSyntaxError ,使用下面的语句:
    #html = etree.parse('./test.html', etree.HTMLParser())
    #这个错误是由于xml比html要求更严格,而etree默认用xml解析

    print(html.xpath(r"XPath表达式"))
    #这里最好用r即自然字符串

在python中的XPath表达式,不能写成 //标签名/name() 这样,而只能写 print(html.xpath(r"//标签名")[0].tag) 这样来返回单个标签的标签名。但是contains函数里可以用name(),如 contains(name(),"img")

(4)其他

除了上面的bs4库、XPath、re,还有一些爬虫中解析html的方法,如CSS选择器等,这里就不进一步扩展了。

标签: python, HTML

添加新评论