爬虫 基本库的使用

223 阅读10分钟

requests

GET 请求

import json
import requests

params = {
	'age': 25,
	'name': 'John'
}
response = requests.get(
	url='https://www.httpbin.org/get',   
	params=params,  # GET请求传参
)
print(type(response))
print(type(response.text))
# 调用json()方法,可以将JSON格式的返回结果转化为字典
# 如果返回结果不是JSON格式,会抛出解析错误JSON.decoder.JSONDecodeEorror
print(type(response.json()))  
print(response.text)

捕获.PNG

抓取网页

import requests
import re

response = requests.get('https://ssr1.scrape.center/')
# 正则表达式
pattern = re.compile('<h2.*?>(.*?)</h2>', re.S)
titles = re.findall(pattern, response.text)
print(titles)

捕获.PNG

抓取二进制数据(图片、音频、视频等)

import requests

response = requests.get('https://scrape.center/favicon.ico')
# 将结果转化成bytes类型数据
print(response.content)
with open('favicon.ico', 'wb') as f:
	f.write(response.content)

捕获.PNG

最前面的b表示这是bytes类型数据。

favicon.ico

添加请求头

没有设置请求头,会被某些网站发现这并不是由正常浏览器发起的请求,导致返回结果异常。

import requests

headers = {
	'User-Agent': 'Mozilla/5.0 (Machintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko)' 
                      'Chrome/52.0.2743.116 Safari/    537.36'
}

response = requests.get('https://ssr1.scrape.center/', headers=headers)
print(response.text)

image.png

POST请求

import requests

data = {
	'name': 'John',
	'age': 25
}
response = requests.post("https://www.httpbin.org/post", data=data)
print(response.text)

image.png

返回结果中的form部分就是提交的数据,证明POST请求发送成功。

响应

text和content可以获取响应内容,除此之外还有其他属性和方法用来获取响应的其他信息。

import requests

response = requests.get("https://ssr1.scrape.center/")
print(type(response.status_code), response.status_code)  # 获取响应的状态码
print(type(response.headers), response.headers)  # 获取响应头
print(type(response.cookies), response.cookies)  # 获取Cookies
print(type(response.url), response.url)  # 获取URL
print(type(response.history), response.history)  # 获取请求历史

image.png

requests库提供内置状态码查询对象requests.codes

import requests

response = requests.get("https://ssr1.scrape.center/")
print("Request Failed") if response.status_code != requests.codes.ok else print("Request Successfully")

image.png

这里通过比较返回的状态码和内置的表示成功的状态码,来保证请求是否得到了正常响应。这样就不需要再在程序里写状态码对应的数字了,用字符串表示状态码会更加直观。

高级用法

文件上传

import requests

files = {'file': open('favicon.ico', 'rb')}
response = requests.post("https://www.httpbin.org/post", files=files)
print(response.text)

image.png

Cookie设置

response = requests.get("https://www.baidu.com")
print(response.cookies)
for cookie in response.cookies:
	print(type(cookie))
	print(cookie.__dict__)
	print(cookie.name+"="+cookie.value)

# items()方法可以将Cookies转化成元组列表
for key, value in response.cookies.items():
	print(key+"="+value)

image.png

也可以直接用Cookie来维持登录状态。将百度的Cookie复制下来,添加在请求的headers中:

import requests

headers = {
	'Cookie': "...",
	'User-Agent': '...'
} 

response = requests.get("https://www.baidu.com", headers=headers)

print(response.text)

结果会包含很多登录后才可见的内容,这样就可以成功模拟登录状态。爬取登录之后才可见的页面。

也可以通过cookies参数来设置Cookie信息。

import requests

cookies = "..."
# 构造一个RequestsCookieJar对象
jar = requests.cookies.RequestsCookieJar()

headers = {
	'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'
}

for cookie in cookies.split(";"):
    # 对Cookies进行处理和赋值
    key, value = cookies.split("=", 1)
    jar.set(key, value)
response = requests.get("https://www.baidu.com", headers=headers, cookies=jar)

print(response.text)

Session 维持

维持同一个Session,这样多次访问同一个网站,就不算各自独立的访问。

import requests

# 设置一个Cookie条目,名称是number,内容是123456789
requests.get("https://www.httpbin.org/cookies/set/number/123456789")
# 发送请求以获取当前的Cookie信息
response = requests.get("https://www.httpbin.org/cookies")

print(response.text)

image.png

发现并不能获取Cookie。

import requests

session = requests.Session()
session.get("https://www.httpbin.org/cookies/set/number/123456789")
response = session.get("https://www.httpbin.org/cookies")
print(response.text)

image.png

