Python自动化开发学习之如何实现爬虫
这篇文章主要介绍Python自动化开发学习之如何实现爬虫,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!
讲师的博客:https://www.cnblogs.com/wupeiqi/articles/6283017.html
建立本地缓存
用下面的命令,就可以把一个页面爬取下来。不过再继续其他操作之前先把爬取的内容在本地建立缓存:
import requests r = requests.get('http://www.autohome.com.cn/news') # 爬取页面 print(r.text) # 打印响应的内容
下面会试很多的方法,还是要避免每次都去爬一次相同的页面。主要爬的太频繁,不知道会不会被封。所以爬取过一次之后,在本地建立缓存,之后的各种分析就不用再去爬一遍了。
要缓存的就是 r = requests.get('http://www.autohome.com.cn/news')
这个,也就是这里的r这个对象。不缓存的话,r是保存在内存中的,程序一旦退出就没有了。这里要做的就是对r这个对象进行序列化,把它保存为本地的文件。由于r是一个python对象,无法使用JSON序列化,这里可以用pickle,保存为一个二进制文件。
序列化与反序列化
首先是把对象序列化,保存为本地的二进制文件:
import pickle with open('test.pk', 'wb') as f: pickle.dump(r, f)
只有再用的时候,就不需要再通过requests.get再去爬一遍了,直接从本地文件中取出内容反序列生成r对象:
import pickle with open('test.pk', 'rb') as f: r = pickle.load(f)
封装个模块
然后,每次自己都要想一下之前有没有缓存过也很麻烦,所以在封装一下,自动判断有没有缓存过。如果没有就去爬网页,然后生成缓存。如果有就去缓存的文件里读。
创建一个文件夹“pk”专门存放缓存的文件。假设测试的python文件是 s1.py 那么就生成一个 pk/s1.pk 的缓存文件,只要判断是否存在该文件,就可以知道是否缓存过了:
import os import pickle import requests def get_pk_name(path): basedir = os.path.dirname(path) fullname = os.path.basename(path) name = os.path.splitext(fullname)[0] pk_name = '%s/pk/%s.%s' % (basedir, name, 'pk') return pk_name pk_name = get_pk_name(__file__) response = None if os.path.exists(pk_name): print("已经爬取过了,获取缓存的内容...") with open(pk_name, 'rb') as f: response = pickle.load(f) # 只有在没有缓存过页面的时候才进行爬取 if not response: print("开始爬取页面...") response = requests.get('http://www.autohome.com.cn/news') # 爬完之后记得保存,下次就不用再去爬取了 with open(pk_name, 'wb') as f: pickle.dump(response, f) # 从这里开始写真正的代码 print(response.text)
Requests
中文官方文档:http://cn.python-requests.org/zh_CN/latest/user/quickstart.html
安装模块:
pip install requests
发送请求
r = requests.get('http://www.autohome.com.cn/news')
读取响应内容
print(r.text)
文本编码
上面可能会有乱码,那就是编码不对,可以查看当前的编码,也可以改变它。默认的编码就是 'ISO-8859-1' :
print(r.encoding) r.encoding = 'ISO-8859-1'
另外还可以自动获取页面的编码,解决乱码问题:
r.encoding = r.apparent_encoding print(r.text)
二进制响应内容
如果要自己找编码,应该也是在这里面找
print(r.content)
在下载的时候,就要用到二进制的响应内容了
响应状态码
print(r.status_code)
正常返回的状态码是200
Cookie
cookie_obj = r.cookies cookie_dict = r.cookies.get_dict()
r.cookies 是一个对象,这个对象的的行为和字典类似,也可以像对象那样使用。这里还可以用 get_dict() 方法转成原生的字典。
Beautiful Soup
中文官方文档:https://beautifulsoup.readthedocs.io/
安装模块:
pip install beautifulsoup4
这里继续对上面爬取到的内容进行分析,把爬取到的内容先把编码转正确了,然后这里要分析的是 r.text 文本的响应内容:
import requests from bs4 import BeautifulSoup r = requests.get('http://www.autohome.com.cn/news') r.encoding = r.apparent_encoding soup = BeautifulSoup(r.text, features='html.parser')
features 参数是指定一个处理引擎,这里用的是默认的,效率一般,但是不用额外的安装。如果是生产环境,还有更高效的处理引擎。
这里最后拿到了一个 soup 对象,之后又一系列的方法,可以提取出各种内容。
查找方法
soup.find方法,可以找到第一个符合条件的对象。可以找标签,也可以找id等,还可以多条件组合使用:
soup.find("div") soup.find(id="link3") soup.find("div", id="link3")
soup.find_all方法,和find的用法一样,实际上find方法的实现也是调用find_all方法。find_all方法会返回所有符合条件的对象,返回的对象是在一个列表里的。
打印对象和对象的文本
直接打印对象会打印整个html标签,如果只需要标签中的文本,可以通过对象的text属性:
soup = BeautifulSoup(r.text, features='html.parser') target = soup.find('div', {'class': "article-bar"}) print(type(target), target, target.text)
获取对象的所有属性
对象的attrs属性里是这个html标签的所有的属性:
target = soup.find(id='auto-channel-lazyload-article') print(target.attrs)
获取属性的值
用get方法可以通过属性的key获取到对应的value。下面2个方法都可以:
v1 = target.get('name') v2 = target.attrs.get('value') # get方法的源码 def get(self, key, default=None): """Returns the value of the 'key' attribute for the tag, or the value given for 'default' if it doesn't have that attribute.""" return self.attrs.get(key, default)
实战
仅凭上面这点知识点就可以开始下面的实战了
爬取汽车之家新网咨询
下面是代码,找到了没一条新闻咨询的a连接的地址,以及标题,最后还把对应的图片下载到了本地(先建一个img文件夹):
# check_cache.py """用来检查是否有本地缓存的小模块""" import os def get_pk_name(path): basedir = os.path.dirname(path) fullname = os.path.basename(path) name = os.path.splitext(fullname)[0] pk_name = '%s/pk/%s.%s' % (basedir, name, 'pk') return pk_name # s1.py """爬取汽车之家新网咨询""" import os import pickle import requests from bs4 import BeautifulSoup from check_cache import get_pk_name pk_name = get_pk_name(__file__) response = None if os.path.exists(pk_name): print("已经爬取过了,获取缓存的内容...") with open(pk_name, 'rb') as f: response = pickle.load(f) # 只有在没有缓存过页面的时候才进行爬取 if not response: print("开始爬取页面...") response = requests.get('http://www.autohome.com.cn/news') # 爬完之后记得保存,下次就不用再去爬取了 with open(pk_name, 'wb') as f: pickle.dump(response, f) response.encoding = response.apparent_encoding # 获取页面的编码,解决乱码问题 # print(response.text) soup = BeautifulSoup(response.text, features='html.parser') target = soup.find(id='auto-channel-lazyload-article') # print(target) # obj = target.find('li') # print(obj) li_list = target.find_all('li') # print(li_list) for i in li_list: a = i.find('a') # print(a) # print(a.attrs) # 有些li标签里没有a标签,所以可能会报错 if a: # 这样判断一下就好了 # print(a.attrs) # 这是一个字典 print(a.attrs.get('href')) # 那就用操作字典的方法来获取值 # tittle = a.find('h4') # 这个类型是对象 tittle = a.find('h4').text # 这样拿到的才是文本 print(tittle, type(tittle)) # 不过打印出来差不多,都会变成字符串,差别就是h4这个标签 img_url = a.find('img').attrs.get('src') print(img_url) # 上面获取到了图片的url,现在可以下载到本地了 img_response = requests.get("http:%s" % img_url) if '/' in tittle: file_name = "img/%s%s" % (tittle.replace('/', '_'), os.path.splitext(img_url)[1]) else: file_name = "img/%s%s" % (tittle, os.path.splitext(img_url)[1]) with open(file_name, 'wb') as f: f.write(img_response.content)
登录抽屉
这里要解决一个登录的问题。
登录有2种,一种是Form表单验证,还有一种是AJAX请求。这是一个使用AJAX做登录请求的网站。
下面是几张浏览器调试工具的截图,主要是要找一下,登录请求需要提交到哪里,提交哪些信息,以及最后会返回的内容。
登录的AJAX请求:
请求正文:
响应正文:
登录请求的代码如下:
import requests post_dict = { 'phone': '8613507293881', # 从请求正文里发现,会在手机号前加上86 'password': '123456', } # 所有的请求头可以从请求标头里找到,不过不是必须的 headers = { 'User-Agent': '', # 这个网站要验证这个请求头,不过只要有就可以通过 } # 从标头里可以得知,请求的url和请求的方法 response = requests.post( url='https://dig.chouti.com/login', data=post_dict, headers=headers, ) print(response.text) # 这里还有返回的cookies信息,登录成功关键是要拿到成功的cookie cookie_dict = response.cookies.get_dict() print(cookie_dict)
登录的套路
上面使用了错误的用户名和密码,在继续登录验证之前,看了解下登录的机制。
登录肯定是要提交验证信息的,一般就用户名和密码。然后请求验证之后,服务端会记录一个session,然后会返回给客户端一个cookie。之后用户每次请求都带着这个cookie,服务端收到请求后就知道这个请求是那个用户提交的了。
不过这个网站有一点不一样,用户在提交验证信息的时候,不但要提交用户名和密码,还要提交一个gpsd。然后服务端验证通过后,会把这次收到的gpsd记录下来。用户之后的cookie里就是要带着这个gpsd就能验证通过。验证请求的gpsd可以从第一次发送get请求的返回的cookie里获取到。另外用户验证通过后,服务端会返回一个cookie,这个cookie里也有一个gpsd,但是是一个新的gpsd,并且是没有用的,这里就会混淆我们,在进行验证这不的时候造成一些困扰。
具体如何应对这类特殊情况,只能用浏览器,打开调试工具,然后一点一点试了。
登录并点赞
下面就是登录验证,获取到第一条咨询的标题和id,发送post请求点赞:
import requests from bs4 import BeautifulSoup headers = { 'User-Agent': '', # 这个网站要验证这个请求头,不过只要有就可以通过 } r1 = requests.get('https://dig.chouti.com', headers=headers) r1_cookies = r1.cookies # 这里有个gpsd,登录验证的时候要一并提交 print(r1_cookies.get_dict()) # 不能把密码上传啊 with open('password/s2.txt') as f: auth = f.read() auth = auth.split('\n') post_dict = { 'phone': '86%s' % auth[0], # 从请求正文里发现,会在手机号前加上86 'password': auth[1], } # 这个网站的登录机制是,发送验证信息和cookies里的gpsd,成功后给你的gpsd授权 # 之后的请求只有cookies里有这个授权过的gpsd就能认证通过 r2 = requests.post( url='https://dig.chouti.com/login', data=post_dict, headers=headers, cookies={'gpsd': r1_cookies['gpsd']} ) print(r2.text) r2_cookies = r2.cookies # 这里也会返回一个新的gpsd,但是无用。 print(r2_cookies.get_dict()) # 获取咨询,然后点赞 r3 = requests.get( url='https://dig.chouti.com', headers=headers, cookies={'gpsd': r1_cookies['gpsd']}, ) r3.encoding = r3.apparent_encoding soup = BeautifulSoup(r3.text, features='html.parser') target = soup.find(id='content-list') item = target.find('div', {'class': 'item'}) # 就只给第一条点赞吧 news = item.find('a', {'class': 'show-content'}).text linksId = item.find('div', {'class': 'part2'}).attrs['share-linkid'] print('news:', news.strip()) # 点赞 r = requests.post( url='https://dig.chouti.com/link/vote?linksId=%s' % linksId, headers=headers, cookies={ 'gpsd': r1_cookies['gpsd'], } ) print(r.text)
Requests 模块详细
找到requests.get()方法的源码,在 requests/api.py 这个文件里,有如下这些方法:
requests.get()
requests.options()
requests.head()
requests.post()
requests.put()
requests.patch()
requests.delete()
另外还有一个 requests.request() 方法。上面这些方法里最终调用的都是这个request方法。下面就来看下这些方法里都提供了写什么参数。
参数
在 requests.request() 方法里所有的参数如下:
method : 提交方式。request方法里的参数,其他方法里在调用request方法时,都会填好。
url : 提交地址
params : 在url中传递的参数。也就是get方式的参数
data : 在请求体里传递的参数,Form表单提交的内容。
json : 在请求体里传递的参数,AJAX提交的内容。和data不同,会把参数序列化后,把整个字符串发出去。
headers : 请求头。有几个重要的请求头信息,下面会列出
cookies : 这个就是Cookies。它是放在请求头的Cookie里发送给服务端的。
files : 上传文件。下面有使用示例
auth : 设置 HTTP Auth 的认证信息。下面有展开
timeout : 超时时间。单位是秒,类型是float。有连接超时和等待返回超时,同时会设置这两个时间。也可以是个元祖分别设置两个时间(connect timeout, read timeout)
allow_redirects : 是否允许重定向。默认是True。
proxies : 使用代理。下面有展开
verify : 对于https的请求,如果设为Flase,会忽略证书。
stream : 下载时的参数,如果是False,则先一次全部下载到内存。如果内容太大,下面有展开。
cert : 提交请求如果需要附带证书文件,则要设置cert。
data 和 json 参数
这两个参数都是在请求体力传递的参数。但是格式不同,在网络上最终传递的一定都是序列化的字符串。不同的类型会生成一个不同的请求头。在 requests/models.py 文件里可以找到如下的代码:
if not data and json is not None: content_type = 'application/json' if data: if isinstance(data, basestring) or hasattr(data, 'read'): content_type = None else: content_type = 'application/x-www-form-urlencoded'
也就是不同的格式,会设置不同的 Content-Type 请求头:
data 请求头:'application/x-www-form-urlencoded'
json 请求头:'application/json'
而后端收到请求后,也就可以先查找请求头里的 Content-Type ,然后再解析请求体里的数据。
为什么要用两种格式?
Form表单提交的是data数据,并且Form只能提交字符串或列表,是没有字典的。也就是data这个字典里的value的值只能是字符串或列表,不能是字典。(data字典里不能套字典)
如果就是需要向后端提交一个字典的话,那么只能使用josn了。
请求头
Referer : 上一次请求的url
User-Agent : 客户端使用的浏览器
发送文件
这是最基本的用法,字典的key f1,就是Form表单的name。这里实例用了request方法来提交请求,之后的例子只有file_dict不同:
file_dict = { 'f1': open('test1.txt', rb) } requests.request( method='POST', url='http://127.0.0.1:8000/test/', files=file_dict )
定制文件名:
file_dict = { 'f2': ('mytest.txt', open('test2.txt', rb)) }
定制文件内容(没有文件对象了,文件名当然也得自己定了):
file_dict = { 'f3': ('test3.txt', "自己写内容,或者从文件里读取到的内容") }
HTTP Auth
HTTP Auth是一种基本连接认证。比如家里用的路由器、ap,用web登录时会弹框(基本登录框,这个不是模态对话框),就是这种认证方式。它会把用户名和密码通过base64加密后放在请求头的 Authorization 里发送出去。
使用的示例代码:
import requests def param_auth(): from requests.auth import HTTPBasicAuth ret = requests.get('https://api.github.com/user', auth=HTTPBasicAuth('wupeiqi', 'sdfasdfasdf')) print(ret.text)
在 requests.auth 里看到了几个类,应该是不同的加密或者认证方式,但是本质都是把认证信息加密后放在请求头里发送。这里就用 HTTPBasicAuth 举例了。下面是 HTTPBasicAuth 的源码:
class HTTPBasicAuth(AuthBase): """Attaches HTTP Basic Authentication to the given Request object.""" def __init__(self, username, password): self.username = username self.password = password def __eq__(self, other): return all([ self.username == getattr(other, 'username', None), self.password == getattr(other, 'password', None) ]) def __ne__(self, other): return not self == other def __call__(self, r): r.headers['Authorization'] = _basic_auth_str(self.username, self.password) return r
上面的过程很简单,把用户名和密码通过 _basic_auth_str
方法加密后,加到请求头的 'Authorization' 里。
这种认证方式比较简单,发布到公网上的网站不会用这种认证方式。
proxies 代理
把代理的设置都写在一个字典里,使用代理的设置如下:
import requests proxies1 = { 'http': '61.172.249.96:80', # http的请求用这个代理 'https': 'http://61.185.219.126:3128', # https的请求用这个代理 } proxies2 = {'http://10.20.1.128': 'http://10.10.1.10:5323'} # 这特定的站定使用代理 r = requests.get('http://www.google.com', proxies=proxies1)
如果是需要用户名和密码的代理,需要用到上面的auth,这里auth也是一样,是放在请求头里的:
from requests.auth import HTTPProxyAuth auth = HTTPProxyAuth('my_username', 'my_password') # 这里一次输入用户名和密码 r = requests.get('http://www.google.com', proxies=proxies1, auth=auth)
stream 下载
发送完请求,不立即下载全部内容(一次把完整的内容全部下载到内存)。而是通过迭代的方式,一点一点进行下载:
import requests def param_stream(): from contextlib import closing with closing(requests.get('http://httpbin.org/get', stream=True)) as r: # 在此处理响应。 for i in r.iter_content(): print(i) # 这里用二进制打开个文件写,应该就好了
Session
多次请求的时候,使用 requests.Session() 会自动帮我们管理好Cookie,另外还会设置好一些默认信息,比如请求头等等。
用法如下:
import requests session = requests.Session() # 生成一个session实例 # 之后的requests请求,使用session替代requests,比如get请求如下 r1 = session.get('https://dig.chouti.com')
不如看下源码:
class Session(SessionRedirectMixin): """A Requests session. Provides cookie persistence, connection-pooling, and configuration. Basic Usage:: >>> import requests >>> s = requests.Session() >>> s.get('http://httpbin.org/get') <Response [200]> Or as a context manager:: >>> with requests.Session() as s: >>> s.get('http://httpbin.org/get') <Response [200]> """ __attrs__ = [ 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify', 'cert', 'prefetch', 'adapters', 'stream', 'trust_env', 'max_redirects', ]
除了实例化后使用,还可以像文件操作一样用with的方法使用。
attrs 列表里的值,就是session会自动帮我们设置的所有的属性。
比如headers,它会默认在每次发送的时候添加如下的请求头:
def default_headers(): """ :rtype: requests.structures.CaseInsensitiveDict """ return CaseInsensitiveDict({ 'User-Agent': default_user_agent(), 'Accept-Encoding': ', '.join(('gzip', 'deflate')), 'Accept': '*/*', 'Connection': 'keep-alive', }) # User-Agent 的值是这样的,"python-requests/2.19.1" 后面是requests模块的软件版本,会变。 # 可以方便的改掉 s = requests.Session() s.headers['User-Agent'] = ""
学到这里,之后再发送请求,尤其是要和网站进行多次交互的。就新把Session设置好,然后用Session来请求。所有的设置都会保存在Session的实例里,重复使用,自动管理。
优化登录点赞
之前自动登录点赞的例子,如果使用session改一下就简单多了,完全不用管cookie:
import requests from bs4 import BeautifulSoup session = requests.Session() # 默认的 User-Agent 的值是 "python-requests/2.19.1" 会被反爬,需要改一下 session.headers['User-Agent'] = "" session.get('https://dig.chouti.com') # 不能把密码上传啊 with open('password/s2.txt') as f: auth = f.read() auth = auth.split('\n') post_dict = { 'phone': '86%s' % auth[0], # 从请求正文里发现,会在手机号前加上86 'password': auth[1], } session.post('https://dig.chouti.com/login', data=post_dict) # 获取咨询,然后点赞 r3 = session.get('https://dig.chouti.com') r3.encoding = r3.apparent_encoding soup = BeautifulSoup(r3.text, features='html.parser') target = soup.find(id='content-list') item = target.find('div', {'class': 'item'}) news = item.find('a', {'class': 'show-content'}).text linksId = item.find('div', {'class': 'part2'}).attrs['share-linkid'] print('news:', news.strip()) # 点赞 r = session.post('https://dig.chouti.com/link/vote?linksId=%s' % linksId) print(r.text)
以上是“Python自动化开发学习之如何实现爬虫”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注行业资讯频道!