您的位置:首页 > 编程语言 > Python开发

Python自动化开发学习-爬虫1

2018-10-15 16:21 871 查看

讲师的博客: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)

实战

仅凭上面这点知识点就可以开始下面的实战了

爬取汽车之家新网咨询

下面是代码,找到了没一条新闻咨询的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('h3')  # 这个类型是对象
tittle = a.find('h3').text  # 这样拿到的才是文本
print(tittle, type(tittle))  # 不过打印出来差不多,都会变成字符串,差别就是h3这个标签
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 : 在请求体里传递的参数。
  • json : 在请求体里传递的参数。和data不一样,会把json对象序列化后,把整个字符串发出去。
  • 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' 里。
这种认证方式比较简单,发布到公网上的网站不会用这种认证方式。

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 爬虫