安装scrapy框架
pip install scrapy
pip install pypiwin32
快速入门
- Spider:根据start_urls列表,自动调用start_requests()方法,想目标网站发送请求,默认是以parse作为回调函数,所以在类中有个parse函数让我们编写
- CrawlSpider:根据start_urls列表,发送请求;然后在rules里的规则进行过滤得到有效的连接在发送请求,各自有各自的回调函数进行处理
官方的案例
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
author_page_links = response.css('.author + a') # 获取作者链接
yield from response.follow_all(author_page_links, self.parse_author)
# 解析作者内容
pagination_links = response.css('li.next a')
# 获取下一页连接
yield from response.follow_all(pagination_links, self.parse)
# 请求下一页来继续获取作者链接
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
具体案例解析
创建项目
scrapy startproject ['项目名称']
cd ['项目名称'] && scrapy genspider itcast 'qiushibaike.com'
# 注意:爬虫名字不可以与项目名字重复,也不能以spider.py命名
##修改配置文件:
1: ROBOTSTXT_OBEY = False # 不准守机器人协议
2: DEFAULT_REQUEST_HEADERS = { # 设置请求头
‘Accept’: ‘text/html,application/xhtml+xml,application/ xml;q=0.9,/;q=0.8’,
‘Accept-Language’: ‘en’,
}
3. USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36' # 设置user—agent
spiders包:
爬虫包,包含所有的小爬虫
1. itcast.py: 要继承自scrapy.Spider类 # 爬虫通过 scrapy genspider [name] [address.com]
2. 实现三个类属性:name = 'itcast' # 爬虫的小蜘蛛的名字
allow_domains = ['qiushibaike.com'] # 防止跑到其他网站,限制在这个域名内
start_urls = ['http://www.qiushibaike.com/']
# 在start_requests函数中,而这个函数的默认解析函数就是parse
3. 根据开始的url,--> 引擎 --> 调度器(Schedule) --> 引擎 --
--> 下载器 --> 引擎 --> spider的parse方法,并将response
传给此方法
parse函数:
**对网页的解析操作 **
- 运行:在根目录新建一个start.py
from scrapy import cmdline
cmdline.execute("scrapy crawl itcast".split())
运行start.py就可以运行指定的spider
xpath语法
1. 层级: / 表示直接子集
// 表示子集或者孙子集
2. 属性: @[属性]
例如:"//div[@class='red']"
[]表示集合,通常用在[]中用来过滤
[last()]表示最后一个元素
3. 函数:contains(), text(), not() ...
例如:"//div[not(@class)]" 没有class属性的div
"//div[contains(@class,"red green")]" 属性中包含red green的div
"//div[@id='queue']//span/text()" # 获取span的值,这里还是一个选择器,可以通过get获取
一个,getall()获取全部,获取for循环中get()
1. 地区:大陆,地区是固定的,但是后面的值可能是其他,定位方式:"//span[./text()='地区国家']/following::text[1]",following表示后面的内容
2. attribute属性之类的都是当前节点的属性::
3. 找某些特定标签然后[n]取不到值时,可能时因为你抓取的多个节点(并排),而不是一个列表(竖排),可以找父亲节点或者爷爷节点,在上一级[n]再进一步获取
注意:get()获取的内容是str,getall()是str-list,
例如:text()函数可能会返回多个标签的文本信息,用getall()可以把多个标签的text都获取,然后通过join方法拼接成完整的字符串
yield [Request|item]
获取的数据通过yield的方式返回,可以省内存,引擎根据返回的对象,决定是发请求还是将item返回给pipeline
# itcast.py [spieder]
class ItcastSpider(scrapy.Spider):
name = 'itcast' # 爬虫
allowed_domains = ['qiushibaike.com'] # 允许范围
start_urls = ['https://www.qiushibaike.com/text/page/1/']
def parse(self, response):
# 先获取整个div,然后在里面找文章信息和作者信息
# duanzidivs = response.xpath("//div[contains(@class,'article block untagged mb15')]")
divs = response.xpath("//div[@class='col1 old-style-col1']/div")
# 是selector类型,还可以xpath
# duanzi = response.xpath("//div[contains(@class,'article block untagged mb15')]/a//span[not(@class)]")
# author = response.xpath("//div[contains(@class,'article block untagged mb15')]//h2/text()").getall()
for duanzidiv in divs:
author = duanzidiv.xpath(".//h2/text()").get().strip()
content = duanzidiv.xpath(".//div[@class='content']//text()").getall()
content = ''.join(content).strip()
# 不加//text,还不是列表就是一个selector对象,
# 加上//text获取下面的所有标签的文本,所以要用getll(),开发人员老阴B,把内容放在多个标签中
duanzi = {'author':author,"content":content}
yield duanzi
# 段子进过引擎到达pipeline中
数据Json格式保存 - pipelines管道的使用
import json
# pipelines.py
class MyspiderPipeline:
def __init__(self):
self.fp = open('duanzi.json','w',encoding='utf-8')
def open_spider(self,spider):
# 爬虫一开始就会运行
print('爬虫开始了')
pass
def process_item(self, item, spider):
# 当spider向这里yield数据的时候会运行
item_json = json.dumps(item,ensure_ascii=False) # 保证中文的正常保存---※※※※
self.fp.write(item_json+'\n')
return item
def close_spider(self,spider):
# 爬虫结束就会运行
self.fp.close()
print('爬虫结束了')
注意: 要想pipeline起作用,就要把settings中的配置打开
```
ITEM_PIPELINES = {
'myspider.pipelines.MyspiderPipeline': 300, # 后面是优先级,小为高
}
```
使用模型类构建数据模型
- 定义爬取的数据的模型
class MyspiderItem(scrapy.Item): # 一般在生成项目的时候会有一个这样的类
author = scrapy.Field()
content = scrapy.Field()
- 返回内容的时候不用字典,用模型
from myspider.myspider import items
item = items.MyspiderItem(author=author,content=content)
yield item # 更加专业
保存数据 - 实际中可能用的方法
(1) 在初始化中声明一个属性为exporter = JsonLineItemExporter()
(2) 在传入值的时候执行self.exporter.export_item(item)
from scrapy.exporters import JsonLinesItemExporter
class MyspiderPipeline:
def __init__(self):
self.fp = open('duanzi.json', 'wb') # 'wb' 打开二进制
self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='UTF-8')
def open_spider(self, spider):
print('爬虫开始了')
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self, spider):
self.fp.close()
print('爬虫结束了')
其中process_item()必须将item返回
爬取第二页,第三页的数据
-
提取下一页的连接
(1)使用xpath提取,例如
next_url = response.xpath(".//ur[@class=‘pagination’]/li[last()]/a/@href").get()如果有值:# 对于这个url可能会拼接一下 yield scrapy.Request(next_url,self.parse)
(2)为了防止网站崩溃,打开settings中的delay
python DOWNLOAD_DELAY = 1 # 一秒一回头
-
某些网站使用ajax请求渲染第二页的数据,
(1)找到请求的地址:一般是当前地址&page=\d&sort=\d的格式
(2)通过正则找到当前的页面和总页面:re.findall(r"var current_page=(.+?);",response.body.decode())[0]
(3) 构造请求:根据请求的地址和当前的页码+1构造出类似的请求地址yield Request
Request对象
url
callback
method
header
mata:用户中间件和扩展之间的通信,也可以使用这个在请求和下一步的响应之间进行通信
cb_kwargs:是一个字典,其中包含要传递给回调函数的关键字参数。
在多个请求之间通信的方式可以通过将属性加到request对象上面
encoding,dot_filter重复请求的过滤,在验证码的时候可能会多次请求要设置False
Response对象
url:
meta:上一次请求和本次响应传递数据, 注意:传入的值如果是可变类型,可能会要用到深copy,否则多线程会影响
meta = {‘item’:deepcopy(item)}:
只有在大分类层中创建了item,在小分类中共用这个item的时候才会用到深拷贝:这时候建议不共用字典item,用变量代替。
encoding:
text():unicode的字符串
body():bites字符串返回
xpath():xpath选择器
css():css选择器
follow():scrapy.Request(url,callback=self.parse)的简写
follow_all():如果获取的连接是列表就可以使用follow_all()方法,省去for循环
模拟登陆
1. 使用cookie模拟登录
def start_requests(self):
cookies = "获取的cookie"
cookie = dict(i.split('=') for i in cookies.split('; '))
yield scrapy.Request(
self.start_urls[0],
callback = self.parse,
cookies = cookies,
# 将cookie放到headers中然后传入headers参数是没有用的,浏览器直接找cookie字段,
# 而不是去header中找cookie
)
2. FormRequest
**2.1 如果想要在一开始就发送post请求就需要重写Spider类的start_request(self)方法
**2.2对于某些网站登录后只返回json信息,可以在回调函数中在此请求网站的首页,因为是同一个request,所以登录信息会记住。
class RenSpider(scrapy.Spider):
name = 'ren'
allowed_domains = ['duboku.co']
start_urls = ['https://www.duboku.co/']
def start_requests(self):
url = 'https://www.duboku.co/user/login.html'
data = {
'user_name':'shuaiqi',
'user_pwd':'pythonspider'
}
yield scrapy.FormRequest(url,formdata=data,callback=self.parse_page)
def parse_page(self, response):
# 此响应式json信息,在此请求改网站
url = 'https://www.duboku.co/'
yield scrapy.Request(url,callback=self.parse_newpage)
def parse_newpage(self,response):
with open('main.html','wb') as f:
f.write(response.body)
3. 对于某些暴露form表单的网站,可以使用FormRequest.from_response():
- 这个是自动的,所以准确性没有手动的高,必传参数是response。
验证码登录
使用第三方的图片识别平台,自动识别验证码,模拟登陆豆瓣网
- 获取验证码图片,按照base64进行加密,然后作为参数请求识别平台,将获取的内容填写到文本框中
图片批量下载
使用imagepipeline下载
重写imagepipeline实现下载的路径分类
-
主要方法:file_path() 在保存图片的时候执行此方法,获取路径
:get_media_requests() 获取下载的图片链接,通过item对象的image_urls获取 -
这两个方法是先执行get_mediarequests() 得到图片信息,然后在保存的时候执行file_path()方法
-
我们主要是重写file_path方法,返回自定义的图片的路径
from scrapy.pipelines.images import ImagePipeline
import os
from project import settings
class ImagePipeLine(ImagePipeline):
def get_media_requests(self,item,info):
request_objs = super().get_media_requests(item,info)
# 为request对象添加item属性,也算是一种标识
for request_obj in request_objs:
request_obj.item = item
return request_objs
def file_path(self,request,response=None,info=None):
path = super().file_path(request,response,info)
# 这是应该返回的路径,但是这个多了full
path = path.replace('full','')
category = request.item.get('category')
image_store = settings.IMAGES_STORE
# 将image存储的路径设置到全局变量中
category_path = os.path.join(image_store,category)
image_path = os.path.join(category_path,path)
return image_path
CrawlSpider 爬取规律性网站
使用CrawlSpider爬取汽车之家高清图片:适合爬取url规则的网站
Rule: 规 则 列 表
LinkExtractor
-
allow设置匹配的url正则,要能限制在我们想要的url上面,不要跟其他url产生相同正则即可
-
follow:在爬取页面的时候,如果想要在页面上既要寻找符合正则url就是让他为True,否则False
-
callback: 想要获取此页面内容,设置解析函数,如果只是想找url,则不指定
callback = 'parse_page’或者 callback = self.parse_page
- 在回调函数中获取类别和该类别图片的urls,交给imagepipeline处理保存
from bmw.items import BmwItem
from scrapy.spiders import CrawlSpider,Rule
from scrapy.linkextractors import LinkExtractor
class Bmw5Spider(CrawlSpider):
'''
使用CrawlSpider在详情页面爬取,详情页面
'''
name = 'bmw5'
allowed_domains = ['car.autohome.com.cn']
start_urls = ['https://car.autohome.com.cn/pic/series/65.html']
rules = (
Rule(LinkExtractor(allow=r'https://car\.autohome\.com\.cn/pic/series/65.+'),callback="parse_page",follow=True),
# 有点,问号的地方要注意转义
)
def parse_page(self,response):
category = response.xpath("//div[@class='uibox']/div/text()").get()
srcs = response.xpath("//div[contains(@class,'uibox-con')]/ul/li/a/img/@src").getall()
srcs = list(map(lambda x: response.urljoin(x.replace('240x180_0_q95_c42_','')),srcs))
yield BmwItem(category=category,image_urls=srcs)
反 反爬虫
更换随机的请求头
- httpbin.org可以获取你请求的参数信息,所以爬这个网站看你的请求头是否更换
httpbin.org/user-agent
httpbin.org/ip
httpbin.org/headers - 代码:注意使用dont_filter为True,否则因为是重复的请求会被停掉
import scrapy
import json
class HttpbinSpider(scrapy.Spider):
name = 'httpbin'
allowed_domains = ['httpbin.org']
start_urls = ['http://httpbin.org/user-agent']
def parse(self, response):
user_agent = json.loads(response.text)['user-agent']
print('='*30)
print(user_agent)
# yield response.follow(self.start_urls[0])
yield scrapy.Request(self.start_urls[0],self.parse,dont_filter=True)
- 中间件,使用random选取随机请求头,赋值给request.headers[‘user-agent’]
import random
class UseAgentDownloadMiddleware():
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2919.83 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',
'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27',
]
def process_request(self,request,spider):
user_agent = random.choices(self.USER_AGENTS)
request.headers['User-Agent'] = user_agent
特殊的
- 上面的
spider
是有很多信息的,包含这个爬虫的配置信息;例如可以使用spider.settings[‘USER_AGENTS’]获取当前爬虫的当前settings配置中的USER_AGENTS
字段的值,前提是在settings值有设置
ip代理池
- 开放代理可以直接赋值
request.meta[‘proxy’] = proxy - 独享代理要有认证信息
user_password = ‘用户名:密码’
request.meta[‘proxy’] = proxy
b64_user_password = base64.b64encode(user_password.encode(‘utf-8’))
request.headers[‘Proxy-Authorization’] = ‘Basic’ + b64_user_password.decode(‘utf-8’)
代理模型
''''
创建代理模型类
'''
fro datetime import datetime,timedelta
class Proxy():
def __init__(data): # 将之前的json传过来,得到一个代理实例
self.ip = data['ip']
self.port = data['port']
self.expire_str = data['expire_time']
self.proxy = f"https:{ip}:{port}"
self._expire_time = None
self.blacked = False # 是否被拉黑
@property
def expire_time(self):
if not self._expire_time: # 防止多次调用expire_time方法,单例的运用
# 将expire_str进行解析得到datetime类型的过期时间
return 'datetime类型的时间'
return self._expire_time
def is_expire(self):
if self.expire_time-datetime.now() < timedelta(second=5):
return True
else:
return False
- 一般不要每次都去换代理去访问,访问网站返回403或者404的时候再去换代理
import requests
import json
import Proxy # 下面定义的代理模型类
from twisted.internet.defer import DeferredLock
class IPProxyDownLoadMiddleware():
PROXYS_URL = '第三方平台请求接口'
def __init__(self):
super().__init__()
self.current = None # 用这个来保存当前代理,获取代理的时候要为这个属性设置值
self.lock = DeferredLock()
def process_reuest(self,request,spider):
if 'proxy' not in request.meta or self.current.is_expire or self.current.blacked: # 没有代理或者代理将要过期或拉黑,所以要找个地方保存当前的代理
# 请求代理
# p = self.get_proxy
self.update_proxy() # 不需要返回值,直接放到current里面了
request.meta['proxy'] = self.current.proxy
def process_response(self,request,response,spider):
if response.status != 200 or 'captcha' in response.url: # 返回的状态码不是200 或者返回的是验证码页面
if not self.current.blacked:
self.current.blacked = True # 符合条件就是被拉黑了
self.update_proxy
# 继续本次请求
return request # 不返回这个请求,此次的数据就没有了,所以要回去request
else:
return response # 如果是正常的请求,要返回response,否则后面接收不到数据
def update_proxy(self): # 获取代理,同时为他的current设置值
with self.lock.acquire():
if self.current or self.current.is_expire:
response = requests.get(self.PROXYS_URL)
ret = json.loads(response.text) # 得到的是一个json格式
# 因为会多次用到代理类,索性就创建一个模型
data = ret['data'][0] # 创建代理类需要的数据
p = Proxy(data)
self.current = p
加锁的原因
可能会多次调用update_proxy()导致不仅费代理,而且第三方的供应商也会告诉你请求频率过高,请x秒后在尝试,所以这个函数应该加锁
加锁之后,在某个协程成功请求后,要判断:若当前代理存在并且没有过期,就不再执行了
简书网 全站爬取
- 抓取js渲染的数据,并将数据保存到mysql数据库,动态网站爬取的思路。
-
普通的请求不会渲染数据,所以在中间件中截获这个请求交给selenium,然后通过webdriver获取页面信息,
在将此信息包装成httpresponse返回,此response带有渲染的数据。 -
在spider中定义爬取的网站,和爬取的规则,以及处理响应的回调函数,在回调函数中通过xpath语法
获取内容,将数据yield。
为了保证响应中有动态加载的数据,所以在中间件中:定义一个selenium的webdriver,
在请求发出之前(process_request)通过这个webdriver请求加载的地址,有些数据需要点击’更多’,就需要获取元素触发click事件;有些数据
还会在滚动条下拉到最后的时候才会出现,可以通过webdriver触发js事件,然后得到的响应就是最普通的数据
使用HtmlResonse(url,body,request) 创建一个response对象并返回。
# jianshu_spider.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from jianshu_spider.items import Article
import re
class JianshuSpider(CrawlSpider):
name = 'jianshu'
allowed_domains = ['jianshu.com']
start_urls = ['https://www.jianshu.com/']
rules = (
Rule(LinkExtractor(allow=r'.*/p/[0-9a-z]{12}.*'), callback='parse_item', follow=True),
)
def parse_item(self, response):
title = response.xpath("//h1[@class='_1RuRku']/text()").get()
avatar = response.xpath("//img[@class='_13D2Eh']/@src").get()
author = response.xpath("//div[@class='rEsl9f']//a[@class='_1OhGeD']").get()
content = response.xpath("//article").get()
article_id = response.url.split('?')[0].split('/')[-1]
origin_url = response.url.split('?')[0]
pub_time = response.xpath("//div[@class='s-dsoj']//time/@datetime").get()
word_count = response.xpath("//div[@class='s-dsoj']/span[2]/text()").get()
read_count = response.xpath("//div[@class='s-dsoj']/span[3]/text()").get()
like_count = response.xpath("//span[@class='_1LOh_5']").get()
subject = response.xpath("//div[@class='_2Nttfz']/a/span/text()").getall()
subject = ','.join(subject)
yield Article(title=title,avatar=avatar,author=author,content=content,
article_id=article_id,origin_url=origin_url,pub_time=pub_time,read_count=read_count,word_count=word_count,like_count=like_count,subject=subject
)
# items.py
class Article(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
content = scrapy.Field()
article_id = scrapy.Field()
origin_url = scrapy.Field()
author = scrapy.Field()
avatar = scrapy.Field()
pub_time = scrapy.Field()
read_count = scrapy.Field()
like_count = scrapy.Field()
word_count = scrapy.Field()
subject = scrapy.Field()
# middlewares.py
from selenium import webdriver
import time
from scrapy.http.response.html import HtmlResponse
class SeleniumDownLoadMiddleware():
def __init__(self):
self.driver = webdriver.Chrome(executable_path=r'D:\\WorkPlace\\chromedriver.exe')
def process_request(self,request,spider):
# 在这里可以拿到请求,用selenim处理这个请求,得到响应就返回
# 将请求拦截,并给scrapy一个selenium的响应 妙
self.driver.get(request.url)
time.sleep(1)
js="document.documentElement.scrollTop=999999"
self.driver.execute_script(js)
time.sleep(0.2)
self.driver.execute_script(js)
try:
while True:
bt = self.driver.find_element_by_xpath("//div[@class='H7E3vT']")
bt.click()
time.sleep(0.5)
if not bt:
break
except:
pass
source = self.driver.page_source
response = HtmlResponse(self.driver.current_url,encoding='utf-8',body=source,request=request)
return response
# pipelines.py
import pymysql
class JianshuSpiderPipeline:
'''
将数据存储到mysql
'''
def __init__(self):
params = {
'host':'127.0.0.1',
'port':3306,
'user':'root',
'password':'root',
'database':'jianshu',
'charset':'utf8',
}
self.conn = pymysql.connect(**params)
self.cursor = self.conn.cursor()
self._sql = None
def process_item(self, item, spider):
self.cursor.execute(self.sql,(item["title"],item["content"],item["author"],item["avatar"],item["pub_time"],item["origin_url"],item["article_id"]))
self.conn.commit()
return item
@property
def sql(self):
if not self._sql:
self._sql ='''INSERT INTO ARTICLE (id,title,content,author,avatar,pub_time,origin_url,article_id) VALUES (null,%s,%s,%s,%s,%s,%s,%s)'''
return self._sql
return self._sql
简书全站爬取的执行流程
- 首先执行start_requests方法,开始请求,这一部分不用注意中间件,然后将响应中的url链接Rule获取,然后
发送请求 - 在中间件中进行截断,process_request()方法中通过selenium请求获取响应,在此方法中返回,得到的数据是js渲染后的数据
- 在Rule的parse_item方法中,处理响应,此时的的数据是通过js渲染的,所以不会有动态加载的问题,
- 将数据yield返回给pipeline,在pipeline中,;连接数据库,并执行insert语句,完成数据库的添加
分布式爬虫:
专注于爬虫和模型类的构建,保存的工作交给redis,根据情况处理响应信息。
redis的作用
- 登录会话存储
- 计数器,排名,经常访问的
- 消息队列
- 在线人数
- 做网站的缓存
- 好友关系
- 订阅发布
将项目改为分布式爬虫
fang.com
源码:
https://github.com/shangdidaren/spider
- 安装scrapy-redis
- 将Spider改为scrapy_redis.spiders.RedisSpider
或者将CrawlSpider改为scrapy_redis.spiders.RedisCrawlSpider - 将start_urls删除,把url放在redis上;添加一个redis_key:redis_key = ‘fang:start_urls’
- 配置文件中:调度器和去重和管道改为Scrapy_Redis的组件
# 调度器
SCHEDULER = 'scrapy_redis.scheduler.Scheduler'
# 链接去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 将数据保存到redis中
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline':300,
#
}
# 在redis保持队列不被清空,可以暂停和恢复
SCHEDULER_PERSIST = True
# 设置redis的信息
# Redis独一台服务器,或者是做集群保证高可用
REDIS_HOST = 'Redis服务器的ip'
REDIS_PORT = 6379
- 将项目放到爬虫服务器上,安装相应的依赖包,运行scrapy runspider name.py ;
此时爬虫服务器应处于等待状态 - 连接redis服务器,使用lpush命令插入start_urls,键为爬虫中的redis_key,值为目标网站的地址。
scrapy_redis源码分析
保存数据pipelines.py
- 在process_item()方法中return deferToThread(self._process_item,item,spider)
- 这个方法大致就是使用调用线程来处理
- _process_item()方法是前者的具体实现
多线程实现
key = self.item_key(item,spider) # 获取key
data = self.serialize(item) # 将itme序列化
self.server.rpush(key,data) # 存储在redis中
数据过滤dupefilter.py
- request-fingerprint(request,include_headers=None)
(1). url不是唯一的标识:www.example.com/?id=1&car=222
www.example.com/?cat=222&id=1
这两个url应该是同一个地址,所以会对url做排序处理
(2). 如果我们有两个用户,我们已经有了唯一标识指纹,所以请求的cookie应该也是一样的,
带了headers可能会导致不一样,所以就默认不带cookie。
(3).生成指纹是hashlib.sha1()方法
fp = hashlib.sha1()
fp.update(to_bytes('对象'))
fp.update(to_bytes(cannonicalize_url(response.url)))
fp.update(request.body or b'')
(4).将指纹插入到redis,使用的是redis.sadd(key,fp)
如果数据存在返回0,即request_seen
否则返回1,request没有看过
调度器schedule.py
- close()方法中判断,如果persist
继续下载的开关
为False,执行flush
flush()会将所有的指纹删除,下次再执行的时候就是全新的 - enqueue_request():
dont_filter为True:一定会入队
url在start_urls中会入队:因为在构造start_reqeusts时,dont_filer默认为True
dont_filter为False:全新url:入队
看过的url:不入队
简书网 全站爬取
- 通过获取json数据,构造合法请求获取数据
打开F12查看异步请求:
在动态请求中有以下几种:
- audio:没用
- book:没用
- current:没用
- includeed_collections?page=1&coount=10:返回的是收入的专题
https://www.jianshu.com/shakespeare/notes/69052585/included_collections?count=31
notes后面是文章的id,所以要获取文章的id
{
*"id"*: 1720632,
*"slug"*: "3771548474a2",
*"title"*: "其他零散知识点",
*"avatar"*: "https://upload.jianshu.io/collections/images/1720632/crop1550280817442.jpg",
*"owner_name"*: "鹏_0d3c"
},
{
*"id"*: 1864630,
*"slug"*: "ae71e1311508",
*"title"*: "工具集合",
*"avatar"*: "https://upload.jianshu.io/collections/images/1864630/1584356274.jpg",
*"owner_name"*: "Jackzhou3927"
}
-
返回的是json数据,取title得到当前文章所在的专题
-
取slug拼接
jianshu.com/c/{slug}
得到专题列表,使用Link Extractor提取文章链接。(特殊的,这个列表也也是动态加载,只有当进度条翻到下面才会加载,除了selenium暂时没发现怎么反 反爬)
,不过可以拿到10条足够了
- mark_viewed:没用:提升文章的阅读数目
- reconmmendations:推荐的文章的信息:
{
*"id"*: 68512684,
*"slug"*: "dc09f86ca4ba",
*"list_image_url"*: "https://upload-images.jianshu.io/upload_images/5275417-d267dca5c31e05e5",
*"title"*: "为什么说Redis是单线程的以及Redis为什么这么快!Redis、面试、缓存、雪崩、分布式锁实现一篇文章搞定!",
*"views_count"*: 1338
},
{
*"id"*: 72990466,
*"slug"*: "384ca5efba36",
*"list_image_url"*: "https://upload-images.jianshu.io/upload_images/19895418-012de5aa9699764d",
*"title"*: "Spring Boot “内存泄漏”?看看美团大牛是如何排查的",
*"views_count"*: 967
}
- id应该是暂时没用的,slug拼接文章的详情页面jianshu.com/p/{slug}
得到详情页面又可以获取列出的信息
- reward_section:没用
- user_notes:用户的热门文章信息,类似于专题
{
*"id"*: 73043642,
*"slug"*: "e5cb75bacb8a",
*"title"*: "连RabbitMQ的5种核心消息模式都不懂,也敢说自己会用消息队列!",
*"view_count"*: 71,
*"user"*: {
*"id"*: 1939592,
*"nickname"*: "梦想de星空",
*"slug"*: "9bdcaae6d6b7",
*"avatar"*: "https://upload.jianshu.io/users/upload_avatars/1939592/c9a25006-52d5-4cb5-8812-ca4c0b8d2bdb.png"
}
}
-
slug拼接文章的详情页面
jianshu.com/p/{slug}
-
用户的信息中又可以拼接用户的个人中心,得到文章的列表页,但是文章的列表也是动态加载的,所以只能获取前几条, 重复会很多
-
动态加载是
www.jianshu.com/u/9bdcaae6d6b7?order_by=added_at&page=10
page无线大没用,最多加载20条数据,然后就去请求https://www.jianshu.com/users/9bdcaae6d6b7/timeline?max_id=639979257&page=2
,和专题一样,没想到怎么反 反爬
- response:里面有详情页所有的数据,包括用户的部分数据,在id为
__NEXT_DATA__
的 script 标签中,json格式数据提取出来。可以获取信息。
思路二源码:未解决
# -*- coding: utf-8 -*-
import scrapy
import re
import json
import time
from copy import deepcopy
from scrapy.linkextractors import LinkExtractor
from collections import Iterable
class JianshuSpider(scrapy.Spider):
name = 'jianshu'
allowed_domains = ['jianshu.com']
# start_urls = ['https://www.jianshu.com/p/2c228cccca31']
start_urls = ['https://www.jianshu.com']
def parse_item(self, response):
item = {}
source_data = response.xpath("//script[@id='__NEXT_DATA__']//text()").getall()
data = (''.join(source_data))
data = json.loads(data)
# with open('save.json', 'w',encoding='utf-8') as f:
# f.write(''.join(data))
try:
data = data['props']['initialState']['note']['data']
item['title'] = data['public_title']
item['slug'] = data['slug']
item['views_count'] = data['views_count']
# item['free_content'] = data['free_content']
item['likes_count'] = data['likes_count']
item['wordage'] = data['wordage']
item['avatar'] = data['user']['avatar']
last_updated_at = int(data['last_updated_at'])
item['last_updated_at'] = t1 = time.strftime(r'%Y-%m-%d %H:%M:%S',time.localtime(last_updated_at))
item['user_name'] = data['user']['nickname']
id = data['id'] # 用于构建专题url
# 被什么专题收入:https://www.jianshu.com/shakespeare/notes/{文章id}/included_collections?page=1&count=7
subject_json = f"https://www.jianshu.com/shakespeare/notes/{id}/included_collections?page=1&count=50"
yield scrapy.Request( # 专题
url = subject_json,
callback=self.parse_sub,
meta={'item':deepcopy(item)},
)
# # 推荐:https://www.jianshu.com/shakespeare/notes/{文章slug}/recommendations
reconmmend_json = 'https://www.jianshu.com/shakespeare/notes/' + item['slug'] + '/recommendations'
yield scrapy.Request( # 推荐文章
url=reconmmend_json,
callback=self.parse_recon,
) # 这里想到将id,slug,title,views_count传过来,但是没必要。
# 自己的热门文章:https://www.jianshu.com/shakespeare/notes/{文章id}/user_notes
hot_json = f'https://www.jianshu.com/shakespeare/notes/{id}/user_notes'
yield scrapy.Request( # HOT文章
url = hot_json,
callback=self.parse_hot
)
except:
print('文章审核!!!') # 该文章在审核中
def parse_sub(self,response):
'''专题解析函数:取出json数据,完善item, 构建到达专题列表页的请求'''
# with open('sub.json','w',encoding='utf-8') as fp:
# fp.write(response.body.decode())
item = response.meta['item']
data = json.loads(response.body.decode())
item['subjects'] = ' '.join([collection['title'] for collection in data['collections']])
yield item # 得到专题信息后,item就完成了
print(item)
pre = 'https://www.jianshu.com/c/'
sub_url_list = [pre+collection['slug'] for collection in data['collections']]
if len(sub_url_list) == 1:
yield scrapy.Request(
url = sub_url_list[0],
callback =self.parse_subject_detail
)
elif len(sub_url_list) == 0: # 可能是空列表,或者长列表
pass
for url in sub_url_list:
yield scrapy.Request(
url=url,
callback=self.parse_subject_detail
)
# 在此访问主页就是下一页的请求:除非它一直推送相同的文章
yield scrapy.Request(url='https://www.jianshu.com',callback=self.parse,dont_filter=True)
print('True')
def parse_recon(self,response):
'''
取出 推荐 列表数据,构建请求,这是文章详情页了,所以交给parse
'''
pre = 'https://www.jianshu.com/p/'
data_list = json.loads(response.body.decode()) # 推荐的list,内含字典
article_url_list = [pre+data['slug'] for data in data_list]
if len(article_url_list) == 1:
yield scrapy.Request(url=article_url_list,callback=self.parse)
elif len(article_url_list) == 0:
# do nothing
pass
for url in article_url_list:
yield scrapy.Request(
url=url,
callback=self.parse,
)
def parse_hot(self,response):
'''
自己的热门文章,取出列表,构建请求
'''
pre = 'https://www.jianshu.com/p/'
data_list = json.loads(response.body.decode()) # 内含字典
article_url_list = [pre+data['slug'] for data in data_list]
if len(article_url_list) > 1: # 一般都是3个,这步判断没啥意义
if isinstance(article_url_list,Iterable):
for url in article_url_list:
yield scrapy.Request(
url=url,
callback=self.parse,
)
elif len(article_url_list) == 1:
yield scrapy.Request(url=article_url_list[0])
def parse(self,response): # 解析列表页 包括主页和专题页
extractor = LinkExtractor(restrict_xpaths=('//ul/li/div/a'))
link_list = extractor.extract_links(response)
yield from response.follow_all(urls=link_list,callback=self.parse_item)