爬虫学习:基本库的使用
1. 使用 urllib
1.1 发送请求
1.1.1 urlopen()
- urllib.request 模块提供了最基本的构造 HTTP 请求的方法,它可以模拟浏览器的一个请求发起过程,同时还带有处理授权验证,重定向,浏览器 Cookies 以及其他内容
# 抓取 Python 官网
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))
<!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 lt-ie8 lt-ie9"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
...(文档太长此处省略)
- 利用 type 方法输出相应的类型
print(type(response))
<class 'http.client.HTTPResponse'>
- 响应为 HTTPResponse 对象,主要包含 read(), readinto(), getheader(name), getheaders(), fileno() 等方法,以及 msg, version, status, reason, debuglevel, closed 等属性
response.status # 结果的状态码
200
response.getheaders() # 响应头
[('Server', 'nginx'),
('Content-Type', 'text/html; charset=utf-8'),
('X-Frame-Options', 'DENY'),
('Via', '1.1 vegur'),
('Via', '1.1 varnish'),
('Content-Length', '48402'),
('Accept-Ranges', 'bytes'),
('Date', 'Fri, 26 Jul 2019 02:17:41 GMT'),
('Via', '1.1 varnish'),
('Age', '1290'),
('Connection', 'close'),
('X-Served-By', 'cache-iad2132-IAD, cache-hnd18731-HND'),
('X-Cache', 'MISS, HIT'),
('X-Cache-Hits', '0, 1457'),
('X-Timer', 'S1564107461.004033,VS0,VE0'),
('Vary', 'Cookie'),
('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
response.getheader('Server') # 响应头中 Server 的值
'nginx'
-
urlopen 函数的 API:
urlllib.request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
- data 参数
- data 参数是可选的,如果要添加该参数,需要使用 byte() 方法将参数转化为字节流编码格式的内容,即 bytes 类型。
- 如果传递了 data 参数,则它的请求方式不再是 GET,而是 POST
- bytes()方法的第一个参数是 str 类型,需要用 urllib.parse 模块里的 urlencode()方法来将参数字典转换为字符串。第二个参数指定编码格式。
- 结果中可以看到传递的参数出现在了form字段中,这表明是模拟了表单提交方式,以 POST 方式传输数据
import urllib.parse
import urllib.request
data = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8')
response = urllib.request.urlopen('http://httpbin.org/post', data=data) # data 参数
print(data)
print(response.read().decode('utf-8'))
b'word=hello'
{
"args": {},
"data": "",
"files": {},
"form": {
"word": "hello"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "10",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7"
},
"json": null,
"origin": "119.123.40.149, 119.123.40.149",
"url": "https://httpbin.org/post"
}
- timeout 参数
- timeout 参数用于设置超时时间,单位为秒,意思就是如果请求超出了设置的时间还没有得到响应,就会抛出 timeout 异常。
- 若不指定该参数,则使用全局默认时间。
- 它支持 HTTP, HTTPS, FTP 请求。
- 可以使用 try except 语句实现超时后跳过抓取:捕获 URLError 异常,判断该异常是否为 socket.timeout 类型
import socket
import urllib.error
try:
response = urllib.request.urlopen('http://httpbin.org/get', timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason, socket.timeout):
print('TIME OUT')
TIME OUT
- 其他参数
- context 参数:必须是 ssl.SSLContext 类型,用来指定 SSL 设置
- cafile 和 capath 参数分别指定 CA 证书和它的路径
- cadefault 参数已弃用,默认为 False
1.1.2 Request
-
Request 对象的构造方法:
class urllib.request.Request(url, data=None, header={}, origin_req_host=None, unverifiable=False, method=None)
- url:用于请求的 URL,必选参数
- data: 同 urlopen 里的 data
- headers:请求头,是一个字典,既可以在构造请求时通过 headers 参数直接构造,也可以通过调用请求实例的 add_header() 添加
- origin_req_host:请求方的 host 名称或 IP 地址
- unverifiable:表示这个请求是否是无法验证的,默认为 False,意思是用户有没有足够的权限来选择接收这个请求的结果(暂未理解)
- method:请求方式(GET, POST, PUT 等)
# 传入多个参数构建请求
from urllib import request, parse
url = 'http://httpbin.org/post'
headers = {
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
'Host': 'httpbin.org'
}
dict = {'name': 'Germey'}
data = bytes(parse.urlencode(dict), encoding='utf8')
req = request.Request(url, data=data, headers=headers, method='POST')
response = request.urlopen(req)
print(response.read().decode('utf-8'))
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "Germey"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"
},
"json": null,
"origin": "119.123.40.149, 119.123.40.149",
"url": "https://httpbin.org/post"
}
# 使用 add_header() 方法添加 headers
req = request.Request(url=url, data=data, method='POST')
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
1.1.3 高级用法(Cookies处理、代理设置等)——Handler
-
什么是 Handler
- Handler 可以理解为各种处理器,专门处理登录验证,或者 Cookies 以及代理设置。
- urllib.request 模块里的 BaseHandler 类是其他所有 Handler 的父类,它提供了最基本的方法,如 default_open(), protocol_request()等
- Handler 子类的例子:HTTPDefaultErrorHandler(处理HTTP响应错误),HTTPRedirectHandler(处理重定向),HTTPCookieProcesssor(处理Cookies),ProxyHandler(用于设置代理,默认代理为空),HTTPPasswordMgr(用于管理密码),HTTPBasicAuthHandler(用于管理认证)
- 另一个重要的类就是 OpenDirector,称为 Opener(实际 urlopen()就是一个Opener),Opener 可以使用 open()方法,和 urlopen()如出一辙,Opener的构建是利用的 Handler
-
验证
- 对于打开弹出提示框需要输入用户名和密码的页面,使用 HTTPBasicAuthHandler 完成:
from urllib.request import HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm, build_opener
from urllib.error import URLError
username = 'username'
password = 'password'
url = 'http://localhost:5000'
p = HTTPPasswordMgrWithDefaultRealm() # 首先实例化 HTTPPasswordMgrWithDefaultRealm 对象
p.add_password(None, url, username, password) # 利用 add_password() 添加用户名和密码
auth_handler = HTTPBasicAuthHandler(p) # 实例化 HTTPBasicAuthHandler 对象,其参数是 HTTPPasswordMgrWithDefaultRealm 对象
opener = build_opener(auth_handler)
try:
result = opener.open(url)
html = result.read().decode('utf-8')
print(html)
except URLError as e:
print(e.reason)
- 代理
- 使用 ProxyHandler 添加代理:
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener
proxy_handler = ProxyHandler({
'http': 'http://127.0.0.1:9743',
'https': 'https://127.0.0.1:9743'
}) # ProxyHandler 的参数是一个字典,键名是协议类型,键值是代理链接
opener = build_opener(proxy_handler)
try:
response = opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)
- Cookies
- 获取网站的 Cookies
- 将 Cookies 保存为 Mozilla 型浏览器的 Cookies 格式:
- 读取 Cookies 文件并利用:
# 获取网站的 Cookies
import http.cookiejar, urllib.request
cookie = http.cookiejar.CookieJar() # 声明 CookieJar 对象
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = build_opener(handler)
response = opener.open('https://www.baidu.com')
for item in cookie:
print(item.name+'='+item.value)
BIDUPSID=F930DC7B2787707F675C4D0962FBE525
PSTM=1564122900
BD_NOT_HTTPS=1
# 将 Cookies 保存为 Mozilla 型浏览器的 Cookies 格式
filename = 'cookies.txt'
cookie = http.cookiejar.MozillaCookieJar(filename) # 改为 LWPCookieJar 即可保存为 LWP 格式的 Cookies
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = build_opener(handler)
response = opener.open('https://www.baidu.com')
cookie.save(ignore_discard=True, ignore_expires=True)
# 读取 Cookies 文件并利用
cookie = http.cookiejar.MozillaCookieJar(filename)
cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = build_opener(handler)
response = opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
<html>
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>
<body>
<noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
1.2 处理异常
- urllib 的 error 模块定义了由 request 模块产生的异常,如果出现了问题,request 模块便会抛出 error 模块中定义的异常
1.2.1 URLError
- URLError 类继承自 OSError 类,是 error 异常模块的基类,它具有一个属性 reason,代表着错误的原因:
from urllib import request, error
try:
response = request.urlopen('https://cuiqingcai.com/index.htm')
except error.URLError as e:
print(e.reason)
Not Found
1.2.2 HTTPError
- HTTPError 是 URLError 的子类,专门用来处理 HTTP 请求错误,有如下三个属性:code:返回 HTTP 的状态码;reason:同父类一样,错误原因;headers: 返回请求头;
from urllib import request, error
try:
response = request.urlopen('https://cuiqingcai.com/index.htm')
except error.HTTPError as e:
print(e.reason, e.code, e.headers, sep='\n')
Not Found
404
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 26 Jul 2019 07:09:04 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Set-Cookie: PHPSESSID=8jneufd9uur912goa2ljidb1a1; path=/
Pragma: no-cache
Vary: Cookie
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Link: <https://cuiqingcai.com/wp-json/>; rel="https://api.w.org/"
- 因为 URLError 是 HTTPError 的父类,所以可以先选择捕获子类的错误,再去捕获父类的错误,所以上述代码更好的写法如下:
from urllib import request, error
try:
response = request.urlopen('https://cuiqingcai.com/index.htm')
except error.HTTPError as e:
print(e.reason, e.code, e.headers, sep='\n')
except error.URLError as e:
print(e.reason)
else:
print('Request successfully')
Not Found
404
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 26 Jul 2019 07:11:59 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Set-Cookie: PHPSESSID=v48n158pas6k0gcs0bdoq4sdb0; path=/
Pragma: no-cache
Vary: Cookie
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Link: <https://cuiqingcai.com/wp-json/>; rel="https://api.w.org/"
- 有时候 reason 属性返回的不一定是字符串,也可能是对象,通过 isinstance() 方法来判断对象类型,以便判断异常
import socket
from urllib import request, error
try:
response = request.urlopen('https://www.baidu.com', timeout=0.01)
except error.URLError as e:
print(type(e.reason))
if isinstance(e.reason, socket.timeout):
print('TIMEOUT')
else:
print('Request successfully')
<class 'socket.timeout'>
TIMEOUT
1.3 解析链接
- urllib 库提供了 parse 模块,它定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取、合并以及链接转换
1. urlparse()
- 该方法可以实现 URL 的识别和分段,如下简单例子:
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html;user?id=5#comment')
print(type(result), result, sep='\n')
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
其中,: //前面是 scheme(协议),第一个/前面是 netloc(域名),其后面是 path(访问路径),;后面是 params(参数),?后面是 query(查询条件,一般用作GET类型的URL),#后面是fragment(锚点,用于直接定位页面内部的下拉位置)。标准链接格式如下:
scheme://netloc/path;params?query#fragment
-
urlparse() 的 API 用法:
urllib.parse(urlstring, scheme='', allow_fragments=True)
- urlstring: 必填项,即待解析的 URL
- scheme:默认协议,假设输入的链接没有带协议信息,会将此参数作为默认的协议(只在 URL 不带 scheme 信息时有效)
- allow_fragments:是否忽略 fragment,若为 False,则 fragment 部分则被忽略,它会被解析为 path,params 或者 query 的一部分,而 fragment 部分为空
- 返回的结果 ParseResult 实际上是个元组,可通过索引或属性名来获取,如 result.scheme 等于 result[0], result.netloc 等于 result[1]
2. urlunparse()
- urlunparse() 为 urlparse() 的对立方法,它接受的参数是一个可迭代对象,长度必须是6,否则会参数数量错误:
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
# 除了列表类型,也可以使用元组或者其他特定数据结构
print(urlunparse(data))
http://www.baidu.com/index.html;user?a=6#comment
3. urlsplit()
- 该方法和 urlparse()相似,只是只返回 SplitResult 对象,包含 5 个结果,它将 params 这部分合并到了 path 中:
from urllib.parse import urlsplit
result = urlsplit('http://www.baidu.com/index.html;user?id=5#comment')
print(result)
SplitResult(scheme='http', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')
4. urlunsplit()
- 与 urlunparse() 相似,只是传入的参数长度必须为 5:
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']
print(urlunsplit(data))
http://www.baidu.com/index.html?a=6#comment
5. urljoin()
- 生成链接的另一个方法是 urljoin(),第一个参数 base_url 作为基础,第二个参数 new_url 作为补充,urljoin() 方法会分析 base_url 的 scheme, netloc, path 这 3 个内容(其他内容不起作用)并对新链接缺失的部分进行补充,若新链接存在这些内容则使用新链接中的内容:
from urllib.parse import urljoin
print(urljoin('http://www.baidu.com', 'FAQ.html')) # 补充 scheme 和 netloc
print(urljoin('http://www.baidu.com', 'https://raymoneshaw.com/FAQ.html')) # 没有补充
print(urljoin('http://www.baidu.com?wd=abc', 'https://raymoneshaw.com')) # query 不起作用
http://www.baidu.com/FAQ.html
https://raymoneshaw.com/FAQ.html
https://raymoneshaw.com
6. urlencode()
- urlencode() 方法可以将字典参数序列化为 GET 请求参数,如下:
from urllib.parse import urlencode
params = {
'name': 'germey',
'age': '22',
}
base_url = 'http://www.baidu.com?'
url = base_url + urlencode(params)
print(url)
http://www.baidu.com?name=germey&age=22
7. parse_qs()
- parse_qs() 可以将 GET 请求参数转换为字典:
from urllib.parse import parse_qs
query = 'name=germey&age=22'
print(parse_qs(query))
{'name': ['germey'], 'age': ['22']}
8. parse_qsl()
- parse_qs() 将参数转化为元组组成的列表:
from urllib.parse import parse_qsl
query = 'name=germey&age=22'
print(parse_qsl(query))
[('name', 'germey'), ('age', '22')]
9. quote()
- quote() 方法可以将内容转化为 URL 编码格式,例如 URL 中带有中文参数时,有时可能会导致乱码的问题,此时使用 quote() 方法则可以将中文转化为 URL 编码:
from urllib.parse import quote
keyword = '壁纸'
url = 'https://www.baidu.com/s?wd=' + quote(keyword)
print(url)
https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8
10. unquote()
- unquote() 对 URL 进行解码:
from urllib.parse import unquote
url = 'https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))
https://www.baidu.com/s?wd=壁纸
1.4 分析 Robot 协议
- 利用 urllib 的 robotparser 模块分析网站的 Robot 协议:
1.4.1 Robot 协议
-
Robot 协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准,用来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以,通常是一个 robots.txt 文件,放在网站根目录下。
-
当爬虫访问一个站点时,会首先检查 robots.txt 文件,如果该文件存在,则按照文件中定义的爬取范围来爬取,如果不存在,则访问所有可直接访问的页面。
-
一个 robots.txt 样例:
User-agent: * Disallow: / Allow: /public/
其中:
- User-agent: 搜索爬虫的名称,* 代表对任何爬虫有效,设置为 Baiduspider 则只对百度爬虫有效,可以设置多个,但至少有1个
- Disallow:指定不允许抓取的目录,上述例子代表不允许抓取所有页面
- Allow: Allow 一般和 Disallow 一起使用,用来排除某些限制,上述例子表示除了 public 目录外,其他页面都不可抓取
1.4.2 robotparser
-
robotparser() 方法提供了一个类 RobotFileParser,它可以根据网站的 robots.txt 文件来判断一个爬虫是否有权限爬取网页,实例声明为:
urllib.robotparser.RobotFileParser(url='')
-
该类只需要传入 robots.txt 的链接即可,也可以先不传入,默认为空,之后使用 set_url 方法设置
-
该类的常用方法:
- set_url(): 用于设置 robots.txt 的文件链接
- read(): 读取 robots.txt 文件的内容并进行分析,一定要调用这个方法,否则以下的判断都会为 False
- parse():用来解析 robots.txt 文件,传入的参数时 robots.txt 某些行的内容,它会按照 robots.txt 的语法规则来分析这些内容
- can_fetch(): 该方法传入两个参数,第一个是 User-agent,第二个是要抓取的 URL,返回的内容是该搜索引擎是否可以抓取这个 URL,返回的结果是 True or False
- mtime(): 返回的是上次抓取和分析 robots.txt 的时间,对长时间分析和抓取的爬虫很有必要
- modified(): 将当前时间设置为上次抓取和分析 robots.txt 的时间
1. 使用 read() 方法:
from urllib.robotparser import RobotFileParser
rp = RobotFileParser('http://www.jianshu.com/robots.txt')
rp.read()
print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d'))
print(rp.can_fetch('*', 'http://www.jianshu.com/search?q=python&page=1&type=collections'))
False
False
2. 使用 parse() 方法:
from urllib.robotparser import RobotFileParser
from urllib.request import urlopen, Request
rp = RobotFileParser()
# 原书里直接使用 urlopen,没有添加请求头,运行报错403禁止访问,需伪装成浏览器进行访问
headers = {
'User-Agent': 'Mozilla/4.0(compatible; MSIE 5.5; Windows NT)'
}
url = 'http://www.jianshu.com/robots.txt'
req = Request(url=url, headers=headers)
response = urlopen(req)
rp.parse(response.read().decode('utf-8').split('\n'))
print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d'))
print(rp.can_fetch('*', 'http://www.jianshu.com/search?q=python&page=1&type=collections'))
True
False
_________________________
???为何两种方法结果不一样
2. 使用 Requests
2.1 基本用法
2.1.1 一个实例:
import requests
r = requests.get('https://www.baidu.com/')
print(type(r))
print(r.status_code)
print(type(r.text))
print(r.text)
print(r.cookies)
<class 'requests.models.Response'>
200
<class 'str'>
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;...(文档太长此处省略)
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
2.1.2 GET 请求
1. 基本实例
- 构造一个最简单的 GET 请求,请求的链接为 http://httpbin.org/get,该网站会判断如果客户端发起的是 GET 请求的话,则返回相应的请求信息;
- 可以使用 params 参数传入一个字典,以添加额外的参数信息;
- 网页返回的类型是 JSON 格式的字符串,可以直接调用 json 方法将其转化为字典;
import requests
r = requests.get('http://httpbin.org/get')
print(r.text)
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.21.0"
},
"origin": "219.134.114.18, 219.134.114.18",
"url": "https://httpbin.org/get"
}
# 使用 params 参数传入一个字典
data = {
'name': 'Germey',
'age': '22'
}
r2 = requests.get('http://httpbin.org/get', params=data)
print(r2.text)
{
"args": {
"age": "22",
"name": "Germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.21.0"
},
"origin": "219.134.114.18, 219.134.114.18",
"url": "https://httpbin.org/get?name=Germey&age=22"
}
# 调用 json 方法将其转化为字典
print(r.json())
print(type(r.json()))
{'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.21.0'}, 'origin': '219.134.114.18, 219.134.114.18', 'url': 'https://httpbin.org/get'}
<class 'dict'>
2. 抓取网页
- 以“知乎”–>“发现”为例,在请求中加入 headers 信息,包含 User-Agent 字段信息,否则会被拒绝抓取;
- 使用最基础的正则表达式来匹配出所有问题的内容
import requests
import re
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
} # 不要把 User-Agent 写成了 User_Agent。。。
r = requests.get('https://www.zhihu.com/explore', headers=headers)
pattern = re.compile('explore-feed.*?question_link.*?>(.*?)</a>', re.S)
titles = re.findall(pattern, r.text)
print(titles)
['\n如何评价杨超越粉丝自制反网络暴力科普视频《心理少女小越》?\n', '\n游戏设计中,如何避免出现“打不过所以没资源,没资源所以打不过”的现象?\n', '\n韩国人眼中的中国是怎么样的?\n', '\n各个传统相声段子都是哪一版比较好?\n', '\n历史名人都有些什么轶事、趣事?\n', '\n一年过去,怎样评价腾讯“大文娱”布局下,哇唧唧哇对火箭少女101的运营?\n', '\n怎么样客观看待张云雷现象?\n', '\n勇士篮网先签后换,4 年 1.17 亿美元拿下德安吉洛·拉塞尔,如何评价这笔操作?\n', '\n是罗云熙成就了润玉,还是润玉成就了罗云熙?\n', '\n罗云熙在白发中饰演的容齐与香蜜中润玉的妆容造型区别在哪里?\n']
3. 抓取二进制数据
- 图片、音频、视频这些文件本质上都是由二进制码组成的,由于有特定的保存格式和对应的解析方式,所以要抓取到它们,就要拿到它们的二进制码
- 抓取 GitHub 站点的图标
import requests
r = requests.get('https://github.com/favicon.ico')
# print(r.text) 图片直接转换为字符串,出现乱码
# print(r.content) 结果以 b 开头,代表 bytes 类型数据
# 保存图片
with open('favicon.ico', 'wb') as f:
f.write(r.content)
2.1.3 POST 请求
- 以请求 http://httpbin.org/post 为例:
import requests
data = {'name': 'Germey', 'age': '22'}
r = requests.post('https://httpbin.org/post', data=data)
print(r.text)
{
"args": {},
"data": "",
"files": {},
"form": {
"age": "22",
"name": "Germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "18",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.21.0"
},
"json": null,
"origin": "119.123.34.219, 119.123.34.219",
"url": "https://httpbin.org/post"
}
2.1.4 响应
- 除了 text 和 content 可以获取响应的内容外,还可以获取状态码、响应头、Cookies 等:
import requests
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
r = requests.get('http://www.jianchu.com', headers=headers)
print(type(r.status_code), r.status_code)
print(type(r.headers), r.headers)
print(type(r.cookies), r.cookies)
print(type(r.url), r.url)
print(type(r.history), r.history)
<class 'int'> 200
<class 'requests.structures.CaseInsensitiveDict'> {'Connection': 'close', 'Date': 'Tue, 30 Jul 2019 02:49:49 GMT', 'Server': 'Microsoft-IIS/6.0', 'X-Powered-By': 'ASP.NET, ThinkPHP', 'Content-Type': 'text/html; charset=utf-8', 'Cache-control': 'private'}
<class 'requests.cookies.RequestsCookieJar'> <RequestsCookieJar[]>
<class 'str'> http://www.jianchu.com/
<class 'list'> []
- 通过状态码判断是否请求成功,requests 提供了一个内置的状态码查询对象 requests.codes,如下示例:
import requests
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
r = requests.get('http://www.jianchu.com', headers=headers)
exit() if not r.status_code == requests.codes.ok else print('Request Successfully')
Request Successfully
2.2 高级用法
2.2.1 文件上传
- requests 可以模拟提交一些数据,示例如下
import requests
files = {'file': open('favicon.ico', 'rb')}
r = requests.post('https://httpbin.org/post', files=files)
print(r.text)
{
"args": {},
"data": "",
"files": {
"file": "data:application/octet-stream;base64,AAABA...(Cookie 太长此处省略)},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "6665",
"Content-Type": "multipart/form-data; boundary=c91243e76affc0aba448829a1ac67793",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.21.0"
},
"json": null,
"origin": "113.91.43.181, 113.91.43.181",
"url": "https://httpbin.org/post"
}
2.2.2 Cookies
- 获取 Cookies:首先调用 cookies 属性即可成功获取 Cookies,然后用 item() 方法将其转化为元组组成的列表,遍历输出每一个 Cookie 的名称和值。
import requests
r = requests.get('https://www.baidu.com')
print(r.cookies)
for key, value in r.cookies.items():
print(key + '=' + value)
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
BDORZ=27315
- 可以直接用 Cookies 来维持登录状态,以知乎为例,首先登陆知乎,将 Headers 中 Request Headers 的 Cookie 内容复制下来,然后替换成自己的 Cookie,并设置到 headers 里面:
import requests
headers = {
'Cookie':'l_n_c=1; q_c1=0b3845e618e84f97b07763a0067ef058|...', # Cookie 太长此处省略
'host': 'www.zhihu.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
r = requests.get('https://www.zhihu.com', headers=headers)
print(r.text)
<!doctype html>
<html lang="zh" data-hairline="true" data-theme="light"><head><meta charSet="utf-8"/><title data-react-helmet="true">首页 - 知乎</title>...
(文档太长此处省略)
- 也可以通过设置 cookies 参数来完成,相对比较繁琐:
import requests
cookies = 'l_n_c=1; q_c1=0b3845e618e84f97b07763a0067ef058|...', # Cookie 太长此处省略
# 首先实例化 RequestsCookieJar 对象
jar = requests.cookies.RequestsCookieJar()
headers = {
'host': 'www.zhihu.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
for cookie in cookies.split(';'): # 分割 cookies
key, value = cookie.split('=', 1) # 分割 key 与 value
jar.set(key, value) # 设置每个 cookie 的 key 与 value
r = requests.get('https://www.zhihu.com', cookies=jar, headers=headers)
print(r.text)
<!doctype html>
<html lang="zh" data-hairline="true" data-theme="light"><head><meta charSet="utf-8"/><title data-react-helmet="true">首页 - 知乎</title>...
(文档太长此处省略)
2.2.3 会话维持
- 在 requests 中,直接利用 get() 或 post() 请求页面,实际上相当于两个不同的会话,即用两个浏览器打开了不同的页面
- 维持同一个会话的一种方法是使用相同的 cookies,但这个方法较为繁琐
- 使用 Session 对象可以很方便地维护一个会话,通常用于模拟登录成功之后在进行下一步的操作:
1. 不使用 Session:
import requests
requests.get('http://httpbin.org/cookies/set/number/123456789') # 设置一个 cookie
r = requests.get('http://httpbin.org/cookies') # 直接请求获取 cookies
print(r.text) # 返回为空,说明未获取到 cookies
{
"cookies": {}
}
2. 使用 Session:
s = requests.Session()
s.get('http://httpbin.org/cookies/set/number/123456789')
r = s.get('http://httpbin.org/cookies')
print(r.text)
{
"cookies": {
"number": "123456789"
}
}
2.2.4 SSL 证书验证
- 当发送 HTTP 请求时,会检查 SSL 证书,可以使用 verify 参数控制是否检查此证书,verify 默认为 True,会自动验证。例如 12306 的证书没有被官方 CA 机构信任,会出现证书验证错误(事实证明目前 12306 已经被官方 CA 机构信任了,因此不会报错):
import requests
response = requests.get('https://www.12306.cn')
print(response.status_code)
200
- 使用 verify=False 来控制不检查证书:
import requests
response = requests.get('https://www.12306.cn', verify=False)
print(response.status_code)
200
d:\python 3.7\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning)
d:\python 3.7\lib\site-packages\urllib3\connectionpool.py:847: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning)
- 消除警告:
# 设置忽略警告的方式:
import requests
from requests.packages import urllib3
urllib3.disable_warnings()
response = requests.get('https://www.12306.cn', verify=False)
print(response.status_code)
# 捕获警告到日志的方式
import logging
import requests
logging.captureWarnings(True)
response = requests.get('https://www.12306.cn', verify=False)
print(response.status_code)
200
200
- 指定本地证书用作客户端证书,可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组。本地私有证书的 key 必须是解密状态,加密的 key 是不支持的:
# 只是一段演示示例
import requests
response = requests.get('https://www.12306.cn', cert=('/path/server.crt', '/path/key'))
print(response.status_code)
2.2.5 代理设置
- 使用 proxies 参数设置代理:
import requests
proxies = {
'http': 'http://121.33.220.158:808', # 网上找的免费代理IP
'https': 'http://119.28.118.116:1080' # 只用 http 代理的话下面不会报证书错误
}
requests.get('https://www.taobao.com', proxies=proxies, verify=False) # 报证书错误,禁用证书检查
<Response [200]>
- 若代理需要使用 HTTP Basic Auth,可以使用类似 http://user:password@host:port 这样的语法来设置,如下:
import requests
proxies = {
'http': 'http://user:password@http://121.33.220.158:808',
}
requests.get('https://www.taobao.com', proxies=proxies)
<Response [200]>
- requests 还支持 SOCKS 协议的代理:
import requests
proxies = {
'http':'socks5://user:password@http://121.33.220.158:808',
# 'https':'socks5://user:password@http://119.28.118.116:1080' # 该行报错:InvalidSchema: Missing dependencies for SOCKS support
}
requests.get('https://www.taobao.com', proxies=proxies, verify=False)
<Response [200]>
2.2.6 超时设置
- 防止服务器不能及时响应,应该设置也给超时时间,超过该时间还没有得到响应则报错
import requests
r = requests.get('https://www.taobao.com', timeout=1) # timeout 默认为 None,即永久等待
print(r.status_code) # 若要分别指定连接和读取时间,则传入一个元组,如 (5, 30)
200
2.2.7 身份认证
- 使用 requests 自带的身份认证功能:
import requests
from requests.auth import HTTPBasicAuth
r = requests.get('http://localhost:5000', auth=HTTPBasicAuth('username', 'password'))
print(r.status_code)
# 用户名和密码正确的话返回 200,认证失败的话返回 401
- 更简单的写法:直接传入元组,会默认使用 HTTPBasicAuth 来认证:
import requests
r = requests.get('http://localhost:5000', auth=('username', 'password'))
print(r.status_code)
# 用户名和密码正确的话返回 200,认证失败的话返回 401
- requests 还提供了其他认证方式,如 OAuth 认证,需安装 oauth 包:
import requests
from requests_oauthlib import OAuth1
url = 'https://api.twitter.com/1.1/account/verify_credentials.json'
auth = OAuth1('YOUR_APP_KEY', 'YOUR APP_SECRET',
'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET')
requests.get(url, auth=auth)
2.2.8 Prepared Request
- 与 urllib 一样,requests 也可以将请求表示为数据结构,这个数据结构叫做 Prepared Request。该方法是使用 Session 对象的 prepare_request()方法,将 Request 对象转化为 Prepared_Request 对象,然后再通过 Session 对象的 send() 方法将 Prepared_Request 对象发送,得到响应
from requests import Request, Session
url = 'http://httpbin.org/post'
data = {
'name': 'germey'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
s = Session() # 创造一个 Session 实例
req = Request('POST', url, data=data, headers=headers) # 使用 url, data, headers 构造一个 Request 实例
prepped = s.prepare_request(req) # 调用 Session 对象的 prepare_request 方法,将 Request 对象转化为 Prepared_Request 对象
r = s.send(prepped) # 使用 Session 对象的 send() 方法发送
print(r.text)
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
},
"json": null,
"origin": "119.123.34.219, 119.123.34.219",
"url": "https://httpbin.org/post"
}
3. 正则表达式
- 正则表达式是处理字符串的强大工具,它有自己特定的语法结构,它可以实现字符串的检索、替换、匹配验证等
3.1 实例引入
- 开源中国提供的正则表达式工具:http://tool.oschina.net/regex;
- Python 中主要使用 re 库实现正则表达式
3.2 match()
- 向 match() 方法传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串
- match() 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果,如果不匹配,就返回 None:
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) # 结果是 Match 对象
print(result.group()) # Match 对象的 grouop() 方法返回匹配的内容
print(result.span()) # Match 对象的 span() 方法输出匹配的范围
41
<re.Match object; span=(0, 25), match='Hello 123 4567 World_This'>
Hello 123 4567 World_This
(0, 25)
3.2.1 匹配目标
- 使用小括号()提取匹配的字符串中的子字符串:()标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group() 方法传入分组索引即可获取提取的结果:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content) # 提取 1234567
print(result)
print(result.group()) # 输出完整的匹配结果
print(result.group(1)) # 输出第一个被 () 包围的结果
print(result.span())
<re.Match object; span=(0, 19), match='Hello 1234567 World'>
Hello 1234567 World
1234567
(0, 19)
3.2.2 通用匹配
- 使用 .* 匹配任意字符;
- . 代表匹配除换行符以外的任意字符(当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符)
- *代表匹配前面的字符任意次
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('.*', content)
print(result)
print(result.group())
print(result.span())
<re.Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'>
Hello 123 4567 World_This is a Regex Demo
(0, 41)
3.2.3 贪婪与非贪婪
- 上述的通用匹配符 .* 是贪婪的,即会尽可能匹配多的字符,这时有可能影响到我们的匹配结果,如下例子中,想要获取中间的数字,由于小括号里的 \d+ 表示至少匹配一个数字,由于前面的 .* 是贪婪方式,因此只会给 group(1) 匹配一个 7,并没有匹配到 1234567:
- 这种情况下,需要使用 ? 将 .* 设置为非贪婪方式,即 .*?。在做匹配的时候,尽量使用非贪婪匹配,避免出现匹配结果缺失的情况:
- 如果匹配的结果在字符串结尾,.*? 就可能匹配不到任何内容,因为它会尽可能匹配少的字符:
# 贪婪
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
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))
<re.Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
1234567
# 匹配结果在字符串结尾
import re
content = 'http://weibo.com/comment/kEraCn'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content)
print('result1', result1.group(1)) # .*? 匹配不到任何结果
print('result2', result2.group(1)) # .* 匹配到了结果
result1
result2 kEraCn
3.2.4 修饰符
- 正则表达式可以包含一些可选标志修饰符来控制匹配的模式,修饰符被指定为一个可选的标志;
- 如下例子,字符串中加了换行符,于是未匹配上导致报错,只要加个修饰符 re.S 即可:
- 一些修饰符:
- re.I:是匹配对大小写不敏感
- re.L:做本地化识别(locale-aware)匹配
- re.M:多行匹配,影响 ^ 和 $
- re.S: 使 . 匹配包括换行符在内的所有字符
- re.U:根据 Unicode 字符集解析字符,影响 \w, \W, \b 和 \B
- re.X:该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*Demo$', content)
print(result.group(1))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-187-a3eb03b96ee3> in <module>
6
7 result = re.match('^He.*?(\d+).*Demo$', content)
----> 8 print(result.group(1))
AttributeError: 'NoneType' object has no attribute 'group'
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*Demo$', content, re.S)
print(result.group(1))
1234567
3.2.5 转义匹配
- 当目标字符串中包含正则表达式中的特殊字符时,使用转义符\进行转义即可
3.3 search()
- match() 是从字符串开头进行匹配的,一旦开头不匹配,整个匹配就会失败,因此 match() 更适合用来检测某个字符串是否符合某个正则表达式的规则;
- search() 方法在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果,也就是说正则表达式可以是字符串的一部分:
import re
content = 'Extra string Hello 1234567 World_This is a Regex Demo Extra string'
result = re.search('He.*?(\d+).*?Demo', content)
print(result)
<re.Match object; span=(13, 53), match='Hello 1234567 World_This is a Regex Demo'>
- 对于如下的 HTML 文本,可以看到有很多 li 节点,其中 li 节点中有的包含 a 节点,有的不包含,a 节点还有一些属性(歌曲链接和歌手名)。
- 尝试使用 search 提取 class 为 active 的 li 节点内部的歌手名和歌名:
- .*? 不能存在于开头或结尾,否则什么都匹配不到
- search() 返回第一个匹配到的目标,因此如果将 active 去掉会匹配到第二个li节点
- 若不加 re.S 则会匹配到第四个li节点,因为第三个包含换行符,一般 HTML 都包含换行符,因此尽量都加上 re.S
html = '''<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
<a href="/2.mp3" singer="任贤齐">沧海一声笑</a>
</li>
<li data-view="4" class="active">
<a href="/3.mp3" singer="齐秦">往事随风</a>
</li>
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
<li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li>
<li data-view="5">
<a href="/6.mp3" singer="邓丽君">但愿人长久</a>
</li>
</ul>
</div>'''
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S) # .*? 不能存在于开头或结尾
# search() 返回第一个匹配到的目标,因此如果将 active 去掉会匹配到第二个li节点
# 若不加 re.S 则会匹配到第四个li节点,因为第三个包含换行符
if result:
print(result.group(1), result.group(2))
齐秦 往事随风
3.4 findall()
- findall() 会搜索整个字符串,并返回匹配正则表达式的所有内容
- 获取 a 节点的超链接、歌手和歌名:
results = re.findall('<a.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
print(results)
print(type(results))
for result in results:
print(result)
print(result[0], result[1], result[2])
[('/2.mp3', '任贤齐', '沧海一声笑'), ('/3.mp3', '齐秦', '往事随风'), ('/4.mp3', 'beyond', '光辉岁月'), ('/5.mp3', '陈慧琳', '记事本'), ('/6.mp3', '邓丽君', '但愿人长久')]
<class 'list'>
('/2.mp3', '任贤齐', '沧海一声笑')
/2.mp3 任贤齐 沧海一声笑
('/3.mp3', '齐秦', '往事随风')
/3.mp3 齐秦 往事随风
('/4.mp3', 'beyond', '光辉岁月')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', '记事本')
/5.mp3 陈慧琳 记事本
('/6.mp3', '邓丽君', '但愿人长久')
/6.mp3 邓丽君 但愿人长久
3.5 sub()
- sub() 可以通过正则表达式来修改文本,第一个参数为正则表达式,第二个参数为替换成的字符串,第三个参数是原字符串:
import re
content = '1234afasdf678sdlkfjk'
content = re.sub('\d+', '', content)
print(content)
afasdfsdlkfjk
- 如果只获取 html 中所有 li 节点的歌曲名,直接用 findall()的话,正则表达式会比较复杂。可以借助 sub() 先把 a 节点去掉,再通过 findall() 提取:
# 直接使用 findall
results = re.findall('<li.*?>\s*?(<a.*?>)?(\w+)(</a>)?\s*?</li>', html, re.S)
for result in results:
print(result[1])
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久
html = re.sub('<a.*?>|</a>', '', html)
print(html)
results = re.findall('<li.*?>(.*?)</li>', html, re.S)
for result in results:
print(result.strip())
<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
沧海一声笑
</li>
<li data-view="4" class="active">
往事随风
</li>
<li data-view="6">光辉岁月</li>
<li data-view="5">记事本</li>
<li data-view="5">
但愿人长久
</li>
</ul>
</div>
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久
3.6 compile()
- compile() 可以将正则字符串编译为正则表达式对象,以便在后面的匹配中复用;
- compile() 还可以传入修饰符,这样在 search()、findall() 等方法中就不用传了
import re
content1 = '2016-12-15 12:00'
content2 = '2016-12-16 12:05'
content3 = '2016-12-19 11:00'
pattern = re.compile('\d{2}:\d{2}')
result1 = re.sub(pattern, '', content1)
result2 = re.sub(pattern, '', content2)
result3 = re.sub(pattern, '', content3)
print(result1, result2, result3)
2016-12-15 2016-12-16 2016-12-19
4. 抓取猫眼电影排行
-
本节目标为提取出猫眼电影 TOP100 的电影名称,时间,评分,图片等信息,站点为 https://maoyan.com/board/4, 提取的结果以文件形式保存;
-
网页分析:
- 观察页面可以发现每页显示 10 部电影,包含名称,主演,上映时间,上映地区(部分有),评分,图片等信息;
- 点击第二页、第三页,观察 URL 变换,可知存在一个 offset 偏移量,offset = n,则该页面显示第 n+1~n+11 部,因此我们分开请求 10 次,offset 依次为 0,10,20,30,…即可获取到 TOP100;
- 抓取首页:
import requests
def get_one_page(url):
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
return None
def main():
url = 'https://maoyan.com/board/4'
html = get_one_page(url)
print(html)
main()
<!DOCTYPE html>
<!--[if IE 8]><html class="ie8"><![endif]-->
<!--[if IE 9]><html class="ie9"><![endif]-->
<!--[if gt IE 9]><!--><html><!--<![endif]-->
<head>
<title>TOP100榜 - 猫眼电影 - 一网打尽好电影</title>
...
(文档太长此处省略一部分)
<dd>
<i class="board-index board-index-1">1</i>
<a href="/films/1203" title="霸王别姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}">
<img src="//s3plus.meituan.net/v1/mss_e2821d7f0cfe4ac1bf9202ecf9590e67/cdn-prod/file:5788b470/image/loading_2.e3d934bf.png" alt="" class="poster-default" />
<img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王别姬" class="board-img" />
</a>
<div class="board-item-main">
<div class="board-item-content">
<div class="movie-item-info">
<p class="name"><a href="/films/1203" title="霸王别姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王别姬</a></p>
<p class="star">
主演:张国荣,张丰毅,巩俐
</p>
<p class="releasetime">上映时间:1993-01-01</p> </div>
<div class="movie-item-number score-num">
<p class="score"><i class="integer">9.</i><i class="fraction">5</i></p>
</div>
</div>
</div>
</dd>
...后面省略
2. 正则提取:
-
观察返回的 HTML 代码,发现每部电影都在 dd 节点中;
-
首先提取排名信息:排名信息在 class 为 board-index 的 i 节点中,利用非贪婪方式匹配,正则表达式如下:
<dd>.*?board-index.*?>(.*?)</i>
-
然后提取电影图片:图片在 a 节点的第二个图片链接中,即 data-src 属性中,正则表达式修改如下:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)"
-
提取电影名称:电影名称在 p 节点内,class 为 name,使用 name 作为一个标志位,进一步提取其内 a 节点的正文内容:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>
-
提取主演:主演在 class 为 star 的 p 节点内,与上面同样的方式:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>
-
提取发布时间:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>
-
提取评分:
<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>
def parse_one_page(html):
pattern = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'image': item[1],
'title': item[2].strip(),
'actor': item[3].strip()[3:], # if len(item[3]) > 3 else '',
'time': item[4].strip()[5:], # if len(item[4]) > 5 else '',
'score': item[5].strip() + item[6].strip()
}
url = 'https://maoyan.com/board/4'
html = get_one_page(url)
for i in parse_one_page(html):
print(i)
{'index': '1', 'image': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'title': '霸王别姬', 'actor': '张国荣,张丰毅,巩俐', 'time': '1993-01-01', 'score': '9.5'}
{'index': '2', 'image': 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', 'title': '肖申克的救赎', 'actor': '蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿', 'time': '1994-09-10(加拿大)', 'score': '9.5'}
{'index': '3', 'image': 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', 'title': '罗马假日', 'actor': '格利高里·派克,奥黛丽·赫本,埃迪·艾伯特', 'time': '1953-09-02(美国)', 'score': '9.1'}
{'index': '4', 'image': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', 'title': '这个杀手不太冷', 'actor': '让·雷诺,加里·奥德曼,娜塔莉·波特曼', 'time': '1994-09-14(法国)', 'score': '9.5'}
{'index': '5', 'image': 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', 'title': '泰坦尼克号', 'actor': '莱昂纳多·迪卡普里奥,凯特·温丝莱特,比利·赞恩', 'time': '1998-04-03', 'score': '9.5'}
{'index': '6', 'image': 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', 'title': '唐伯虎点秋香', 'actor': '周星驰,巩俐,郑佩佩', 'time': '1993-07-01(中国香港)', 'score': '9.1'}
{'index': '7', 'image': 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', 'title': '魂断蓝桥', 'actor': '费雯·丽,罗伯特·泰勒,露塞尔·沃特森', 'time': '1940-05-17(美国)', 'score': '9.2'}
{'index': '8', 'image': 'https://p0.meituan.net/movie/223c3e186db3ab4ea3bb14508c709400427933.jpg@160w_220h_1e_1c', 'title': '乱世佳人', 'actor': '费雯·丽,克拉克·盖博,奥利维娅·德哈维兰', 'time': '1939-12-15(美国)', 'score': '9.1'}
{'index': '9', 'image': 'https://p1.meituan.net/movie/ba1ed511668402605ed369350ab779d6319397.jpg@160w_220h_1e_1c', 'title': '天空之城', 'actor': '寺田农,鹫尾真知子,龟山助清', 'time': '1992', 'score': '9.1'}
{'index': '10', 'image': 'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@160w_220h_1e_1c', 'title': '辛德勒的名单', 'actor': '连姆·尼森,拉尔夫·费因斯,本·金斯利', 'time': '1993-12-15(美国)', 'score': '9.2'}
-
写入文件:
通过 JSON 库的 dumps() 方法实现字典的序列化,并指定 ensure_ascii=False,避免输出结果是中文形式而不是 Unicode 编码
import json
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
print(type(json.dumps(content)))
f.write(json.dumps(content, ensure_ascii=False) + '\n')
- 加上分页爬取并整合代码:
import json
import requests
import re
import time
def get_one_page(url):
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
return None
def parse_one_page(html):
pattern = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'image': item[1],
'title': item[2].strip(),
'actor': item[3].strip()[3:], # if len(item[3]) > 3 else '',
'time': item[4].strip()[5:], # if len(item[4]) > 5 else '',
'score': item[5].strip() + item[6].strip()
}
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
print(type(json.dumps(content)))
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def main(offset):
url = 'https://maoyan.com/board/4?offset=' + str(offset)
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)
if __name__ == '__main__':
for i in range(10):
main(offset=i * 10)
time.sleep(1)
{'index': '1', 'image': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'title': '霸王别姬', 'actor': '张国荣,张丰毅,巩俐', 'time': '1993-01-01', 'score': '9.5'}
<class 'str'>
{'index': '2', 'image': 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', 'title': '肖申克的救赎', 'actor': '蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿', 'time': '1994-09-10(加拿大)', 'score': '9.5'}
<class 'str'>
{'index': '3', 'image': 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', 'title': '罗马假日', 'actor': '格利高里·派克,奥黛丽·赫本,埃迪·艾伯特', 'time': '1953-09-02(美国)', 'score': '9.1'}
<class 'str'>
{'index': '4', 'image': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', 'title': '这个杀手不太冷', 'actor': '让·雷诺,加里·奥德曼,娜塔莉·波特曼', 'time': '1994-09-14(法国)', 'score': '9.5'}
<class 'str'>
{'index': '5', 'image': 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', 'title': '泰坦尼克号', 'actor': '莱昂纳多·迪卡普里奥,凯特·温丝莱特,比利·赞恩', 'time': '1998-04-03', 'score': '9.5'}
<class 'str'>
...后面省略