一、爬虫总复习
这张图,它被用来描述浏览器的工作原理:
请求和响应可以说贯穿了我们后面的所有学习内容。
一开始,我们给爬虫下了一个定义:利用程序在网上获取对我们有用的数据。而获取数据最关键的步骤正是“请求”和“响应”。
由此,我们将爬虫大致分成了四个步骤:获取数据(包含请求和响应两个动作)、解析数据、提取数据、存储数据。
首先我们学习了最简单的请求方式:requests.get()
import requests
url = '(网址)'
response = requests.get(url)
工具
利用Network
,我们能看到所有的请求,它是我们用来编写爬虫的利器。
Network
能够记录浏览器的所有请求。而ALL(查看全部)和XHR(仅查看XHR)在我们的课程关卡中则是最常用的两类。其他的诸如Doc(Document,第0个请求一般在这里)、mg(仅查看图片)、仅查看媒体文件)以及Other(其他)等偶尔也会用到
它们共同组成Elements
中的内容,也就是你平时所见的各式各样的网页。
在爬虫里,我们最常用的是XHR
和Doc
。我们能在Doc
里找到一个网页的源代码,而在网页源代码里找不到的信息,通常你都能在XHR
里找到。有了它,我们不必刷新/跳转网页,即可加载新的内容。在今天,已经学过“同步/异步”概念的你,也可以说XHR
帮我们实现了异步请求。
而如何找到我们想要的数据呢?
解析和提取
当我们爬取的是静态页面,也就是说数据藏匿于网页源代码时,爬虫中至关重要的一环叫BeautifulSoup,它能提供一套完整的数据解析、数据提取解决方案。用法如下:
import requests
# 引用requests库
from bs4 import BeautifulSoup
# 引用BeautifulSoup库
headers={"user-agent":""}
res_movies = requests.get('https://movie.douban.com/chart',headers=headers)
# 获取数据
bs_movies = BeautifulSoup(res_movies.text,'html.parser')
# 解析数据
list_movies= bs_movies.find_all('div',class_='pl2')
# 查找最小父级标签
list_all = []
# 创建一个空列表,用于存储信息
for movie in list_movies:
tag_a = movie.find('a')
# 提取第0个父级标签中的<a>标签
name = tag_a.text.replace(' ', '').replace('\n', '')
# 电影名,使用replace方法去掉多余的空格及换行符
url = tag_a['href']
# 电影详情页的链接
tag_p = movie.find('p', class_='pl')
# 提取父级标签中的<p>标签
information = tag_p.text.replace(' ', '').replace('\n', '')
# 电影基本信息,使用replace方法去掉多余的空格及换行符
tag_div = movie.find('div', class_='star clearfix')
# 提取父级标签中的<div>标签
rating = tag_div.text.replace(' ', '').replace('\n', '')
# 电影评分信息,使用replace方法去掉多余的空格及换行符
list_all.append([name,url,information,rating])
# 将电影名、URL、电影基本信息和电影评分信息,封装为列表,用append方法添加进list_all
print(list_all)
# 打印
#以下是另外一种写法
import requests
# 引用requests库
from bs4 import BeautifulSoup
# 引用BeautifulSoup库
headers={"user-agent":""}
res_movies = requests.get('https://movie.douban.com/chart',headers=headers)
# 获取数据
bs_movies = BeautifulSoup(res_movies.text,'html.parser')
# 解析数据
tag_name = bs_movies.find_all('div', class_='pl2')
# 查找包含电影名和电影详情URL的<a>标签
tag_p = bs_movies.find_all('p',class_='pl')
# 查找包含电影基本信息的<p>标签
tag_div = bs_movies.find_all('div', class_='star clearfix')
# 查找包含电影评分信息的<div>标签
list_all = []
# 创建一个空列表,用于存储信息
for x in range(len(tag_name)):
# 启动一个循环,次数等于电影名的数量
list_movie = [tag_name[x].find('a').text.replace(' ', '').replace('\n', ''),tag_name[x].find('a')['href'],tag_p[x].text.replace(' ', '').replace('\n', ''),tag_div[x].text.replace(' ', '').replace('\n', '')]
# 提取信息,封装为列表。
list_all.append(list_movie)
# 将信息添加进list_all
print(list_all)
# 打印
需要额外强调一下,当response.text
自动解码出问题时,一定要使用response.encoding=''
来对编码进行修改。
我们来看当数据在XHR
中时该如何处理。
XHR
所传输的数据,最重要的一种是用json
格式写成的,把它打印出来显示的样式和字典是一样的。但是,json
的数据类型是“文本”,在Python语言当中,我们把它称为字符串。json
格式和列表/字典之间是能相互转化的。
解析json
数据并不难,步骤如下:
有了json,就可以对数据进行提取,将其转化为字典/列表:
更厉害的请求
我们再来研究一下“请求”。最开始,requests.get()里面只有一个参数,即url。
随着学习的深入,我们知道请求可以有多个参数。
params,可以让我们带着参数来请求数据,例如我们想要跳转到第几页、每次请求多少个数据等等。
访问的时候带上headers,即请求头。它的作用是在访问的时候告诉服务器:发出请求的浏览器是什么?我从哪个页面而来?
除了get请求之外,还存在着post请求方式。二者的区别在于:get是明文显示参数,post是非明文显示参数。
在post请求里,又多了两个参数:data和cookies。我们使用来data传递参数,其用法和params非常相像。
而cookies的作用是让服务器“记住你”,比如一般当你登录一个网站,你都会在登录页面看到一个可勾选的选项“记住我”。如果你点了勾选,服务器就会生成一个cookies和你的账号绑定。接着,它把这个cookies告诉你的浏览器,让浏览器把cookies存储到你的本地电脑。当下一次,浏览器带着cookies访问博客,服务器会知道你是何人,你不需要再重复输入账号密码,就能直接访问。
import requests
url_1 = 'https://…'
headers = {'user-agent':''}
data = {}
# 定义url,headers和data
login_in = requests.post(url,headers=headers,data=data)
cookies = login_in.cookies
# 完成登录,获取cookies
url_2 = 'https://…'
params = {}
# 定义url和params
response = requests.get(url,headers=headers,params=params,cookies=cookies)
# 带着cookies重新发起请求
存储
成功获取数据,接着解析数据,然后提取数据,最后都需要我们存储数据。
存储数据的方法有许多,其中最常见的是:csv和excel。
import csv
# 需要写入的数据
score1 = ['math', 95]
score2 = ['english', 90]
# 打开文件,追加a, newline="",可以删掉行与行之间的空格
file= open("score.csv", "a", newline="")
# 设定写入模式
csv_write = csv.writer(file)
# 写入具体内容
csv_write.writerow(score1)
csv_write.writerow(score2)
import csv
# 打开csv文件
file = open("score.csv")
# 读取文件内容,构造csv.reader对象
reader = csv.reader(file)
# 打印reader中的内容
for item in reader:
print(item)
import openpyxl
# 引用openpyxl
wb = openpyxl.Workbook()
# 利用openpyxl.Workbook()函数创建新的workbook(工作薄)对象,就是创建新的空的Excel文件。
sheet = wb.active
# wb.active就是获取这个工作薄的活动表,通常就是第一个工作簿,也就是我们在上面的图片中看到的sheet1。
sheet.title = 'kaikeba'
# 可以用.title给工作表重命名。现在第一个工作表的名称就会由原来默认的“sheet1”改为"kaikeba"。
sheet['A1'] = 'kaikeba'
# 向单个单元格写入数据
score1 = ['math', 95]
sheet.append(score1)
# 写入整行的数据,变量类型是一个列表
wb.save('score.xlsx')
# 保存修改的Excel
wb.close()
# 关闭Excel
import openpyxl
wb = openpyxl.load_workbook('score.xlsx')
# 打开的指定的工作簿
sheet = wb['kaikeba']
# 指定读取的工作表的名称
A1_value = sheet['A1'].value
print(A1_value)
# 获取
更多爬虫
掌握了这四步,世上已经少有你搞不定的爬虫。但是,如果要爬取的数据特别特别多,以至于程序会被拖得很慢怎么办?用协程。
使用多协程的话,就能让多个爬取任务用异步的方式交替执行。
from gevent import monkey
#从gevent库里导入monkey模块
import gevent
import time
import requests
from gevent.queue import Queue
#从gevent库里导入queue模块
monkey.patch_all()
#monkey.patch_all()能把程序变成协作式运行,就是可以帮助程序实现异步。
start = time.time()
url_list = ['https://www.baidu.com/',
'https://www.sina.com.cn/',
'http://www.sohu.com/',
'https://www.qq.com/',
'https://www.163.com/',
'http://www.iqiyi.com/',
'https://www.tmall.com/',
'http://www.ifeng.com/']
work = Queue()
#创建队列对象,并赋值给work
for url in url_list:
#遍历url_list
work.put_nowait(url)
#用put_nowait()函数可以把网址都放进队列里
def crawler():
while not work.empty():
#当队列不是空的时候,就执行下面的程序
url = work.get_nowait()
#用get_nowait()函数可以把队列里的网址都取出
r = requests.get(url)
#用requests.get()函数抓取网址
print(url,work.qsize(),r.status_code)
#打印网址、队列长度、抓取请求的状态码
tasks_list = [ ]
#创建空的任务列表
for x in range(2):
#相当于创建了2个爬虫
task = gevent.spawn(crawler)
#用gevent.spawn()函数创建执行crawler()函数的任务
tasks_list.append(task)
#往任务列表添加任务。
gevent.joinall(tasks_list)
#用gevent.joinall方法,执行任务列表里的所有任务,就是让爬虫开始爬取网站
end = time.time()
print(end-start)
更强大的爬虫
当你爬虫代码越写越多,重复性的动作越来越多时,一个一站式解决所有爬虫问题的框架,就对你越来越有吸引力。
Scrapy出现在你眼前。Scrapy的结构——
Scrapy的用法——
更进一步
为了能让我们能更好的使用爬虫,我们还穿插学习了三个有力工具:selenium,邮件通知和定时。
先说selenium,我们学习了使用.get(‘URL’)获取数据,以及解析与提取数据的方法。
在这个过程中,对象的转换过程:
使用selenium时,还可以搭配BeautifulSoup解析提取数据,前提是先获取字符串格式的网页源代码。
HTML = driver.page_source
以及如何使用selenium自动操作浏览器。
而关于邮件,它是这样一种流程:
我们要用到的模块是smtplib和email,前者负责连接服务器、登录、发送和退出的流程。后者负责填输邮件的标题与正文。
import smtplib
from email.mime.text import MIMEText
from email.header import Header
#引入smtplib、MIMETex和Header
mailhost='smtp.qq.com'
#把qq邮箱的服务器地址赋值到变量mailhost上,地址应为字符串格式
qqmail = smtplib.SMTP()
#实例化一个smtplib模块里的SMTP类的对象,这样就可以调用SMTP对象的方法和属性了
qqmail.connect(mailhost,25)
#连接服务器,第一个参数是服务器地址,第二个参数是SMTP端口号。
#以上,皆为连接服务器。
sender = input('请输入你的邮箱:')
#获取邮箱账号,为字符串格式
password = input('请输入你的密码:')
#获取邮箱密码,为字符串格式
qqmail.login(sender,password)
#登录邮箱,第一个参数为邮箱账号,第二个参数为邮箱密码
#以上,皆为登录邮箱。
receiver=input('请输入收件人的邮箱:')
#获取收件人的邮箱。
content=input('请输入邮件正文:')
#输入你的邮件正文,为字符串格式
message = MIMEText(content, 'plain', 'utf-8')
#实例化一个MIMEText邮件对象,该对象需要写进三个参数,分别是邮件正文,文本格式和编码
subject = input('请输入你的邮件主题:')
#输入你的邮件主题,为字符串格式
message['Subject'] = Header(subject, 'utf-8')
#在等号的右边是实例化了一个Header邮件头对象,该对象需要写入两个参数,分别是邮件主题和编码,然后赋值给等号左边的变量message['Subject']。
#以上,为填写主题和正文。
try:
qqmail.sendmail(sender, receiver, message.as_string())
print ('邮件发送成功')
except:
print ('邮件发送失败')
qqmail.quit()
#以上为发送邮件和退出邮箱
关于如何定时启动爬虫,我们选取了schedule模块,它的用法非常简洁。
import schedule
import time
#引入schedule和time
def job():
print("Working in progress...")
#定义一个叫job的函数,函数的功能是打印'Working in progress...'
#部署情况
schedule.every(10).minutes.do(job) #部署每10分钟执行一次job()函数的任务
schedule.every().hour.do(job) #部署每×小时执行一次job()函数的任务
schedule.every().day.at("10:30").do(job) #部署在每天的10:30执行job()函数的任务
schedule.every().monday.do(job) #部署每个星期一执行job()函数的任务
schedule.every().wednesday.at("13:15").do(job)#部署每周三的13:15执行函数的任务
while True:
schedule.run_pending()
time.sleep(1)
#检查部署的情况,如果任务准备就绪,就开始执行任务
二、爬虫进阶路线
这些内容对于爬虫来说,仅仅算是入门。接着我们会谈谈,如何继续深入地学习爬虫。
按照之前我们学习爬虫的路径,我把进阶指引分为这几个板块:解析与提取、存储、数据分析与可视化(新增)、更多的爬虫、框架,以及其他。
解析与提取
先说数据的解析和提取,这里指学习新的解析库。除了我们关卡里所用的BeautifulSoup解析、Selenium的自带解析库之外,还会有:xpath/lxml等。它们可能语法有所不同,但底层原理都一致,有了之前的基础,加上一些实战练习,相信你能轻松的掌握它们。
另外,强烈地推荐去学习正则表达式(re模块)。正则表达式功能强大,它能让你自己设定一套复杂的规则,然后把目标文本里符合条件的相关内容给找出来。比如说我们在第6课中爬取的周杰伦的歌词数据中,除了歌词外还有很多其他的符号。我们之前在课程中学到的方法是一遍一遍的使用「切片」来处理。但是如果你学习了正则表达式,那么提取歌词就变得异常轻松。
存储
说完了数据的获取,接下来就该谈谈如何存储存储。我们目前已经掌握的知识是csv和excel。掌握该如何使用它们并不难,要了解更多的用法,我推荐你翻阅它们的官方文档。
但是,这只适用于存储少量的数据。数据量变得海量时(事实上也是如此),尤其是涉及数据与数据之间的关系是,我们是难以用一张简单的二维平面表格来承载大量的数据的。
此时,推荐使用数据库。你可以先从MySQL和MongoDB这两种数据库开始。它们一个是关系型数据库的典型代表,一个是非关系型数据库的典型代表。
要掌握数据库,你需要学习另一种语言:SQL。
数据分析与可视化
徒有海量的原始数据的价值非常有限。数据,要被分析过才能创造出更深远的价值。举个例子:拿到全中国万达广场的地址和营收数据意义并不大,但如果能从中总结出大型商超经营的最优策略,这就能指导商业决策。
这样的技能被称为数据分析。将数据分析的结论,直观、有力地传递出来,就是我们经常提到的数据可视化。
严格的说,学习数据分析,要比爬虫还要花费更多的时间。因为我们不仅要知道数据本身所描述的现象,还要深入挖掘数据背后所带有含义。这里推荐的模块与库:Pandas/Matplotlib/Numpy/Scikit-Learn/Scipy。
更多爬虫
当爬取的内容变多时,协程可以使爬虫的速度得到显著的提升。
严格来说这并不是同时工作,而是电脑在多个任务之间快速地来回切换,看上去就仿佛是爬虫们同时工作。因此协成给爬虫速度带来的优化是有上限的。
如何突破这样的上限,就要用到多进程了。我们已经知道,协程在本质上只用到CPU的一个核。而多进程(multiprocessing库)爬虫允许你使用CPU的多个核,所以你可以使用多进程,或者是多进程与多协程结合的方式进一步优化爬虫。
理论上来说,只要CPU允许,开多少个进程,就能让你的爬虫速度提高多少倍。但这种方式会因受到CPU核数的限制。
如何实现进一步的突破呢?那我们就要用到分布式爬虫了:即用多个设备,去跑同一个项目。
这样一来,多加设备就能提升爬虫速度。很多企业在面对大量爬虫任务时就是使用分布式的方式来进行爬虫作业。
而实现分布式爬虫,需要下一个组块的内容——框架。
更强大的爬虫——框架
我们已经简单地学过Scrapy框架的基本原理和用法。而一些更深入的用法:使用Scrapy模拟登录、存储数据库、使用HTTP代理、分布式爬虫……这些就还不曾涉及。
在深入地学习和了解Scrapy框架的基础之上,你还可以了解一些其他的优秀框架,例如PySpider。
三、反爬虫应对策略简要汇总
几乎所有的技术人员对反爬虫都有一个共识:所谓的反爬虫,从不是将爬虫完全杜绝;而是想办法将爬虫的访问量限制在一个可接纳的范围,不要让它过于肆无忌惮。
原因很简单:爬虫代码写到最后,服务器无法真正区分是人还是爬虫。如果想要完全禁止爬虫,正常用户也会无法访问。所以只能想办法进行限制,而非禁止。
因此我们可以了解“反爬虫”的策略是怎样的,再思考如何应对“反爬虫”。
- 填写user-agent声明自己的身份,以此来应对网站对请求头的限制,即Request Headers,有时还要去填写origin和referer声明请求的来源。
- 可以用cookies和session的知识去模拟登录,来应对网站对登录的限制。
- 有的网站会做一些复杂的交互,比如设置“验证码”。解决方案也各有不同,例如:我们用Selenium去手动输入验证码;我们用一些图像处理的库自动识别验证码(tesserocr/pytesserart/pillow)。
- 有的网站则会限制IP。当我们的IP地址爬取网站的频次太高,那么服务器就会暂时封掉来自这个IP地址的请求。
解决方案例如:使用time.sleep()来对爬虫的速度进行限制;建立IP代理池(你可以在网络上搜索可用的IP代理),一个IP不能用了就换一个用。
import requests
url = 'https://…'
proxies = {'http':'http://…'}
# ip地址
response = requests.get(url,proxies=proxies)
四、小结
我们学习过的所有模块、库或者框架都是易于掌握的工具,只要经过一定时间的学习和一定量的训练你就能掌握。对于一个爬虫工程师来说最重要的,是达成目标的思维。
在我们所有的项目型关卡里,在我们开始编写我们代码之前,都遵循着这三个核心步骤:确认目标、分析过程和代码实现(对于复杂项目,也要去做代码封装)。
其中,难度最大的就是确认一个合理的目标,分析这个目标如何实现,设计这些工具模块如何组合应用。
我们在编写爬虫代码时所遵循的四个步骤:获取数据、解析数据、提取数据和存储数据,都是建立在“分析过程”的基础之上的。
要养成这样的思维模式,我们需要大量的项目实操练习。
因此,建议在完成项目练习的基础之上,多读一读编程前辈们的博客、github主页学习案例;同时,探索更多的项目实操,丰富自己的爬虫经验。