利用Session可以做到模拟同一个会话,而不用担心Cookie问题。Session通常在模拟登录之后,进行下一步操作时用到。

SSL证书验证

并不是所有网站都设置好了HTTPS证书,访问这些网站可能会出现SSL证书错误的提示。如果希望用requests库访问这些网站,需要将控制是否验证证书的参数verify设置为False。

import requests

response = requests.get("https://ssr2.scrape.center/", verify=False)
print(response.status_code)

image.png

虽然响应的状态码是200,但是还是收到了一个警告。

超时设置

timeout参数设置从发出请求到服务器返回响应的时间。超过这个时间会报错。

import requests

response = requests.get("https://www.httpbin.org/get", timeout=1)  # 超时时间单位为秒
print(response.status_code)

实际上请求分两个阶段:连接(connect)和读取(read)。上面的例子中设置的timeout是连接和读取时间的总和,如果要分开设置,则传入一个元组。

import requests

response = requests.get("https://www.httpbin.org/get", timeout=(5, 10)) 
print(response.status_code)

如果希望永久等待,可以将timeout设置为None,或者省缺,因为timeout默认取值为None。这样永远不会返回超时错误。

身份认证

用auth参数设置用户名和密码信息

import requests

response = requests.get("https://www.httpbin.org/get", auth=('admin', 'admin'))
print(response.status_code)

这里用户名和密码都是admin。

代理设置

大规模爬取会产生大量频繁的请求,要被爬取的网站就可能弹出验证码,或者跳转到登录认证页面。为了防止这种问题,我们需要代理来解决这个问题。

import requests

proxies = {
    'http': 'http://10.10.10.10:1080',
    'https': 'http://10.10.10.10:1080'
}

requests.get("http://www.httpbin.org/get", proxies=proxies)

如果需要身份验证,可以使用类似http://user:password@host:port这样的语法来设置代理。

import requests

proxies = {
    'https': 'http://user:password@10.10.10.10:1080', 
}

requests.get("http://www.httpbin.org/get", proxies=proxies)

Prepared Request

请求在requests库内部是怎么实现的呢?实际上,requests库在发送请求的时候,在内部构造了一个Request对象,并给这个对象赋值了各种参数,包括url, headers, data等,然后直接把这个Request对象发送出去。请求成功之后再得到一个Response对象,解析这个对象即可。

from requests import Request, Session

url = "https://www.httpbin.org/post"
data = {'name': 'Mack'}
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0'
}
# 构造了一个Request对象
request = Request('POST', url, data=data, headers=headers)

session = Session()
# 使用Session类的prepare_request()方法将其转换成一个Prepared Request对象
prepped = session.prepare_request(request)
# 调用send()方法发送Prepared Request对象
response = session.send(prepped) 
print(response.text)

image.png

从结果看,我们达到了POST请求的效果。有了Request对象,就可以将请求当作独立的对象来对待,能更灵活地实现请求调度和各种操作。

正则表达式库re

request库可以获取网页的源代码,得到HTML代码。数据则是在HTML中的,正则表达式可以实现字符串的检索、替换、匹配、验证,可以从HTML中提取想要的信息。

常用匹配规则

模式匹配
\w字母 数字 下划线
\W不是字母、数字和下划线的字符
\s任意空白字符,等价于[\t\n\r\f]
\S任意非空白字符
\d任意数字,等价于[0-9]
\D任意非数字字符
\A字符串开头
\Z字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z字符串结尾,如果存在换行,还会匹配换行符
\G最后匹配完成的位置
\n一个换行符
\t一个制表符
一行字符串的开头
$一行字符串的结尾
.任意字符,除了换行符。当re.DOTALL标记被指定时,可以必备包括换行符在内的任意字符
[...]用来表示一组字符,单独列出,例如[amk]用来匹配a, m或者k
[^...]匹配不在[]中的字符串,例如[^amk]用来匹配a, m或者k以外的字符
*0个或者多个表达式
+1个或者多个表达式
?0个或者1个前面的正则表达式片段,非贪婪方式
{n}精准匹配n个前面的表达式
{n, m}n到m次由前面正则表达式定义的片段,贪婪方式
aba到b
()括号内的一个表达式,也表示一个组

match

import re

content = "Hello 123 4567 World_This is a Regex Demo"
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}', content)
print(result)
# group()方法输出匹配到的内容
print(result.group())
# span()方法输出匹配到的范围
print(result.span())

image.png

正则表达式'^Hello\s\d\d\d\s\d{4}\s\w{10}'用来匹配content字符串。

  • ^表示匹配字符串的开头,这里就是以Hello开头。
  • \s表示匹配空白字符,用来匹配Hello之后的空格。
  • \d表示匹配数字,\d\d\d匹配123
  • \d{4}表示表示数字匹配4次,匹配4567
  • \w{10}表示匹配10次数字、字母和下划线

