Python-爬虫篇

爬虫,即我们编写一个程序,让它不断发送网络请求去访问各个网站,再通过解析响应结果获取我们想要的数据

从而极大简化我们手动查网页获取数据的操作

极简操作

本文需要用到两个库:

  • Requests(发送请求)
  • Beautiful Soup(bs4)(解析数据)

因此重点在于两点:

  1. 发送请求

    通过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)	# 添加到请求中
    
  2. 解析数据

    • 下载第三方库

      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、图片等)渲染成页面展示给我们看

但服务器响应数据有两种方式:

  1. 普通单次请求

    服务器在响应数据给浏览器时,会将页面格式(html、css、js)以及页面涉及的数据(也就是我们要爬取的数据)一次性响应

    这种情况让我们爬取数据时更加方便,拿到响应的数据就可以直接进行数据解析

  2. 格式与数据分离

    服务器先将页面格式响应给浏览器

    浏览器根据响应的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发送请求

  1. 发送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())
  1. 发送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>.*?)&nbsp;'
                     r'.*?<br>\s*(?P<year>.*?)&nbsp;'  # 这里排除了空格,也可以在后面中通过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)
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