Py008-01-10堆排序之topk问题

topk问题

  • 现在有n个数,设计算法得到前k大的数。 (k<n)
  • 解决思路:
1
2
3
4
5
6
- 1.排序后切片 O(nlogn) + k

- 2.排序lowB三人组 O(kn)
如果是冒泡只需要k趟就可以

- 3.堆排序思路 O(nlogk)

堆排序解决思路

  • 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
  • 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;
  • 遍历列表所有元素后,倒序弹出堆顶

如假设一个长度为10的列表,取最大的前5个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[6,8,1,9,3,0,7,2,4,5]
思路
step001
从列表里取出前5个数[6,8,1,9,3]
step002 将这5个数变成一个小根堆
1
3 6
9 8
step003 循环剩余列表[0,7,2,4,5]依次跟堆顶比较

0 < 1 不换 [7,2,4,5]

7 > 1 换 [2,4,5]
7
3 6
9 8
此时向上调整
3
7 6
9 8
2 < 3 不换 [4,5]
4 > 3 换 [5]
4
7 6
9 8
无需调整继续循环
5 > 4 换 []
5
7 6
9 8
  • 此时时间复杂度是O(nlogk)

topk实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#!/usr/bin/env python
#-*- encoding:utf-8 -*-

# 原地排序,所以 大根堆 堆顶下来的元素会放到列表的末尾
# high则标记 堆的最后一个元素
def sift(li,low,high):
'''

:param li: 列表
:param low: 堆的根节点位置
:param high: 堆的最后一个元素的位置
:return:
'''

i = low # i最开始指向根节点
j = 2 * i + 1 # j就是i的左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] < li[j]: # 如果有右孩子而且 右孩子比左孩子小
j = j + 1 # 指向右孩子

if li[j] < tmp: # 要么是左孩子 要么是右孩子
li[i] = li[j]
i = j # 往下看一层
j = 2 * i + 1

else: # tmp最大,把tmp放到i的位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp # 把tmp放到叶子节点上


