python爬虫
基础爬虫
requests
import requests
# get请求
response = requests.get(url=url, params=params,headers=headers)
# post请求
response = requests.post(url=url, data=data_form, headers=headers)
get和post请求的数据格式都是字典,除此之外,比较重要的就是找到关键url
response返回数据
- text:字符串
- content:二进制
- json:对象
持久化存储
文件夹操作
import os
if not os.path.exists('./test'):
os.mkdir('./test')
如果
普通存储
with open("../file/baidu.html", 'w', encoding='utf-8') as fp:
fp.write(response.text)
json格式(json对象存储)
dic_obj = response.json()
fp = open('./1.json','w',encoding='utf-8')
json.dump(dic_obj,fp=fp,ensure_ascii=False)
数据解析
- 标签定位
- 对标签或标签对应的属性中存储的数据值进行提取
正则re
-
re.findall(pattern,string,flag)
import re a = re.findall(ex,str,re.S)
ex为正则表达式,str为给定的字符串,re.S表明re会将这个字符串作为一个整体
-
re.sub(pattern,repl,string)
pattern:正则表达式
repl:要替换的字符串
string:待处理字符串
# 将string中处于filter的字符串全部剔除 filter=[' ','\n'] re.sub('|'.join(filter),'',string)
特殊re字符
字符 | 含义 |
---|---|
. | 匹配除了换行符以外的任意字符 |
^ | 匹配以某字符串开头的字符串,放在字符串前 |
$ | 匹配以某字符结尾的字符串,放在字符串以后 |
* | 匹配前面的子模式零次或多次(greedy)。要匹配 * 字符,请使用 \*。 |
+ | 匹配前面的子模式1次或多次(greedy) |
? | 匹配前面的子模式零次或1次(greedy) |
*?,+?,?? | 非greedy版本的*、+、? |
{m,n} | 匹配前面一个模式串m到n次(greedy) |
{m,n}? | 匹配前面一个模式串m到n次(non-greedy) |
\\ | 转义特殊字符或发出特殊序列信号 |
[] | 匹配字符集合 |
| | or,A|B,匹配模式串A或模式串B |
() | 匹配括号以内的部分,在括号内包含模式串 |
特殊序列
序列 | 含义 |
---|---|
\b | 匹配空字符串,但仅在单词的开头或结尾。 |
\B | 匹配空字符串,但不在单词的开头或结尾。 |
\d | 匹配任意十进制数字,等价于[0-9] |
\D | 匹配任意非十进制数字,等价于[^\d] |
\s | 匹配任意空白字符 |
\S | 匹配任意非空白字符 |
\w | 匹配任意字母数字字符 |
\W | 匹配任意非字母或数字字符 |
\\ | 匹配反斜杠 |
flags
flags | 含义 |
---|---|
A | 使\w, \w, \b, \b, \d, \d匹配相应的ASCII字符类别 |
I | 执行不区分大小写的匹配。 |
M | "^“匹配行首(换行符之后)以及字符串。”$"匹配行尾(换行符之前)以及字符串的结尾。 |
S | "."匹配任何字符,不再排除换行符。 |
X | 忽略空格和注释,以获得更好看的正则。 |
使用的时候为函数的flag参数指定re.S
即可
xpath
最常用且最高效便捷的一种解析方式,通用性最强。
- 实例化一个etree对象,且需要将被解析的页面源码数据加载到该对象中
- 调用etree对象中的xpath方法结合xpath表达式实现标签的定位和内容的捕获
实例化etree对象
from lxml import etree
tree = etree.parse(filePath)#加载html文档源码
tree.HTML('page_text')#将数据加载到该对象中
tree.xpath('xpath表达式')#解析
xpath表达式
语法 | 含义 | 示例 |
---|---|---|
/ | 从当前标签位置开始定位标签 | /html/div |
// | 从任意位置定位标签 | //div |
[@] | 属性定位 | //div[@class=“song”],定位到class为song的div |
[] | 索引定位 | //div/p[3],定位第三个p标签(索引从1开始) |
/text() | 获取标签的直系文本 | //div/p[3]/text(),定位第三个p标签并取出其直系文本 |
//text() | 获取标签的所有文本 | //div//text(),取出所有div标签中的所有文本内容 |
/@attrName | 获取属性值 | //img/@src,取出所有img标签的src属性值 |
./ | 当前标签 | ./@method,取出当前标签的method属性值 |
反爬绕过
伪装绕过
-
UA(User-Agent)伪装
安装
fake_useragent
包:pip install fake_useragent
然后:
from fake_useragent import UserAgent ua = UserAgent() user_agent = ua.random
-
cookie伪装
-
Referer伪装
验证码绕过
- 第三方验证码识别api,云打码识别,收费较低
模拟登陆
在使用requests进行模拟登陆的时候可能需要使用requests.Session才可以
session = requests.Session()
response = session.get(url=url, headers=headers)
在登录过后想要获取其他页面的数据需要携带cookie,该cookie是登陆后返回的set-cookie的值,如果使用session,则session会自动存储cookie,下次请求就没有必要携带cookie
控制请求间隔
对于只有一个线程或一个进程的程序,使用time.sleep
来控制间隔
代理
绕过ip封禁,在requests中指定proxies参数值,这是一个字典
代理池的使用暂时遇到两种格式:
协程(aiohttp.ClientSession.get
)、scrapy格式:
proxies = ["http://61.216.185.88:60808", "http://121.13.252.58:41564","http://113.124.86.24:9999","http://27.42.168.46:55481","http://117.114.149.66:55443"]
requests格式:
proxies = [{"http": "61.216.185.88:60808"}, {"http": "121.13.252.58:41564"},{"http": "113.124.86.24:9999"},{"http": "27.42.168.46:55481"},{"http": "117.114.149.66:55443"}]
快代理代理爬取函数:
def get_proxy():
ip_list = []
port_list = []
anonymity_level_list = []
type_list = []
location_list = []
response_speed_list = []
page_nums = 15
for page_num in range(1, page_nums):
url = "https://www.kuaidaili.com/free/inha/" + str(page_num) + '/'
headers = {
'User-Agent': user_agents_pool[page_num % len(user_agents_pool)]
}
# response = requests.get(url=url, headers=headers, proxies=proxies[page_num % 5])
response = requests.get(url=url, headers=headers)
print(response.status_code)
page_text = etree.HTML(response.text)
ip_s = page_text.xpath("//td[@data-title='IP']/text()")
port_s = page_text.xpath("//td[@data-title='PORT']/text()")
anonymity_level_s = page_text.xpath("//td[@data-title='匿名度']/text()")
type_s = page_text.xpath("//td[@data-title='类型']/text()")
location_s = page_text.xpath("//td[@data-title='位置']/text()")
response_speed_s = page_text.xpath("//td[@data-title='响应速度']/text()")
ip_list.extend(ip_s)
port_list.extend(port_s)
anonymity_level_list.extend(anonymity_level_s)
type_list.extend(type_s)
location_list.extend(location_s)
response_speed_list.extend(response_speed_s)
time.sleep(2)
dic = {
'ip': ip_list,
'port': port_list,
'anonymity level': anonymity_level_list,
'type': type_list,
'location': location_list,
'response_speed': response_speed_list
}
df = pd.DataFrame(dic)
# 去除ip列和port列都重复的行
df.drop_duplicates(df[df.duplicated('ip')&df.duplicated('port')], inplace=True)
df.to_csv(path_or_buf='./proxy.csv', encoding='gbk', index=False)
检测到非法调试
卡死在debbuge处
-
让开发者工具界面作为独立窗口存在
-
hook代码注入绕过,注意,刷新页面后就需要重新注入。在
源代码
添加新片段
,新片段
为以下代码,之后继续执行即可var AAA=Function.prototype.constructor Function.prototype.constructor=function(x){ if(X!="debugger"){ return AAA(x) }; return function(){}; }
异步爬虫
在爬虫中使用异步实现高性能的数据爬取操作
异步爬虫方式:
-
多进程或多线程:阻塞操作可以异步执行,但进程和线程不可无限制创建
-
线程池或进程池:可以降低系统对进程或线程创建和销毁的一个频率,从而很好地降低系统的开销,但池中的数量仍有上限
import time from multiprocessing.dummy import Pool # 进程池和线程池 def get_page(): return url_list=[] start_time = time.time() #实例化一个拥有4个线程的线程池 pool = Pool(4) # 将列表中每一个列表元素传递给函数进行处理并返回一个列表 result_list = pool.map(get_page, url_list) pool.close() pool.join()#主线程等待子线程运行结束后再结束运行
线程池处理的是阻塞且耗时的操作
-
单线程+异步协程
-
event_loop
,事件循环,将函数注册到该事件循环,当满足某些条件,函数就会被执行 -
coroutine
,协程对象 -
task
,任务,是协程对象的进一步封装,包含了任务的各个状态# 创建task的第一种方式 task = asyncio.create_task(...) #创建task的第二种方式 task = asyncio.ensure_future(...)
-
future
:代表将来执行或还没执行的任务,实际上和task没有本质区别 -
async
:定义一个协程 -
await
:用来将可等待的对象挂起(协程对象、Future、Task),等待对象的值得到结果后再继续向下走
对于主线程是
loop=get_event_loop()
. 对于其他线程需要首先loop=new_event_loop()
,然后set_event_loop(loop)
。
new_event_loop()
是创建一个event loop
对象,而set_event_loop(eventloop对象)
是将event loop对象指定为当前协程的event loop,一个协程内只允许运行一个event loop,不要一个协程有两个event loop交替运行。 -
协程不是由计算机提供,而是人为创造的,是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块互相切换。
在一个线程中如果遇到IO等待时间,线程不会一直等着,而是利用空闲的时间去干别的事。(IO多路复用)
实现协程:
- greenlet:早期模块
- yield关键字
- asyncio装饰器
- async、await关键字
多任务异步协程(asyncio)
import asyncio
import time
async def request(url):
print(url)
# 在异步协程中如果出现了同步模块相关的代码,那么就无法实现异步
# 使用time.sleep就无法实现异步
# 对阻塞操作手动挂起
await asyncio.sleep(2)
urls = {
"www.baidu.com",
"www.163.com",
"www.souhu.com"
}
# 任务列表:存放多个任务对象
tasks = []
# 生成事件循环
loop = asyncio.new_event_loop()
for url in urls:
c = request(url=url)
task = asyncio.ensure_future(c, loop=loop)
tasks.append(task)
# 使用wait封装task列表,然后一次性往事件循环中注册任务列表中的任务
results = loop.run_until_complete(asyncio.wait(tasks))
在使用requests进行爬取时,requests.get基于同步模块,必须使用基于异步请求的模块aiohttp
async def get_page(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
# text返回文本,read返回二进制,json返回json对象
page_text = await response.text()# 使用await进行挂起
with open("../file/"+url[7:]+".html", 'w', encoding='utf-8') as fp:
fp.write(page_text)
uvloop
是asyncio的事件循环的替代方案,事件循环>默认asyncio的事件循环
pip install uvloop
import uvloop
asyncio.set_event_loop_policy(uvloop.EvventLoopPolicy())
其他的处理与asyncio的事件循环处理的方式一致
爬虫框架
selenium
- 便捷地获取网站中动态加载的数据
- 便捷实现模拟登陆
基于浏览器的自动化模块
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains
from lxml import etree
from time import sleep
options = webdriver.ChromeOptions()
# 设置无头模式,浏览器无可视化界面运行
# options.headless = True
# options.add_argument('lang=zh_CN.UTF-8')
options.add_argument('lang=zh_CN')
# 设置加载策略,默认为normal,很慢
options.page_load_strategy = 'eager'
# 将驱动程序放在python目录下可以不用指定executable_path路径
# browser = webdriver.Chrome(options=Chrome_options)
browser = webdriver.Chrome()
browser.get(url = "https://cn.bing.com/")
page_text = browser.page_source
# print(page_text)
# 搜索框定位
search_input = browser.find_element(value="sb_form_q")
search_input.send_keys("你好")
# 提交按钮定位
go_input = browser.find_element(value="sb_form_go")
go_input.submit()
# 执行js代码,将页面滚动到最底部
browser.execute_script(script="window.scrollTo(0, document.body.scrollHeight)")
# 回退
browser.back()
#前进
browser.forward()
sleep(10)
# 关闭浏览器
browser.quit()
标签定位
search_input = browser.find_element(value="sb_form_q")
默认是通过id定位标签
iframe
browser.switch_to.frame("待切换的iframe的id")# 切换标签定位作用域
查看一个页面的第一层iframe有多少(在控制台执行)
frames.length()
通过索引可以获取对应的iframe
frames[i]
查看第i个iframe的子iframe个数
frames[i].frames.length()
动作链
from selenium.webdriver import ActionChains
action = ActionChain(browser)
action.click_and_hold(标签变量)#点击并抓住这个标签
action.move_by__offset(x,y).perfome()#拖动到某个位置并立即执行
action.release()#释放动作链
规避检测
# 规避检测
options.add_experimental_option('excludeSwitches', ['enable-automation'])
# 禁用浏览器自动控制
options.add_argument("--disable-blink-features=AutomationControlled")
无头浏览器
# 无头浏览器
options.add_argument("--headless")
options.add_argument("--disable-gpu")
scrapy
集成了很多功能,并且具有很强通用性的项目模板,其功能为:
- 高性能的持久化存储
- 异步数据下载
- 高性能的数据解析
- 分布式
scrapy原理
scrapy五大核心组件
-
调度器:过滤器和队列,过滤完成的请求放入队列
-
管道
-
引擎
-
下载器
-
spider:发送请求,数据解析
中间件
-
下载中间件:处于引擎与下载器之间,批量拦截到整个工程中所有的请求和响应
-
拦截请求
可以做UA伪装,设置代理ip
-
拦截响应
可以篡改响应数据,响应对象
-
-
爬虫中间件:处于spider与引擎之间
创建工程
在终端输入scrapy startproject project_name
目录结构:
└─scrapy_test
│ items.py
│ middlewares.py
│ pipelines.py
│ settings.py
│ __init__.py
│
└─spiders
__init__.py
-
spiders
:爬虫文件夹,在该文件夹下必须创建一个爬虫文件scrapy genspider spiderName www.xxx.com
,如果是基于CrawlSpider则按以下方式创建爬虫文件:scrapy genaspider -t crawl xxx www.xxx.com
-
settings.py
:存放工程配置 -
items.py
:数据的封装,使用scrapy.field
-
pipelines.py
:专门用来处理item
工程执行
scrapy crawl spliderName
就会执行指定的爬虫文件
--nolog
:不打印日志
如果需要在某个python文件中对spider进行调用,那么可以采用以下方式:
-
cmd方式(cmdline.execute或者os.system):该方式需要切换工作目录,比较麻烦
-
CrawlerProcess方式,相比cmd来说更方便,而且可以看日志
from crawler.crawler.spiders.crawlAll import CrawlallSpider from scrapy.crawler import CrawlerProcess from scrapy.utils.project import get_project_settings if __name__ == '__main__': process = CrawlerProcess(get_project_settings()) process.crawl(CrawlallSpider) process.start()
CrawlallSpider
是自定义的爬虫类,其文件路径为/crawler/crawler/spider/crawlAll
-
CrawlerRunner方式,看不到日志
from scrapy.crawler import CrawlerRunner from scrapy.utils.project import get_project_settings from twisted.internet import reactor from crawler.crawler.spiders.crawlAll import CrawlallSpider runner = CrawlerRunner(get_project_settings()) # [runner.crawl(spider) for spider in spiders] runner.crawl(CrawlallSpider) d = runner.join() d.addBoth(lambda _: reactor.stop()) reactor.run()
注意,在使用第二种或第三种方式时,在setting文件中定义的管道等配置可能不会被读取,最好放在爬虫文件的custom_settings中或者如此指定
process = CrawlerProcess(settings={
'ITEM_PIPELINES': {
'crawl.crawler.crawler.pipelines.CrawlerRedisPipeline': 300
},
'REQUEST_FINGERPRINTER_IMPLEMENTATION': '2.7'
})
spiders
指的是使用genspider
创建的处于spiders
目录下的文件
-
name
:爬虫源文件名 -
allow_domains
:限定哪些域名下的url可以别允许发送请求,但一般不用 -
start_urls
:在该列表中存放的url会被自动发送请求 -
parse
函数:用于数据解析response.xpath
:与etree的xpath有一些不同,spider经过xpath后的结果是一个Selector
列表,通过extract
函数可以取出每个selector
的data
参数存放的值
-
start_requests
函数:最开始发起请求的地方
setting配置文件
# 是否遵循robots.txt
ROBOTSTXT_OBEY = False
# 只显示error等级的日志信息
LOG_LEVEL = 'ERROR'
# 指定代理
USER_AGENT = ''
# 开启管道
ITEM_PIPELINES = {
'xxxxx.pipelines.xxxxxPipeline':300,#xxxxx代指工程名,300表示优先级,数值越小优先级越高
}
# 开启爬虫中间件
SPIDER_MIDDLEWARES = {
'xxxxx.middlewares.xxxxxSpiderMiddleware': 543,
}
# 开启下载中间件
DOWNLOADER_MIDDLEWARES = { 'xxxxx.middlewares.xxxxxDownloaderMiddleware': 543,
}
# 在高延迟情况下设置的最大下载延迟
AUTOTHROTTLE_MAX_DELAY = 60
# 配置Scrapy的最大并发请求 (default: 16)
CONCURRENT_REQUESTS = 32
# 指定一网站的请求延迟
DOWNLOAD_DELAY = 3
# 禁用 cookies (enabled by default)
COOKIES_ENABLED = False
# 指定基于ImagesPipeline管道的存储目录路径
IMAGES_STORE = 'path'
在执行工程时,通过指定-s
参数可以设置临时配置,比如
scrapy crawl spiderName -s DOWNLOAD_DELAY=10
在自定义的spider类中添加一个变量,如下:
class ImgSpiderSpider(scrapy.Spider):
name = 'img_spider'
allowed_domains = ['sc.chinaz.com']
start_urls = ['https://sc.chinaz.com/tupian/xingkongtupian.html']
custom_settings = {
'DOWNLOAD_DELAY': 5
}
def parse(self, response):
print(self.crawler.settings.get('DOWNLOAD_DELAY'))
然后执行工程可以得到DOWNLOAD_DELAY
的值8即为custom_settings中定义的值。
持久化存储
基于终端
将parse
方法的返回值存储于本地文件中
scrapy crawl spider_name -o filepath
存储的文件格式有限制
基于管道
-
在爬虫文件的
parse
中进行数据解析 -
在
items.py
中定义相关的属性 -
将解析的数据封装存储到item类型的对象里,使用
scrapy.field
进行对象的定义obj1 = scrapy.field() obj2 = scrapy.field()
-
从
items.py
importitems
类并进行实例化,然后进行将解析的数据存入item对象中,最后使用yield
把item
传递给管道from items import xxxxItem def parse(self, response): ..... ..... ..... item = xxxxItem()#实例化 item[obj_name] = data yield item
-
在
pipelines.py
的process_item
方法中接受爬虫文件提交过来的item对象存储的数据并进行持久化存储操作,注意需要重写父类方法# 对父类的方法进行重写,该方法只在开始爬虫的时候被调用一次 fp = None def open_spider(self,spider): self.fp = open(...) def close_spider(self, spider): self.fp.close()
-
在配置文件中开启管道
数据库持久化存储
可以自己在pipelines.py
中新写一个管道类,然后在其中重写方法
class sqlitePipeLine(object)
conn = None
cursor = None
def open_spider(self, spider):
self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='123456',charset='utf8')
def process_item(self,spider):
self.cursor = self.conn.cursor()
try:
self.cursor.execute('sql语句')
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self, spider)
self.cursor.close()
self.conn.close()
最后别忘记还要在setting
文件中添加该管道。另外,只有在process_item
方法中return item
,其他pipelie才可以拿到item并对其中的数据做处理
请求传参
使用场景:深度爬取,需要发出新的请求
yield scrapy.Request(url=new_url, callback=self.xxx_parse)
自定义解析函数后可以将其作为回调函数
def xxx_parse(self, response):
.....
请求传参指的是将当前parse中的变量传递给其他parse函数,可以在Request中指定meta参数
yield scrapy.Request(url=new_url, callback=self.xxx_parse, meta={'item':item})
然后在xxx_parse
解析函数里面按照字典的方式取数据:
item = response.meta['item']
另外,Request既可用于get请求,又可用于post请求(指定method
参数),进行post请求发送,还可以通过FormReques
ImagesPipeline(图片数据爬取)
专门用于图片数据爬取,只需要将图片的url发送给该管道,管道就可以对图片的src进行请求发送获取图片的二进制数据。
继承ImagesPipeline后重写以下方法:
from scrapy.pipelines.images import
class MyInagesPipeline(ImagesPipeline):
# 提供url自动下载图片
def get_media_requests(self, item, info):
yield scrapy.Request(item['url'])
# 指定图片存储名
def file_path(self, request, response=None, info=None, *, item=None):
img_name = request.url.split('/')[-1]
return img_name
#将item传递给下一个即将执行的管道类
def item_completed(self, results, item, info):
return item
图片存储的目录需要通过setting来指定,具体查看setting配置文件。
middlewares中间件
中间件的使用需要在setting文件中开启
下载中间件(DownloadMiddlewares)
scrapy启用下载中间件时会结合DOWNLOADER_MIDDLEWARES_BASE
和DOWNLOADER_MIDDLEWARES
,自带的DOWNLOADER_MIDDLEWARES_BASE
不可覆盖,如果要覆盖,则需要在自定义的DOWNLOADER_MIDDLEWARES
中将对应的下载中间件置为None,以下为相应的下载中间件:
DOWNLOADER_MIDDLEWARES_BASE =
{
'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,
'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,
'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,
'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,
'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,
'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,
'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,
'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,
}
自定义下载中间件:
class xxxDownloadMiddlewares(object):
user_agent_pool=[xxxxxxx]
proxy_http = [xxxxx]
proxy_https = [xxxxx]
# 拦截正常请求
def process_request(self, request, spider):
# UA伪装
request.headers['User-Agent'] = random.choice(self.user_agent_pool)
return None
# 拦截所有的响应
def process_response(self, request, response, spider):
return response
# 拦截发生异常的请求
def process exception(self, request, exception, spider):
if request.url.split(':')[0]=='http':
# 代理ip 'proxy'是request固定携带的
request.meta[ 'proxy'] = random.choice(proxy_http)
else
request.meta[ 'proxy'] = random.choice(proxy_https)
return request
-
process_request
return 说明 None
Request对象会被递交给下一个下载中间件 Response
返回Response对象后会交由process_response处理 Request
Scrapy将停止调用process_request()方法并重新调度返回的请求。 raise IgnoreRequest
不处理该请求 -
process_response
return 说明 Response
Response对象会被递交给下一个下载中间件 Request
中间件链将停止,返回的请求将重新安排在将来发送。这与从process_request()返回请求的行为相同。 raise IgnoreRequest
不处理该响应
重定向中间件
为scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware
设置一个优先级,然后在setting文件中启用它:
'REDIRECT_ENABLED': 'True', # 启用重定向中间件
然后scrapy就可以自动重定向
cookie中间件
为scrapy.downloadermiddlewares.cookies.CookiesMiddleware
设置一个优先级,然后在setting文件中启用它:
COOKIES_ENABLED=True# 自动添加cookie
这会使得爬取到的cookie会自动附加到后续的request中
如果想要在debug看cookie的信息,则可以再添加一个配置:
COOKIES_DEBUG=True
重试中间件
为scrapy.downloadermiddlewares.retry.RetryMiddleware
设置一个优先级,然后在setting文件中启用它:
RETRY_ENABLE=True
然后scrapy就可以再次处理原来失败的请求。
还可以设置retry的次数和碰到哪些http code就retry
RETRY_TIMES=5
RETRY_HTTP_CODES=[302]
CrawSpider全站数据爬取
spider的子类。
全站数据爬取的方式:
- 基于spider:手动请求
- 基于CrawSpider
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class AllspiderSpider(CrawlSpider):
name = 'allSpider'
allowed_domains = ['www.xxx.com']
start_urls = ['http://www.xxx.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
-
链接提取器(LinkExtractor):根据指定规则进行指定链接的提取,即便有重复的链接也会自动过滤
- allow:一个用来提取链接的正则表达式,为空字符串时提取页面中的所有链接
-
规则解析器(Rule)
link
:指定链接提取器callback
:指定回调函数follow
:指定是否可以自动更换start_url,其实就是将链接提取器继续作用到链接提取器提取到的链接所对应的页面中。
-
parse_item
对链接提取器中的链接得到的response进行解析
crawlspider的url均获取自response,重定向的response不会流向parse_item,需要自行在下载中间件中处理,比如访问一个页面302重定向到登录页面,这个时候你是获取不到登录页面数据的,只能在下载中间件中的process_response中获取
链接提取器
from scrapy.linkextractors import LinkExtractor
协程引用
在setting配置文件中添加:
TWISTED_REACTOR = 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'
xpath选择器(response.xpath)
使用xpath选择器会返回一个selector对象,该对象格式为:
[<Selector xpath='xpath expression' data='Example website'>]
方法 | 说明 |
---|---|
get | 提取单个文本数据 |
getall | 提取所有文本数据,返回列表 |
以下是示例,其中response就是parse函数中的response:
def parse_item(self, response):
form_list = response.xpath("//form")
print(response.status, ':', response.request.method, ':', response.url)
for form in form_list:
method = form.xpath('./@method').get()
action = form.xpath('./@action').get().strip()
if action == "":
url = response.url
elif str(action).startswith("http"):
url = action
else:
url = response.url[:(response.url).rindex('/') + 1] + action
input_list = form.xpath('.//input')
params = {}
for _input in input_list:
params[_input.xpath('./@name').get()] = '1'
yield scrapy.FormRequest(url=url, method=method, formdata=params, callback=self.parse_item)
form_list是一个selector对象列表,迭代其中的每个form表单对象,并以当前表单对象为基准提取其对象的method和action属性,又以单签表单对象为基础,获取其下的input对象,然后与前面类似,最后每个表单都会再次发出一次请求,并且其响应依旧由当前函数解析。
分布式爬虫
搭建分布式机群,让其对一组资源进行分部联合爬取,可以提高爬取的效率
实现分布式需要安装scrapy-redis
,原生的scrapy不能实现分布式爬虫
-
创建工程
-
创建基于
CrawlSpider
的爬虫 -
修改爬虫文件
from scrapy_redis.spiders import RedisCrawlSpider class LocalSpider(RedisCrawlSpider): name = '58' # 注释以下两行变量 # allowed_domains = ['cq.58.com'] # start_urls = ['http://cq.58.com/'] redis_key = '58' # 被共享的调度器队列的名称 rules = ( Rule(LinkExtractor(allow=r'shouji/pn\d+'), callback='parse_item', follow=True), ) def parse_item(self, response): item = {} title_list = response.xpath("//td[@class='t'/a[1]//text()").extract() location_list = response.xpath("//td[@class='t'/p[1]//text()").extract() price_list = response.xpath("//td[@class='t'/p[3]//text()").extract() print(title_list) for key, title in enumerate(title_list): item = ScrapyRedisTestItem() item['title'] = title item['location'] = location_list[key] item['price'] = price_list[key] yield item
-
修改
setting
配置文件-
指定可以被共享的管道
ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline':400 }
-
指定调度器
# 增加一个去重容器类的配置,使用redis的set集合来存储请求的指纹数据,从而实现请求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" #使用scrapy-redis自己调度器,不使用scrapy默认的调度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" #配置调度器是否要持久化,不调度状态持久化,不清理redis缓存,允许暂停/启动爬虫 SCHEDULER_PERSIST = True
-
-
redis相关配置
redis的配置文件(
.conf
文件):# 关闭保护模式,允许写数据 protected-mode no # 注释以下语句 # bind 127.0.0.1
-
启动redis服务
redis-server 配置文件
-
启动客户端(其他要使用redis的分布式主机)
redis-cli
-
执行工程
scrapy runspider xxx.py
-
启动客户端后指定调度器队列名并向其中放入起始url
lpush 调度器队列名 起始url
-
在setting文件中指定提供redis服务的主机和端口
REDIS_HOST = '127.0.0.1' REDIS_PORT = 6379
-
除了使用默认的redispipeline,也可以自定义redispipeline:
class CrawlerRedisPipeline(RedisPipeline):
r = None
def open_spider(self, spider):
self.r=redis.Redis(host=redis_host, port=redis_port, db=0, decode_responses=False
)
def process_item(self, item, spider):
response = item['response']
ser_response = pickle.dumps(response)
self.r.push(redis_response_1_key, ser_response)
return item
def close_spider(self, spider):
self.r.close()
decode_responses=False
可以绕过一些编码问题
增量式爬虫
用于检测网站数据更新的情况,只会爬取网站最新更新出来的数据,这意味着需要对上一次爬取得到的链接进行存储(可以存到redis的set集合中)
scrapy-redis在进行爬取后会自动去重,也就是说遇到重复的链接将不再存储
报错修改
编码为utf-8 response仍为乱码
一般来说,response返回的内容如果为乱码,那么基本就是没有设置正确的编码,这可以使用以下代码解决
response.text.encode('utf8')
但是还有一个情况,当在header中设置了以下参数也会出现乱码
"Accept-Encoding":"gzip, deflate, br"
accept-encoding表示你发送请求时告诉服务器,我可以解压这些格式的数据,因此爬取的response被压缩成了这样的格式。requests不支持br,所以一旦Accept-Encoding包含br且服务器返回br格式的数据,那么就会出现乱码,故删除br即可解决问题。
"Accept-Encoding":"gzip, deflate"
pip安装包时报错 ERROR: Failed building wheel for xxx
去以下网站下载对应的whl包并使用pip安装
Archived: Python Extension Packages for Windows - Christoph Gohlke (uci.edu)
Scrapy 中 ImagesPipeline 无法执行
pip install pillow
启用下载中间件后卡死在telnet的监听处
检查process_request函数返回值是不是None