Python-爬虫篇
Python-爬虫篇
爬虫,即我们编写一个程序,让它不断发送网络请求去访问各个网站,再通过解析响应结果获取我们想要的数据
从而极大简化我们手动查网页获取数据的操作
极简操作
本文需要用到两个库:
- Requests(发送请求)
- Beautiful Soup(bs4)(解析数据)
因此重点在于两点:
-
发送请求
通过requests发送http,获取html内容
-
下载第三方库
pip install requests -
使用
import requests response = requests.get('http://books.toscrape.com/') if response.ok: print(response.text) # 这里拿到的response.text就是html文件 else: print("请求失败" + response.status_code)
很多网站并不提供服务给程序,此时就需要我们手动添加请求头,让程序伪装成客户端电脑
headers = { "User-Agent": 用自己电脑去访问网站,查看请求头来获取 } response = requests.get('http://books.toscrape.com/', headers) # 添加到请求中 -
-
解析数据
-
下载第三方库
pip install bs4 -
拿到soup对象
content = response.text soup = BeautifulSoup(content, 'html.parser') -
根据需求从soup中筛选数据
# 获取所有class为price_color的p标签 result = soup.findAll("p", attrs ={"class": "price_color"}) for price in result: print(price.string) # 打印标签夹住的文本
-
!!!!!!!!!!!!!!!!!!!!!
若想要深入学习爬虫,请逐步学习下面内容
!!!!!!!!!!!!!!!!!!!!!
网页请求
请求的过程:
- 我们通过浏览器发送请求到各个网站的服务器
- 服务器响应数据给浏览器
- 浏览器根据数据(html、css、js、图片等)渲染成页面展示给我们看
但服务器响应数据有两种方式:
-
普通单次请求
服务器在响应数据给浏览器时,会将页面格式(html、css、js)以及页面涉及的数据(也就是我们要爬取的数据)一次性响应
这种情况让我们爬取数据时更加方便,拿到响应的数据就可以直接进行数据解析
-
格式与数据分离
服务器先将页面格式响应给浏览器
浏览器根据响应的js文件再发送一次请求
服务器再响应页面数据
这种情况也就是动态加载页面
我们需要绕过初始 HTML,直接定位并请求动态数据接口
爬取动态页面我们会用到Selenium
总而言之,爬虫重点还是发送请求、数据解析
接下来会逐步讲解怎么实现这两步骤,技术点不断变化,需要耐心学习
发送请求
传统发送请求
# 传统发送请求,并将请求结果写入文件中
# 引入官方库,无需额外下载
from urllib.request import urlopen
url = "http://www.baidu.com"
resp = urlopen(url) # 获取响应结果
str = resp.read().decode("utf-8") # 拿到页面的源代码(字符串类型)
print(str)
# 写入文件
with open ("baidu.html", "w", encoding="utf-8") as f:
f.write(str)
Requests发送请求
- 发送get请求
import requests
url = "http://www.baidu.com"
# 设置请求头
# headers = {
# "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0"
# }
#resp = requests.get(url,headers=headers)
resp = requests.get(url) # 发送get请求
resp.encoding = "utf-8"
print(resp.text) # 文本字符串
print(resp.request.headers) # 请求头
发送get请求时,可能需要传递参数
通常直接在url后面拼接参数
也可以和添加请求头一样,指定请求参数
import requests
url = "https://movie.douban.com/j/chart/top_list"
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0"
}
params = { # 这些参数最后也是拼接在url后面发送请求
"type": "13",
"interval_id": "100:90",
"action": "",
"start": "0",
"limit": "20"
}
resp = requests.get(url,params=params,headers=headers)
print(resp.json())
- 发送post请求
import requests
url = "https://fanyi.baidu.com/sug"
data = {
"kw": input("请输入一个单词:")
}
resp = requests.post(url, data=data)
# 响应结果为json格式
print(resp.json())
解析数据
解析数据有多种方法:
- re解析(正则表达式,较复杂,但效率高)
- bs4解析(标签名+属性,极简操作中讲到了)
- xpath解析(标签层级表达式,简洁快速)
- pyquery解析(css选择器,模仿前端JQuery)
正则表达式
元字符:具有固定含义的特殊符号
测试网站:https://tool.oschina.net/regex/
| 元字符 | 描述 |
|---|---|
. |
匹配任意单个字符(换行不能匹配,后面re模块会出问题) |
\ |
转义 |
^ |
匹配开头 |
$ |
匹配结尾 |
[] |
字符集,匹配其中任意一个字符 |
| ` | ` |
() |
分组,将子表达式视为整体;捕获分组 |
\d |
匹配数字(等价于 [0-9]) |
\D |
匹配非数字(等价于 [ ^0-9 ]) |
\w |
匹配字母 / 数字 / 下划线(等价于 [a-zA-Z0-9_]) |
\W |
匹配非字母 / 数字 / 下划线 |
\s |
匹配空白字符(空格 / 制表符 / 换行等) |
\S |
匹配非空白字符 |
\b |
匹配单词边界(词与非词的位置) |
\B |
匹配非单词边界 |
小写匹配是,如\d匹配数字,大写匹配不是,如\D匹配非数字
| 量词 | 描述 |
|---|---|
* |
匹配 0 次及以上,{0,},贪婪 |
+ |
匹配 1 次及以上,{1,},贪婪 |
? |
匹配 0-1 次,{0,1},惰性 |
{n} |
匹配恰好 n 次 |
{n,} |
匹配至少 n 次 |
{n,m} |
匹配 n 到 m 次 |
量词放在要匹配的元素后面
贪婪两次会尽可能多的匹配元素,例如:
文本:你好呀,席德,你今天打游戏了吗,游戏好玩吗,我也想玩游戏!
正则:席德.*游戏
结果:席德,你今天打游戏了吗,游戏好玩吗,我也想玩游戏
而结果不是:席德,你今天打游戏
惰性尽可能匹配少的元素,还是上面的例子
正则:席德.*?游戏
结果:席德,你今天打游戏
re解析
我们请求拿到的页面源代码,其实也就是很长的字符串
re解析通过定义好正则表达式,从字符串提取想要的数据
基本使用:
import re # 官方库,不需要下载,直接引入即可
str = "你好呀,席德,你今天打游戏了吗,游戏好玩吗,我也想玩游戏!"
result = re.findall("席德.*?游戏", str) # 第一个参数写正则,第二个写待匹配文本
print(result) # ['席德,你今天打游戏']
# 预加载正则表达式
obj = re.compile("席德.*?游戏")
result = obj.findall(str)
print(result) # ['席德,你今天打游戏']
str = "我今年100岁,有1000万存款"
result = re.findall(r"\d+", str) # 这里的r避免转义
print(result) # ['100', '1000']
# 使用迭代器,常用
result = re.finditer(r"\d+", str)
for item in result:
print(item.group())
# 还有search方法以及match方法
# 前者只匹配一次就返回,用item.group()
# 后者只匹配开头,开头没有则返回none,也只匹配一次,直接打印
分组提取:
import re
str = """
<div class = 'test'><span id = '100'>广东</span></div>
<div class = 'test'><span id = '101'>湖南</span></div>
<div class = 'test'><span id = '102'>湖北</span></div>
"""
# 这里通过 小括号 分组
# 通过 ?P<分组名> 给分组定义名字
obj = re.compile(r"<span id = '(?P<id>\d+)'>(?P<name>.*?)</span>")
# 通过迭代器获取到分组
result = obj.finditer(str)
for i in result:
print(i.group()) # <span id = '100'>广东</span>
print(i.group("id")) # 100
print(i.group("name")) # 广东
案例:爬取豆瓣Top250数据
import requests
import re
# 豆瓣Top250案例
# 保存到csv文件(数据之间用逗号分隔)
f = open("top250.csv","w",encoding="utf-8")
# 循环读取页面,每页读取25条,供250条数据
for i in range(0,250,25):
url = f"https://movie.douban.com/top250?start={i}&filter="
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0"
}
resp = requests.get(url, headers=headers)
pageSource = resp.text
# re.S让正则中的.可以匹配换行
obj = re.compile(r'<div class="item">.*?<span class="title">(?P<name>.*?)</span>'
r'.*?导演: (?P<dao>.*?) '
r'.*?<br>\s*(?P<year>.*?) ' # 这里排除了空格,也可以在后面中通过str.strip()方法去除空格
r'.*?<span class="rating_num" property="v:average">(?P<score>.*?)</span>'
r'.*?<span>(?P<num>.*?)人评价', re.S)
result = obj.finditer(pageSource) # !!!!!!
for item in result:
name = item.group("name")
dao = item.group("dao")
year = item.group("year")
score = item.group("score")
num = item.group("num")
f.write(f"{name},{dao},{year},{score},{num}\n")
f.close()
resp.close()
前端讲解
后续使用bs4、xpath、pyquery都会用到前端html、css的相关知识,包括html格式、css选择器等
本文不赘述,以后再讲
bs4解析
前面极简操作中讲过了
通过使用前端标签名以及属性名从页面源代码中拿到指定标签,通过text、get()等又可以拿到标签内值以及属性值
先要下载第三方库pip install bs4
from bs4 import BeautifulSoup
# 这里的字符串也就是之前请求页面拿到的页面源代码
html = """
<html><body>
<div id="main">
<h1 class="title">Some title</h1>
<p class="description">Some description</p>
<p class="description1">Some description1</p>
</div>
</body></html>
"""
# 初始化BeautifulSoup对象
soup = BeautifulSoup(html, 'html.parser') # !!!!!
result = soup.find("p", attrs ={"class": "description"}) # 重点,写标签和标签属性
result1 = soup.find("div", attrs ={"id": "main"}) # 拿到的结果是完整标签,result.text对应标签中的内容
# 上面结果还可以继续查询
result2 = result1.find("h1")
print(result2.text, result2.name,result2.get("class")) # 拿到结果的内容,标签名,标签属性
# Some title h1 ['title']
# 拿到所有标签组成的列表
p_list = soup.findAll("p")
for p in p_list:
print(p.text)
Xpath解析
Xpath是一门在XML文档中查找信息的语言,可在XML文档中对元素、属性进行遍历
而我们拿到的页面源文件(HTML)属于XML的子集
因此可以用Xpath来查找HTML中的内容
类似re,使用表达式来进行匹配,简洁快速
通过标签层级来定位数据
概念:
- 节点
- 子节点
- 父节点
- 同胞节点
使用:
先要下载第三方库pip install lxml
from lxml import etree
import requests
# 也可以如下导入:
# from lxml import html
# etree = html.etree
url = "https://movie.douban.com/top250?start=0&filter="
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0"
}
resp = requests.get(url,headers = headers)
html = resp.text
et = etree.HTML(html) # !!!!!
result = et.xpath("/html/head")
print(result)
重点就在于xpath方法中的参数
result = et.xpath("/html") # 根节点
result = et.xpath("/html/head/p") # 所有子节点
result = et.xpath("/html/head/p/text()")[0] # 所有子节点p标签包裹的值,结果是列表,通过[索引]获取具体值
result = et.xpath("/html/head//p") # 所有子孙节点p
result = et.xpath("/html/*/p") # html节点下的所有子节点下的所有p子节点
result = et.xpath("/html/*/p[@class='title']") # 只拿class属性值为title的标签
result = et.xpath("/html/*/p/@class") # 拿p标签的class属性值
result = et.xpath("/html/*/li[2]") # html节点下所有子节点下的li子节点的第三个
PyQuery解析
通过css选择器来定位数据,和前端的JQuery很像
常见css选择器:
| 选择器类型 | 语法 | 功能描述 | 示例代码及效果 |
|---|---|---|---|
| 元素选择器 | element |
选中所有指定类型的 HTML 元素 | p { color: red; }效果:所有<p>标签内的文字变为红色 |
| ID 选择器 | #id |
选中页面中唯一拥有指定id属性的元素 |
#header { font-size: 24px; }效果:id="header"的元素字体大小为 24px |
| 类选择器 | .class |
选中所有拥有指定class属性的元素 |
.active { background: blue; }效果:所有class="active"的元素背景为蓝色 |
| 后代选择器 | selector1 selector2 |
选中selector1内部所有的selector2元素 |
nav a { text-decoration: none; }效果:<nav>内所有<a>标签去除下划线 |
| 子选择器 | selector1 > selector2 |
选中selector1直接子级的selector2元素 |
ul > li { list-style: none; }效果:<ul>直接包含的<li>去除列表样式 |
| 相邻兄弟选择器 | selector1 + selector2 |
选中selector1后紧邻的、同层级的selector2元素 |
h2 + p { margin-top: 10px; }效果:<h2>后面紧跟的第一个<p>上外边距为 10px |
| 通用兄弟选择器 | selector1 ~ selector2 |
选中selector1后所有同层级的selector2元素 |
h2 ~ p { color: gray; }效果:<h2>后面所有同层级的<p>文字变为灰色 |
| 属性选择器 | [attribute] |
选中拥有指定属性的元素 | [disabled] { opacity: 0.5; }效果:所有带disabled属性的元素透明度为 0.5 |
| 属性值选择器 | [attribute=value] |
选中属性值等于value的元素 |
[type="button"] { padding: 8px; }效果:type="button"的按钮内边距为 8px |
| 伪类选择器(常用) | selector:pseudo-class |
选中元素特定状态(如 hover、激活等) | a:hover { color: orange; }效果:鼠标悬浮在<a>标签上时文字变为橙色 |
| 伪元素选择器(常用) | selector::pseudo-element |
选中元素的特定部分(如首行、前后内容) | p::first-line { font-weight: bold; }效果:所有<p>标签的第一行文字加粗 |
使用:
依旧先要下载第三方库pip install pyquery
from pyquery import PyQuery
import requests
url = "https://movie.douban.com/top250?start=0&filter="
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36 Edg/141.0.0.0"
}
resp = requests.get(url,headers = headers)
html = resp.text
p = PyQuery(html) # !!!!!这个p可以直接打印出html结果,但不是字符串类型,而是pyquery类型
# -----------------------定位数据-------------------------
# 通过pyquery对象获取数据,直接 P(css选择器)
result = p("head meta") # 这里的result依旧是pyquery类型
result = p("head")("meta") # 因此后代选择器也可以这样被替换
# 拿属性、文本时,对单个标签如下:
result = p("head link").attr("href") # 拿属性
result = p("head link").text() # 拿文本
result = p("head meta").html() # 拿html(如果p是包裹文本的标签,则和text()效果一样)
# 拿属性、文本时,对多个标签如下:
result = p("head link").items() # 拿到属性的迭代器
for item in result:
result = item.attr("href")
print(result)
# -----------------------修改数据-------------------------
p("div.a").after("<div class='b'>b</div>") # 修改原本的html,在指定标签后面添加新的标签
p("div.a").append("<div class='b'>b</div>") # 指定标签内部添加(子标签)
p("div.a").attr("class", "b") # 新增、修改标签的属性
p("div.a").remove() # 删除标签
p("div.a").remove_attr("class") # 删除标签的属性
print(p)
更多推荐



所有评论(0)