#
def topk(li,k):
heap = li[0:k];
for i in range((k-2)//2,-1,-1):
sift(heap,i,k-1)
# 1。建堆
# 循环 k到 len(li)-1
for i in range(k,len(li)-1):
if li[i] > heap[0]:
heap[0] = li[i]
sift(heap,0,k-1)
# 2。遍历
for i in range(k-1,-1,-1):
heap[0],heap[i] = heap[i],heap[0]
sift(heap,0,i-1)

# 3。出数
return heap

import random
li = list(range(1000))
random.shuffle(li)

print(topk(li,10))

Py008-01-09堆排序

堆排序过程

  • 1.建立堆
  • 2.得到堆顶元素,为最大元素
  • 3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
  • 4.堆顶元素为第二最大元素
  • 5.重复步骤3,直到堆变空

堆向下调整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 原地排序,所以 大根堆 堆顶下来的元素会放到列表的末尾
# high则标记 堆的最后一个元素
def sift(li,low,high):
'''

:param li: 列表
:param low: 堆的根节点位置
:param high: 堆的最后一个元素的位置
:return:
'''

i = low # i最开始指向根节点
j = 2 * i + 1 # j就是i的左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] > li[j]: # 如果有右孩子而且 右孩子比左孩子大
j = j + 1 # 指向右孩子

if li[j] > tmp: # 要么是左孩子 要么是右孩子
li[i] = li[j]
i = j # 往下看一层
j = 2 * i + 1

else: # tmp最大,把tmp放到i的位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp # 把tmp放到叶子节点上

建立堆

1
2
3
4
5
6
7
8
def heap_sort(li):
n = len(li)
# 倒着循环 找村级别
for i in range((n-2)//2,-1,-1):
# i表示建堆时候调整的部分的根的下标
# 此时的high不用 2*i+1
sift(li,i,n-1)
# 建堆完成了

挨个出数

  • 因为是原地排序
  • 所以堆顶下台的元素会放到末尾
  • high = 列表长度 - 下太元素的个数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 原地排序,所以 大根堆 堆顶下来的元素会放到列表的末尾
# high则标记 堆的最后一个元素
def sift(li,low,high):
'''

:param li: 列表
:param low: 堆的根节点位置
:param high: 堆的最后一个元素的位置
:return:
'''

i = low # i最开始指向根节点
j = 2 * i + 1 # j就是i的左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] > li[j]: # 如果有右孩子而且 右孩子比左孩子大
j = j + 1 # 指向右孩子

if li[j] > tmp: # 要么是左孩子 要么是右孩子
li[i] = li[j]
i = j # 往下看一层
j = 2 * i + 1

else: # tmp最大,把tmp放到i的位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp # 把tmp放到叶子节点上

def heap_sort(li):
n = len(li)
# 倒着循环 找村级别
for i in range((n-2)//2,-1,-1):
# i表示建堆时候调整的部分的根的下标
# 此时的high不用 2*i+1
sift(li,i,n-1)
# 建堆完成了

for i in range(n-1,-1,-1):
# i 指向当前堆的最后一个元素
# 把堆的最后一个元素放到堆顶
li[0],li[i] = li[i],li[0]
# 此时high = i-1 就是前一个元素
sift(li,0,i-1)

li = [i for i in range(100)]
import random
random.shuffle(li)
print(li)

heap_sort(li)
print(li)

堆排序的时间复杂度

  • 时间复杂度:O(nlogn)

堆排序——内置模块 heapq

常用函数

  • heapify(x)
  • heappush(heap,item)
  • heappop(heap)
1
2
3
4
5
6
7
8
9
10
11
12
13
import heapq # q=>queue 优先队列
import random

li = [i for i in range(100)]
random.shuffle(li)
print(li)

heapq.heapify(li) # 建堆

n = len(li)
for i in range(n):
# 每次出数
print(heapq.heappop(li),end=',')

Py008-01-08堆和堆的向下调整

一种特殊的完全二叉树结构

  • 大根堆:一颗完全二叉树,任意节点都比其孩子节点大
  • 小根堆:一颗完全二叉树,任意节点都比其孩子节点小

一个具体的方式来看堆的问题(如上图的大根堆)

1
2
3
4
第一层 9 好比 省长
第二层 8,7 好比 县长
第三层 6,5,0,1 好比 村长
第四层 2,4,3 好比 村民

堆的向下调整性

假设:节点的左右子树都是堆,但自身不是堆

很明显第一层坐在省长位置的2不合格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
于是把 2 撸下来
?
9 7
8 5 0 1
6 4 3

让谁上去?待选人肯定是县长级别的 所以是 9 和 7,但是肯定要权限值大的上去

9
? 7
8 5 0 1
6 4 3
此时问? 2能做一个县长吗? 不能因为它领导不了下面的村长 8和5 而此时需要上位一个县长 8>5 8上位了
9
8 7
? 5 0 1
6 4 3
此时问? 2能做一个村长吗? 不能因为它领导不了下面的村民 6,4和3 而此时需要上位一个村长 6>4>3 6上位了

9
8 7
6 5 0 1
? 4 3

最后没法在撸了 ,所以2只能当村民了
9
8 7
6 5 0 1
2 4 3

堆排序过程演示

原理就是挨个出数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
res = []
如大根堆的情况:
9
8 7
6 5 0 1
2 4 3
step001 第一次 先让最大的下台 9下台了
res=[9]
 ?
8 7
6 5 0 1
2 4 3
---------------------------------
思考假设 让谁上台
如果是8上台
导致8的位置空了
 8
? 7
6 5 0 1
2 4 3
就必须 6上台
导致6的位置空了
 8
6 7
? 5 0 1
2 4 3
就必须 4上台
导致4的位置空了
 8
6 7
4 5 0 1
2 ? 3
此时就不知道4的位置有一个空位
这样就很麻烦
这样就很麻烦
这样就很麻烦
---------------------------------
 ?
8 7
6 5 0 1
2 4 3
另一种思路 选一个备胎上去 取树的最后一层的最后的孩子也就是3
 3
8 7
6 5 0 1
2 4
此时只要通过一次向下调整就能保持堆的层级:如下
 8
6 7
4 5 0 1
2 3
step002 先让最大的下台 8下台了
取树的最后一层的最后的孩子也就是3
res=[9,8]
 3
6 7
4 5 0 1
2
通过一次向下调整就能保持堆的层级
res=[9,8]
 7
6 3
4 5 0 1
2
step003 先让最大的下台 7下台了
取树的最后一层的最后的孩子也就是2
res=[9,8,7]
 2
6 3
4 5 0 1
通过一次向下调整就能保持堆的层级
 6
5 3
4 2 0 1
step004 先让最大的下台 6下台了
取树的最后一层的最后的孩子也就是1
res = [9,8,7,6]
1
5 3
4 2 0
通过一次向下调整就能保持堆的层级
5
4 3
1 2 0
step005 先让最大的下台 5下台了
取树的最后一层的最后的孩子也就是0
res = [9,8,7,6,5]
0
4 3
1 2
通过一次向下调整就能保持堆的层级
4
2 3
1 0
step006 先让最大的下台 4下台了
取树的最后一层的最后的孩子也就是0
res = [9,8,7,6,5,4]
0
2 3
1
通过一次向下调整就能保持堆的层级
3
2 0
1
step007 先让最大的下台 3下台了
取树的最后一层的最后的孩子也就是1
res = [9,8,7,6,5,4,3]
1
2 0
通过一次向下调整就能保持堆的层级
2
1 0
step008 先让最大的下台 2下台了
取树的最后一层的最后的孩子也就是0
res = [9,8,7,6,5,4,3,2]
0
1
通过一次向下调整就能保持堆的层级
1
0
step008 先让最大的下台 1下台了
取树的最后一层的最后的孩子也就是0
res = [9,8,7,6,5,4,3,2,1]
0
step008 先让最大的下台 0下台了
res = [9,8,7,6,5,4,3,2,1,0]

构造堆

  • 堆一开始是没有秩序的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

6
8 1
9 3 0 7
2 4 5

先管理一个村 3
3和5调换
看前一个村 9发现是好的
6
8 1
9 5 0 7
2 4 3
村都好了
管理县7
6
8 7
9 5 0 1
2 4 3
管理前一个县8
6
9 7
8 5 0 1
2 4 3
县管理好了,管理省6
9
8 7
6 5 0 1
2 4 3
此时构造堆的过程就结束了

结论就是:每次从最底层构建堆

1
2
3
4
5
6
7
管理村级别的最后一个村
村级别都好了
管理县级别的最后一个县
县级别都好了
管理省级别的最后一个省
县级别都好了
最后就成了一个堆

Py008-01-07堆排序前传树与二叉树

树是一种数据结构

比如:目录结构

  • 树是一种可以递归定义的数据结构
  • 树由n个节点组成的集合
    • 如果n=0,那是一棵空树
    • 如果n>0,那存在一个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树

树的基本概念

根节点,叶子节点

  • 如上图,A就是根节点
  • 不能分叉的节点就是叶子节点 如B,C,H,I,P,Q,K,L,M,N

树的深度(高度)

  • 看它的递归深度 如上为 4

树的度

  • 就是最大分叉的个数
1
2
3
4
5
如 E 的度是2
如 F 的度是3
如 A 的度是6

所以如上图 树的度就是6

孩子节点,父节点

1
2
E是I的父节点
I是E的子节点

子树

1
如 把 E,I,J,P,Q拎出来 E就是一个子树

二叉树

  • 二叉树就是度不超过2的树
  • 每个节点最多有两个子节点
  • 两个孩子节点被区分为左孩子节点和右孩子节点

二叉树的存储方式

  • 链式存储方式
  • 顺序存储方式(就是用一个列表来存储)

思考:如果从孩子找父亲该怎么找?

1
2
如子节点下标为9
(9-1)整除2=4

Py008-01-06快速排序

快速排序

第一个实现方式

非原地排序,非常费空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import math
# 非原地排序 每次都把列表分为左右两个区间
def quickSort(arr):
if len(arr) <= 1: return arr

# 取中间位
pivotIndex = math.ceil(len(arr) / 2);
pivot = arr[pivotIndex];
del arr[pivotIndex];

# 左右区间,用于存放排序后的数
left = [];
right = [];
for i in range(len(arr)):
print('分区操作的第 ' + str(i + 1) + ' 次循环:');
# 小于基准,放于左区间,大于基准,放于右区间
if arr[i] < pivot:
left.append(arr[i]);
print('左边:' + str(arr[i]))
else:
right.append(arr[i]);
print('右边:' + str(arr[i]))
print(left, right)
return quickSort(left) + [pivot] + quickSort(right)


arr = [14, 3, 15, 7, 2, 76, 11];
print(quickSort(arr));

原地排序

  • 以第一个值为起点 li[0]
  • 从右开始找 li[len(li)-1] 比 li[0] 小则交换,索引往左移动一位。
  • 然后从左往右找 比li[0]大 则交换,索引往右移动一位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def partition(li,left,right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp: # 从右面找比tmp小的数
right -= 1 # 往左走一步
print(li,'right')
li[left] = li[right] # 把右边的值填到左边的空位上

while left < right and li[left] <= tmp: # 从左面找比tmp大的数
left += 1 # 往右走一步
print(li,'left')
li[right] = li[left] # 把左边的值填到右边的空位上

li[left] = tmp # 把tmp归位
return left # 返回中间值

li = [5,7,4,6,3,1,2,9,8]

print(li)
partition(li,0,len(li)-1)
print(li)

第二步,递归的方式来排序左面的和右边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def partition(li,left,right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp: # 从右面找比tmp小的数
right -= 1 # 往左走一步
li[left] = li[right] # 把右边的值填到左边的空位上

while left < right and li[left] <= tmp: # 从左面找比tmp大的数
left += 1 # 往右走一步
li[right] = li[left] # 把左边的值填到右边的空位上

li[left] = tmp # 把tmp归位
return left # 返回中间值

def quick_sort(li,left,right):
if left<right :# 至少两个元素
mid = partition(li,left,right)
quick_sort(li,left,mid-1)
quick_sort(li,mid+1,right)

li = [5,7,4,6,3,1,2,9,8]
quick_sort(li,0,len(li)-1)
print(li)

快速排序的效率

  • 时间复杂度 O(n*log n)

快排的问题

  • 递归 (python有个递归层数的问题默认1000)
  • 最坏情况

什么是最坏情况

  • 当列表是 倒序排列的时候 [9,8,7,6,5,4,3,2,1]
1
基本和冒泡一样了

解决方案就是随机一个数字,而不是固定取第一位

  • 随机快排

Py007-03-02权限系统介绍

权限

  • 回顾电影 《2012》 那些能登上“诺亚方舟”的人都是有“权”有“势”的人,而不能登上船的人就只能等死。——说明他们 level不够

  • 公司里员工的薪水——保密

为什么开发权限组件?

为什么那些在公司的老员工开发系统时,比你要快。

  • 年限多,写的熟,经验多
  • 老油条
  • 最最重要的是——自己的组件(通用性的组件)

web程序中什么是权限

一个url

一个url

一个url

结论

1
2
3
url ==> 权限

人 ==> url

第一版权限控制

  • 多对多关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户表 
id user
1 马云 CEO
2 CTO
3 技术
4 前台


权限表
id auth
1 user/list/ 查看用户列表
2 user/add/ 增加用户
3 user/edit/ 编辑用户
4 user/del/ 删除用户
5 sale/list 商品销售列表
6 sale/add
7 sale/edit
8 sale/del

一些虚拟条件

1
2
3
4
5
6
7
# 用户权限关系表
id uid aid

CEO——1,2,3,4,5,6,7,8所有权限
CTO——1,2,3,4,5,6
技术——1,2,3,4
前台——1,2

问题来了

1
2
3
4
5
6
7
8
//假设又来了一个cto
此时这个cto要新增 1,2,3,4,5,6的权限

如果此时ceo宣布 cto权限加2个
此时就是两个cto操作2*2 新增数据为4条

过了一周,ceo说,cto权限删除3个
此时就是两个cto操作2*3 删除数据为6条

此时问题就来了,每次新增用户和新增权限都会导致批量的处理权限

  • 麻烦

角色的概念来了

  • 不再给人指定权限,而是针对角色
  • 每个角色对应一个权限列表 一个或者多个
  • 每个用户对应一个角色

Py007-03-01crm介绍

CRM

你一定听说过crm

但是这次要分为三个目标来讲述crm

  • 权限
  • stark组件
  • CRM

权限组件

1
通用的权限组件

stark组件

1
2
# 20分钟的实现
通用的增删改查组件

CRM系统

1
2
3
4
真实业务


权限+增删改查

Py007-02-13分布式爬虫简介

分布式爬虫概念

  1. 多台机器可以执行同一个爬虫程序,实现网站数据的分布爬取
  2. 原生的scrapy是不可以实现分布式爬虫的
    • 调度器无法在多台机器共享
    • 管道无法共享
  3. scrapy-redis组件:专门为scrapy开发的一套组件。
    • 该组件可以让scrapy实现分布式

scrapy-redis组件

1
2
# 下载
pip install scrapy-redis

分布式爬取的流程

1. redis配置(事先安装好redis)

1
2
3
4
5
6
我的是 5.0版本
切换到 redis5.0.0 的文件目录
找到redis.conf

- 注释掉 bind 127.0.0.1 ::1 ==> # bind 127.0.0.1 ::1
- 关闭保护模式 protected-mode yes ==> protected-mode no

2. redis服务器的开启 基于配置文件进行开启

1
./redis-server  ../redis.conf

再开一个客户端

1
./redis-cli

3. 创建scrapy工程

1
2
3
scrapy startproject redisPro

cd redisPro

注意: 此时要创建一个基于 crawlSpider 的应用

1
2
# 我们现在就爬取  糗百 糗图的数据
scrapy genspider -t crawl qiubai www.qiushibaike.com/pic

4. 导入RedisCrawlSpider 类 ,然后将爬虫文件修改成基于该类的源文件

spiders/qiubai.py

1
2
3
4
5
6
7
...

from scrapy_redis.spiders import RedisCrawlSpider
# class QiubaiSpider(CrawlSpider):
# 修改原先继承的 CrawlSpider 为 RedisCrawlSpider
class QiubaiSpider(RedisCrawlSpider):
...

5. 将 start_urls 修改成 redis_key = ‘调度器队列的名称’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

from scrapy_redis.spiders import RedisCrawlSpider
class QiubaiSpider(CrawlSpider):
name = 'qiubai'

# 此时要注释掉 allowed_domains 和 start_urls
# allowed_domains = ['https://www.qiushibaike.com/pic']
# start_urls = ['http://https://www.qiushibaike.com/pic/']

# redis_key 它代表调度器队列的名称
redis_key = 'qiubaispider' # 该行代码和 start_urls 一样
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)

def parse_item(self, response):
i = {}
#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
#i['name'] = response.xpath('//div[@id="name"]').extract()
#i['description'] = response.xpath('//div[@id="description"]').extract()
return i

6. 将项目的管道和调度器配置成基于 scrapy-redis的组件

修改爬虫的代码

  • 处理链接提取器
  • 处理规则处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

from scrapy_redis.spiders import RedisCrawlSpider
class QiubaiSpider(CrawlSpider):
name = 'qiubai'

# 此时要注释掉 allowed_domains 和 start_urls
# allowed_domains = ['https://www.qiushibaike.com/pic']
# start_urls = ['http://https://www.qiushibaike.com/pic/']

# redis_key 它代表调度器队列的名称
redis_key = 'qiubaispider' # 该行代码和 start_urls 一样

# 糗百糗图 底部分页器
'''
<a href="/pic/page/2?s=5145873" rel="nofollow">
解析规则
/pic/page/2/?s=5145873
为啥有个s=5145873 刷新下页面后 发现 s会变化 所以可以把s参数忽略
/pic/page/\d+
'''
link = LinkExtractor(allow=r'/pic/page/\d+')

rules = (
Rule(link, callback='parse_item', follow=True),
)

def parse_item(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')

for div in div_list:
img_url = 'https:'+ div.xpath('.//div[@class="thumb"]/a/img/@src').extract_first()

pass

定义items

items.py

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-

import scrapy

class RedisproItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
img_url = scrapy.Field()

导入items类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

from scrapy_redis.spiders import RedisCrawlSpider
from redisPro.items import RedisproItem

class QiubaiSpider(RedisCrawlSpider):
name = 'qiubai'

# 此时要注释掉 allowed_domains 和 start_urls
# allowed_domains = ['https://www.qiushibaike.com/pic']
# start_urls = ['http://https://www.qiushibaike.com/pic/']

# redis_key 它代表调度器队列的名称
redis_key = 'qiubaispider' # 该行代码和 start_urls 一样

# 糗百糗图 底部分页器
'''
<a href="/pic/page/2?s=5145873" rel="nofollow">
解析规则
/pic/page/2/?s=5145873
为啥有个s=5145873 刷新下页面后 发现 s会变化 所以可以把s参数忽略
/pic/page/\d+
'''
link = LinkExtractor(allow=r'/pic/page/\d+')

rules = (
Rule(link, callback='parse_item', follow=True),
)

def parse_item(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')

for div in div_list:
img_url = 'https:'+ div.xpath('.//div[@class="thumb"]/a/img/@src').extract_first()
item = RedisproItem()
item['img_url'] = img_url

# 现在是 scrapy的管道, 此时要使用 scrapy_redis 提供的管道 去settings.py 里修改配置
yield item

使用scrapy_redis 提供的管道 RedisPipeline

  • 修改settings.py
1
2
3
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}

同时对 RedisPipeline 的调度器进行配置

1
2
3
4
5
6
7
8
9
10
11
# 使用scrapy-redis组件的去重队列
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis组件自己的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 是否允许暂停
SCHEDULER_PERSIST = True

'''
SCHEDULER_PERSIST = True 代表 如果分布的一台机器宕机了,
当机器重启后 继续爬虫时, 从上次失败的地方开始爬取,而不是从头爬取
'''

7. REDIS_HOST 配置

爬虫程序为分布式的所以不仅部署在你的机子上

1
2
REDIS_HOST = 'redis服务的ip地址'
REDIS_PORT = 6379

8.执行爬虫文件

1
2
3
4
5
6
# 此时要定位到 爬虫文件的目录 
cd redisPro/spiders
scrapy runspider qiubai.py

# 最后你发现有一句
linstening at xxxx 代表监听在一个端口

还记得刚刚开启的redis-cli吗?

此时你要告诉redis-spider 起始url

还记得刚刚起名的 redis_key 吗?

1
redis_key = 'qiubaispider'

糗百-糗图的起始url

1
https://www.qiushibaike.com/pic/

在redis-cli里 输入如下信息

1
2
3
4
5
lpush 队列名称  起始url

# 我们当前应用为

lpush qiubaispider https://www.qiushibaike.com/pic/
1
2
3
4
5
6
7
8
9
[scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'finish_reason': 'finished',
'finish_time': datetime.datetime(2018, 12, 2, 15, 26, 8, 106343),
'log_count/DEBUG': 1,
'log_count/INFO': 7,
'memusage/max': 49971200,
'memusage/startup': 49971200,
'start_time': datetime.datetime(2018, 12, 2, 15, 26, 8, 96989)}
2018-12-02 23:26:08 [scrapy.core.engine] INFO: Spider closed (finished)

出现这个就去看看 继承的类是否是 RedisCrawlSpider

出现这个就去看看 继承的类是否是 RedisCrawlSpider

出现这个就去看看 继承的类是否是 RedisCrawlSpider

1
class QiubaiSpider(RedisCrawlSpider)

Py007-02-12scrapy之CrawlSpider

如果想对网址进行全站的爬取?

1
2
3
4
解决方案:
1. 手动请求的发送 (yield) 递归调用

2. CrawlSpider(推荐)

CrawlSpider

  • 其实就是Spider的一个子类。
  • 功能更加强大(链接提取器,规则解析器)

项目实战

项目初始化

1
2
3
4
5
6
7
8
9
10
11
scrapy startproject crawlSpiderPro

cd crawlSpiderPro

# 以前生成应用是 scrapy genspider chouti dig.chouti.com

# 此时我们的项目是 CrawlSpider的 所以 生成命令要修改一下
# 此时我们的项目是 CrawlSpider的 所以 生成命令要修改一下
# 此时我们的项目是 CrawlSpider的 所以 生成命令要修改一下

scrapy genspider -t crawl chouti dig.chouti.com

创建基于CrawlSpider的的爬虫文件

1
scrapy genspider -t 爬虫名称 起始url

此时 spiders/chouti.py 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# -*- coding: utf-8 -*-
import scrapy
# 链接提取器
from scrapy.linkextractors import LinkExtractor
# 规则解析器
from scrapy.spiders import CrawlSpider, Rule

class ChoutiSpider(CrawlSpider):
name = 'chouti'
# allowed_domains = ['dig.chouti.com']
start_urls = ['https://dig.chouti.com/']

# 实例化一个链接提取器
# 链接提取器:用来提取指定的链接 (url)
# allow参数:赋值一个正则表达式
# 链接提取器就可以根据正则在页面中提取指定的链接
# 提取到的链接会全部交给规则解析器
link = LinkExtractor(allow=r'Items/')
rules = (
# 实例化一个规则解析器
# 规则解析器接收了 链接提取器发送的链接后,就会对这些链接发起请求,获取链接对应的页面内容,就会根据指定的规则对页面内容中指定的数据值进行解析
# callback:指定一个解析规则(方法/函数)
Rule(link, callback='parse_item', follow=True),
)

def parse_item(self, response):
pass

settings.py修改

1
2
3
4
5
# 伪装请求载体身份
19行:USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'

22行:ROBOTSTXT_OBEY = False #可以忽略或者不遵守robots协议
# 不遵守robots协议

经过如上初始化 我们来对抽屉新热榜进行 爬取

1
2
3
4
5
访问 https://dig.chouti.com/

看到底部的分页器中的 a标签的href="/all/hot/recent/12"

这就是对应的页面url路径

修改代码里的 rule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# -*- coding: utf-8 -*-
import scrapy
# 链接提取器
from scrapy.linkextractors import LinkExtractor
# 规则解析器
from scrapy.spiders import CrawlSpider, Rule


class ChoutiSpider(CrawlSpider):
name = 'chouti'
# allowed_domains = ['dig.chouti.com']
start_urls = ['https://dig.chouti.com/']

# 实例化一个链接提取器
# 链接提取器:用来提取指定的链接 (url)
# allow参数:赋值一个正则表达式
# 链接提取器就可以根据正则在页面中提取指定的链接
# 提取到的链接会全部交给规则解析器
link = LinkExtractor(allow=r'/all/hot/recent/\d+')
rules = (
# 实例化一个规则解析器
# 规则解析器接收了 链接提取器发送的链接后,就会对这些链接发起请求,获取链接对应的页面内容,就会根据指定的规则对页面内容中指定的数据值进行解析
# callback:指定一个解析规则(方法/函数)
# follow: 先设置为False 后期详细说明
Rule(link, callback='parse_item', follow=False),
)

def parse_item(self, response):
print(response)

执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
scrapy crawl chouti --nolog

# 打印如下信息
<200 https://dig.chouti.com/all/hot/recent/1>
<200 https://dig.chouti.com/all/hot/recent/5>
<200 https://dig.chouti.com/all/hot/recent/4>
<200 https://dig.chouti.com/all/hot/recent/7>
<200 https://dig.chouti.com/all/hot/recent/9>
<200 https://dig.chouti.com/all/hot/recent/3>
<200 https://dig.chouti.com/all/hot/recent/6>
<200 https://dig.chouti.com/all/hot/recent/10>
<200 https://dig.chouti.com/all/hot/recent/2>
<200 https://dig.chouti.com/all/hot/recent/8>

follow参数

1
2
3
4
5
6
7
8
9
10
11
# follow代表 是否将提取器继续作用到 链接提取器取出的链接所表示的页面数据中

# 刚刚执行爬虫命令后
scrapy crawl chouti --nolog
# 仅仅打印了 10个页码的链接
# 而我们在 到第十页的时候 https://dig.chouti.com/all/hot/recent/10

# 底部的分页器是
1. 。。。 7 8 9 10 11 12 13 14

# 这就意味着 当到第十页的时候 会继续提取后面的页面链接 ==> 直到最后一页数据

修改follow:True

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# -*- coding: utf-8 -*-
import scrapy
# 链接提取器
from scrapy.linkextractors import LinkExtractor
# 规则解析器
from scrapy.spiders import CrawlSpider, Rule


class ChoutiSpider(CrawlSpider):
name = 'chouti'
# allowed_domains = ['dig.chouti.com']
start_urls = ['https://dig.chouti.com/']

# 实例化一个链接提取器
# 链接提取器:用来提取指定的链接 (url)
# allow参数:赋值一个正则表达式
# 链接提取器就可以根据正则在页面中提取指定的链接
# 提取到的链接会全部交给规则解析器
link = LinkExtractor(allow=r'/all/hot/recent/\d+')
rules = (
# 实例化一个规则解析器
# 规则解析器接收了 链接提取器发送的链接后,就会对这些链接发起请求,获取链接对应的页面内容,就会根据指定的规则对页面内容中指定的数据值进行解析
# callback:指定一个解析规则(方法/函数)
# follow: 先设置为False 后期详细说明
# follow代表 是否将提取器继续作用到 链接提取器取出的链接所表示的页面数据中
Rule(link, callback='parse_item', follow=True),
)

def parse_item(self, response):
print(response)

再次执行爬虫命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scrapy crawl chouti --nolog

# 打印如下
<200 https://dig.chouti.com/all/hot/recent/1>
<200 https://dig.chouti.com/all/hot/recent/2>
<200 https://dig.chouti.com/all/hot/recent/3>
...
...
...
<200 https://dig.chouti.com/all/hot/recent/115>
<200 https://dig.chouti.com/all/hot/recent/116>
<200 https://dig.chouti.com/all/hot/recent/117>
<200 https://dig.chouti.com/all/hot/recent/118>
<200 https://dig.chouti.com/all/hot/recent/119>
<200 https://dig.chouti.com/all/hot/recent/120>

Py007-02-11scrapy之请求传参

请求传参

爬取的数据值不在同一页面中

需求爬取 www.id97.com 电影数据和对应电影详情页面的数据

项目初始化

1
2
3
4
5
scrapy startproject moviePro

cd moviePro/

scrapy genspider movie www.id97.com

spiders/movie.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import scrapy

class MovieSpider(scrapy.Spider):
name = 'movie'
# allowed_domains = ['www.id97.com']
'''
www.id97.com 的域名已经换了 www.55xia.com
'''
start_urls = ['http://www.id97.com/']

def parse(self, response):
# 电影名称,类型,导演,语言,片长
# 浏览器里 找到电影列表外层容器 copy-xpath
div_list = response.xpath('/html/body/div[1]/div[2]/div')
for div in div_list:
name = div.xpath('.//div[@class="meta"]/h1/a/text()').extract_first()
kind = div.xpath('.//div[@class="otherinfo"]//text()').extract_first()
url = div.xpath('.//div[@class="meta"]/h1/a/@href').extract_first()

# 而此时 导演和演员 不再当前页面 在对应的详情页面
# 需要对url发请求,获取页面数据,进行数据解析

yield scrapy.Request(url=url,callback=self.secondParse)


# 专门用于处理二级自页面的解析函数
def secondParse(self,response):
author = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[1]/td[2]/a/text()').extract_first()
language = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[6]/td[2]/text()').extract_first()
longTime = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[8]/td[2]/text()').extract_first()

items.py

1
2
3
4
5
6
7
8
import scrapy

class MovieproItem(scrapy.Item):
name = scrapy.Field()
kind = scrapy.Field()
actor = scrapy.Field()
language = scrapy.Field()
longTime = scrapy.Field()

问题来了,数据分散在两次请求里,如何处理数据?

请求传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# -*- coding: utf-8 -*-
import scrapy

from moviePro.items import MovieproItem
class MovieSpider(scrapy.Spider):
name = 'movie'
# allowed_domains = ['www.id97.com']
'''
www.id97.com 的域名已经换了 www.55xia.com
'''
start_urls = ['http://www.id97.com/']

def parse(self, response):
# 电影名称,类型,导演,语言,片长
# 浏览器里 找到电影列表外层容器 copy-xpath
div_list = response.xpath('/html/body/div[1]/div[2]/div')
for div in div_list:
name = div.xpath('.//div[@class="meta"]/h1/a/text()').extract_first()
kind = div.xpath('.//div[@class="otherinfo"]//text()').extract_first()
url = div.xpath('.//div[@class="meta"]/h1/a/@href').extract_first()

# 创建items对象
item = MovieproItem()
item['name'] = name
item['kind'] = kind
# 如何将剩下的数据进行传递呢? ====》 请求传参(meta参数)
'''
meta 需赋值为一个字典 将item封装到字典里
'''

# 而此时 导演和演员 不再当前页面 在对应的详情页面
# 需要对url发请求,获取页面数据,进行数据解析

yield scrapy.Request(url=url,callback=self.secondParse,meta={'item':item})

# 专门用于处理二级自页面的解析函数
def secondParse(self,response):
author = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[1]/td[2]/a/text()').extract_first()
language = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[6]/td[2]/text()').extract_first()
longTime = response.xpath('/html/body/div[1]/div/div/div[1]/div[1]/div[2]/table/tbody/tr[8]/td[2]/text()').extract_first()

# 取出Request方法的meta参数
item = response.meta['item']
item['author'] = author
item['language'] = language
item['longTime'] = longTime

# 将item提交给管道
yield item

编写pipelines.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: utf-8 -*-
class MovieproPipeline(object):
fp = None

def open_spider(self,spider):
self.fp = open('movie.txt','w',encoding='utf-8')

def close_spider(self,spider):
self.fp.close()

def process_item(self, item, spider):
detail = item['name']+':'+item['kind']+':'+item['author']+':'+item['language']+':'+item['longTime']

self.fp.write(detail)
return item

修改settings.py

1
2
3
4
5
6
7
8
9
19行:USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' 

22行:ROBOTSTXT_OBEY = False #可以忽略或者不遵守robots协议
# 不遵守robots协议

# 解开注释
ITEM_PIPELINES = {
'moviePro.pipelines.MovieproPipeline': 300,
}

执行命令

1
2
3
4
5
6
7
scrapy crawl movie --nolog

此时发现movie.txt 是空的 因为起始url是首页 并不是对应的电影列表页面
实际url为
https://www.55xia.com/movie/

再次运行