match方法中,第一个参数是正则表达式,第二个参数是要匹配的字符串。

可以看到,result是re库的Match类对象。证明匹配成功。

匹配目标

import re

content = "Hello 1234567 World_This is a Regex Demo"
print(len(content))
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result)
print(result.group())
print(result.group(1))
print(result.span())

image.png

()可以将想要提取的子字符串括起来,被标记的每个子表达式依次对用每个分组,调用group()方法时候传入分组的索引就能提取结果。

import re

content = "Hello 1234567 World_This is a Regex Demo"
print(len(content))
result = re.match('^Hello\s(\d+)\s(\w+)', content)
print(result)
print(result.group())
print(result.group(1))
print(result.group(2))
print(result.span())

image.png

这回World_This也被匹配出来了

通用匹配

.可以匹配任意字符(除换行符),*可以表示匹配前面的字符无限次,.*就能表示匹配任意字符了。

import re

content = "Hello 123 4567 World_This is a Regex Demo"

result = re.match('Hello.*Demo$', content)
print(result)
print(result.group())
print(result.span())

image.png

贪婪与非贪婪

import re

content = "Hello 1234567 World_This is a Regex Demo"

result = re.match('He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))

image.png

group(1)匹配道德只有数字7,并没有比配上全部数字。

贪婪匹配下,.*会尝试匹配尽可能多的字符。(\d+)至少一位数字,那么.*则匹配了'llo 123456',只给\d+留下了一个可满足条件的数字7。

import re

content = "Hello 1234567 World_This is a Regex Demo"

result = re.match('He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))

image.png .*?可以转化为非贪婪匹配。.*?会匹配尽可能少的字符。

修饰符

import re

content = "Hello 1234567 World_This \n is a Regex Demo"

result = re.match("He.*?(\d+).*?Demo", content)
print(result)

image.png

添加了换行符之后,就没法匹配了。说明.*?无法匹配换行符。这里需要添加一个修饰符re.S。

import re

content = "Hello 1234567 World_This \n is a Regex Demo"

result = re.match("He.*?(\d+).*?Demo", content, re.S)
print(result)
print(result.group(1))

image.png

re.S修饰符的作用是使匹配内容包括换行符在内的所有字符。常见修饰符如下:

修饰符描述
re.I使匹配对大小写不敏感
re.L实现本地化识别(locale-aware)匹配
re.M多行匹配,影响^和$
re.S使匹配内容包括换行符在内的所有字符
re.U根据Unicode字符集解析字符。这个标志会影响\w, \W, \b和\B
re.X该标志能够给予你更灵活的格式,以便将正则表达式书写的更易于理解

转义匹配

import re

content = "(百度)www.baidu.com"
result = re.match('\(百度\)www\.baidu\.com', content)
print(result.group())

image.png

这样就能匹配特殊字符,例如.和(。

search

match()方法是从字符串的开头开始匹配,意味着一旦开头不匹配,则整个匹配就失败了。

import re

content = "Extra stings Hello 1234567 World_This is a Regex Demo Extra    stings"
result = re.match('Hello.*?(\d+).*?Demo', content)
print(result)
print(result.group(1))

image.png

字符串以Extra开头,但是正则表达式以Hello开头,所以匹配失败了。

换成search()方法:

import re

content = "Extra stings Hello 1234567 World_This is a Regex Demo Extra    stings"
result = re.search('Hello.*?(\d+).*?Demo', content)
print(result)
print(result.group(1))
print(result.span(1))

image.png

获取一个视频网站的HTML,然后找出第一个热门视频的标题。

import re
import requests

headers = {
	'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
}
response = requests.get('https://www.bilibili.com/v/dance/otaku/?spm_id_from=333.1073.0.0', headers=headers)
result = re.search('<span>热门.*?title=.*?>(.+?)</h3>', response.text, re.S)
print(result.group(1))

image.png

这样会返回第一个匹配结果。

findall

search()方法只能匹配第一个与正则表达式相符的字符串。findall()可以匹配全部符合的正则表达式:

import re
import requests

headers = {
	'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
}
response = requests.get('https://www.bilibili.com/v/dance/otaku/?spm_id_from=333.1073.0.0', headers=headers)
results = re.findall('<h3 class.*?title=.*?>([^<]+?)</h3>', response.text, re.S)
print(type(results))
for result in results:
	print(result)

image.png

可以看到findall的返回结果是一个list。如果匹配多个子字符串,返回的每一项是一个元组,可以通过下标访问具体的子字符串。

sub

sub()方法可以用来修改字符串。

import re

content = "54aK54yr5oiR54ix5L2g"
content = re.sub("\d", "", content)
print(content)

image.png 这样可以把content中的数字\d都替换成空字符""。

sub()可以用于删除HTML中无关的节点,使得正则表达式变得简单易写。

compile

compile()方法可以将字符串编译成正则表达式对象,以便在后面的匹配中复用。

import re

contents = ['2019-12-25 12:00', '2021-03-05 13:54', '2024-01-26']
pattern = re.compile('\d{2}:\d{2}')
print(type(pattern))
results = []

for content in contents:
	results.append(re.sub(pattern, '', content))

print(results[0], results[1], results[2])

image.png

这里删除了日期中的时间信息,compile()方法将一个正则表达式转化成一个正则表达式对象(Pattern对象)。

complie还可以传入修饰符,例如re.S等修饰符,这样search(), findall()等方法中就不需要额外传了。可以说compile()方法是给正则表达式做了一层封装,以便我们更好的复用。

httpx库

如果网站强制使用HTTP/2.0协议访问,requests库就无法爬取数据,因为requests库只支持HTTP/1.1,不支持HTTP/2.0协议。

import requests

response = requests.get("https://spa16.scrape.center/")
print(response.text)

image.png

抛出了RemoteDisconnected错误,请求失败

httpx库支持HTTP/2.0,requests库的功能它几乎都支持。

安装

要安装支持HTTP、2.0协议的httpx库,可以这样安装:

pip3 install httpx[http2]

基本使用

httpx和rquests库有很多API存在相似之处。

import httpx

response = httpx.get("https://httpbin.org/get")
print(response.status_code)
print(response.headers)
print(response.text)

image.png

注意到这里User-Agent的值是python-httpx/0.24.1,代表我们是用httpx请求的。换一个User-Agent再试一次:

import httpx

headers = {
	'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, Gecko) ' \
	'Chrome/90.0.4430.93 Safari/537.36'
}
response = httpx.get("https://httpbin.org/get", headers=headers)
print(response.text)

image.png

更换后User-Agent的生效了。

利用httpx访问spa16.scrape.center/

import httpx

response = httpx.get("https://spa16.scrape.center/")
print(response.text)

image.png

也报错了,原因是httpx默认支持HTTP/1.1而不是HTTP/2.0,需要手动声明才能使用HTTP/2.0。

import httpx

client = httpx.Client(http2=True)
response = client.get('https://spa16.scrape.center/')
print(response.text)

image.png

这里声明了一个Client对象,同时显式地设置其http2参数为True,这样就开启了对HTTP/2.0的支持

其他方法的请求也是类似的:

import httpx

response = httpx.post('https://www.httpbin.org/post', data={'name': 'John'})
# 响应的状态码
print(response.status_code)
# 响应头
print(response.headers)
# 将文本内容转换成JSON对象
print(response.json())
# 响应体的二进制内容,用于下载图片、视频等
print(response.content)
# 响应体的文本内容
print(response.text)

image.png

Client对象

httpx的Client类,可以与requests的Session类进行类比。

import httpx

with httpx.Clinet() as client:
    response = client.get('https://www.httpbin.org/get')
    print(response)

这种写法等价于:

import httpx

client = httpx.Clinet()
try:
    response = client.get('https://www.httpbin.org/get')
finally:
    client.close()

这两种写法的运行结果是一样的,只不过第二种需要手动调用close()方法关闭Client对象。

此外在声明Client对象的时候可以指定一些参数,使用该对象发起的请求都会默认带上这些参数配置:

import httpx

url = 'http://www.httpbin.org/headers'
headers = {'User-Agent': 'My-app/0.0.1'}
with httpx.Client(headers=headers) as client:
    response = client.get(url)
    print(response.json()['headers'])

image.png

更多Clinet类的高级用法,请参考官方文档:www.python-httpx.org/advanced/

支持HTTP/2.0

就像最前面的例子一样,想要让Client对象支持HTTP/2.0协议,需要将其参数http2设置为True。

import httpx

client = httpx.Client(http2 = True)
response = client.get('https://www.httpbin.org/get')
print(response.http_version)

image.png

response变量的http_version属性是requests库中不存在的属性,代表了访问网站的传输协议。

异步请求

httpx支持异步客户端请求(即AsyncClient),支持Python的async请求模式:

import httpx
import asyncio

async def fetch(url):
	async with httpx.AsyncClient(http2=True) as client:
		response = await client.get(url)
		print(response.text)

asyncio.get_event_loop().run_until_complete(fetch('https://www.httpbin.org/get'))

image.png