3 4月份为跳槽的高峰期,应同学们的需求及我这边的一些经验,整理一份python爬虫面试题,仅供参考
一、python基础知识
1. requests的参数有哪些?
考察对requests的熟悉程度
参数有: method、url、params、data、headers、cookies、files、auth、timeout、allow_redirects、proxies、hooks、stream、verify、cert、json
可能会随机抽出几个参数考察你对他的理解,比如headers、proxies、stream、verify
2. python的数据类型有哪些?
Python3 中有六个标准的数据类型:
- Number(数字)
- String(字符串)
- List(列表)
- Tuple(元组)
- Set(集合)
- Dictionary(字典)
这里可能会追问 list与tuple的区别, list的切片操作返回的区间是怎样的,前闭后开还是前后包含?
3. 创建一个类时init是最先进入的函数么?
不是, __new__
才是 __new__
是用来创建类并返回这个类的实例, 而__init__
只是将传入的参数来初始化该实例
4. 上下文管理 with as 的作用
方便做一些收尾的工作,具体如下:
__enter会返回一个值,并赋值给as关键词之后的变量。在这里,你可以定义代码段开始的一些操作。 __exit 定义了代码段结束后的一些操作,可以这里执行一些清除操作,或者做一些代码段结束后需要立即执行的命令,比如文件的关闭,socket断开等。
如果exit返回True, 那么with声明下的代码段的一切异常将会被屏蔽。 如果exit返回None, 那么如果有异常,异常将正常抛出。
5. 对yield的理解
yield 返回的是一个迭代器,原函数并不会直接退出,当迭代器迭代完毕后,原函数会继续执行
6. 用迭代器写斐波那契数列
def f(n):
a, b = 0, 1
while n:
yield a
a, b = b, a+b
n -= 1
for i in f(10):
print(i)
7. 列表底层实现原理
底层为一个连续数组,指向这个数组的指针及其长度被保存在一个列表头结构中。这意味着,每次添加或删除一个元素时,由引用组成的数组需要该标大小(重新分配)。幸运的是,Python在创建这些数组时采用了指数过分配,所以并不是每次操作都需要改变数组的大小。但是,也因为这个原因添加或取出元素的平摊复杂度较低。
list实现采用了如下的策略:在建立空表(或者很小的表)时,系统分配一块能容纳8个元素的存储区;在执行插入操作(insert或append)时,如果元素存储区满就换一块4倍大的存储区。但如果此时的表已经很大(目前的阀值为50000),则改变策略,采用加一倍的方法。引入这种改变策略的方式,是为了避免出现过多空闲的存储位置。
时间复杂度:
- append append(object):向列表尾部添加元素,最好情况即列表容量足够,添加元素一步完成,对应的时间复杂度为O(1);最坏情况即列表需要扩容,这时需要对现有元素一一遍历移动位置,此时时间复杂度为O(n)。
- pop pop([index]):删除并返回特定索引的元素,默认删除最后一个元素。默认的pop操作最好情况是底层不需要收缩数组,一步操作即可,对应的时间复杂度为O(1);最坏的情况是删除元素后,紧接着python要执行收缩数组的操作,此时时间复杂度为O(n);对于删除指定索引的元素,不管这步操作是否需要收缩数组,该索引以后的数据都要向前移动位置,以确保存储空间的连续性,因此对应的时间复杂度为O(n)。
- len len(list):获取列表内元素的个数,因为在列表实现中,其内部维护了一个Py_ssize_t类型的变量表示列表内元素的个数,因此时间复杂度为O(1)。
8. 常用的队列
- collections.deque
deque是双端队列,相比于list实现的队列,deque实现拥有更低的时间和空间复杂度。list实现在出队(pop)和插入(insert)时的空间复杂度大约为O(n),deque在出队(pop)和入队(append)时的时间复杂度是O(1)。
线程安全方面,collections.deque中的append()、pop()等方法都是原子操作,所以是GIL保护下的线程安全方法。 - queue.Queue & asyncio.Queue
queue.Queue和asyncio.Queue都是支持多生产者、多消费者的队列,基于collections.deque - multiprocessing.Queue
multiprocessing.Queue既是线程安全也是进程安全的
9. 协程、线程、进程区别与应用场景
- 进程拥有自己独立的堆和栈,堆和栈都不共享,进程由操作系统调度
- 线程拥有自己独立的栈,但是堆却是共享的,标准的线程是由操作系统调度的
- 协程共享堆却不共享栈,协程是由程序员在协程的代码块里显示调度
由于python的GIL锁存在,当CPU密集时协程的效率会高于线程,IO密集时协程与线程效率差不多,因为线程在等待I/O时共享锁。
CPU密集型:大量的计算,比如计算圆周率、对视频进行高清解码等等 IO密集型:涉及到网络、磁盘IO的任务都是IO密集型任务
关于GIL,见11小节
10. 垃圾回收
参考: https:// zhuanlan.zhihu.com/p/83 251959
- 引用计数:ob_ref 来记引用次数 (不能回收循环引用)
- 标记清除: 标记清除就是用来解决循环引用的问题的只有容器对象才会出现引用循环,比如列表、字典、类、元组。
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。 mark-sweepg 在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。 标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。 上面描述的垃圾回收的阶段,会暂停整个应用程序,等待标记清除结束后才会恢复应用程序的运行。
3. 分代回收
在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过“分代回收”(Generational Collection)以空间换时间的方法提高垃圾回收效率。
分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。
新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象
有三种情况会触发垃圾回收:
-
- 调用gc.collect(),需要先导入gc模块。
- 当gc模块的计数器达到阀值的时候。
- 程序退出的时候
11. GIL
- GIL是一个互斥锁,它防止多个线程同时执行Python字节码。这个锁是必要的,主要是因为CPython的内存管理不是线程安全的
- Python内部对变量或数据对象使用了引用计数器,当多个线程同时修改这个值时,可能会导致内存泄漏,为了避免内存泄漏和死锁问题,CPython使用了单锁,即全局解释器锁(GIL),即执行Python字节码都需要获取GIL,这可以防止死锁,但它有效地使任何受CPU限制的Python程序都是单线程.
- GIL对多线程Python程序的影响
- 程序的性能受到计算密集型(CPU)的程序限制和I/O密集型的程序限制影响,那什么是计算密集型和I/O密集型程序呢?
- 计算密集型(CPU):高度使用CPU的程序,例如: 进行数学计算,矩阵运算,搜索,图像处理等.
- I/O密集型:I/0(Input/Output)程序是进行数据传输,例如: 文件操作,数据库,网络数据等
- GIL对I/O绑定多线程程序的性能影响不大,因为线程在等待I/O时共享锁.
- GIL对计算型绑定多线程程序有影响,例如: 使用线程处理部分图像的程序,不仅会因锁定而成为单线程,而且还会看到执行时间的增加,这种增加是由锁的获取和释放开销的结果.
- 标准库中所有执行阻塞型 IO 操作的函数,在等待结果返回时都会释放GIL。这意味着尽管有GIL,Python线程还是能在 IO 密集型任务中一展身手
二、网络知识
参考: https://www. cnblogs.com/Javi/p/9303 020.html
1. tcp
连接三次握手: 1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。 2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。 3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
关闭 4次握手:
2. udp
无连接协议,不需要先建立连接
UDP与TCP的区别
- 连接和无连接
- 数据完整性 TCP协议会检验数据完整性,及当接收方收到数据时,会向发送方发出确认信息。发送方只有在接收到该确认消息之后才继续传送其它信息,否则将一直等待直到收到确认信息为止。
UDP则不检验数据完整性,在发送数据过程中出现丢包的情况,协议本身不做任何检测和提示 - 一对一和一对多: TCP 一对一 UDP 支持一对一,一对多,多对一和多对多的交互通信
3. socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
Socket是传输控制层协议
4. websocket
WebSocket是应用层协议。
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。
5. http
基于tcp,属于应用层。TCP是传输层
6. http2
头部压缩,可多路复用
三、linux常用命令
1. 查看日志文件
tail -f xxx.log
2. 日志内容格式如下,输出最后一列日志内容
比如日志内容为:
aaa,bbb,ccc
ddd,eee,fff
输出:
ccc
fff
这个答案留给评论区...
四、数据库
1. mysql 索引
- 单列索引(普通索引,唯一索引,主键索引)
- 普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点
- 唯一索引:索引列中的值必须是唯一的,但是允许为空值,
- 主键索引:是一种特殊的唯一索引,不允许有空值。
- 组合索引
在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用 - 全文索引
- 全文索引,只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引
- 全文索引,就是在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行,比如有"你是个靓仔,靓女 ..." 通过靓仔,可能就可以找到该条记录
- 空间索引
空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用SPATIAL关键字。要求,引擎为MyISAM,创建空间索引的列,必须将其声明为NOT NULL。
2. mysql中in和exists()区别
exists()适合B表比A表数据大的情况
当A表数据与B表数据一样大时,in与exists效率差不多,可任选一个使用
3. redis 数据类型
- string
- hash
- list
- set
- soreset
4. redis 数据落盘的几种方式
- RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
- 优点:
- 只包含一个文件,易备份
- 性能高:fork出子进程,之后再由子进程完成这些持久化的工作
- 占用空间小
- 缺点
- 系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
- 优点:
- AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。落盘策略为每秒同步、每修改同步和不同步
- 优点
- 由于落盘机制,可有效的保证数据的安全性
- 缺点
- 文件大
- 效率比RDB低
- 优点
五、算法
1. 排序
https:// blog.csdn.net/sx2448826 571/article/details/80487531
1. 堆排序
算法描述参考: https:// blog.csdn.net/u01045238 8/article/details/81283998
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)
def build_tree(arr, root, arr_length):
max_node = root
left_child = root * 2 + 1
right_child = root * 2 + 2
if left_child < arr_length and arr[max_node] < arr[left_child]: # 如果根结点比左孩子小,则标记最大节点位置
max_node = left_child
if right_child < arr_length and arr[max_node] < arr[right_child]: # # 如果根结点比右孩子小,则标记最大节点位置
max_node = right_child
if max_node != root:
arr[root], arr[max_node] = arr[max_node], arr[root]
build_tree(arr, max_node, arr_length) # # 根结点 A 与 B交换后,largest 为A的位置,需要继续跟子节点比较
def heap_sort(arr: list):
# 构建大根堆
for i in range(len(arr) // 2 - 1, -1, -1): # 因为左右孩子的关系,所以根结点从len(arr) // 2 - 1 开始
build_tree(arr, i, len(arr))
print(arr)
# 交换
for i in range(len(arr) - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0] # 将根 i=0 与最后一个元素交换,然后将剩余的数再构造成一个大根堆。 及固定最大值再构造堆
build_tree(arr, 0, i)
a = [1, 4, 6, 1, 3, 8, 3, 5, 0, 12, 4]
heap_sort(a)
print(a)
2. 快排
参考 https://www. jianshu.com/p/2b2f1f799 84e
def get_pivokey(arr, left, right):
pivokey = arr[left]
while left < right:
while left < right and arr[right] >= pivokey:
right -= 1
arr[left] = arr[right]
while left < right and arr[left] <= pivokey:
left += 1
arr[right] = arr[left]
arr[left] = pivokey
return left
def quick_sort(arr, left, right):
pivokey = get_pivokey(arr, left, right)
if left < right:
quick_sort(arr, left, pivokey - 1)
quick_sort(arr, pivokey + 1, right)
if __name__ == "__main__":
arr = [2, 3, 4, 1, 4, 7]
quick_sort(arr, 0, len(arr) - 1)
print(arr)
3.冒泡
def bubble_sort(arr):
for i in range(len(arr)):
for j in range(len(arr)-1-i): # 沉底之后的就不用排了
if arr[j] > arr[j+1]: # 大的沉底
arr[j+1], arr[j] = arr[j], arr[j+1]
return arr
arr = [3, 6, 8, 19, 1, 5]
bubble_sort(arr)
print(arr)
4. 插入排序
def insert_sort(arr):
for i in range(len(arr)):
temp = arr[i]
j = i - 1
while j >= 0 and temp < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = temp
print(arr[:i], arr)
return arr
arr = [3, 6, 8, 19, 1, 5]
insert_sort(arr)
print(arr)
2. 动态规划
核心思想自底向上:先解决子问题,在解决父问题
1. 找零钱问题
问题:如果金钱的面值为1, 5, 11,求找出15元,所需要用到的最少张数
答案:3张:两张5块的,一张1块的,代码如下:
# 使用张数最少, 找出15元
money = [1, 5, 11]
total_money = 15
money_used = list(range(16))
cost = list(range(16)) # cost[金钱] = 找零的张数, 初始化为都用1块钱找领
for i in range(1, total_money + 1):
coin = None
for j in range(len(money)):
if money[j] <= i: # 当前找领的金额不能需要找零的金额
if (
cost[i - money[j]] + 1 < cost[i]
): # 如果本次所用money[j]找零 1张 + 之前找零 cost[i - money[j]] 张,小于cost[i]张, 则更新cost[i]
cost[i] = cost[i - money[j]] + 1
coin = money[j]
money_used[i] = coin
print(cost[15]) # 下标就是金钱
print(money_used)
# 3
# [0, None, None, None, None, 5, 1, 1, 1, 1, 5, 11, 1, 1, 1, 5] # 下标为金钱, 值为使用的面值
2. 最大上升子序列
给定N个数,求这N个数的最长上升子序列的长度。
o(n)^2
a = [1, 7, 6, 2, 3, 4]
dp = [1] * len(a) # dp[i]dp[i] 表示以第 i 元素为结尾的最长上升子序列长度,那么对于每一个 dp[i] 而言,初始值即为 1 ;
for i in range(len(a)):
for j in range(i):
if a[j] < a[i] and dp[i] < dp[j] + 1: # 用if判断是否可以拼凑成上升子序列, 并且判断当前状态是否优于之前枚举
dp[i] = dp[j] + 1
# print("-", a[i]) # 更新最优状态
# print(j)
print(max(dp))
3. 其他题目
- 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
a = [2,2,1]
number = a[0] for i in a[1:]: number = number ^ i
print(number) - 最长子串
开始的时候,begin和end都指向0的位置即‘a’,然后end不断后移(窗口变宽),当遇到第二个‘a’时(遇见重复字符)就得到一个子串,其长度就是end和begin位置的差。
如何判断是否遇到了重复字符‘a’呢?需要一个字典作为辅助数据结构,把end从头开始遇到的每个字符及其索引位置都放到字典里面,end每次移动到新字符就查一下字典即可。
通过字典,我们遇到第二个‘a’时就可以找到存在字典里面的第一个‘a’的位置。为了继续寻找无重复子串,begin就要指向第一个‘a’后面一个的位置即‘b’。然后end继续后移到‘b’,有发现它与前面的‘b’重复,计算子串长度赋值给最大长度(需要比较),同时begin要移动第一个‘b’后面的位置即‘c’。
这样依次移动end到字符串末尾就可以找到最长的子串,“子串窗口”也就从头移到了末尾。而只需要end从头到尾的一次循环即可。
- 如何实现一个优先队列
可以参考堆排序的大顶推、小顶堆 - 链表逆序
六、爬虫框架
1. 是否自己开发过爬虫框架
如果是自己开发的框架,会让你详细的讲解下框架结构,优缺点
2. scrapy 包含那几个组件
https:// scrapy-chs.readthedocs.io /zh_CN/latest/intro/tutorial.html#spider https:// blog.csdn.net/ck7841017 77/article/details/104468780/
Scrapy框架主要由五大组件组成,它们分别是调度器(Scheduler)、下载器(Downloader)、爬虫(Spider)和实体管道(Item Pipeline)、Scrapy引擎(Scrapy Engine)。下面我们分别介绍各个组件的作用。
- 调度器(Scheduler):
调度器,说白了把它假设成为一个URL(抓取网页的网址或者说是链接)的优先队列,由它来决定下一个要抓取的网址是 什么,同时去除重复的网址(不做无用功)。用户可以自己的需求定制调度器。 - 下载器(Downloader):
下载器,是所有组件中负担最大的,它用于高速地下载网络上的资源。Scrapy的下载器代码不会太复杂,但效率高,主要的原因是Scrapy下载器是建立在twisted这个高效的异步模型上的(其实整个框架都在建立在这个模型上的)。 - 爬虫(Spider):
爬虫,是用户最关心的部份。用户定制自己的爬虫(通过定制正则表达式等语法),用于从特定的网页中提取自己需要的信息,即所谓的实体(Item)。 用户也可以从中提取出链接,让Scrapy继续抓取下一个页面。 - 实体管道(Item Pipeline):
实体管道,用于处理爬虫(spider)提取的实体。主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。 - Scrapy引擎(Scrapy Engine):
Scrapy引擎是整个框架的核心.它用来控制调试器、下载器、爬虫。实际上,引擎相当于计算机的CPU,它控制着整个流程。
七、项目经验
- 谈谈工作经历及每段工作期间做的项目,然后挑2个重点项目详细说明下难点及解决办法
- 遇到过哪些反爬
- 遇到js加密参数时如何解决(考察的是解决技巧,如hook)
- 无限debug如何解决(全局取消定时器、替换函数、never pause here断点、中间人改js代码等)
- app端有哪些抓取方案(逆向、自动化、自动化+中间人)
其他
1. 个人介绍
您好,我叫xxx,来自xxx,于xxx年xxx月毕业于xxx大学xxx学院,xxx年xxx月至xxx年xxx月在xxx公司从事xxx岗位,主要负责xxx等,之后在xxx公司工作,主要负责xxx
2. 职业规划
面试官想考察的是你的规划是否符合公司所需职位 可以回答我想在爬虫这方面继续深造,探索更多的反爬应对策略,当自己的经验及能力足够时,我希望可以带领及帮助其他爬虫同事
总结
- 面试时一定不要紧张,要准备充分,不要死记硬背。理解背后的原理,一切便可应对自如。
- 尽可能的带节奏,主动引导面试官的问题到自己擅长的领域,比如可以多聊聊自己擅长的项目,说说细节,打乱面试官本来想面试的问题。这样既可以使自己不那么紧张,又可以规避一些自己不会的问题。当然不要乱答,明明这个问题不会,但还要装作很懂的样子,这个是大忌,面试官喜欢听简洁利落的答案,不会就是不会,不过可以问下面试官答案,以表示你积极好学
下期分享预告
爬虫框架如何设计
了解更多
欢迎加入知识星球 https://t.zsxq.com/eEmAeae
本星球建设目的是为了分享爬虫技术干货,让爬虫技术在星球内不断的沉淀下来,做到知识积累。 星球每周都会分享爬虫技术干货,涉及的领域包括但不限于js逆向技巧、爬虫框架刨析、爬虫技术分享等。期待您的加入,和我们一起探讨爬虫技术,拓展爬虫思维!
QQ 群:750614606