爬虫
URI和URL的概念
URI通用资源标志符
-
访问资源的命名机制
-
存放资源的主机名
-
资源自身 的名称,由路径表示
http://www.why.com.cn/myhtml/html1223/
①这是一个可以通过HTTP协议访问的资源
②位于主机 www.webmonkey.com.cn上
③通过路径“/html/html40”访问
URL统一资源定位符
-
URI的子集
-
requests
步骤:
1.指定URL
2.进行URL伪装
3.请求参数处理
- 创建字典 字典内放参数
4.发送请求
- get请求 response=request.get(url=,param=,headers=)
- post请求 response=request.post(url=,data=,headers=)
5.获取响应数据
- response.text
- response.json
- 具体情况根据页面信息决定
6.持久化存储
- 存到文件里
7.数据解析
①正则解析
- text返回字符串形式数据
- content返回二进制形式图片数据
- json返回对象类型图片数据
<div class="thumb">
<a href="/article/121721100" target="_blank">
<img src="//pic.qiushibaike.com/system/pictures/12172/121721100/medium/DNXDX9TZ8SDU6OK2.jpg" alt="指引我有前进的方向">
</a>
</div>
ex = '<div class="thumb">.*?<img src="(.*?)" alt.*?</div>'
② bs4解析
-
数据解析的原理:
1.标签定位
2.提取标签、标签属性中存储的数据值 -
bs4数据解析的原理:
- 实例化一个BeautifulSoup对象,并且将页面源码数据加载到该对象中
- 通过调用BeautifulSoup对象中相关的属性或者方法进行标签定位和数据提取
-
如何实例化BeautifulSoup对象:
- from bs4 import BeautifulSoup
- 对象的实例化:
-
将本地的html文档中的数据加载到该对象中
fp = open('./test.html','r',encoding='utf-8') soup = BeautifulSoup(fp,'lxml')
-
将互联网上获取的页面源码加载到该对象中
page_text = response.text soup = BeatifulSoup(page_text,'lxml')
-
- 提供的用于数据解析的方法和属性:
- soup.tagName:返回的是文档中第一次出现的tagName对应的标签
- soup.find():
- find(‘tagName’):等同于soup.div
- 属性定位:
- soup.find(‘div’,class_/id/attr=‘song’)
- soup.find_all(‘tagName’):返回符合要求的所有标签(列表)
- select:
- select(‘某种选择器(id,class,标签…选择器)’),返回的是一个列表。
- 层级选择器:
- soup.select(‘.tang > ul > li > a’):>表示的是一个层级
- oup.select(‘.tang > ul a’):空格表示的多个层级
- 获取标签之间的文本数据:
- soup.a.text/string/get_text()
- text/get_text():可以获取某一个标签中所有的文本内容
- string:只可以获取该标签下面直系的文本内容
- 获取标签中属性值:
- soup.a[‘href’]
③XPath
- xpath表达式:
- /:表示的是从根节点开始定位。表示的是一个层级。
- //:表示的是多个层级。可以表示从任意位置开始定位。
- 属性定位://div[@class=‘song’] tag[@attrName=“attrValue”]
- 索引定位://div[@class=“song”]/p[3] 索引是从1开始的。
- 取文本:
- /text() 获取的是标签中直系的文本内容
- //text() 标签中非直系的文本内容(所有的文本内容)
- 取属性:
- /@attrName ==>img/src
模拟登陆(验证码)超级鹰
①先获取登陆界面的网络源码
②用xpath解析网络源码获取验证码图片的src
③获取验证码图片并下载到本地
④用超级鹰识别验证码图片
(在正式模拟登陆时 先正确登陆一次获取post请求的data将它们以字典形式存在 复制post请求的URL)
⑤模拟登陆 URL=‘post请求里的URL’ request.get(url=url,headers=header,data=data) 发送post请求
⑥运行
import requests
from hashlib import md5
from lxml import etree
from chaojiying import Chaojiying_Client
# 获取网络源码
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
}
url='https://www.chaojiying.com/user/login/'
page_text=requests.get(url=url,headers=headers).text
# 实例化一个etree对象
tree=etree.HTML(page_text)
img_src='https://www.chaojiying.com/'+tree.xpath('/html/body/div[3]/div/div[3]/div[1]/form/div/img/@src')[0]
# 下载验证码图片到本地
img_data=requests.get(url=img_src,headers=headers).content
with open('./code.jpg','wb') as fp:
fp.write(img_data)
# 用超级鹰识别
chaojiying=Chaojiying_Client('jshn9774','luozhi74803','938388')
im = open('./code.jpg', 'rb').read()
print(chaojiying.PostPic(im, 1902))
img_text = chaojiying.PostPic(im, 1902)['pic_str']
# 模拟登陆
data={
'user': '超级鹰账号',
'pass': '超级鹰密码',
'imgtxt': img_text,
'act': '1',
}
# 这里的URL是post请求里的URL network->data
url_login='https://www.chaojiying.com/user/login/'
# 模拟登陆的post请求
data_p=requests.post(url=url,headers=headers,data=data).text
print(data_p)
Cookies
- http/https协议特性:无状态。
- 没有请求到对应页面数据的原因:
- 发起的第二次基于个人主页页面请求的时候,服务器端并不知道该此请求是基于登录状态下的请求。
- cookie:用来让服务器端记录客户端的相关状态。
- 手动处理:通过抓包工具获取cookie值,将该值封装到headers中。(不建议)
- 自动处理:
- cookie值的来源是哪里?
- 模拟登录post请求后,由服务器端创建。
- session会话对象:
- 作用:
1.可以进行请求的发送。
2.如果请求过程中产生了cookie,则该cookie会被自动存储/携带在该session对象中。
- 作用:
- 创建一个session对象:session = requests.Session()
- 使用session对象进行模拟登录post请求的发送(cookie就会被存储在session中)
- session对象对个人主页对应的get请求进行发送(携带了cookie)
- cookie值的来源是哪里?
代理
-
什么是代理:
- 代理服务器。
-
代理的作用:
- 突破自身IP访问的限制。
- 隐藏自身真实IP
-
代理ip的类型:
- http:应用到http协议对应的url中
- https:应用到https协议对应的url中
-
代理ip的匿名度:
- 透明:服务器知道该次请求使用了代理,也知道请求对应的真实ip
- 匿名:知道使用了代理,不知道真实ip
- 高匿:不知道使用了代理,更不知道真实的ip
异步爬虫
协程
- 在一个线程中如果遇到IO等待时间,线程不会傻傻等,而是利用空闲时间去执行别的任务
-
事件循环
-
快速上手
-
await
- await +可等待对象 (协程对象、Future、Task对象->IO等待)
- await 等待对象的值得到结果后再继续往下走
- 下一步要依赖上一步的结构是要用到await 在一条总线上等待 但依旧会切到别的地方执行别的任务
-
Task对象
- 在事件循环中添加多个任务
import asyncio async def func(): print(1) await asyncio.sleep(2) print(2) return "返回值" async def main(): print("main开始") # 创建Task对象,将当前执行func函数任务添加到事件循环 # task1=asyncio.create_task(func()) # # # 创建Task对象,将当前执行func函数任务添加到事件循环 # task2 = asyncio.create_task(func()) task_list=[ asyncio.create_task(func()), asyncio.create_task(func()), ] print("main结束") done,pending=await asyncio.wait(task_list,timeout=None) print(done) asyncio.run(main())
selenium
- selenium是基于浏览器自动化的一个模块
- 编写基于浏览器自动化的操作代码
- 发起请求:get(url)
- 标签定位:find系列的方法
- 标签交互:send_keys(‘xxx’)
- 执行js程序:excute_script(‘jsCode’)
- 前进,后退:back(),forward()
- 关闭浏览器:quit()
selenium模块和爬虫之间具有怎样的关联?
- 便捷的获取网站中动态加载的数据
- 便捷实现模拟登录
selenium处理iframe
- 如果定位的标签存在于iframe标签之中,则必须使用switch_to.frame(id)
- 动作链(拖动):from selenium.webdriver import ActionChains
- 实例化一个动作链对象:action = ActionChains(bro)
- click_and_hold(div):长按且点击操作
- move_by_offset(x,y)
- perform()让动作链立即执行
- action.release()释放动作链对象
无头浏览器
# 实现无可视化界面
from selenium.webdriver.chrome.options import Options
# 实现无可视化界面的操作
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
规避检测
# 实现规避检测
from selenium.webdriver import ChromeOptions
# 实现规避检测
option = ChromeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
Scrapy框架
-
框架是一个集成了很多功能并且具有很强通用性的一个项目模板
-
scrapy使用步骤
- 创建一个工程:scrapy startproject 工程名
- cd 工程文件夹
- 在spiders子目录中创建一个爬虫文件 :
scrapy genspider 文件名 www.xxx.com - 执行工程:
scrapy crawl spiderName(爬虫文件名称)
import scrapy
class FirstSpider(scrapy.Spider):
# 爬虫文件的名称:就是爬虫源文件的一个唯一标识
name = 'first'
# 允许的域名:用来限定start_urls列表中哪些url可以进行请求发送
allowed_domains = ['www.baidu.com']
# 起始的url列表:该列表中存放的url会被scrapy自动进行请求的发送
start_urls = ['https://www.baidu.com/','https://www.sogou.com']
# 用作数据解析:response参数表示请求成功后对应的响应对象
def parse(self, response):
print(response)
Scrapy持久化存储
-
基于终端指令:
-
只可以将parse方法的返回值存储到本地的文本文件中
-
存储类型是’json’, ‘jsonlines’, ‘jl’, ‘csv’, ‘xml’, ‘marshal’, ‘pickle’
-
终端指令:scrapy crawl 项目名称 -o filePath
-
好处:简介高效便捷
-
缺点:局限性比较强(数据只可以存储到指定后缀的文本文件中)
-
-
基于管道:
-
数据解析
-
在item类中定义相关的属性
-
将解析的数据封装存储到item类型的对象
-
将item类型的对象提交给管道进行持久化存储的操作
-
在管道类的process_item中要将其接受到的item对象中存储的数据进行持久化存储操作
-
在配置文件中开启管道
-
好处:通用性强
-
管道文件中一个管道类对应将一组数据存储到一个平台或载体中
class LiepinPipeline: fp=None # 该方法开始爬虫的时候调用一次 def open_spider(self,spider): print("爬虫开始") self.fp=open('./liepin.txt','w',encoding='utf-8') def process_item(self, item, spider): title=item['title'] area=item['area'] money=item['money'] require=item['require'] name=item['name'] self.fp.write(title+area+money+require+name+'\n') return item # 只在结束时打开一次 def close_spider(self,spider): print("结束爬虫") self.fp.close() ```
-
面试题:将爬取到的数据一份存储到本地一份存储到数据库,如何实现?
- 管道文件中一个管道类对应的是将数据存储到一种平台
- 爬虫文件提交的item只会给管道文件中第一个被执行的管道类接受
- process_item中的return item表示将item传递给下一个即将被执行的管道类
基于Spider的全站数据爬取
- 就是将网站中某板块下的全部页码对应的页面数据进行爬取
- 实现方式:
- 将所有页面的url添加到start_urls列表(不推荐)
- 自行手动进行请求发送(推荐)
- 手动请求发送:
- yield scrapy.Request(url,callback):callback专门用做于数据解析
- 手动请求发送:
五大核心
-
引擎(Scrapy)
用来处理整个系统的数据流处理
,触发事务
(框架核心) -
调度器(Scheduler)
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么,同时去除重复的网址
-
下载器(Downloader)
用于下载网页内容
, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的) -
爬虫(Spiders)
爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息
, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面 -
项目管道(Pipeline)
负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息
。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
请求传参
- 使用场景:要爬取解析的数据不再同一张页面中(深度爬取)
- 请求传参传的是item的对象类型 (通过请求 把参数传到对应的函数)
- 因为需要将不同页面的数据解析存储到同一个item里
# 发送获取详情页的请求
# 手动请求的发送
# 请求传参 meta={} ,可以将meta字典传递给请求对应的回调函数
# yield scrapy.Resqult(detail_url,callback=self.parse_detail,meta={'item':item})
用scrapy进行图片爬取 ImagesPipeline
基础scrapy爬取字符串类型的数据和爬取图片类型的数据的区别
- 字符串:只需要基于xpath进行解析且提交管道进行持久化存储
- 图片:xpath解析出图片输入层的属性值 单独的对图片发起请求获取图片二进制类型的数据
- ImagesPipeline:只需要将图片的src属性值进行解析,提交到管道,管道就会
对图片的src进行请求发送请求获取图片的二进制类型的数据
,且还会帮我们进行持久化存储。- 数据解析(图片的地址)
- 将存储图片地址的item提交到指定的管道类
- 在管道文件中自定制一个基于ImagesPipeLine的一个管道类
- get_media_request
- file_path
- item_completed
- 在配置文件中:
- 指定图片存储的目录:IMAGES_STORE = ‘./imgs_bobo’
- 指定开启的管道:自定制的管道类
爬取百度图片
- 百度图片是以json形式保存的
imgs=json.loads(response.body)['data']
for img in imgs:
item=JsonproItem()
try:
item['url']=[img['middleURL']]
yield item
except Exception as e:
print(e)
中间件
-
下载中间件
- 引擎和下载器之间
- 作用:批量拦截到整个工程中所有的请求和响应
- 拦截请求:
- UA伪装 process_request
- 代理ip process_exception :return request
- 拦截响应
- 篡改响应数据、响应对象
class MiddleDownloaderMiddleware:
user_agent_list = [
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0'
,
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50',
'Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11'
, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)'
, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)',
'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)']
# ip代理池需要自己建(要钱)
# 列表内容为 ip地址:端口号
# 例子如下
PROXY_https=[]
PROXY_http=['120.194.55.139:6969','111.3.118.247:30001']
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
# 拦截请求
def process_request(self, request, spider):
# 为了验证代理的操作是否生效
request.meta['proxy']='http://116.62.198.43:8080'
# UA伪装
request.header['User-Agent']=random.choice(self.user_agent_list)
request.meta['proxy']='http://122.9.101.6:8888'
return request
# 拦截所有响应
def process_response(self, request, response, spider):
return response
# 拦截发生异常的请求
def process_exception(self, request, exception, spider):
if request.url.split(':')[0]=='http':
# 代理 request.meta['proxy']='http://'+random.choice(self.PROXY_http)
else: request.meta['proxy']='https://'+random.choice(self.PROXY_https)
# 将修正后的请求对象进行重新的请求发送
return request
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
JSON常识
-
(JavaScript Object Notation ) JavaScript对象表示法是一种轻量级(Light-Meight)、基于文本的(Text-Based)、可读的(Human-Readable)格式
-
JSON 的名称中虽然带有JavaScript,但这是指其语法规则是参考JavaScript对象的,而不是指只能用于JavaScript 语言。
-
相比 XML(另一种常见的数据交换格式),文件更小
-
语法规则:
-
数组(Array)用方括号(“
[]
”)表示 -
对象(0bject)用大括号(“
{}
”)表示 -
名称/值对(
name/value
)组合成数组和对象。 -
名称(
name
)置于双引号中,值(value
)有字符串、数值、布尔值、null、对象和数组。 -
并列的数据之间用逗号(“
,
”)分隔
JSON和XML
-
JSON的优势
-
没有结束标签、长度更短、读写更快
-
能够直接被JavaScript解释器解析
-
可以使用数组
-
-
# JSON
{
"name":"兮动人",
"age":22,
"fruits":["apple","pear","grape"]
}
# XML
<root>
<name>兮动人</name>
<age>22</age>
<fruits>apple</fruits>
<fruits>pear</fruits>
<fruits>grape</fruits>
</root>
- 格式规定
- 对象(0bject)
- 对象用大括号(“
{}
”)括起来,大括号里是一系列的“名称/值对
” - 两个并列的数据之间用逗号(“
,
”)隔开- 使用英文的逗号(“
,
”),不要用中文的逗号(“,
”) - 最后一个“
名称/值对
“之后不要加逗号
- 使用英文的逗号(“
- 对象用大括号(“
- 数组(Array)
- 数组表示一系列有序的值,用方括号(“
[]
”)包围起来,并列的值之间用逗号分隔
- 数组表示一系列有序的值,用方括号(“
- 名称/值对(Name/Value)
- 名称(Name)是一个字符串,要用双引号括起来,不能用单引号,也不能没有引号,这一点与JavaScript不同
- 值的类型只有七种:
字符串(string)、数值(number)、对象(object)、数组(array), true、false、null
。不能有这之外的类型,例如undefined、函数
等。 - 字符串中不能单独出现双引号(
”
)和右斜杠(“\
") - 如果要打双引号或右斜杠,需要使用“
右斜杠+字符
”的形式,例如\”
和\\
,其它的转义字符也是如此
CrawlSpider:类 Spider的一个子类
全站数据爬取的方式:
-
基于Spider:手动请求,然后构造通用url模板
-
基于CrawlSpider
-
CrawlSpider的使用:
-
- 创建一个工程 - cd XXX - 创建爬虫文件(CrawlSpider) - scrapy genspider -t crawl xxx www.xxx.com - 链接提取器 - 根据指定规则(allow 具体参数是正则表达式)进行指定链接的提取 - 规则提取器 - 将链接提取器提取到的链接进行指定规则(callback)的解析操作
-
-
"spider.py"
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from sunPro.items import SunproItem,DetailItem
#需求:爬取sun网站中的编号,新闻标题,新闻内容,标号
class SunSpider(CrawlSpider):
name = 'sun'
# allowed_domains = ['www.xxx.com']
start_urls = ['http://wz.sun0769.com/index.php/question/questionType?type=4&page=']
#链接提取器:根据指定规则(allow="正则")进行指定链接的提取
link = LinkExtractor(allow=r'type=4&page=\d+')
link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml')
rules = (
#规则解析器:将链接提取器提取到的链接进行指定规则(callback)的解析操作
Rule(link, callback='parse_item', follow=True),
#follow=True:可以将链接提取器 继续作用到 连接提取器提取到的链接 所对应的页面中
Rule(link_detail,callback='parse_detail')
)
#http://wz.sun0769.com/html/question/201907/421001.shtml
#http://wz.sun0769.com/html/question/201907/420987.shtml
#解析新闻编号和新闻的标题
#如下两个解析方法中是不可以实现请求传参!
#如法将两个解析方法解析的数据存储到同一个item中,可以以此存储到两个item
def parse_item(self, response):
#注意:xpath表达式中不可以出现tbody标签
tr_list = response.xpath('//*[@id="morelist"]/div/table[2]//tr/td/table//tr')
for tr in tr_list:
new_num = tr.xpath('./td[1]/text()').extract_first()
new_title = tr.xpath('./td[2]/a[2]/@title').extract_first()
item = SunproItem()
item['title'] = new_title
item['new_num'] = new_num
yield item
#解析新闻内容和新闻编号
def parse_detail(self,response):
new_id = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first()
new_content = response.xpath('/html/body/div[9]/table[2]//tr[1]//text()').extract()
new_content = ''.join(new_content)
# print(new_id,new_content)
item = DetailItem()
item['content'] = new_content
item['new_id'] = new_id
yield item
分布式爬虫
-
概念:需要搭建一个分布式的机群,让其对一组资源进行分布联合爬取
-
作用:提升爬取数据的效率
-
为什么scrapy不嗯呢该实现分布式?
- 调度器不可以被分布式机群共享
- 管道不能被嗯不是机群共享
-
scrapy-Redis组件的作用:
-
可以给原生的的scrapy框架提供可以被共享的管道和调度器
-
- 实现流程 - 创建一个工程 - 创建一个基于CrawlSpider的爬虫文件 - 修改当前的爬虫文件: - 导包:from scrapy_redis.spiders import RedisCrawlSpider - 将start_urls和allowed_domains进行注释 - 添加一个新属性:redis_key = 'sun' 可以被共享的调度器队列的名称 - 编写数据解析相关的操作 - 将当前爬虫类的父类修改成RedisCrawlSpider - 修改配置文件settings - 指定使用可以被共享的管道: ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } - 指定调度器: # 增加了一个去重容器类的配置, 作用使用Redis的set集合来存储请求的指纹数据, 从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis组件自己的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置调度器是否要持久化, 也就是当爬虫结束了, 要不要清空Redis中请求队列和去重指纹的set。如果是True, 就表示要持久化存储, 就不清空数据, 否则清空数据 SCHEDULER_PERSIST = True - 指定redis服务器: - redis相关操作配置: - 配置redis的配置文件: - linux或者mac:redis.conf - windows:redis.windows.conf - 代开配置文件修改: - 将bind 127.0.0.1进行删除 - 关闭保护模式:protected-mode yes改为no - 结合着配置文件开启redis服务 - redis-server 配置文件 - 启动客户端: - redis-cli - 执行工程: - scrapy runspider xxx.py - 向调度器的队列中放入一个起始的url: - 调度器的队列在redis的客户端中 - lpush xxx www.xxx.com - 爬取到的数据存储在了redis的proName:items这个数据结构中
-