【爬虫】02 - 静态页面的抓取和解析
爬虫02 - 静态页面的抓取和解析
文章目录
- 爬虫02 - 静态页面的抓取和解析
- 一:requests介绍
- 1:基本请求和请求头
- 2:实战演练
- 3:高阶使用
- 3.1:动态cookie维护
- 3.2:随机请求头轮换
- 3.3:请求频率控制
- 二:数据提取介绍
- 1:BeautifulSoup
- 1.1:准备工作
- 1.2:tag获取
- 1.3:输出
- 1.4:豆瓣读书的例子
- 2:xpath
- 2.1:准备工作
- 2.2:xpath概述
- 2.3:xpath语法
- 2.4:节点文本定位
- 2.5:XPath Axes(轴)和Step(步)
- 2.6:函数
发送请求 -> 解析响应 -> 数据清洗
- 如何避免被封禁? → 伪装请求头(User-Agent/Cookie)。
- 如何处理登录状态? → 携带Cookie维持会话。
一:requests介绍
1:基本请求和请求头
# 安装requests库
pip install requests# 发送GET请求示例
import requests
url = "https://www.example.com"
response = requests.get(url)
print(response.status_code) # 输出状态码
print(response.text) # 输出HTML内容
请求头(Headers)是HTTP请求的元数据,用于告知服务器客户端信息。爬虫需重点关注以下字段
字段 | 作用 |
---|---|
User-Agent | 标识客户端类型(浏览器/设备),帮助服务器识别客户端环境 |
cookie | 维持会话状态,包含登录凭证、页面偏好等关键信息 |
Referer | 声明请求来源页面,用于防跨站请求伪造(CSRF)等安全机制 |
以百度为例:
以此,可以伪造请求头,然后使用requests进行请求
import requests# 1:构建请求,通过request先获取到对应的HTML/XML
url = "https://movie.douban.com/top250"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36","Cookie": "Hm_lvt_abc=123456; Hm_lpvt_abc=654321","Referer": "https://www.baidu.com/"
}response = requests.get(url, headers=headers)# 准备使用bs4进行解析...# 数据清洗...# 保存...
如何获取User-Agent?
- 浏览器开发者工具(F12 → Network → Headers → User-Agent)
- 第三方库fake_useragent随机生成
from fake_useragent import UserAgent
ua = UserAgent()# ua.chrome会自动生成chrome的User-Agent
headers = {"User-Agent": ua.chrome}
如何获取cookie
- 手动获取:登录目标网站后,从浏览器开发者工具复制Cookie。
- 自动获取:通过requests.Session模拟登录流程(需分析登录接口)。
2:实战演练
准备爬取豆瓣读书top250
-> https://book.douban.com/top250
先完成注册和登录,获取到对应的UserAgent & cookie
import requests
from bs4 import BeautifulSoup# 准备发起请求,获取到页面数据
url = "https://book.douban.com/top250"
# cookie部分换成你的cookie信息,user-agent换成你的user-agent
cookie = '__utma=81379588.874323546.1752315448.1752315448.1752315448.1; __utmb=81379588.3.10.1752315448; __utmt=1; __utmz=81379588.1752315448.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=30149280.1402409421.1752315448.1752315448.1752315448.1; __utmb=30149280.17.10.1752315448; __utmt_douban=1; __utmv=30149280.19253; __utmz=30149280.1752315448.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_doumail_num=0; push_noty_num=0; _pk_ses.100001.3ac3=1; _vwo_uuid_v2=D501C4E74303BF33B06F1D9BB7692CB0A|67d6631a6a36cb00160993fdda15cd50; frodotk_db="732db6f7230b1cba598924e56921562e"; ll="118123"; ck=8unM; dbcl2="192531695:awY/q+/BD2A"; __utmc=81379588; __utmc=30149280; ap_v=0,6.0; bid=Y8amoakxSRQ; _pk_id.100001.3ac3=e520f541299e8001.1752315448.'
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"
headers = {"User-Agent": user_agent,"Cookie": cookie
}response = requests.get(url, headers=headers)if response.status_code == 200:soup = BeautifulSoup(response.text, "html.parser")books = []# 找到所有的class为item的tr标签作为解析的item基础for item in soup.find_all("tr", class_="item"):# 获取title, rating, comment_num, publish_info, 其实就是找item子元素的指定的内容title = item.find("div", class_="pl2").a["title"].strip()rating = item.find("span", class_="rating_nums").textcomment_num = item.find("span", class_="pl").text.split()[-1].strip("()")publish_info = item.find("p", class_="pl").text.split("/")[-3:]# 添加到列表中books.append({"title": title,"rating": rating,"comment_num": comment_num,"publish_info": publish_info})# 输出,正常会写入文件,这里只是测试,所以输出到控制台for index, book in enumerate(books):print(f"{index + 1}. {book['title']}")print(f" 评分: {book['rating']}")print(f" 评价数: {book['comment_num']}")print(f" 发布信息: {book['publish_info']}")print("*" * 50)
else:print("请求失败")
3:高阶使用
3.1:动态cookie维护
使用requests.Session对象自动管理Cookie:
session = requests.Session()
# 模拟登录(需分析登录接口)
login_data = {"username": "user", "password": "pass"}
session.post("https://www.example.com/login", data=login_data)
# 后续请求自动携带Cookie
response = session.get("https://www.example.com/protected-page")
3.2:随机请求头轮换
结合fake_useragent与代理IP,降低封禁风险:
from fake_useragent import UserAgent
import randomua = UserAgent()
headers_list = [{"User-Agent": ua.chrome}, {"User-Agent": ua.firefox}]# 随机选择请求头
headers = random.choice(headers_list)
response = requests.get(url, headers=headers)
3.3:请求频率控制
添加随机延迟,模拟人类操作:
import time
import randomfor page in range(1, 6):response = requests.get(f"https://example.com/page/{page}", headers=headers)time.sleep(random.uniform(1, 3)) # 随机延迟1~3秒
二:数据提取介绍
通过requests获取到response之后,下一步就是使用bs4或者xpath进行数据信息的提取了
我的css介绍,了解选择器部分, 因为要用选择对应位置的内容
场景 | 推荐工具 |
---|---|
快速原型开发、简单页面 | BeautifulSoup |
复杂结构、高性能需求 | Xpath |
1:BeautifulSoup
1.1:准备工作
先安装bs4
pip install beautifulsoup4
然后再爬虫脚本中引入
from bs4 import BeautifulSoup
BeautifulSoup语法
# 常用解析器:html.parser, lxml, lxml-xml, html5lib
soup = BeautifulSoup(解析内容,解析器)
解析器 | 使用方法 | 优势 | 劣势 |
---|---|---|---|
Python标准库 | html.parser | 执行速度适中、文档容错能力强 | |
lxml-HTML解析器 | lxml | 速度快、文档容错能力强 | 需要安装C语言库 |
lxml-XML解析器 | lxml-xml | 速度快、唯一支持XML的解析器 | 需要安装C语言库 |
html5lib | html5lib | 最好的容错性、以浏览器的方式解析文档、生成HTML5格式的文档 | 速度慢 |
1.2:tag获取
先介绍几个简单的浏览方法
#获取Tag,通俗点就是HTML中的一个个标签
soup.title # 获取整个title标签字段:<title>The Dormouse's story</title>
soup.title.name # 获取title标签名称 :title
soup.title.parent.name # 获取 title 的父级标签名称:head
soup.p # 获取第一个p标签字段:<p class="title"><b>The Dormouse's story</b></p>
soup.p['class'] # 获取第一个p中class属性值:title
soup.p.get('class') # 等价于上面
soup.a # 获取第一个a标签字段
soup.find_all('a') # 获取所有a标签字段
soup.find(id="link3") # 获取属性id值为link3的字段
soup.a['class'] = "newClass" # 可以对这些属性和内容等等进行修改
del bs.a['class'] # 还可以对这个属性进行删除
soup.find('a').get('id') # 获取class值为story的a标签中id属性的值
soup.title.string # 获取title标签的值 :The Dormouse's story
获取拥有指定属性的标签
# 方法一:获取单个属性
soup.find_all('div',id="even") # 获取所有id=even属性的div标签
soup.find_all('div',attrs={'id':"even"}) # 效果同上# 方法二:
soup.find_all('div',id="even",class_="square") # 获取所有id=even&class=square属性的div标签
soup.find_all('div',attrs={"id":"even","class":"square"}) # 效果同上
获取标签的属性值
# 方法一:通过下标方式提取
for link in soup.find_all('a'):print(link['href']) # 等同于 print(link.get('href'))# 方法二:利用attrs参数提取
for link in soup.find_all('a'):print(link.attrs['href'])
获取标签中的内容
divs = soup.find_all('div') # 获取所有的div标签
for div in divs: # 循环遍历div中的每一个diva = div.find_all('a')[0] # 查找div标签中的第一个a标签 print(a.string) # 输出a标签中的内容, 如果结果没有正确显示,可以转换为list列表# 去除\n换行符等其他内容 stripped_strings
divs = soup.find_all('div')
for div in divs:infos = list(div.stripped_strings) # 去掉空格换行等bring(infos)
1.3:输出
prettify()
方法将soup的文档树格式化后以Unicode编码输出,每个XML/HTML标签都独占一行
markup = '<a href="http://example.com/" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n <a href="http://example.com/" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >\n...'
print(soup.prettify())
# <html>
# <head>
# </head>
# <body>
# <a href="http://example.com/" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >
# I linked to
# <i>
# example.com
# </i>
# </a>
# </body>
# </html>
如果只想得到tag中包含的文本内容,那么可以调用 get_text() 方法
这个方法获取到tag中包含的所有文版内容包括子孙tag中的内容,并将结果作为Unicode字符串返回:
markup = '<a href="http://example.com/" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)
soup.get_text()
'\nI linked to example.com\n'
soup.i.get_text()
'example.com'
1.4:豆瓣读书的例子
进入浏览器的开发者模式准备进行检查,分析html格式,为下面的解析代码编写做准备。
import requests
from bs4 import BeautifulSoup# 准备发起请求,获取到页面数据
url = "https://book.douban.com/top250"
# cookie部分换成你的cookie信息,user-agent换成你的user-agent
cookie = '__utma=81379588.874323546.1752315448.1752315448.1752315448.1; __utmb=81379588.3.10.1752315448; __utmt=1; __utmz=81379588.1752315448.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=30149280.1402409421.1752315448.1752315448.1752315448.1; __utmb=30149280.17.10.1752315448; __utmt_douban=1; __utmv=30149280.19253; __utmz=30149280.1752315448.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_doumail_num=0; push_noty_num=0; _pk_ses.100001.3ac3=1; _vwo_uuid_v2=D501C4E74303BF33B06F1D9BB7692CB0A|67d6631a6a36cb00160993fdda15cd50; frodotk_db="732db6f7230b1cba598924e56921562e"; ll="118123"; ck=8unM; dbcl2="192531695:awY/q+/BD2A"; __utmc=81379588; __utmc=30149280; ap_v=0,6.0; bid=Y8amoakxSRQ; _pk_id.100001.3ac3=e520f541299e8001.1752315448.'
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15"
headers = {"User-Agent": user_agent,"Cookie": cookie
}response = requests.get(url, headers=headers)if response.status_code == 200:soup = BeautifulSoup(response.text, "html.parser")books = []# 找到所有的class为item的tr标签作为解析的item基础for item in soup.find_all("tr", class_="item"):# 获取title, rating, comment_num, publish_info, 其实就是找item子元素的指定的内容# 找class="pl2"的div, 其中a的title就是书的名称title = item.find("div", class_="pl2").a["title"].strip()# 找到class="rating_nums"的span, 内容就是评分rating = item.find("span", class_="rating_nums").text# 获取class="pl"的span, 内容中()内的部分就是评论数comment_num = item.find("span", class_="pl").text.split()[-1].strip("()")# 发布信息publish_info = item.find("p", class_="pl").text.split("/")[-3:]# 添加到列表中books.append({"title": title,"rating": rating,"comment_num": comment_num,"publish_info": publish_info})# 输出,正常会写入文件,这里只是测试,所以输出到控制台for index, book in enumerate(books):print(f"{index + 1}. {book['title']}")print(f" 评分: {book['rating']}")print(f" 评价数: {book['comment_num']}")print(f" 发布信息: {book['publish_info']}")print("*" * 50)
else:print("请求失败")
2:xpath
2.1:准备工作
先安装lxml
pip install lxml
然后再爬虫脚本中引入
from lxml import etree
2.2:xpath概述
xpath节点
在 XPath 中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释以及文档节点(或称为根节点)。
XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。(所以引入的是etree)
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore> <!-- 文档节点 -->
<book><title lang="en">Harry Potter</title> <!-- lang="en"叫做属性节点 --><author>J K. Rowling</author> <!-- 元素节点 --><year>2005</year><price>29.99</price>
</book>
</bookstore>
节点的关系(这些概念都在css中出现过)
父关系,每个元素以及属性都有一个父:
<book> <!-- book 元素是 title、author、year 以及 price 元素的父 --><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
子关系,和父关系对应,元素节点可有零个、一个或多个子
<book> <!-- title、author、year 以及 price 元素都是 book 元素的子 --><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
同胞关系(兄弟关系), 如果两个元素有这相同的父元素,那么这两个元素就是同胞关系
<book> <!-- title、author、year 以及 price 元素都是同胞 --><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
先辈关系,某节点的父、父的父…
<bookstore> <!-- title 元素的先辈是 book 元素和 bookstore 元素 -->
<book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
</bookstore>
后代关系,和先辈对应,某个节点的子,子的子…
<bookstore> <!-- bookstore 的后代是 book、title、author、year 以及 price 元素 -->
<book><title>Harry Potter</title><author>J K. Rowling</author><year>2005</year><price>29.99</price>
</book>
</bookstore>
🎉 其实如果你了解过JS的DOM的文档树,就非常的好理解
2.3:xpath语法
🎉 这部分是重中之重,通过这里才能在整个response.text中选择你想要的部分
XPath 使用路径表达式来选取 XML 文档中的节点或节点集。节点是通过沿着路径 (path) 或者步 (steps) 来选取的
假设现在response.text是这样的,下面的所有的例子将以这个为例:
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book><title lang="eng">Harry Potter</title><price>29.99</price>
</book>
<book><title lang="eng">Learning XML</title><price>39.95</price>
</book>
</bookstore>
xpath语法 - 节点选取
表达式 | 描述 | 用法 | 说明 |
---|---|---|---|
nodename | 选取此节点的所有的子节点 | div | 选取div下的所以的标签 |
// | 从全局节点中选择节点,任意的位置都可以 | //div | 选取所有的div |
/ | 选取某一个节点下的节点 | //head/title | 选取head下的title标签 |
@ | 选取带有某个属性的节点 | //div[@id] | 选择带有id属性的div标签 |
. | 当前的节点下 | ./span | 选择当前节点下的span标签 |
from lxml import etreetext = '''
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book><title lang="eng">Harry Potter</title><price>29.99</price>
</book>
<book><title lang="eng">Learning XML</title><price>39.95</price>
</book>
</bookstore>
'''html_tree = etree.HTML(text)print(html_tree.xpath('//title/text()')) # 获取所有title标签的文本 ['Harry Potter', 'Learning XML']
print(html_tree.xpath('//title/@lang')) # 获取所有title标签的lang属性 ['eng', 'eng']
print(html_tree.xpath('//book/title/text()')) # 获取所有book标签的title标签的文本 ['Harry Potter', 'Learning XML']
print(html_tree.xpath('//book/price/text()')) # 获取所有book标签的price标签的文本 ['29.99', '39.95']
print(html_tree.xpath('//book[price>30]/title/text()')) # 获取所有price标签的值大于30的title标签的文本 ['Learning XML']
xpath-谓词
谓语用来查找某个特定的节点或者包含某个指定的值的节点,被包含在方括号中
路径表达式 | 说明 |
---|---|
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last() - 1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position() < 3] | 选取最前面两个属于 bookstore 子元素的 book 元素 |
//title[@lang] | 选取拥有lang属性的title元素 |
//title[@lang='eng'] | 选取拥有lang属性,并且属性值为eng的title元素 |
/bookstore/book[price > 35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
选取未知的节点
通配符 | 描述 |
---|---|
* | 匹配任何元素节点 |
@* | 匹配任何属性节点 |
node() | 匹配任意类型节点 |
路径表达式 | 说明 |
---|---|
/bookstore/* | 选取bookstore下的所有的子元素节点 |
//* | 选取文档中的所有的元素 |
//title[@*] | 选取所有的带有属性的title |
验证手段
在google浏览器中下载xpath helper
扩展
刷新网页或者打开新的网页,再点击xpath helper, 就可以看到xpath helper的工作页面了
在一个xpath中写多个表达式用,用|分开,每个表达式互不干扰,意思是一个xpath可以匹配多个不同条件的元素
//i[@class='c-icon'] | //span[@class='hot-refresh-text']
2.4:节点文本定位
text()
:获取当前节点下所有的子节点文本,并组合成一个字符串列表 - liststring()
:获取当前节点下所有的子节点文本,并组合成一个字符串 - string
r = tree.xpath("//div[@class='tang']//li[5]/a/text()") # class=tang的div中低第五个li中的a中的文本
# 有id="hotsearch-content-wrapper"的节点,找到li/a/子节点,子节点中span子节点的内容包含孙兴的节点
//*[@id="hotsearch-content-wrapper"]/li/a/span[contains(text(),"孙兴")]
normalize-space()
normalize-space()
主要完成以下操作:
- 去掉字符串开头和结尾的空白字符
- 将字符串中间的连续空白字符(包括空格、制表符、换行符等)替换为单个空格
- 如果参数为空,则处理当前节点的文本内容
//div[normalize-space(text())="登录"]# 这会匹配所有div元素,其文本内容在规范化空白后等于"登录",例如:
# <div> 登录 </div>
# <div>登 录</div> (注意中间有空格)
# <div>登\t录</div> (包含制表符)# 这会匹配class属性值(去除多余空格后)包含"btn"的span元素
//span[contains(normalize-space(@class), "btn")]
函数 | 功能 |
---|---|
normalize-space() | 规范化空白字符 |
string() | 将参数转换为字符串 |
concat() | 连接多个字符串 |
substring() | 提取子字符串 |
2.5:XPath Axes(轴)和Step(步)
以上是普通的情况,存在可以定位的属性
当某个元素的各个属性及其组合都不足以定位时,我们可以利用其兄弟节点或者父节点等各种可以定位的元素进行定位
轴 - 非常重要!!!
child
选取当前节点的所有子元素parent
选取当前节点的父节点descendant
选取当前节点的所有后代元素(子、孙等)ancestor
选取当前节点的所有先辈(父、祖父等)descendant-or-self
选取当前节点的所有后代元素(子、孙等)以及当前节点本身ancestor-or-self
选取当前节点的所有先辈(父、祖父等)以及当前节点本身preceding-sibling
选取当前节点之前的所有同级节点following-sibling
选取当前节点之后的所有同级节点preceding
选取文档中当前节点的开始标签之前的所有节点following
选取文档中当前节点的结束标签之后的所有节点self
选取当前节点attribute
选取当前节点的所有属性namespace
选取当前节点的所有命名空间节点
步
- 轴(axis)-> 定义所选节点与当前节点之间的树关系
- 节点测试(node-test)-> 识别某个轴内部的节点
- 零个或者更多谓语(predicate)-> 更深入地提炼所选的节点集
# 步的语法如下
轴名称::节点测试[谓语]
例子 | 结果 |
---|---|
child::book | 选取所有属于当前节点的子元素的 book 节点 |
attribute::lang | 选取当前节点的 lang 属性 |
child::* | 选取当前节点的所有子元素 |
attribute::* | 选取当前节点的所有属性 |
child::text() | 选取当前节点的所有文本子节点 |
child::node() | 选取当前节点的所有子节点 |
descendant::book | 选取当前节点的所有 book 后代 |
ancestor-or-self::book | 选取当前节点的所有 book 先辈以及当前节点(如果此节点是book节点) |
child::*/child::price | 选取当前节点的所有 price 孙节点 |
xpath= "//form[@id='form']/descendant::input[@id='su']"# //form[@id='form']表示找到id属性为'form'的<form>标签,
# descendant::input表示找到<form>标签的所有后代<input>标签,
# 然后通过[@id='su']精准定位到id属性为'su'的<input>标签xpath= "//span[@id='s_kw_wrap']/following::input[@id='su']"# //span[@id='s_kw_wrap']表示定位到id属性为s_kw_wrap的<span>标签,
# /following::input[@id='su']表示找到<span>结束标签(即</span>)后的所有input标签,
# 然后通过[@id='su']精准定位到id属性为'su'的<input>标签
其他说明
- 父节点是个div,即可写成
parent::div
,如果要找的元素不是直接父元素,则不可使用parent,可使用ancestor child::
表示直接子节点元素,following-sibling
只会标识出当前节点结束标签之后的兄弟节点,而不包含其他子节点;
2.6:函数
'count(//li[@data])' #节点统计# 字符串连接
'concat(//li[@data="one"]/text(),//li[@data="three"]/text())''local-name(//*[@id="testid"])' #local-name解析节点名称,标签名称'//h3[contains(text(),"H3")]/a/text()'[0] #使用字符内容来辅助定位'count(//li[not(@data)])' #不包含data属性的li标签统计#string-length函数+local-name函数定位节点名长度小于2的元素
'//*[string-length(local-name())<2]/text()'[0]# 可以使用or或者 | 进行多条件匹配
'//li[@data="one" or @code="84"]/text()' #or匹配多个条件
'//li[@data="one"]/text() | //li[@code="84"]/text()' #|匹配多个条件# 可以使用 >, <等数值比较
'//li[@code<200]/text()' #所有li的code属性小于200的节点# div, floor, ceil等
'//div[@id="testid"]/ul/li[3]/@code div //div[@id="testid"]/ul/li[1]/@code'
#position定位+last+div除法,选取中间两个
'//div[@id="go"]/ul/li[position()=floor(last() div 2+0.5) or position()=ceiling(last() div 2+0.5)]/text()'