Python-面试题(面试)
进程、线程、协程
什么是进程、线程、协程?以及它们的优缺点?
-
进程(Process)
- 定义:进程是操作系统分配资源的基本单位,每个进程都有自己独立的内存空间和系统资源。
- 优点:
- GIL限制:Python 的全局解释器锁(GIL)限制了同一时间只能有一个线程执行字节码,但进程间互不影响,可以利用多核 CPU。
- 独立性:每个进程都有独立的内存空间,一个进程崩溃不会影响其他进程。
- 缺点:
- 开销大:创建和销毁进程的开销较大,进程间通信(IPC)相对复杂。
- 资源消耗高:因为每个进程都有独立的资源,占用的系统资源较多。
-
线程(Thread)
- 定义:线程是进程中的一个执行单元,多个线程共享同一进程的内存空间和资源。
- 优点:
- 轻量级:线程的创建和销毁开销小于进程。
- 共享资源:线程间通信和数据共享比进程更方便。
- 缺点:
- GIL限制:由于 GIL,同一时间只能有一个线程执行 Python 字节码,影响多线程程序在 CPU 密集型任务中的性能。
- 同步问题:需要小心处理线程同步,避免竞争条件和死锁。
-
协程(Coroutine)
- 定义:协程是一种轻量级的线程,它们在一个线程内进行调度,允许在一个函数执行过程中暂停并在稍后恢复执行。
- 优点:
- 轻量级:协程切换的开销非常小,不涉及系统调用。
- 无阻塞:适合 I/O 密集型任务,例如网络请求和文件操作。
- 简化异步编程:使得异步编程更加直观和可维护。
- 缺点:
- 单线程局限:无法利用多核 CPU 并行执行 CPU 密集型任务。
- 同步问题:和线程一样,共享同一线程的资源,存在相同的同步问题。
-
总结
- 进程:适合 CPU 密集型任务,可以突破 GIL 限制,资源消耗高。
- 线程:适合 I/O 密集型任务,但受 GIL 限制,需注意同步问题。
- 协程:非常轻量级,适合大量 I/O 密集型任务,但无法利用多核 CPU。
Python 多线程和全局解释器锁(GIL)
-
Python 多线程
多线程是一种并发编程的方法,它允许多个线程在同一个进程内同时运行。线程共享整个进程的资源,包括内存空间和文件句柄,因此在线程之间进行数据共享和通信非常方便。
-
全局解释器锁(GIL)
GIL(Global Interpreter Lock,全局解释器锁)是 Python 解释器(CPython 实现)中的一个机制,它限制了在任何时刻只有一个线程执行 Python 字节码。这是为了确保对 Python 对象的内存管理是线程安全的,但也导致了一些性能问题,特别是在多线程环境下。
-
GIL 的存在原因:
- 内存管理安全:Python 的内存管理不是线程安全的,GIL 确保了在一个时刻只有一个线程操作对象,这样可以避免竞争条件和数据不一致。
- 简化实现:GIL 简化了 Python 解释器的实现,使得许多操作变得更简单、更高效。
-
GIL 的影响:
- CPU 密集型任务:对于 CPU 密集型任务,多线程应用表现不佳,因为 GIL 限制了并行执行,多个线程竞争 GIL,导致性能受限。
- I/O 密集型任务:对于 I/O 密集型任务(文件读写、网络请求等),多线程仍然能够带来性能提升,因为 I/O 操作会释放 GIL,使得其他线程能够运行。
-
解决方案:
- 多进程:使用
multiprocessing模块创建多个进程,每个进程都有自己的 Python 解释器实例和 GIL,可以充分利用多核 CPU。 - 异步编程:使用
asyncio模块进行协程编程,适合处理大量 I/O 密集型任务。 - 其他解释器:使用不包含 GIL 的 Python 实现,例如 Jython(基于 Java 的 Python 实现)和 IronPython(基于 .NET 的 Python 实现)。
- 多进程:使用
-
总结
- 多线程:在 Python 中,通过
threading模块实现,但受 GIL 限制,特别是在处理 CPU 密集型任务时。 - GIL:全局解释器锁限制了同时只能有一个线程执行 Python 字节码,确保了线程安全但也限制了多线程的性能。
- 解决方案:对于 CPU 密集型任务,可以使用多进程;对于 I/O 密集型任务,可以使用多线程或异步编程。
- 多线程:在 Python 中,通过
Python 中并发和并行有啥区别?
-
并发(Concurrency)
- 定义:并发是指在同一时间段内,系统能够处理多个任务,但并不要求这些任务同时进行。重点在于任务的切换和调度。
- 特点:
- 多个任务可以在同一个处理器核心上交替执行。
- 任务的执行顺序是交错的,但不一定是同时的。
- 适用于 I/O 密集型任务,如网络请求、文件读写等。
- 在 Python 中的实现:
- 多线程:使用
threading模块。 - 协程:使用
asyncio模块。
- 多线程:使用
-
并行(Parallelism)
- 定义:并行是指多个任务在同一时刻同时执行。重点在于任务的同时进行,通常需要多核处理器。
- 特点:
- 多个任务在多个处理器核心上同时执行。
- 提高了任务的执行速度,减少了总执行时间。
- 适用于 CPU 密集型任务,如复杂计算、图像处理等。
- 在 Python 中的实现:
- 多进程:使用
multiprocessing模块。 - 分布式计算:使用分布式计算框架,如 Dask、Ray 等。
- 多进程:使用
-
比较
- 并发:
- 多任务在同一时间段内交替进行。
- 适合 I/O 密集型任务。
- Python 中可以通过多线程和协程实现。
- 受 GIL 影响,Python 的多线程在 CPU 密集型任务中表现不佳。
- 并行:
- 多任务在同一时刻同时进行。
- 适合 CPU 密集型任务。
- Python 中可以通过多进程实现。
- 由于每个进程有自己的 GIL,多进程可以充分利用多核 CPU。
- 并发:
-
总结
- 并发侧重于任务的调度和管理,使多个任务在同一时间段内交替进行。主要用于 I/O 密集型任务,可以通过多线程和协程实现。
- 并行侧重于任务的同时执行,使多个任务在同一时刻并行进行。主要用于 CPU 密集型任务,可以通过多进程实现。
Python 如何提高并发?
-
使用多线程(Threading)
多线程适用于 I/O 密集型任务,如文件读写、网络请求等。尽管受限于全局解释器锁(GIL),但在 I/O 密集型任务中,多线程仍然能够带来显著的性能提升。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import threading
import requests
def fetch_url(url):
response = requests.get(url)
print(f"Fetched {url} with status {response.status_code}")
urls = ["http://example.com", "http://example.org", "http://example.net"]
threads = []
for url in urls:
t = threading.Thread(target=fetch_url, args=(url,))
threads.append(t)
t.start()
for t in threads:
t.join() -
使用多进程(Multiprocessing)
多进程适用于 CPU 密集型任务,因为每个进程都有自己的 Python 解释器实例和 GIL,可以利用多核 CPU 进行并行计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from multiprocessing import Process
def compute_square(num):
print(f"Square of {num} is {num * num}")
if __name__ == "__main__":
processes = []
for i in range(5):
p = Process(target=compute_square, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join() -
使用协程(Asyncio)
协程是一种轻量级的并发实现,特别适用于大量 I/O 密集型任务。协程通过事件循环来管理任务的调度,非常高效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
print(f"Fetched {url} with status {response.status}")
async def main():
urls = ["http://example.com", "http://example.org", "http://example.net"]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main()) -
使用并发库(如 concurrent.futures)
concurrent.futures提供了一种高级接口来方便地实现多线程和多进程并发,可以通过ThreadPoolExecutor或ProcessPoolExecutor来实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
def fetch_url(url):
response = requests.get(url)
return url, response.status_code
urls = ["http://example.com", "http://example.org", "http://example.net"]
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(fetch_url, url) for url in urls]
for future in as_completed(futures):
url, status = future.result()
print(f"Fetched {url} with status {status}") -
使用异步 Web 框架(如 FastAPI, Tornado)
对于 Web 应用,可以使用异步 Web 框架来提高并发性能。这些框架能够处理大量并发请求,非常适合 I/O 密集型 Web 服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14from fastapi import FastAPI
import aiohttp
app = FastAPI()
async def fetch_url(url: str):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return {"url": url, "status": response.status}
# 运行命令:uvicorn filename:app --reload
-
选择适合的并发模型
- 多线程:适合 I/O 密集型任务,可以通过
threading或concurrent.futures.ThreadPoolExecutor实现。 - 多进程:适合 CPU 密集型任务,可以通过
multiprocessing或concurrent.futures.ProcessPoolExecutor实现。 - 协程:适合大量 I/O 密集型任务,可以通过
asyncio实现。 - 异步 Web 框架:适合高并发 Web 服务,可以使用 FastAPI 或 Tornado。
- 多线程:适合 I/O 密集型任务,可以通过
线程上下文开销大是指什么?
-
简介
在计算机科学中,线程上下文开销指的是在线程之间切换所需的资源消耗和时间开销。这包括保存和恢复线程的状态信息,如寄存器、程序计数器、栈指针等。在 Python 中,也存在线程上下文开销,特别是在多线程编程中。
-
线程上下文开销的具体内容:
- 寄存器状态保存与恢复:
- 当线程切换时,当前线程的寄存器状态(如通用寄存器、程序计数器、栈指针等)需要保存到内存中。
- 新的线程则需要将其寄存器状态从内存中恢复到 CPU 寄存器。
- 内存页切换:
- 线程的栈和全局变量可能驻留在不同的内存页中,切换线程时可能会导致内存页切换,这也会带来一定的开销。
- 内核调度:
- 线程切换通常需要内核的调度器介入,以决定下一个要运行的线程。这个过程涉及系统调用和内核态的处理,增加了额外的开销。
- 缓存无效化:
- 线程切换可能会导致 CPU 缓存无效化(cache invalidation),降低缓存命中率,从而影响性能。
- 寄存器状态保存与恢复:
-
在 Python 中的影响
Python 中的多线程受限于全局解释器锁(GIL),GIL 确保在任何时刻只有一个线程执行 Python 字节码。尽管 GIL 确保了线程安全,但也带来了额外的上下文切换开销。
- GIL 争用:在多线程程序中,不同线程争用 GIL 会导致频繁的锁竞争和上下文切换,进而增加开销。
- 性能瓶颈:由于上下文切换的开销和 GIL 的限制,Python 的多线程在处理 CPU 密集型任务时往往不如多进程高效。
-
减少上下文开销的建议
- 使用协程:
- 对于 I/O 密集型任务,使用
asyncio库实现协程可以减少上下文切换开销。
- 对于 I/O 密集型任务,使用
- 使用多进程:
- 对于 CPU 密集型任务,使用
multiprocessing模块创建多个进程,每个进程有自己的 GIL,可以充分利用多核 CPU,减少线程上下文切换开销。
- 对于 CPU 密集型任务,使用
- 批处理任务:
- 尽量减少频繁的线程切换,可以通过批处理任务或增加任务粒度来减小线程切换的频率。
- 使用高效的并发库:
- 例如使用
concurrent.futures提供的线程池或进程池,可以更高效地管理线程和进程,减少上下文切换的开销。
- 例如使用
- 使用协程:
-
总结
线程上下文开销包括保存和恢复线程状态、内存页切换、内核调度、缓存无效化等。在 Python 中,由于 GIL 的存在,线程上下文开销在多线程编程中特别明显。通过使用协程、多进程或高效的并发库,可以减少上下文切换开销,提高程序的并发性能。
乐观锁和悲观锁
-
悲观锁(Pessimistic Locking)
-
简介:
悲观锁假定在访问共享资源时总会发生冲突,因此在操作资源之前,先对资源进行加锁,以确保独占访问。悲观锁会在操作完成后解锁,因此在锁持有期间,其他线程或进程无法访问该资源。
-
特点:
- 阻塞:其他线程在等待锁释放时会被阻塞。
- 开销大:频繁的锁操作会带来性能开销。
- 适用场景:适用于冲突较多或冲突代价较大的场景。
-
示例:使用 Python 的
threading.Lock1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import threading
lock = threading.Lock()
def critical_section():
with lock:
# 资源操作,确保在加锁期间独占访问
print("Resource is being accessed")
threads = [threading.Thread(target=critical_section) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
-
-
乐观锁(Optimistic Locking)
-
简介:
乐观锁假定在访问共享资源时很少发生冲突,因此不在操作之前加锁,而是在提交操作时检查资源是否被修改。如果资源没有冲突,则提交成功;如果资源已经被其他线程修改,则操作失败,需要重新尝试。
-
特点:
- 非阻塞:线程在操作资源时不阻塞其他线程。
- 重试机制:在提交操作时检测冲突,如果冲突则重试。
- 适用场景:适用于冲突较少的场景,减少锁的开销。
-
示例:使用版本号的简单乐观锁实现
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
36class OptimisticLock:
def __init__(self):
self.value = 0
self.version = 0
def read(self):
return self.value, self.version
def write(self, new_value, version):
if self.version == version:
self.value = new_value
self.version += 1
return True
return False
optimistic_lock = OptimisticLock()
def worker():
while True:
value, version = optimistic_lock.read()
new_value = value + 1 # 业务逻辑
if optimistic_lock.write(new_value, version):
break # 写成功则退出循环
# 否则重试
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Final value: {optimistic_lock.value}")
-
-
比较
- 冲突处理:
- 悲观锁:通过阻塞其他线程来避免冲突,性能开销大,但确保了资源的完整性。
- 乐观锁:通过检测冲突并重试来解决冲突,适用于冲突少的场景。
- 性能:
- 悲观锁:频繁的加锁和解锁操作会带来性能开销。
- 乐观锁:减少了锁的使用,性能较好,但在高冲突情况下,重试机制可能导致性能下降。
- 适用场景:
- 悲观锁:适用于高冲突、高代价的场景,如数据库修改、文件操作等。
- 乐观锁:适用于低冲突、并发访问频繁的场景,如缓存更新等。
- 冲突处理:
-
总结
悲观锁和乐观锁是两种不同的并发控制机制。悲观锁通过阻塞其他线程来避免冲突,适用于高冲突场景;乐观锁通过检测冲突并重试来解决冲突,适用于低冲突场景。根据具体需求选择适当的锁机制,可以有效提高程序的并发性能和资源利用率。
Python 中常用的线程锁?
-
threading.Lockthreading.Lock是最基础的锁类型,通常也被称为互斥锁(mutex)。它用于确保某一时刻只有一个线程可以访问共享资源。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import threading
lock = threading.Lock()
def critical_section():
with lock:
# 资源操作,确保在加锁期间独占访问
print("Resource is being accessed")
threads = [threading.Thread(target=critical_section) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join() -
threading.RLockthreading.RLock是可重入锁。它允许同一线程在持有锁的情况下再次获得锁,而不会造成死锁。这在递归函数或需要多次获取锁的情况下非常有用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import threading
rlock = threading.RLock()
def recursive_function(n):
if n > 0:
with rlock:
print(f"Acquiring lock, recursion depth: {n}")
recursive_function(n - 1)
print(f"Releasing lock, recursion depth: {n}")
thread = threading.Thread(target=recursive_function, args=(3,))
thread.start()
thread.join() -
threading.Conditionthreading.Condition是条件锁。用于复杂的线程间通信。它是更高级的同步原语,可以让线程等待特定的条件状态,然后再继续执行。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
31import threading
condition = threading.Condition()
shared_data = []
def consumer():
with condition:
while not shared_data:
condition.wait() # 等待生产者发出信号
item = shared_data.pop()
print(f"Consumed: {item}")
def producer():
with condition:
shared_data.append(1)
print("Produced: 1")
condition.notify() # 通知消费者
threads = [
threading.Thread(target=consumer),
threading.Thread(target=producer)
]
for t in threads:
t.start()
for t in threads:
t.join() -
threading.Semaphorethreading.Semaphore是信号量锁。用于控制对资源的访问,允许多个线程同时访问,但限制最大访问数量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import threading
semaphore = threading.Semaphore(3) # 允许最多3个线程同时访问
def worker():
with semaphore:
print(f"{threading.current_thread().name} is working")
# 模拟工作
import time
time.sleep(1)
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join() -
threading.Eventthreading.Event是一种简单的线程间通信机制,通常用于一个线程等待另一个线程发送信号。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
28import threading
event = threading.Event()
def waiter():
print("Waiting for event to be set")
event.wait() # 等待事件信号
print("Event is set, proceeding")
def setter():
import time
time.sleep(2)
print("Setting event")
event.set() # 发送事件信号
threads = [
threading.Thread(target=waiter),
threading.Thread(target=setter)
]
for t in threads:
t.start()
for t in threads:
t.join() -
threading.Barrierthreading.Barrier是屏障锁。用于让指定数量的线程都达到某个点后再继续执行。适用于需要线程同步的场景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import threading
barrier = threading.Barrier(3)
def worker():
print(f"{threading.current_thread().name} is waiting at the barrier")
barrier.wait() # 等待所有线程到达屏障
print(f"{threading.current_thread().name} passed the barrier")
threads = [threading.Thread(target=worker) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
Python 中线程之间如何通信?
-
概述
在 Python 中,线程之间的通信是多线程编程中的一个重要方面。Python 提供了多种机制和同步原语来实现线程间通信,以确保数据的一致性和线程的协调。这些机制主要包括队列、事件、条件变量、信号量和共享数据结构等。
-
使用
queue.Queuequeue.Queue是一个线程安全的队列,可以用于线程之间的通信。它提供了先进先出 (FIFO)、先进后出 (LIFO) 和优先队列等多种队列类型。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
34import threading
import queue
# 创建一个队列对象
q = queue.Queue()
def producer():
for i in range(5):
item = f"item {i}"
q.put(item)
print(f"Produced: {item}")
def consumer():
while True:
item = q.get() # 从队列中取出一个项目
if item is None:
break
print(f"Consumed: {item}")
q.task_done() # 标记任务完成
# 创建并启动线程
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
q.put(None) # 向队列发送停止信号
consumer_thread.join() -
使用
threading.Eventthreading.Event用于线程间的简单信号通信。一个线程可以等待事件发生(等待信号),另一个线程可以设置事件(发送信号)。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
27import threading
import time
event = threading.Event()
def worker():
print("Worker is waiting for the event to be set")
event.wait() # 等待事件被设置
print("Event is set, worker proceeding")
def setter():
time.sleep(2)
print("Setter is setting the event")
event.set() # 设置事件
worker_thread = threading.Thread(target=worker)
setter_thread = threading.Thread(target=setter)
worker_thread.start()
setter_thread.start()
worker_thread.join()
setter_thread.join() -
使用
threading.Conditionthreading.Condition提供了更复杂的线程间通信方法,允许多个线程等待某个条件并在条件满足时被唤醒。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
30import threading
condition = threading.Condition()
shared_data = []
def consumer():
with condition:
while not shared_data:
condition.wait() # 等待生产者发出信号
item = shared_data.pop()
print(f"Consumed: {item}")
def producer():
with condition:
shared_data.append(1)
print("Produced: 1")
condition.notify() # 通知消费者
consumer_thread = threading.Thread(target=consumer)
producer_thread = threading.Thread(target=producer)
consumer_thread.start()
producer_thread.start()
consumer_thread.join()
producer_thread.join() -
使用
threading.Semaphorethreading.Semaphore可以用于控制对资源的访问,允许多个线程同时访问,但限制最大访问数量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import threading
semaphore = threading.Semaphore(3) # 允许最多3个线程同时访问
def worker():
with semaphore:
print(f"{threading.current_thread().name} is working")
import time
time.sleep(1)
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join() -
使用共享数据结构
线程之间可以通过共享数据结构(例如全局变量、列表、字典等)进行通信,但需要使用锁(如
threading.Lock或threading.RLock)来确保数据的一致性和线程安全。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
33import threading
shared_data = []
lock = threading.Lock()
def producer():
for i in range(5):
with lock:
shared_data.append(i)
print(f"Produced: {i}")
def consumer():
while True:
with lock:
if shared_data:
item = shared_data.pop(0)
print(f"Consumed: {item}")
else:
break
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
producer_thread.join()
consumer_thread.join()
Python 爬虫
Scrapy 的工作原理
-
概述
Scrapy 是一个基于 事件驱动 和 异步非阻塞 I/O 模型构建的高性能 Python 爬虫框架,其核心设计目标是高效、可扩展地抓取大规模网页并提取结构化数据。它的底层依赖于 Twisted 异步网络引擎(早于 asyncio),通过组件化架构实现职责分离。
-
Scrapy 的五大核心组件
组件 职责 Engine(引擎) 整个系统的“总指挥”,协调所有组件的数据流和事件调度 Scheduler(调度器) 接收并管理待处理的请求(Request),支持去重、优先级队列 Downloader(下载器) 发送 HTTP 请求,获取响应(Response),基于 Twisted 实现异步下载 Spider(爬虫) 用户自定义逻辑:解析响应、提取数据(Item)、生成新请求 Item Pipeline(数据管道) 处理 Spider 提取的 Item:清洗、验证、存储(如数据库、文件) 此外还有两类中间件(Middleware):
- Downloader Middleware:拦截请求/响应(如更换代理、User-Agent)
- Spider Middleware:处理 Spider 的输入(Response)和输出(Item/Request)
-
Scrapy 的完整工作流程(数据流)
整个过程由 Engine 驱动,形成一个闭环流水线:
-
Spider 生成初始请求
-
用户在 Spider 中定义
start_urls或start_requests(),返回scrapy.Request对象。1
2def start_requests(self):
yield scrapy.Request(url='https://example.com', callback=self.parse)
-
-
Engine 将请求交给 Scheduler
- Engine 接收 Request,发送给 Scheduler 入队。
-
Scheduler 返回下一个请求给 Engine
- Scheduler 按策略(如 FIFO、优先级)出队一个 Request,并进行去重(基于 Request Fingerprint)。
-
Engine 将请求转发给 Downloader(经 Downloader Middleware)
- 请求在发送前可被中间件修改(如添加代理、Headers)。
-
Downloader 异步下载页面
- 利用 Twisted 的非阻塞 I/O 并发发起多个请求,不阻塞主线程。
-
Downloader 返回 Response 给 Engine(经 Downloader Middleware)
- 响应可被中间件处理(如重试失败请求、解压内容)。
-
Engine 将 Response 交给 Spider(经 Spider Middleware)
- Engine 调用 Spider 中对应的
callback函数(如parse())。
- Engine 调用 Spider 中对应的
-
Spider 解析响应,产出 Item 或新 Request
-
使用 XPath/CSS 提取数据,
yieldItem 或新的 Request:1
2
3def parse(self, response):
yield {'title': response.css('h1::text').get()}
yield scrapy.Request(url=next_page, callback=self.parse)
-
-
Engine 分发结果
- 若是 Item → 发送给 Item Pipeline 处理;
- 若是 Request → 回到步骤 2,重新入队。
-
Item Pipeline 处理数据
- 数据依次通过多个 Pipeline(如去重 → 验证 → 存入 MySQL/MongoDB)。
-
✅ 循环持续,直到 Scheduler 中无待处理请求,且 Spider 无新产出。
-
-
关键技术特性
-
异步非阻塞模型(基于 Twisted)
- 单线程内通过事件循环(Reactor)处理成百上千并发请求;
- 当一个请求等待响应时,立即处理其他任务,极大提升吞吐量。
-
自动去重机制
- 默认使用
RFPDupeFilter(Request Fingerprint Filter); - 对每个 Request 计算 SHA1 指纹(基于 URL、Method、Body 等),避免重复抓取。
- 默认使用
-
中间件扩展机制
- 可插入自定义逻辑,如:
- 动态切换 User-Agent
- 自动重试失败请求
- 处理登录 Cookie
- 可插入自定义逻辑,如:
-
工程化结构
项目结构清晰,职责分明:
1
2
3
4
5
6
7myproject/
├── myproject/
│ ├── spiders/ # 爬虫逻辑
│ ├── items.py # 数据结构定义
│ ├── pipelines.py # 数据处理管道
│ ├── middlewares.py # 中间件
│ └── settings.py # 全局配置(并发数、延迟、启用组件等)
-
-
执行示例(简化版数据流图)
graph LR A[Spider] -->|1. yield Request| B(Engine) B -->|2. send to| C[Scheduler] C -->|3. next Request| B B -->|4. send to| D[Downloader] D -->|5. fetch page| E[(Web Server)] E -->|6. Response| D D -->|7. return Response| B B -->|8. send to| A A -->|9a. yield Item| F[Item Pipeline] A -->|9b. yield Request| B F -->|10. store data| G[(Database/File)] -
优势总结
- ✅ 高并发:单机轻松支持数百~数千 QPS;
- ✅ 结构清晰:组件解耦,易于维护和扩展;
- ✅ 内置功能丰富:自动去重、重试、Cookie 管理、导出 JSON/CSV 等;
- ✅ 生态强大:支持 Scrapy-Redis(分布式)、Scrapy-Splash(JS 渲染)等插件。
- 💡 一句话理解 Scrapy:它不是简单的“发请求 + 解析”脚本,而是一个工业级数据采集流水线系统,将爬虫开发从“手工作坊”升级为“自动化工厂”。
Scrapy-Redis 的工作原理
-
概述
Scrapy-Redis 是 Scrapy 框架的分布式扩展组件,其核心目标是将原本单机运行的 Scrapy 爬虫改造为支持 多节点协同工作的分布式爬虫系统。它通过 Redis 作为中央协调器,实现任务队列、去重状态和起始 URL 的全局共享,从而突破单机资源限制。
-
核心思想:用 Redis 替换 Scrapy 的本地组件
-
Scrapy 默认使用 内存中的调度器(Scheduler)和去重过滤器(DupeFilter),这导致多个 Scrapy 实例无法共享状态。
-
Scrapy-Redis 则将这两个关键组件“外置”到 Redis 中:
Scrapy 原生组件 Scrapy-Redis 替代方案 存储结构 作用 Scheduler(调度器)scrapy_redis.scheduler.SchedulerRedis 有序集合(ZSET) 或 List 共享请求队列,所有节点从同一队列取任务 DupeFilter(去重器)scrapy_redis.dupefilter.RFPDupeFilterRedis Set 集合 全局记录已爬请求指纹,避免重复抓取 Spider.start_urls从 Redis List 动态读取 Redis List(如 myspider:start_urls)支持运行时动态添加起始 URL ✅ 本质:将 Scrapy 的“本地状态”变为“分布式共享状态”。
-
-
Scrapy-Redis 工作流程详解
-
初始化 —— 向 Redis 注入种子 URL
-
外部程序(或手动)向 Redis 推送初始 URL:
1
LPUSH myspider:start_urls https://example.com/page1
-
-
爬虫启动 —— 监听 Redis 起始队列
- 所有 Worker 节点启动
RedisSpider,自动监听redis_key = 'myspider:start_urls'; - 若队列为空,爬虫进入阻塞等待状态,不退出,便于动态追加任务。
- 所有 Worker 节点启动
-
请求生成与入队
- Spider 解析页面后,
yield Request(url); - Engine 将请求交给 Redis Scheduler;
- Scheduler 执行:
- 调用
RFPDupeFilter.request_seen(request); - 计算请求指纹(SHA1 哈希);
- 查询 Redis Set(如
myspider:dupefilter)是否已存在; - 若未重复,则将序列化后的请求存入 Redis ZSET(键如
myspider:requests),以优先级为 score。 - 📌 2025 新特性:支持 Redis Stream 存储 Item,提升数据可靠性。
- 调用
- Spider 解析页面后,
-
多节点并发消费任务
- 所有 Worker 的 Scheduler 定期从 Redis ZSET 执行
ZPOPMIN(取最高优先级请求); - 请求被反序列化为 Scrapy
Request对象; - Downloader 异步下载页面,返回
Response给对应 Spider。
- 所有 Worker 的 Scheduler 定期从 Redis ZSET 执行
-
数据输出与状态同步
- 提取的
Item可通过RedisPipeline存入 Redis,供其他系统消费; - 新发现的 URL 自动入队,形成自动扩散式爬取(类似广度优先搜索)。
- 提取的
-
容错与持久化
- 设置
SCHEDULER_PERSIST = True:即使所有爬虫重启,Redis 中的任务和去重状态仍保留,支持断点续爬。
- 设置
-
-
关键技术细节
- 请求指纹(Fingerprint)
- 默认使用
scrapy.utils.request.request_fingerprint(),基于 URL、Method、Body、Headers 等生成唯一 SHA1 值; - 确保不同参数的相同 URL(如带分页参数)不会被误判为重复。
- 默认使用
- 队列类型选择
SpiderPriorityQueue(默认):基于 ZSET,支持优先级;SpiderQueue:基于 List,FIFO,性能更高;SpiderStack:基于 List,LIFO,适合深度优先。
- 去重优化
- 可替换为 布隆过滤器(Bloom Filter) 降低内存占用(需自定义
DupeFilter); - Redis Set 查找复杂度 O(1),百万级去重高效稳定。
- 可替换为 布隆过滤器(Bloom Filter) 降低内存占用(需自定义
- 请求指纹(Fingerprint)
-
典型配置(settings.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 启用 Redis 调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 启用 Redis 去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# Redis 连接地址
REDIS_URL = 'redis://:your_password@redis-server:6379/0'
# 持久化队列(防止重启丢失任务)
SCHEDULER_PERSIST = True
# 使用优先级队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue'爬虫类需继承
RedisSpider:1
2
3
4
5
6
7
8
9
10from scrapy_redis.spiders import RedisSpider
class MySpider(RedisSpider):
name = 'my_spider'
redis_key = 'my_spider:start_urls' # Redis 中的起始 URL 键名
def parse(self, response):
# 解析逻辑
yield {'title': response.css('h1::text').get()}
yield scrapy.Request(next_url, callback=self.parse) -
优势总结
优势 说明 ✅ 高并发 多台机器并行抓取,吞吐量线性提升 ✅ 去重全局化 避免多节点重复抓取同一 URL ✅ 任务持久化 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+---------------------+
| Redis Server |
| +---------------+ |
| | start_urls | | ← 外部注入初始URL
| +---------------+ |
| | requests (ZSET)| | ← 共享任务队列
| +---------------+ |
| | dupefilter(Set)| | ← 全局去重集合
+----------+----------+
^
| (TCP)
+--------------+--------------+
| |
+----v----+ +----v----+
| Worker1 | | Worker2 |
| - RedisSpider | - RedisSpider
| - 共享队列消费 | - 共享去重检查
| - 数据输出 | - 并行抓取
+---------+ +---------+
\ /
\ /
v v
+---------------------+
| Database |
| (MySQL/MongoDB等) |
+---------------------+综上,Scrapy-Redis 的工作原理本质是“以 Redis 为中心,实现 Scrapy 组件的分布式共享”,让多个爬虫实例像一个整体协同工作,是构建工业级分布式爬虫的事实标准方案。
Scrapy-Redis 的优缺点
-
优点(Advantages)
- 简单易用,无缝集成 Scrapy
- 仅需修改
settings.py中的调度器和去重类,并继承RedisSpider,即可将单机爬虫升级为分布式系统。 - 对原有 Scrapy 代码侵入极小,开发者无需重写核心逻辑。
- 仅需修改
- 高效的分布式任务调度
- 所有 Worker 节点共享同一个 Redis 队列(如
scrapy:requests),天然支持负载均衡; - 新任务由任意节点发现并入队,其他节点自动消费,实现“协同爬取”。
- 所有 Worker 节点共享同一个 Redis 队列(如
- 全局去重,避免重复抓取
- 使用 Redis Set 存储请求指纹(Fingerprint),在集群范围内确保 URL 不重复;
- 去重效率高(O(1) 查询),实测百万级数据去重率达 99.9%。
- 支持断点续爬与持久化
- 设置
SCHEDULER_PERSIST = True后,即使所有爬虫崩溃或重启,任务队列和去重状态仍保留在 Redis 中; - 特别适合长期运行的增量爬虫项目。
- 设置
- 动态任务注入
- 可在运行时通过
LPUSH myspider:start_urls URL动态添加新任务,无需重启爬虫; - 适用于监控型爬虫或实时数据追加场景。
- 可在运行时通过
- 可扩展性强
- 理论上可无限增加 Worker 节点(受限于 Redis 性能和网络带宽);
- 实测:3 台普通云服务器 + 1 台 Redis,24 小时完成 300 万条商品数据抓取。
- 简单易用,无缝集成 Scrapy
-
缺点(Disadvantages)
-
Redis 成为性能瓶颈与单点故障
- 所有节点依赖单个 Redis 实例进行通信,当任务量极大(千万级)时:
- Redis 内存消耗剧增(去重 Set 可达 GB 级);
- 网络 I/O 和 CPU 成为瓶颈;
- 若 Redis 宕机,整个爬虫集群瘫痪。
- 解决方案:需部署 Redis 主从 + 哨兵 或 Cluster 模式(2025 年推荐方案)。
- 所有节点依赖单个 Redis 实例进行通信,当任务量极大(千万级)时:
-
请求对象序列化开销大
- Scrapy-Redis 默认调度的是完整的
Request对象(含 URL、callback、headers、meta 等); - 相比仅调度 URL,体积更大,占用更多 Redis 内存和网络带宽;
- 在高并发下可能降低吞吐量。
✅ 优化建议:可自定义队列,只存储 URL + 回调标识,减少序列化负载。
- Scrapy-Redis 默认调度的是完整的
-
内存占用随数据规模线性增长
- 去重集合(dupefilter)会持续累积,无法自动清理;
- 若爬取亿级页面,Redis 内存可能超限。
- 解决方案:
- 使用 布隆过滤器(Bloom Filter) 替代 Set(如
scrapy-redis-bloomfilter),牺牲极低误判率换取内存节省; - 设置 TTL 自动过期旧指纹(需自定义
DupeFilter)。
- 使用 布隆过滤器(Bloom Filter) 替代 Set(如
-
调试与监控复杂度提升
- 分布式环境下,日志分散在多个节点,错误定位困难;
- 任务状态不透明(如某请求卡在队列中)。
- 解决方案:需额外集成日志收集(ELK)和监控系统(Prometheus + Grafana)。
-
不适合强依赖浏览器渲染的场景
- Scrapy-Redis 本身不解决 JS 渲染问题;
- 若目标网站重度依赖 JavaScript(如 React SPA),仍需结合Selenium/Playwright,而这些工具难以高效分布式化。
- 此时整体架构复杂度陡增,Scrapy-Redis 优势减弱。
-
任务分配可能不均(“饥饿”问题)
- 在某些网络延迟差异大的环境中,部分 Worker 可能频繁获取任务,而其他节点空闲;
- 默认队列策略(如 FIFO)无法智能感知节点负载。
- 解决方案:可实现自定义优先级队列或引入外部调度器(如 Celery)。
-
-
适用场景 vs 不适用场景
✅ 推荐使用 Scrapy-Redis 的场景 ❌ 不推荐使用的场景 静态或轻度 JS 网站的大规模抓取(如新闻、电商商品) 重度 JS 渲染网站(需大量无头浏览器) 需要高去重精度的全站爬取 一次性小规模爬取(<1 万条) 长期运行的增量爬虫 对 Redis 单点故障零容忍且无高可用方案 已有 Scrapy 项目需快速分布式化 需要复杂任务依赖或 DAG 调度 -
总结
- Scrapy-Redis 的核心价值在于“以最小改造成本实现 Scrapy 的横向扩展”。
- 它不是万能的分布式框架,但在中等规模、结构化、反爬较弱的数据采集中,仍是性价比最高、生态最成熟的选择。
- 若项目规模进一步扩大(如十亿级 URL)、对容错性要求极高,可考虑更复杂的框架如 Frontera(完全分布式设计,支持 HBase 后端)。但对于绝大多数企业级爬虫需求,Scrapy-Redis 配合 Redis 高可用部署,已足够稳健高效。
Feapder 和 Scrapy 有什么区别?
-
设计目标与定位
维度 Scrapy Feapder 定位 成熟、全栈、企业级异步爬虫框架 轻量、易用、面向大规模数据采集的现代爬虫框架 目标用户 中高级开发者,适合复杂项目 初学者到高级开发者皆宜,强调“开箱即用” 核心理念 模块化、高度可扩展 简洁 API + 内置生产级功能(如断点续爬、分布式) 总结:Scrapy 更像“瑞士军刀”,功能全面但学习成本高;Feapder 更像“智能工具箱”,内置大量实用功能,降低开发门槛。
-
运行与调试体验
特性 Scrapy Feapder 启动方式 必须通过命令行( scrapy crawl xxx)可直接 python xxx.py运行,像普通脚本调试支持 需配合 scrapy shell或额外配置原生支持 PyCharm 断点调试,提供 to_DebugSpider()模式开发效率 配置繁琐(需 settings.py、items.py 等) 单文件即可完成爬虫,结构简洁 优势:Feapder 在开发调试阶段明显更友好,尤其适合快速验证逻辑。
-
异常处理与重试机制
场景 Scrapy Feapder 请求失败 自带重试中间件(仅限网络层) 全链路重试:请求 → 解析 → 入库 解析异常 不会自动重试,任务丢失 自动捕获异常并重试整个任务 入库失败 需手动实现重试逻辑 内置重试 + 报警机制,失败达阈值可告警 关键区别:Scrapy 的重试仅作用于 HTTP 请求;Feapder 对整个任务生命周期进行保护,更适合生产环境
-
数据存储与批量处理
功能 Scrapy Feapder 入库方式 Pipeline 逐条处理( process_item)内存缓冲队列,支持批量入库(默认 5000 条或 0.5 秒触发) 数据安全 若爬虫崩溃,已处理 item 可能丢失 任务在数据成功入库后才标记为完成,崩溃可恢复 数据库支持 需自定义 Pipeline 内置 MySQL、MongoDB、Redis、Elasticsearch 支持 优势:Feapder 的批量入库显著提升数据库写入性能,且更可靠。
-
分布式与任务管理
能力 Scrapy Feapder 分布式支持 需依赖 scrapy-redis插件原生支持(基于 Redis),无需额外插件 任务队列 使用序列化后的 request(可读性差) 保留原始字段,仅对非 JSON 类型序列化 任务丢失风险 从 Redis 弹出即删除,崩溃会丢任务 采用“租约”机制(zset + 时间戳),崩溃后任务自动回滚 种子任务下发 需单独脚本维护 start_requests自动去重 + 进程锁,避免重复下发结论:Feapder 的分布式设计更健壮、易用,适合长期运行的大规模采集。
-
去重机制
方式 Scrapy Feapder 默认去重 基于内存 fingerprint(单机) 支持三种模式: • 内存 BloomFilter • Redis 临时去重(带 TTL) • Redis 永久 BloomFilter 内存占用 海量数据时内存压力大 一亿 URL 去重仅需 ~285MB(BloomFilter) 优势:Feapder 提供更灵活、低内存的去重方案,适合海量数据场景。
-
内置爬虫类型
框架 爬虫类型 Scrapy 主要一种 Spider(可通过继承扩展) Feapder 四种原生爬虫: • AirSpider(轻量单机) •Spider(分布式) •TaskSpider(定时任务) •BatchSpider(分批采集)优势:Feapder 覆盖更多业务场景,开箱即用。
-
生态与扩展性
维度 Scrapy Feapder 社区生态 极其成熟,插件丰富(如 scrapy-splash, scrapy-redis) 较新但增长快,官方提供 feaplat爬虫管理系统学习资源 丰富(书籍、教程、问答) 官方文档完善,中文资料较多 扩展难度 高(需理解中间件、Pipeline、Downloader 等) 低(API 简洁,内置功能多)
-
总结:如何选择?
场景 推荐框架 快速抓取静态页面、学习爬虫 Feapder(AirSpider) 需要断点续爬、分布式、监控报警 Feapder 企业级长期运行的大规模采集系统 Feapder 已有 Scrapy 生态、需要深度定制 Scrapy 需要与 Scrapyd、Scrapy Cloud 集成 Scrapy
常见的反爬措施及其对应的解决方法?
-
基础型反爬(容易绕过)
-
User-Agent 检测
-
原理:服务器检查请求头中的
User-Agent字段,若为默认值(如python-requests/2.x),则拒绝访问。 -
解决方法:
1
2
3
4
5import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36'
}
response = requests.get(url, headers=headers) -
更优策略:维护一个 User-Agent 池,每次请求随机切换。
-
-
Referer / Origin 防盗链
-
原理:限制请求必须来自特定页面(如图片资源只允许从本站引用)。
-
解决方法:在 headers 中添加合法的
Referer:1
headers['Referer'] = 'https://www.example.com/'
-
-
robots.txt 限制
- 原理:通过
/robots.txt文件声明禁止爬取的路径(君子协议)。 - 解决方法:
- 若使用 Scrapy,设置
ROBOTSTXT_OBEY = False - 注意:虽可绕过,但需遵守法律与道德规范。
- 若使用 Scrapy,设置
- 原理:通过
-
-
行为型反爬(需模拟人类行为)
-
请求频率限制(Rate Limiting)
-
原理:单位时间内同一 IP 请求次数超过阈值即封禁(返回 429 或 403)。
-
解决方法:
-
添加随机延迟:
1
2import time, random
time.sleep(random.uniform(1, 3)) # 模拟人工浏览 -
使用 分布式爬虫 或 分时段采集。
-
-
-
IP 封禁(IP Blacklist)
-
原理:高频或异常行为的 IP 被加入黑名单。
-
解决方法:
-
构建 代理 IP 池(免费/付费):
1
2
3
4
5proxies = {
'http': 'http://123.123.123.123:8080',
'https': 'https://123.123.123.123:8080'
}
requests.get(url, proxies=proxies, headers=headers) -
推荐服务:阿布云、芝麻代理、Luminati 等。
-
-
-
-
内容型反爬(需解析动态内容)
-
JavaScript 动态渲染
-
原理:页面内容由 JS 异步加载(如 Vue/React 应用),
requests无法获取完整 HTML。 -
解决方法:
-
使用 Selenium、Playwright 或 Puppeteer 控制真实浏览器:
1
2
3
4from selenium import webdriver
driver = webdriver.Chrome()
driver.get(url)
html = driver.page_source -
或通过 抓包 找到真实 API 接口,直接请求 JSON 数据。
-
-
-
JS 加密参数(Token/Sign)
-
原理:关键请求携带由 JS 动态生成的加密字段(如
sign=md5(timestamp+key))。 -
解决方法:
-
逆向 JS 逻辑:定位加密函数,用 Python 重写;
-
执行 JS 代码:使用
PyExecJS、Node.js或playwright.evaluate()直接调用; -
示例:
1
2
3import execjs
ctx = execjs.compile(open('encrypt.js').read())
sign = ctx.call('getSign', params)
-
-
-
-
高级反爬(需综合对抗)
-
验证码(CAPTCHA)
- 类型:图形验证码、滑动拼图、点选文字、reCAPTCHA 等。
- 解决方法:
- 简单验证码:OCR 识别(
pytesseract+ 图像预处理); - 复杂验证码:接入 打码平台(如超级鹰、云打码);
- 滑动验证码:计算缺口位置 + 模拟人类拖动轨迹(加速度、抖动);
- 终极方案:使用 Playwright + 第三方 CAPTCHA 解决服务(如 2Captcha)。
- 简单验证码:OCR 识别(
-
字体反爬(Font Anti-Crawl)
- 原理:使用自定义
@font-face字体,将数字/文字映射为乱码(如“5”显示为“”)。 - 解决方法:
- 下载
.woff字体文件; - 使用
fontTools解析 Unicode 映射关系; - 建立映射表,将乱码还原为真实字符。
- 下载
- 原理:使用自定义
-
TLS 指纹 / 浏览器指纹检测
-
原理:通过 HTTPS 握手特征(JA3 指纹)、Canvas/WebGL 等识别非浏览器客户端(如 Cloudflare)。
-
解决方法:
-
使用 支持指纹伪装的库:
curl_cffi(推荐):可模拟 Chrome TLS 指纹;playwright/selenium+ undetected-chromedriver;
-
示例(curl_cffi):
1
2
3from curl_cffi.requests import Session
with Session() as s:
r = s.get(url, impersonate="chrome120")
-
-
-
Cookie / Session 绑定
- 原理:首次访问需获取有效 Cookie(含 token),后续请求必须携带。
- 解决方法:
- 使用
requests.Session()自动管理 Cookie; - 先访问首页或登录页,提取 Cookie 再请求目标接口。
- 使用
-
-
合规建议
- 遵守 robots.txt 和网站《服务条款》;
- 控制请求频率,避免对服务器造成压力;
- 优先采集公开数据,不破解登录或敏感接口;
- 遇到验证码/封IP 时及时停止,避免法律风险。
-
总结:反爬与反反爬对照表
反爬类型 识别特征 推荐解决方案 User-Agent 检测 返回 403 / 空内容 随机 UA 池 IP 封禁 固定 IP 无法访问 代理 IP 池 JS 渲染 HTML 中无目标数据 Selenium / 抓包 API JS 加密参数 URL 或 body 含动态 sign/token 逆向 JS / 执行 JS 验证码 跳转至验证页 打码平台 / Playwright 字体反爬 页面显示正常,源码为乱码 fontTools 解析字体映射 TLS 指纹 requests 直接失败,浏览器正常 curl_cffi / undetected-chromedriver 核心原则:让爬虫的行为尽可能像一个真实的人类用户——合理的请求头、随机延迟、IP 轮换、浏览器环境模拟,是突破大多数反爬的关键。
常见的JS混淆?以及其对应的解决方案?
-
概述
Python 爬虫开发中,JavaScript(JS)混淆已成为网站反爬体系中最常见、最有效的手段之一。其核心目的是通过破坏代码可读性与逻辑结构,使自动化程序难以提取关键参数(如 token、sign、cookie 等),从而阻止非人类访问。
-
常见 JS 混淆类型
-
变量名混淆
- 原理:将具有语义的变量名(如
username、encryptKey)替换为无意义字符(如_0x12f、a、x12)。 - 识别特征:代码中大量出现单字母或十六进制命名的变量,命名毫无规律。
- 原理:将具有语义的变量名(如
-
控制流平坦化(Control Flow Flattening)
- 原理:将原本线性的执行流程转换为基于
switch-case的跳转表结构,配合label和break实现非线性执行。 - 识别特征:存在大量
switch语句,配合循环和跳转变量,执行顺序混乱。
- 原理:将原本线性的执行流程转换为基于
-
字符串加密
- 原理:将明文字符串(如
"secretKey")通过 Base64、Unicode、十六进制等方式加密存储,运行时动态解密。 - 识别特征:代码中无直观字符串,但频繁调用
atob()、String.fromCharCode()、\x61\x74\x6f\x62等解码函数。
- 原理:将明文字符串(如
-
自执行函数混淆(IIFE)
- 原理:使用
(function(){...})()结构封装逻辑,结合闭包隐藏作用域,防止外部直接访问内部变量。 - 识别特征:大量匿名函数立即执行,变量定义在函数内部,外部无法直接引用。
- 原理:使用
-
Obfuscator 混淆(OB 混淆)
- 原理:由
javascript-obfuscator等工具生成,通常包含大数组 + 字符串索引 + 控制流平坦化 + 变量混淆等多重技术。 - 识别特征:以
_0x开头的变量名、大数组集中存储字符串、大量push/shift操作。
- 原理:由
-
JSFuck / AA / JJ 混淆
- 原理:仅使用
! [] + () {}等符号构建完整逻辑(JSFuck),或通过Function/eval动态执行编码后的内容。 - 识别特征:代码完全不可读,仅由特殊符号组成;或包含
eval("...")、new Function(...)结构。
- 原理:仅使用
-
-
对应解决方案
-
基础还原:格式化 + 在线工具
- 使用
js-beautify格式化压缩代码,恢复缩进与换行,便于阅读。 - 利用在线反混淆平台(如 deobfuscator.io、sojson.com/jsjiemi.html) 自动处理简单混淆
- 使用
-
浏览器调试 + Hook 技术
- 通过 Chrome DevTools 的 Sources 面板 设置断点,观察变量值变化,定位关键函数(如
genToken)。 - 使用 Hook 技术 拦截
Function、eval、document.cookie等敏感操作,捕获动态生成的参数。
- 通过 Chrome DevTools 的 Sources 面板 设置断点,观察变量值变化,定位关键函数(如
-
AST(抽象语法树)解混淆
- 对于复杂混淆(如控制流平坦化、OB 混淆),需解析 JS 为 AST,再进行结构化还原:
- 工具链:
esprima(解析) +estraverse(遍历) +escodegen(生成) - 示例:将
switch跳转表还原为if-else逻辑,删除冗余代码块
- 工具链:
- 对于复杂混淆(如控制流平坦化、OB 混淆),需解析 JS 为 AST,再进行结构化还原:
-
Python 执行还原后的 JS
-
将还原后的 JS 逻辑保存为
.js文件,通过 Python 调用 Node.js 执行:1
2import subprocess
result = subprocess.check_output(['node', 'decrypt.js', 'arg1', 'arg2'])或使用
PyExecJS、execjs库直接在 Python 中运行 JS。
-
-
AI 辅助分析(GPT + Playwright)
- 针对 Cloudflare 5秒盾等高级反爬,可结合:
- Playwright:模拟真实浏览器环境,绕过指纹检测;
- GPT/Ollama:传入混淆代码,让 LLM 解释逻辑、提取 Token 生成规则,无需手动逆向。
- 针对 Cloudflare 5秒盾等高级反爬,可结合:
-
本地覆盖与环境补全
- 对于依赖浏览器全局对象(如
window、navigator)的 JS,需在 Node.js 中补全环境(如使用jsdom),或直接在浏览器中执行。
- 对于依赖浏览器全局对象(如
-
-
实战建议
- 优先尝试在线工具:对于简单 OB 混淆,
v_jstools、猿人学 OB 解混淆工具效果显著。 - 复杂场景走 AST 路线:控制流平坦化、多态变异等必须通过 AST 分析才能彻底还原。
- 避免硬扣代码:若网站频繁更新混淆逻辑,建议采用 浏览器自动化 + 动态执行 方案(如 Playwright + GPT),提高维护性。
- 优先尝试在线工具:对于简单 OB 混淆,
如果对方网站可以反爬取,封 ip 怎么办?
-
使用代理池
利用代理池可以实现从不同的 IP 地址发送请求,从而避免单个 IP 被封禁。在 Python 中,可以使用
requests库结合代理池来实现这一点。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import requests
from itertools import cycle
# 定义代理池
proxies = [
'http://10.10.1.10:3128',
'http://10.10.1.11:1080',
'http://10.10.1.12:3128',
# 其他代理地址...
]
proxy_pool = cycle(proxies)
# 使用代理池发送请求
url = 'https://example.com'
for i in range(10):
proxy = next(proxy_pool)
try:
response = requests.get(url, proxies={"http": proxy, "https": proxy})
print(response.text)
except requests.exceptions.RequestException as e:
print(f"Error with proxy {proxy}: {e}") -
使用代理服务
使用商业代理服务,如 Luminati、Bright Data、Oxylabs 等,这些服务提供高质量的代理 IP,通常具备较高的稳定性和匿名性。
1
2
3
4
5
6
7
8import requests
proxy = 'http://user:password@proxy-service.com:port'
url = 'https://example.com'
response = requests.get(url, proxies={"http": proxy, "https": proxy})
print(response.text) -
动态 IP 拨号
在本地或云服务器上通过拨号实现动态 IP 更换。可以使用 VPN 服务或设备重启来获取新的 IP 地址。
注意事项:
- 需要具备相应的网络环境和硬件条件。
- 这种方法可能需要手动或编程实现动态 IP 更换。
-
控制请求频率
通过降低请求频率和随机化请求间隔,减少被封禁的概率。可以在请求之间添加一定的延迟,模拟正常用户的行为。
1
2
3
4
5
6
7
8
9
10import time
import random
import requests
url = 'https://example.com'
for i in range(10):
response = requests.get(url)
print(response.text)
time.sleep(random.uniform(1, 3)) # 随机延迟 1-3 秒 -
分布式爬取
使用多个机器或云服务器进行分布式爬取,每个机器使用不同的 IP 地址和代理,从而降低单个 IP 被封禁的风险。
工具:
- Scrapy + Scrapy-Redis:实现分布式爬取和去重。
- Celery:分布式任务队列,可以将爬取任务分发到多个工作节点。
-
使用无头浏览器
利用无头浏览器(如 Selenium、Puppeteer)模拟真实的浏览器行为,这样可以绕过部分简单的反爬机制,同时也可以更好地处理 JavaScript 动态加载的内容。
1
2
3
4
5
6
7
8
9
10
11
12
13from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument('headless')
browser = webdriver.Chrome(options=options)
url = 'https://example.com'
browser.get(url)
html = browser.page_source
print(html)
browser.quit() -
动态代理替换
定期更换代理,避免单个代理长时间使用导致被封禁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import requests
import random
from itertools import cycle
proxies = [
'http://10.10.1.10:3128',
'http://10.10.1.11:1080',
'http://10.10.1.12:3128',
# 其他代理地址...
]
proxy_pool = cycle(proxies)
url = 'https://example.com'
for i in range(10):
proxy = next(proxy_pool)
response = requests.get(url, proxies={"http": proxy, "https": proxy})
print(response.text)
if i % 2 == 0: # 每 2 次请求更换一次代理
proxy = next(proxy_pool) -
模拟真实用户行为
模拟用户的浏览行为,如点击、滚动等操作,尽量让爬虫行为看起来更像真实用户。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18from selenium import webdriver
import time
options = webdriver.ChromeOptions()
options.add_argument('headless')
browser = webdriver.Chrome(options=options)
url = 'https://example.com'
browser.get(url)
time.sleep(2) # 停留一段时间
browser.execute_script('window.scrollTo(0, document.body.scrollHeight);') # 模拟滚动
time.sleep(2)
html = browser.page_source
print(html)
browser.quit()
爬虫滑块验证码解决方法
-
概述
滑块验证码是一种常见的反爬手段,它通过要求用户拖动滑块以完成某种图形匹配,从而确认用户为人类而不是机器人。解决滑块验证码较为复杂,因为它通常涉及图像处理和复杂的交互操作。
-
人工打码
将滑块验证码的截图发送到打码平台,由人工打码来完成验证。这种方法可靠但成本较高。
-
使用第三方打码平台
使用第三方打码平台,如 2Captcha、Anticaptcha 等,这些平台提供 API,可以自动解决滑块验证码。
-
使用无头浏览器模拟用户行为
使用无头浏览器(如 Selenium、Puppeteer)模拟用户拖动滑块的行为。这种方法复杂且不一定能保证成功,但有时会有效。
-
逆向工程和图像处理
通过逆向工程和图像处理技术,自动识别滑块验证码并完成验证。这种方法需要较高的技术水平和大量的开发工作。
步骤:
- 截图和图像处理:截取滑块验证码的图片并进行处理,识别滑块和目标位置。
- 计算位移:根据图像识别结果计算滑块需要移动的距离。
- 模拟拖动:使用无头浏览器或其他工具模拟拖动滑块。
-
使用机器学习
训练机器学习模型识别滑块验证码的图像特征,从而自动解码。这种方法需要大量的数据和相应的技术积累。
爬虫中对于数据库MySQL、PostgreSQL、MongoDB如何选择及原因?
-
概述
在 Python 爬虫项目中,选择合适的数据库对数据存储效率、系统可扩展性、开发便捷性及后期分析能力至关重要。MySQL、PostgreSQL 和 MongoDB 是三类主流数据库,分别代表关系型(SQL) 与 文档型(NoSQL) 的典型方案。
-
核心对比维度
维度 MySQL PostgreSQL MongoDB 数据模型 表结构(强 Schema) 表结构(强 Schema + JSONB 支持) 文档(BSON/JSON,Schema-less) 事务支持 强(ACID,InnoDB) 极强(ACID + 多版本并发控制 MVCC) 4.0+ 支持多文档事务(有限制) 扩展性 垂直扩展为主,分库分表复杂 类似 MySQL,但更重 水平扩展天然支持(分片 Sharding) 写入性能 高(批量插入快) 中高(WAL 日志影响) 极高(尤其非结构化数据) 查询能力 强(JOIN、子查询) 极强(窗口函数、GIS、全文检索) 灵活(嵌套查询、聚合管道) 适用数据 结构化表格数据 结构化 + 半结构化(JSONB) 非结构化 / 动态字段数据 -
按爬虫数据特征选型
-
场景 1:结构清晰、字段固定(如商品列表、股票行情)
- 推荐:MySQL 或 PostgreSQL
- 原因:
- 字段明确(标题、价格、链接等),适合建表;
- 后续需复杂查询(如“价格 > 100 且销量前 10”);
- 需要事务保证数据一致性(如去重后入库)。
- 选 MySQL 还是 PostgreSQL?
- MySQL:部署简单、生态成熟、适合 Web 快速开发;
- PostgreSQL:若需高级功能(如地理坐标、全文搜索、JSON 查询),选它。
- 示例:抓取京东商品 → 字段固定 → 存 MySQL。
-
场景 2:数据结构多变、字段不统一(如不同网站的新闻、评论)
-
推荐:MongoDB
-
原因:
- 无需预定义表结构,每条记录可含不同字段;
- 轻松存储原始 HTML、JSON API 响应;
- 插入速度快,适合高并发写入(如分布式爬虫)。
-
优势体现:
1
2
3# 可同时插入结构不同的文档
db.news.insert_one({"title": "A", "author": "X"})
db.news.insert_one({"title": "B", "tags": ["tech"], "source_url": "..."}) -
示例:爬取多个论坛评论 → 字段差异大 → 存 MongoDB。
-
-
场景 3:需要强一致性 + 复杂关联分析
- 推荐:PostgreSQL
- 原因:
- 支持多表 JOIN(如“用户-订单-商品”关联分析);
- 提供
JSONB类型,兼顾结构化与半结构化; - GIS 扩展(PostGIS)适合地理数据爬虫(如门店位置)。
- 示例:爬取外卖平台 → 需关联商家、菜品、配送范围 → PostgreSQL。
-
场景 4:海量数据 + 高吞吐写入
- 推荐:MongoDB(分片集群)
- 原因:
- 水平扩展简单,通过分片(Sharding)轻松应对 TB 级数据;
- 批量写入性能优异(10万条记录比单条快 15–20 倍);
- 适合日志类、监控类爬虫数据。
- 注意:若后续需复杂 SQL 分析,可采用 混合架构(见下文)。
-
-
性能与运维考量
数据库 优点 缺点 适合团队 MySQL 部署简单、工具链成熟、社区支持好 分库分表复杂、JSON 支持弱于 PG 初创团队、Web 开发者 PostgreSQL 功能强大、扩展性好、数据完整性高 内存占用高、学习曲线较陡 数据分析、GIS、金融类项目 MongoDB 灵活、高写入、易水平扩展 事务弱、JOIN 能力差、内存消耗大 大数据、快速迭代、非结构化场景 -
混合架构建议(大型爬虫系统)
对于企业级爬虫平台,可采用分层存储策略:
graph LR A[爬虫节点] --> B[MongoDB 存储原始数据] B --> C(ETL 清洗/结构化) C --> D[PostgreSQL/MySQL 存分析用数据] D --> E[BI 工具 / API 服务]- MongoDB:接收原始网页、API 响应,保留完整信息;
- PostgreSQL/MySQL:存储清洗后的结构化数据,供报表、API 使用。
优势:兼顾灵活性与分析能力,避免“为查询牺牲写入性能”。
-
Python 驱动与代码示例
数据库 推荐驱动 示例 MySQL PyMySQL/mysql-connector-pythoncursor.execute("INSERT ...")PostgreSQL psycopg2cur.execute("INSERT INTO ...")MongoDB PyMongocollection.insert_one(doc) -
总结:决策流程图
flowchart TD A[你的爬虫数据是否结构固定?] A -->|是| B{是否需要复杂查询/事务?} A -->|否| C{数据是否海量/高并发写入?} B -->|是| D[PostgreSQL(高级需求)或 MySQL(简单需求)] B -->|否| E[SQLite(小项目)或 MySQL] C -->|是| F[MongoDB] C -->|否| G[考虑 JSON 文件 + 后期导入数据库]一句话口诀:“结构定,用 SQL;结构变,用 Mongo;要分析,选 PG;求简单,用 MySQL。”
什么是结构化数据?什么是非结构化数据?
-
概述
结构化数据与非结构化数据是现代数据管理中的两大基本类型,它们在组织形式、存储方式、处理难度和应用场景上存在根本性差异。理解二者区别,对构建高效的数据系统(如爬虫、AI训练、商业智能等)至关重要。
-
结构化数据(Structured Data)
-
定义
结构化数据是指具有预定义格式、固定字段和明确数据模型的数据,通常以行和列的表格形式组织,每一列有确定的含义、顺序和数据类型
-
核心特征
- 有明确的含义:每个字段代表什么内容是已知的(如
user_id表示用户编号); - 有严格一致的顺序:所有记录遵循相同的字段排列;
- 有明确的数据类型:如整数、日期、字符串等,不允许混用(如年龄不能同时用
20和"二十")。
- 有明确的含义:每个字段代表什么内容是已知的(如
-
典型示例
- 关系型数据库表(MySQL、PostgreSQL 中的订单表、用户表);
- Excel / CSV 文件(带表头的表格);
- 在线交易记录(时间戳、金额、商品ID、用户ID);
- 股票行情数据(开盘价、收盘价、成交量)。
-
存储与处理
- 存储方式:关系数据库(RDBMS)、数据仓库;
- 查询语言:SQL(如
SELECT * FROM users WHERE age > 18); - 优势:易于查询、分析、验证,适合机器学习算法直接使用。
- ✅一句话总结:结构化数据 = “整齐划一的表格”,计算机可直接理解。
-
-
非结构化数据(Unstructured Data)
-
定义
非结构化数据是指没有预定义数据模型、格式不规则或不完整的数据,其内容和结构由自然形式决定,无法直接放入二维表中。
-
核心特征
- 格式多样性:文本、图像、音频、视频、PDF、社交媒体帖子等;
- 缺乏统一结构:每条数据可能完全不同;
- 难以直接查询:不能用 SQL 直接检索内容(如“找出所有提到‘AI’的视频”);
- 高价值但高处理成本:蕴含丰富语义信息,但需 NLP、CV 等技术解析。
-
典型示例
- 文本类:电子邮件、新闻文章、客服聊天记录、微博评论;
- 多媒体:照片(JPEG)、监控视频(MP4)、播客(MP3);
- 文档:PDF 报告、Word 合同、PPT 演示文稿;
- 日志与传感器数据:服务器日志、IoT 设备流数据。
-
存储与处理
- 存储方式:文件系统、对象存储(如 AWS S3)、数据湖(Data Lake);
- 处理技术:
- 自然语言处理(NLP)→ 分析文本情感;
- 计算机视觉(CV)→ 识别图像内容;
- 语音识别 → 将音频转为文本;
- 挑战:数据量大(占企业数据 80% 以上)、质量不一、集成困难。
- ✅ 一句话总结:非结构化数据 = “原始的自然信息”,需“翻译”才能被机器理解。
-
-
对比总结
维度 结构化数据 非结构化数据 数据模型 预定义、固定 Schema 无 Schema 或动态 Schema 格式 表格(行/列) 文本、图像、音频、视频等 存储 关系数据库(MySQL, Oracle) 数据湖、文件系统、NoSQL(如 MongoDB) 查询 SQL 直接查询 需全文检索、AI 模型解析 占比 约 20% 约 80% 以上 处理难度 低 高 典型用途 交易系统、报表分析 情感分析、图像识别、知识挖掘 -
补充:半结构化数据(Semi-structured Data)
介于两者之间,有部分结构但不遵循严格表模型,常见于:
- JSON、XML、HTML;
- 邮件(含元数据 + 正文);
- NoSQL 数据库文档(如 MongoDB 的 BSON)。
特点:自描述性(标签说明含义),但字段顺序和类型不强制统一。
-
实际意义
- 爬虫开发:结构化数据可直接入库;非结构化数据需清洗、OCR、NLP 处理后才能结构化;
- AI 训练:大模型依赖海量非结构化文本,但微调常需结构化标注数据;
- 企业决策:结构化数据回答“是什么”(What),非结构化数据揭示“为什么”(Why)。
- 💡 关键洞察:现代数据系统的核心能力之一,就是将非结构化数据转化为结构化数据,从而释放其商业价值。
Python 基础
Python垃圾回收机制?
-
概述
Python 的垃圾回收(Garbage Collection,简称 GC)机制是其自动内存管理的核心组成部分,旨在自动识别并释放不再被程序使用的对象所占用的内存,从而防止内存泄漏、提升程序稳定性与性能。CPython(Python 官方实现)采用 “引用计数 + 标记-清除 + 分代回收” 三重机制协同工作,兼顾实时性、效率与循环引用处理能力。
-
引用计数(Reference Counting)—— 实时回收的基础
每个 Python 对象在底层都包含一个
ob_refcnt字段(位于PyObject结构体中),用于记录当前有多少变量或容器引用该对象。- 增加引用:对象被赋值给新变量、作为函数参数传递、存入列表/字典等容器。
- 减少引用:变量被
del删除、重新赋值、离开作用域或容器被销毁。 - 回收时机:当引用计数降为 0 时,对象立即被销毁,内存被释放。
优点:
- 实时性强,无延迟;
- 回收单个对象开销极小;
- 对象生命周期确定。
缺点:
- 无法处理循环引用(如 A 引用 B,B 又引用 A,即使外部无引用,计数仍 ≥1);
- 维护引用计数本身有性能开销。
示例:
1
2
3
4a = [1, 2]
b = a # 引用计数变为 2
del a # 计数减为 1
del b # 计数为 0,列表立即回收 -
标记-清除(Mark and Sweep)—— 解决循环引用的兜底机制
为应对引用计数无法回收的循环引用问题,Python 引入了基于可达性分析的标记-清除算法。
- 标记阶段:从“根对象”(如全局变量、栈帧中的局部变量)出发,遍历所有可达对象,并打上“存活”标记。
- 清除阶段:扫描所有对象,未被标记的即为“不可达”的垃圾对象(包括循环引用闭环),予以回收。
注意:此机制不适用于所有对象,仅作用于可能参与循环引用的容器对象(如 list、dict、class 实例等),基本类型(int、str)因不可变且不引用其他对象,不参与此过程。
-
分代回收(Generational GC)—— 提升回收效率的优化策略
Python 将对象按“存活时间”分为三代(Generation 0/1/2):
- 0 代:新创建的对象;
- 1 代:经历过一次 GC 仍存活的对象;
- 2 代:长期存活的“老”对象。
触发规则(默认阈值):
- 0 代回收:当
新分配对象数 - 已回收对象数 ≥ 700时触发; - 1 代回收:每完成 10 次 0 代回收后触发;
- 2 代回收:每完成 10 次 1 代回收后触发(即约每 100 次 0 代回收)。
设计思想:
“大多数对象都是短命的”,因此优先高频扫描新生代,减少对老年代的干扰,提升整体效率。
-
垃圾回收的触发方式
-
自动触发
- 引用计数为 0 → 立即回收;
- 分代回收阈值达到 → 自动执行标记-清除;
- 内存不足时 → 触发全面 GC(紧急回收)
-
手动触发
-
通过
gc模块主动干预1
2import gc
gc.collect() # 扫描所有代,强制回收不可达对象 -
适用于:
- 批量任务结束后清理内存;
- 长期运行服务定期维护;
- 调试内存泄漏。
-
-
-
特殊情况与限制
- 含
__del__方法的循环引用对象:GC 可能无法回收,因析构顺序不确定,CPython 会将其放入gc.garbage列表,需手动处理。 - 弱引用(weakref):可打破循环引用,避免 GC 压力。
- 回收时机不可精确预测:分代阈值会动态调整,无法保证固定时间触发。
- 含
-
监控与调优工具
gc.get_count():查看各代当前对象数量;gc.get_threshold()/gc.set_threshold():获取或设置回收阈值;gc.set_debug(gc.DEBUG_LEAK):开启调试,追踪不可回收对象;- 第三方工具:
objgraph、memory_profiler用于分析内存增长与泄漏
-
总结
Python 的垃圾回收机制是一个多层次、自适应的系统:
- 引用计数处理 90% 以上的常规对象,实现实时释放;
- 标记-清除 + 分代回收作为补充,专门解决循环引用并优化性能;
- 三者协同,在“及时回收”与“低性能开销”之间取得平衡。
理解这一机制,有助于开发者写出更高效、更稳定的 Python 程序,尤其在处理大规模数据、长期运行服务或复杂对象关系时至关重要。
Python内存泄漏及解决方案
-
概述
Python 虽然具备自动垃圾回收(GC)机制,但仍可能发生内存泄漏。内存泄漏指程序在运行过程中未能释放不再使用的内存,导致内存占用持续增长,最终可能引发系统性能下降、响应变慢,甚至
MemoryError或服务崩溃。 -
Python 内存泄漏的常见原因
-
循环引用(Circular References)
-
问题:两个或多个对象相互强引用(如 A 引用 B,B 又引用 A),即使外部无引用,引用计数仍 > 0,无法被引用计数机制回收。
-
示例:
1
2
3
4
5
6
7
8
9
10
11class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
a = Node("A")
b = Node("B")
a.children.append(b)
b.parent = a # 形成双向强引用 → 循环引用
del a, b # 对象未被回收! -
注意:虽然 Python 的 GC 能处理大多数循环引用,但若对象包含
__del__方法,GC 可能放弃回收,将其放入gc.garbage。
-
-
全局变量或缓存无限增长
-
问题:将对象存入全局字典/列表,且无淘汰机制,导致内存持续累积。
-
示例:
1
2
3
4
5CACHE = {}
def get_data(key):
if key not in CACHE:
CACHE[key] = load_from_db(key) # 缓存无限增长
return CACHE[key]
-
-
闭包意外持有大对象引用
-
问题:闭包函数捕获了外部作用域的大对象(如大型列表、DataFrame),即使外层函数已执行完毕,该对象仍被闭包持有,无法释放。
-
示例:
1
2
3
4
5
6
7
8def make_processor():
big_data = [i for i in range(10_000_000)] # 大对象
def process(x):
return x + 1 # 但闭包隐式引用 big_data
return process
func = make_processor()
# 此时 big_data 仍驻留内存!
-
-
事件监听器/回调未解绑
-
问题:在 GUI、异步框架(如 asyncio)或自定义事件系统中注册回调,但未在对象销毁时移除,导致对象被长期引用。
-
示例:
1
2
3
4
5
6
7observers = []
class Listener:
def on_event(self): pass
listener = Listener()
observers.append(listener.on_event) # 持有方法引用
del listener # 实例仍被 observers 引用,无法回收
-
-
未正确关闭资源
-
问题:文件、数据库连接、网络套接字等未显式关闭,底层资源未释放,可能伴随内存占用。
-
反例:
1
2f = open("large_file.txt")
data = f.read() # 忘记 f.close()
-
-
C 扩展模块内存管理缺陷
- 如 NumPy、Pandas 等 C 扩展若存在 bug,或 Cython 代码未正确管理引用计数,也可能导致泄漏。
-
-
内存泄漏的检测方法
-
使用
tracemalloc(Python 3.4+ 内置)追踪内存分配源头:
1
2
3
4
5
6
7
8
9import tracemalloc
tracemalloc.start()
# ... 执行可疑代码 ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:5]:
print(stat) # 显示内存分配最多的代码行 -
使用
gc模块检查不可达对象1
2
3
4
5import gc
gc.set_debug(gc.DEBUG_LEAK)
gc.collect()
print(f"无法回收的对象数: {len(gc.garbage)}")
print(gc.garbage) # 查看具体对象 -
使用
objgraph可视化引用关系1
pip install objgraph
1
2
3import objgraph
objgraph.show_most_common_types() # 显示最常见对象类型
objgraph.show_backrefs([some_obj], filename='refs.png') # 生成引用图 -
使用
memory_profiler逐行分析1
pip install memory-profiler
1
2
3
4
5
6from memory_profiler import profile
def my_func():
data = [i for i in range(100000)]
return sum(data)运行:
python -m memory_profiler script.py
-
-
解决方案与最佳实践
-
打破循环引用
使用 弱引用(
weakref):1
2
3
4
5
6
7
8
9
10
11import weakref
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def add_child(self, child):
child.parent = weakref.ref(self) # 弱引用父节点
self.children.append(child) -
限制缓存大小
-
使用
functools.lru_cache:1
2
3
4
5from functools import lru_cache
def expensive_func(x):
return x ** 2 -
或使用
WeakValueDictionary(值为弱引用):1
2import weakref
cache = weakref.WeakValueDictionary()
-
-
及时释放资源
-
始终使用 上下文管理器(
with):1
2
3with open("file.txt") as f:
data = f.read()
# 文件自动关闭
-
-
解绑事件监听器
-
提供
unsubscribe或destroy方法:1
2
3
4
5
6
7
8
9class EventManager:
def __init__(self):
self.handlers = []
def register(self, handler):
self.handlers.append(handler)
def unregister(self, handler):
self.handlers.remove(handler)
-
-
定期手动触发 GC(谨慎使用)
-
在批量任务后清理:
1
2import gc
gc.collect() # 强制回收
-
-
避免在闭包中捕获大对象
-
显式删除或仅捕获必要数据:
1
2
3
4
5
6def make_processor():
big_data = [...]
result = len(big_data) # 仅保留所需信息
def process(x):
return x + result
return process
-
-
-
总结
问题类型 解决方案 循环引用 weakref、手动清空引用全局缓存膨胀 LRU、WeakValueDictionary闭包持有大对象 避免捕获、提前提取必要数据 事件未解绑 显式注销监听器 资源未关闭 使用 with语句C 扩展问题 升级库版本、检查文档 核心原则:“谁引用,谁负责释放”。即使有 GC,也要主动管理对象生命周期,尤其在长期运行的服务(如 Web 后端、爬虫、数据管道)中。
Python 有哪些数据类型以及彼此间的区别?
| 数据 | 类型 | 值/表示方法 | 区别 |
|---|---|---|---|
| 整型 | int | 不可变 | |
| 浮点型 | float | 不可变 | |
| 布尔型 | bool | true/false | |
| 空 | NoneType | None | |
| 字符串 | str | 有序;可重复 | |
| 列表 | list | [] | 有序;可变;可重复 |
| 元组 | tuple | () | 有序;不可变;可重复 |
| 集合 | set | {} | 无序;不重复 |
| 字典 | dict | { key : value } | 无序;可变 |
深拷贝和浅拷贝的区别?
-
浅拷贝(Shallow Copy)
- 定义:创建一个新对象,但不会递归复制原始对象内部的子对象,而是直接引用子对象的内存地址。
- 特点:
- 新对象和原对象是独立的,但它们的子对象是共享的(修改子对象会影响双方)。
- 适用于不可变子对象(如整数、字符串)或不需要独立子对象的情况。
- 实现方式:
copy.copy()- 列表的切片操作
list.copy()或list[:] - 字典的
dict.copy()
- 示例:
1
2
3
4
5
6
7
8import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
# 修改浅拷贝的子对象会影响原对象!
shallow[0][0] = 99
print(original) # 输出: [[99, 2], [3, 4]]
-
深拷贝(Deep Copy)
- 定义:创建一个新对象,并递归复制原始对象及其所有子对象,完全独立于原对象。
- 特点:
- 新对象和原对象完全隔离,修改任何一方都不会影响另一方。
- 适用于需要完全独立的副本(尤其是可变子对象如列表、字典)。
- 实现方式:
copy.deepcopy()
- 示例:
1
2
3
4
5
6
7
8import copy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
# 修改深拷贝的子对象不会影响原对象
deep[0][0] = 99
print(original) # 输出: [[1, 2], [3, 4]]
-
关键区别总结
特性 浅拷贝 深拷贝 复制层级 仅顶层对象 递归复制所有子对象 子对象是否共享 是(修改子对象会影响原对象) 否(完全独立) 性能 更快(不复制子对象) 较慢(需递归复制) 适用场景 子对象不可变或无需独立 子对象可变且需要完全独立副本 -
特殊情况注意
- 不可变对象(如元组、字符串、整数):浅拷贝和深拷贝效果相同(因为它们无法修改)。
- 自定义对象:可通过实现
__copy__()和__deepcopy__()方法控制拷贝行为。
什么是闭包?
-
闭包的定义和行为
- 一个外部函数。
- 在外部函数内部定义的一个或多个内部函数。
- 外部函数返回内部函数,并且内部函数引用了外部函数的变量或参数。
-
闭包的特点
- 变量捕获:内部函数可以捕获并记住其外部函数的变量,即使外部函数已经执行完毕。
- 持久化作用域:闭包使得外部函数的作用域在其返回后依然存在。
-
示例
1
2
3
4
5
6
7
8
9
10
11def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure = outer_function(10)
result = closure(5) # 15
print(result) -
闭包的应用场景
- 工厂函数:闭包可以用于创建带有特定配置或初始状态的函数。
- 数据隐藏:闭包可以用于实现数据封装和隐藏,保护数据不被外部直接访问和修改。
- 回调和事件处理:闭包可以用于管理回调函数和事件处理,捕获特定上下文中的变量。
- 缓存和记忆化:闭包可以用于实现缓存和记忆化,保存以前计算的结果以提高性能。
-
需要注意的事项
- 内存泄漏:如果闭包捕获了大量变量或占用大量资源,可能导致内存泄漏。
- 性能问题:过度使用闭包可能会引入不必要的复杂性和性能开销。
- 调试困难:闭包中变量的捕获和作用域可能使调试变得更加困难。
什么是装饰器?
-
概述
装饰器(Decorator)是 Python 中一个非常强大的功能,用于在不修改原有函数或方法的情况下,动态地给其增加额外的功能。装饰器本质上是一个函数,它可以接收另一个函数作为参数,并返回一个新的函数。装饰器广泛应用于日志记录、权限检查、性能统计、缓存等场景。
-
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_hello():
print("Hello!")
say_hello()
什么是可迭代对象?
-
概述
在 Python 中,可迭代对象(Iterable)是指可以返回一个迭代器的对象。简单来说,可迭代对象是可以逐一访问其元素的对象。常见的可迭代对象包括列表(list)、元组(tuple)、字符串(string)、字典(dictionary)、集合(set)等。一个对象要成为可迭代对象,必须实现
__iter__()方法,或者实现__getitem__()方法并且从索引 0 开始。 -
可迭代对象的定义
- 实现
__iter__()方法:返回一个迭代器对象,该迭代器对象实现了__next__()方法。 - 实现
__getitem__()方法:允许按索引访问其元素,且从索引 0 开始索引。
- 实现
什么是迭代器?
-
概述
在 Python 中,迭代器(Iterator)是一个实现了迭代协议的对象。迭代协议包括
__iter__()方法和__next__()方法。迭代器的主要目的是提供一种访问集合元素的方式,而不需要暴露集合内部的实现细节。迭代器可以被用于for循环、生成器表达式和许多其他需要逐个访问元素的场景。 -
迭代器的定义
__iter__(): 返回迭代器对象自身。这使得迭代器对象也可以用在for循环等上下文中。__next__(): 返回容器的下一个元素,如果没有元素了,则抛出StopIteration异常。
-
迭代器的特性
- 惰性求值: 迭代器一次只计算一个值,这对于处理大数据集或无限序列非常有用。
- 一次性使用: 迭代器一旦遍历完所有元素,就无法重置或重新使用。
可迭代对象与迭代器的区别?
- 迭代器(Iterator):不仅实现了
__iter__()方法,还实现了__next__()方法。 - 可迭代对象(Iterable):只需要实现
__iter__()方法或__getitem__()方法。
什么是生成器?
-
概述
生成器(Generator)是 Python 中的一种特殊类型的迭代器,它使得我们可以使用简单的代码生成复杂的迭代序列。生成器通过定义一个包含
yield表达式的函数来创建。每次调用yield时,函数会生成一个值并暂停其状态,直到下次继续执行。这种特性使得生成器特别适合处理大量数据或无限序列。 -
生成器的特性
- 惰性求值:生成器每次只生成一个值,不会一次性生成所有值。这对于处理大数据集或无限序列非常有用,因为它降低了内存消耗。
- 状态保存:生成器会自动保存上次离开的位置和状态信息,下次从暂停的位置继续执行。
- 一次性使用:生成器一旦遍历完所有元素,就无法重置或重新使用。
-
生成器的创建
- 生成器可以通过生成器函数和生成器表达式这两种方式来创建。
- 生成器函数
生成器函数是包含yield关键字的普通函数。每次调用yield时,函数会生成一个值并暂停其执行,直到下次调用next()时继续执行。1
2
3
4
5
6
7
8
9
10
11
12def my_generator(start, end):
current = start
while current < end:
yield current
current += 1
# 使用生成器函数
gen = my_generator(1, 5)
for num in gen:
print(num) - 生成器表达式
生成器表达式是一种简洁的生成器创建方式,类似于列表推导式,但使用圆括号()。1
2
3
4
5gen = (x * x for x in range(5))
for num in gen:
print(num)
-
生成器与普通函数的区别
- 返回值:普通函数使用
return返回值并终止函数执行,而生成器使用yield暂停执行并返回一个值。 - 状态保存:普通函数在每次调用时都会重新开始执行,而生成器会保存上次离开的状态和位置。
- 返回值:普通函数使用
-
生成器的优点
- 内存效率:生成器不会一次性生成所有值,节省内存。
- 代码简洁:生成器使得编写复杂的迭代逻辑更加简单和直观。
- 惰性求值:生成器可以按需生成值,适合处理流式数据或无限序列。
-
使用生成器的场景
- 处理大数据集:生成器可以逐个处理数据,避免一次性加载大量数据到内存中。
- 流式数据处理:生成器可以按需生成数据,适合处理实时数据流。
- 无限序列:生成器可以用于生成无限序列,例如 Fibonacci 数列等。
什么是单例模式?有哪些实现方法?有哪些使用场景?
-
概述
单例模式(Singleton Pattern)是一种设计模式,其目的是确保一个类在应用程序生命周期中仅有一个实例,并提供一个全局访问点来访问该实例。单例模式通常用于控制对共享资源的访问,比如数据库连接、配置文件、日志记录等。
-
单例模式的实现方法
-
使用类属性
通过类属性来存储单例实例,并在
__new__方法中控制实例的创建。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
# 使用示例
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # 输出: True -
使用装饰器
通过装饰器来实现单例模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
class Singleton:
pass
# 使用示例
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # 输出: True -
使用元类
通过元类(metaclass)来控制类的实例化过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
pass
# 使用示例
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # 输出: True
-
-
单例模式的使用场景
- 日志记录:通常需要一个全局的日志记录对象,以便于在应用程序的各个部分进行统一的日志记录。
- 配置管理:在应用程序中,配置文件通常只有一个实例,用于存储全局配置。
- 数据库连接:对于数据库连接池或数据库引擎实例,通常需要确保只有一个实例,以便共享连接资源。
- 线程池:在多线程环境中,线程池通常只有一个实例,以便于管理和分配线程资源。
- 缓存:在需要全局缓存的数据时,使用单例模式可以确保缓存实例的唯一性。
-
注意事项
虽然单例模式在某些场景下非常有用,但也需要谨慎使用。滥用单例模式可能会导致代码难以测试和维护,因为全局的单例实例会增加代码的耦合性。此外,单例模式还可能导致多线程环境下的竞态条件,需要小心处理并发问题。
hashlib 加密后的内容分别有几位?
在Python中,hashlib模块提供了多种哈希算法用于生成固定长度的哈希值。这些哈希算法包括常见的MD5、SHA-1、SHA-224、SHA-256、SHA-384、SHA-512等。每种哈希算法生成的哈希值长度是固定的,具体长度如下:
| 哈希算法 | 哈希值位数(字符数) | 哈希值字节数 |
|---|---|---|
| MD5 | 32 | 16 |
| SHA-1 | 40 | 20 |
| SHA-224 | 56 | 28 |
| SHA-256 | 64 | 32 |
| SHA-384 | 96 | 48 |
| SHA-512 | 128 | 64 |
1 | import hashlib |
列表和元组有什么区别?
- 列表:可变,使用方括号([]),适合需要频繁修改数据的场景,性能可能会稍微差一些,特别是当列表很大时。
- 元组:不可变,使用小括号(()),适合不需要修改的数据,或者需要作为字典键的场景,在某些情况下比列表更高效。
Python 中 append,insert 和 extend 的区别?
-
append()append方法用于在列表的末尾追加一个元素。它只接受一个参数,该参数可以是任何类型的对象(数字、字符串、列表等)。 -
insert()insert方法用于在列表的指定位置插入一个元素。它接受两个参数:第一个参数是插入的位置索引,第二个参数是要插入的元素。 -
extend()extend方法用于将另一个可迭代对象(如列表、元组、字符串等)的所有元素添加到当前列表的末尾。它接受一个参数,该参数必须是一个可迭代对象。
break、continue、pass 是什么?
-
breakbreak语句用于终止循环(for或while)。当break语句被执行时,循环立即结束,程序控制流继续执行循环之后的代码。 -
continuecontinue语句用于跳过当前循环的剩余代码,直接进入下一次循环迭代。它会终止当前迭代的剩余语句并回到循环的开始。 -
passpass语句是一个空操作,占位符。它什么也不做,但可以用在需要一个语句而实际没有具体操作的场合。pass语句通常用于编写占位代码,以便稍后填充实际实现。
区分 Python 中的 remove、del 和 pop?
-
remove()remove方法用于删除列表中第一个匹配的指定值。如果该值在列表中不存在,会引发ValueError异常。1
2
3
4
5
6
7my_list = [1, 2, 3, 2, 4]
my_list.remove(2)
print(my_list) # 输出: [1, 3, 2, 4]
# 元素不存在时会报错
# my_list.remove(5) # ValueError: list.remove(x): x not in list -
deldel语句用于删除列表中指定位置的元素或整个列表。与remove和pop不同,del不是列表的方法,而是 Python 的一个语句,可以删除任何对象,但常用于删除列表中的元素。1
2
3
4
5
6
7
8
9
10
11
12my_list = [1, 2, 3, 4]
del my_list[1]
print(my_list) # 输出: [1, 3, 4]
# 删除一个切片
del my_list[1:3]
print(my_list) # 输出: [1]
# 删除整个列表
del my_list
# print(my_list) # NameError: name 'my_list' is not defined -
pop()pop方法用于删除列表中指定位置的元素,并返回该元素。默认情况下,它删除并返回列表的最后一个元素。如果指定了索引,则删除该索引位置的元素并返回。1
2
3
4
5
6
7
8
9
10
11
12
13my_list = [1, 2, 3, 4]
element = my_list.pop()
print(element) # 输出: 4
print(my_list) # 输出: [1, 2, 3]
# 删除并返回指定位置的元素
element = my_list.pop(1)
print(element) # 输出: 2
print(my_list) # 输出: [1, 3]
# 索引超出范围时会报错
# my_list.pop(5) # IndexError: pop index out of range
== 和 is 的区别是?
-
==运算符==运算符用于比较两个对象的值是否相等。换句话说,它检查两个对象在内容上是否相等。1
2
3
4
5
6
7a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # 输出: True
print(a == c) # 输出: True -
is运算符is运算符用于比较两个对象的身份,即它们是否是同一个对象。它检查两个对象在内存中的地址是否相同。1
2
3
4
5
6
7a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a is b) # 输出: False
print(a is c) # 输出: True
如何更改列表的数据类型?
-
使用列表推导式
列表推导式是一种简洁且高效的方法,用于创建和转换列表。可以通过列表推导式将列表中的每个元素转换为所需的数据类型。
1
2
3
4
5
6
7
8
9
10# 将字符串列表转换为整数列表
str_list = ['1', '2', '3', '4']
int_list = [int(x) for x in str_list]
print(int_list) # 输出: [1, 2, 3, 4]
# 将整数列表转换为字符串列表
int_list = [1, 2, 3, 4]
str_list = [str(x) for x in int_list]
print(str_list) # 输出: ['1', '2', '3', '4'] -
使用
map函数map函数适用于将一个函数应用于一个或多个序列中的每一个元素,并返回一个迭代器。可以使用list函数将返回的迭代器转换为列表。1
2
3
4
5
6
7
8
9
10# 将字符串列表转换为整数列表
str_list = ['1', '2', '3', '4']
int_list = list(map(int, str_list))
print(int_list) # 输出: [1, 2, 3, 4]
# 将整数列表转换为字符串列表
int_list = [1, 2, 3, 4]
str_list = list(map(str, int_list))
print(str_list) # 输出: ['1', '2', '3', '4'] -
使用循环
可以使用循环遍历列表并手动转换每个元素的数据类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 将字符串列表转换为整数列表
str_list = ['1', '2', '3', '4']
int_list = []
for x in str_list:
int_list.append(int(x))
print(int_list) # 输出: [1, 2, 3, 4]
# 将整数列表转换为字符串列表
int_list = [1, 2, 3, 4]
str_list = []
for x in int_list:
str_list.append(str(x))
print(str_list) # 输出: ['1', '2', '3', '4'] -
使用
numpy数组(适用于数值数据)如果列表中元素都是数值数据,可以使用
numpy库进行高效的数据类型转换。1
2
3
4
5
6
7
8import numpy as np
# 将整数列表转换为浮点数列表
int_list = [1, 2, 3, 4]
float_array = np.array(int_list, dtype=float)
float_list = float_array.tolist()
print(float_list) # 输出: [1.0, 2.0, 3.0, 4.0]
什么是 lambda 函数?
-
概述
lambda函数,也称为匿名函数,是一种在 Python 中用于创建小型、单行函数的简洁方法。与常规的使用def关键字定义的函数不同,lambda函数没有名字,通常用于需要一个简单函数但不想正式定义一个函数的场景。 -
语法
lambda函数的基本语法如下:1
lambda 参数1, 参数2, ... : 表达式
lambda关键字引导。- 后跟一个或多个用逗号分隔的参数。
- 冒号
:分隔参数和表达式。 - 表达式是计算并返回的内容。
-
示例
1
2
3
4# 定义一个 lambda 函数,计算两个数的和
add = lambda x, y: x + y
print(add(3, 5)) # 输出: 8
解释 Python 中的 Map 函数?
-
概述
map函数是 Python 的一个内置函数,用于将一个指定的函数应用到一个或多个序列(如列表、元组等)的每一个元素,并返回一个迭代器,包含应用函数后的结果。map函数对于需要对序列中的每个元素进行相同操作的场景非常有用。 -
语法
1
map(function, iterable, ...)
function:一个函数对象,map会将此函数应用到给定的iterable的每一个元素。iterable:一个或多个序列(如列表、元组、字符串等)。
-
示例
1
2
3
4numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x ** 2, numbers)
print(list(squared_numbers)) # 输出: [1, 4, 9, 16, 25]
解释 Python 中的 Filter 函数?
-
概述
filter函数是 Python 的一个内置函数,用于筛选序列中的元素。它接受一个函数和一个可迭代对象(如列表、元组、字符串等),并返回一个迭代器,包含所有使该函数返回True的元素。 -
语法
1
filter(function, iterable)
function:一个函数对象,用于测试可迭代对象中的每个元素。该函数应返回布尔值True或False。iterable:一个可迭代对象(如列表、元组、字符串等),其元素将被逐一传递给function。
-
示例
1
2
3
4numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # 输出: [2, 4, 6, 8, 10]
解释 Python 中 reduce 函数?
-
概述
reduce函数是 Python 中functools模块提供的一个函数,用于对序列中的元素进行累积操作。与map和filter不同,reduce函数会将序列中的元素逐步聚合为一个单一的结果。 -
语法
1
2
3
4from functools import reduce
reduce(function, iterable[, initializer])function:一个二元函数,即接受两个参数的函数。reduce会将此函数应用于可迭代对象的元素。iterable:一个可迭代对象(如列表、元组等),其元素将被逐一传递给function。initializer(可选):一个初始值。如果提供,它将作为计算的初始值,并与可迭代对象的第一个元素一同传递给function。
-
工作原理
reduce函数从可迭代对象的第一个元素开始,将前两个元素传递给function,然后将结果与第三个元素一起传递给function,以此类推,直到处理完所有元素。最终的结果是单一的聚合值。 -
示例
1
2
3
4
5
6
7
8
9
10from functools import reduce
numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers)
print(result) # 输出: 15
numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers, 10)
print(result) # 输出: 25
解释 Python 中的 pickling 和 unpickling?
-
概述
在 Python 中,pickling 和 unpickling 是用于对象序列化和反序列化的过程。这两个术语源自 Python 的
pickle模块,该模块提供了这些功能。 -
Pickling(序列化)
Pickling 是将 Python 对象(如列表、字典、类实例等)转换为字节流的过程。这个字节流可以被存储在文件中,通过网络传输,或者用于其他需要持久化或传输对象的场景。
1
2
3
4
5
6
7
8
9
10
11
12import pickle
data = {
'name': 'Alice',
'age': 30,
'scores': [85, 90, 92]
}
# 序列化对象到文件
with open('data.pkl', 'wb') as file:
pickle.dump(data, file) -
Unpickling(反序列化)
Unpickling 是将字节流转换回 Python 对象的过程。这个过程与 pickling 相反,可以从文件中读取字节流,或者从网络接收字节流,然后将其转换回原始的 Python 对象。
1
2
3
4
5
6
7
8import pickle
# 从文件反序列化对象
with open('data.pkl', 'rb') as file:
data = pickle.load(file)
print(data) # 输出: {'name': 'Alice', 'age': 30, 'scores': [85, 90, 92]} -
注意事项
- 安全性:Unpickling 可能会执行任意代码,因此在使用
pickle模块时需要格外小心。不要 unpickle 来自不可信或未经验证的源的数据。 - 兼容性:不同版本的 Python 可能会有不同的
pickle协议,因此在不同版本的 Python 之间共享 pickle 文件时需要注意兼容性问题。 - 性能:Pickling 和 unpickling 可能会引入性能开销,特别是对于大型或复杂的数据结构。
- 安全性:Unpickling 可能会执行任意代码,因此在使用
-
优点
- 灵活性:
pickle模块可以处理几乎所有 Python 对象,包括自定义类实例。 - 方便性:通过简单的
pickle.dump和pickle.load函数,可以轻松实现对象的序列化和反序列化。
- 灵活性:
-
使用场景
- 数据持久化:将对象保存到文件中,以便稍后恢复。
- 网络传输:通过网络发送 Python 对象。
- 缓存:将计算结果缓存起来,以便后续快速访问。
解释 *args 和 **kwargs?
-
概述
在 Python 中,
*args和**kwargs是用于函数定义中以处理可变数量的参数的两种特殊语法。它们使函数能够接受任意数量的位置参数和关键字参数,这在编写灵活且通用的函数时非常有用。 -
*args(位置参数)*args允许你传递可变数量的位置参数给函数。在函数内部,这些参数将被收集到一个元组中。1
2
3
4
5
6
7def my_function(*args):
for arg in args:
print(arg)
my_function(1, 2, 3, 4) # 输出: 1 2 3 4 -
**kwargs(关键字参数)**kwargs允许你传递可变数量的关键字参数给函数。在函数内部,这些参数将被收集到一个字典中。1
2
3
4
5
6
7
8
9
10
11def my_function(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
my_function(name="Alice", age=30, city="New York")
# 输出:
# name: Alice
# age: 30
# city: New York -
组合使用
你可以在同一个函数中同时使用
*args和**kwargs,这使得函数能够同时接受任意数量的位置参数和关键字参数。不过,顺序很重要:*args必须在**kwargs之前出现。1
2
3
4
5
6
7
8
9
10def my_function(*args, **kwargs):
print("Arguments:", args)
print("Keyword arguments:", kwargs)
my_function(1, 2, 3, name="Alice", age=30)
# 输出:
# Arguments: (1, 2, 3)
# Keyword arguments: {'name': 'Alice', 'age': 30} -
使用场景
- 灵活参数传递:当你编写的函数需要接受不固定数量的参数时,
*args和**kwargs可以提供很大的灵活性。 - 函数包装器:在编写装饰器时,通常需要使用
*args和**kwargs以确保装饰器能够处理各种类型的被装饰函数。 - 继承和扩展:在面向对象编程中,子类需要调用父类的方法并传递额外的参数时,这种用法也很常见。
- 灵活参数传递:当你编写的函数需要接受不固定数量的参数时,
-
总结
*args用于接收任意数量的位置参数,并将它们存储为一个元组。**kwargs用于接收任意数量的关键字参数,并将它们存储为一个字典。- 可以将
*args和**kwargs组合使用,以创建更加灵活且通用的函数。
类和对象有什么区别?
- 定义与实例
- 类是一个模板,用于定义一组具有相同属性和方法的对象。
- 对象是类的实例,是根据类创建的实际实体。
- 属性与方法
- 类定义了属性和方法,但不包含实际数据。
- 对象包含实际的数据,并且可以调用类中定义的方法。
- 内存分配
- 类不占用实例的内存,它只是一个定义。
- 对象占用内存,每创建一个对象都会在内存中分配空间。
- 通用性与具体性
- 类是通用的,它描述了一类对象的共同特性。
- 对象是具体的,它是类的具体实现,包含实际的属性值。
Python 中 OOPS 是什么?
OOPS(Object-Oriented Programming System)是面向对象编程系统的缩写。在Python中,OOPS是一种编程范式,它基于对象和类来组织代码。面向对象编程使得代码更具模块化、可重用、可扩展和易于维护。
什么是抽象?
-
概述
在面向对象编程(OOP)中,抽象(Abstraction)是一种概念和技术,它通过隐藏复杂的实现细节,只向用户暴露必要的部分,从而简化程序设计和使用。抽象通过接口和抽象类来实现,允许程序员专注于高层次的设计,而不需要关心底层的实现细节。
-
关键点
- 隐藏复杂性:抽象通过隐藏复杂的实现细节,使系统更易于理解和使用。
- 定义接口:通过定义清晰的接口,使得不同部分之间的交互更加简单和明确。
- 提高可维护性:抽象使得代码更加模块化,便于维护和扩展。
-
抽象类(Abstract Class)
抽象类是一种不能实例化的类,它可以包含抽象方法(没有实现的方法)。抽象类通常用于定义子类的通用接口和必须实现的方法。
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
28from abc import ABC, abstractmethod
class Animal(ABC):
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
# 不能实例化抽象类
# my_animal = Animal() # 这会引发错误
my_dog = Dog()
my_cat = Cat()
print(my_dog.speak()) # 输出: Woof!
print(my_cat.speak()) # 输出: Meow! -
抽象的优点
- 代码重用:通过定义抽象类和接口,可以在不同子类中重用通用的代码和接口。
- 模块化:抽象使得代码更加模块化,更易于理解和维护。
- 灵活性和可扩展性:通过使用抽象,可以更容易地扩展系统而不需要修改现有代码。
什么是封装?
-
概述
封装(Encapsulation)是面向对象编程(OOP)的一个核心概念。它指的是将数据(属性)和操作数据的方法(行为)组合在一个单独的单元中,并控制对这些数据的访问。这种机制提供了数据隐藏和保护的功能,使得数据只能通过规定的接口(方法)进行访问和修改。
-
关键点
- 数据隐藏:封装通过限制直接访问对象的某些组件,保护对象的内部状态不被外部直接修改。
- 保护和控制:封装提供了一种机制,可以控制数据访问和修改的权限。
- 模块化:封装使得代码更加模块化,便于维护和理解。
-
如何实现封装
- 在Python中,可以通过属性的访问级别(公共、公有、私有)来实现封装。
- 公共属性和方法(Public)
公共属性和方法可以在类的外部访问和修改。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Dog:
def __init__(self, name, age):
self.name = name # 公共属性
self.age = age # 公共属性
def bark(self):
return f"{self.name} is barking."
my_dog = Dog("Buddy", 3)
print(my_dog.name) # 输出: Buddy
my_dog.name = "Max"
print(my_dog.name) # 输出: Max - 私有属性和方法(Private)
私有属性和方法通过在属性或方法名称前加上双下划线(__)来实现,只能在类的内部访问,不能在外部直接访问。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Dog:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age # 私有属性
def __bark(self): # 私有方法
return f"{self.__name} is barking."
def get_name(self): # 公共方法用于访问私有属性
return self.__name
def set_name(self, name): # 公共方法用于修改私有属性
self.__name = name
my_dog = Dog("Buddy", 3)
# print(my_dog.__name) # 这会引发错误
print(my_dog.get_name()) # 输出: Buddy
my_dog.set_name("Max")
print(my_dog.get_name()) # 输出: Max - 属性方法(Property Method)
Python提供了@property装饰器,可以将方法转换为属性,从而实现更灵活的封装。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Dog:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age # 私有属性
def name(self):
return self.__name
def name(self, name):
self.__name = name
my_dog = Dog("Buddy", 3)
print(my_dog.name) # 输出: Buddy
my_dog.name = "Max"
print(my_dog.name) # 输出: Max
-
封装的优点
- 数据保护:封装通过隐藏数据,防止外部代码意外或恶意地修改对象的内部状态。
- 简化接口:封装提供了简单的接口,隐藏了复杂的实现细节,使得对象的使用更加简洁和直观。
- 模块化:封装使得代码更加模块化,便于维护和理解。
- 灵活性:封装允许对象的内部实现发生变化,而不会影响到外部代码。
什么是多态?
-
概述
多态(Polymorphism)是面向对象编程(OOP)的一个重要特性。它允许对象以不同的形式表现自己,即对象可以是不同类型的实例,并且能够在共享相同接口的情况下,执行不同的行为。这种能力使得代码更加灵活和可扩展。
-
多态的关键点
- 方法重写(Override):子类可以重写父类的方法,从而在调用相同方法时表现出不同的行为。
- 接口一致性:不同类可以实现相同的方法接口,从而可以互换使用,而不需要改变调用代码。
- 动态绑定:在运行时根据实际对象的类型决定调用哪个方法。
-
多态的实现方式
多态通常通过继承和接口(或抽象基类)来实现。在Python中,可以通过继承和方法重写来实现多态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
def make_animal_speak(animal):
print(animal.speak())
my_dog = Dog()
my_cat = Cat()
make_animal_speak(my_dog) # 输出: Woof!
make_animal_speak(my_cat) # 输出: Meow!在这个示例中,
Animal是一个基类,它定义了一个空的speak方法。Dog和Cat类继承了Animal并重写了speak方法。函数make_animal_speak可以接受任何Animal类型的对象,并调用其speak方法,而不需要关心具体对象的类型。 -
接口一致性
通过接口一致性,不同类可以实现相同的方法,从而可以互换使用这些类的实例。
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
30class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
shapes = [Rectangle(2, 3), Circle(1)]
for shape in shapes:
print(f"Area: {shape.area()}")
# 输出:
# Area: 6
# Area: 3.14在这个示例中,不同的形状类(
Rectangle和Circle)实现了相同的接口area方法。通过这种方式,可以在不修改调用代码的情况下,轻松地扩展新类型的形状。 -
多态的优点
- 代码重用:通过多态,可以编写通用的代码来处理不同类型的对象。
- 灵活性:多态使得代码更加灵活,可以轻松地扩展新类型,而不需要修改现有代码。
- 易于维护:通过多态,可以将不同的行为封装在不同的类中,使得代码更易于维护。
- 接口统一:通过定义统一的接口,可以使得不同类可以互换使用,从而提高代码的可读性和一致性。
什么是 Python 中的猴子补丁?
-
概述
猴子补丁(Monkey Patching)是指在运行时动态地修改或扩展类或模块的方法和属性。这种技术可以在不修改原始代码的情况下,改变程序的行为。虽然猴子补丁在某些情况下非常有用,但它也可能导致代码难以维护和调试,因为它改变了程序原本的行为。
-
常见用途
- 修复第三方库中的错误:如果你发现一个第三方库有一个错误,但你无法等待官方修复,你可以使用猴子补丁来临时修复这个错误。
- 添加或修改功能:你可以使用猴子补丁在运行时添加或修改类或模块的功能,而不需要直接修改源代码。
- 测试:在测试环境中,猴子补丁可以用来模拟某些行为或状态。
-
示例1:修改类的方法
假设我们有一个简单的类
Dog,我们想在运行时修改它的bark方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Dog:
def bark(self):
return "Woof!"
my_dog = Dog()
print(my_dog.bark()) # 输出: Woof!
# 使用猴子补丁修改 bark 方法
def new_bark(self):
return "Meow!"
Dog.bark = new_bark
print(my_dog.bark()) # 输出: Meow!在这个示例中,我们定义了一个
Dog类和一个bark方法。然后,我们在运行时用一个新的new_bark方法替换了原始的bark方法。 -
示例2:修复第三方库中的错误
假设我们在使用一个第三方库
some_library,并且发现其中一个类SomeClass的some_method方法有一个错误,我们可以使用猴子补丁来修复它。1
2
3
4
5
6
7
8
9
10
11
12
13
14import some_library
def fixed_some_method(self):
# 修复后的实现
return "Fixed!"
some_library.SomeClass.some_method = fixed_some_method
# 测试修复后的方法
instance = some_library.SomeClass()
print(instance.some_method()) # 输出: Fixed!在这个示例中,我们导入了一个第三方库
some_library,并用一个新的fixed_some_method方法替换了SomeClass的some_method方法。 -
注意事项
虽然猴子补丁在某些情况下非常有用,但它也有一些潜在的风险和缺点:
- 可维护性:猴子补丁可能使代码变得难以维护,因为它改变了程序的原本行为,并且这种改变可能不明显。
- 兼容性:如果第三方库更新,它们可能会破坏你的猴子补丁,从而导致不可预见的问题。
- 调试困难:由于猴子补丁在运行时动态地修改了代码,它可能使调试变得更加困难。
-
最佳实践
- 文档化:确保你已经充分地文档化了猴子补丁的原因和实现,以便其他开发者能够理解和维护代码。
- 测试:在引入猴子补丁时,确保你已经进行了充分的测试,以验证其行为是正确的。
- 避免过度使用:尽量避免在生产代码中过度使用猴子补丁,除非确实没有其他更好的解决方案。
Python 支持多重继承吗?
-
简答
是的,Python 支持多重继承,这意味着一个类可以继承多个父类。多重继承允许一个子类同时从多个父类继承属性和方法,从而实现更复杂的行为和功能。然而,多重继承也带来了复杂性,主要是由于潜在的命名冲突和继承顺序的问题。
-
多重继承的基本语法
在Python中,通过在类定义时括号内列出多个父类的名称来实现多重继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Parent1:
def method1(self):
print("Parent1 method1")
class Parent2:
def method2(self):
print("Parent2 method2")
class Child(Parent1, Parent2):
pass
child = Child()
child.method1() # 输出: Parent1 method1
child.method2() # 输出: Parent2 method2在这个例子中,
Child类继承了Parent1和Parent2,因此它可以访问这两个父类的method1和method2方法。 -
方法解析顺序(MRO)
-
概述
多重继承引入了一个问题,即在多个父类中可能存在相同的方法或属性,Python需要知道应该调用哪个父类的方法。Python通过方法解析顺序(Method Resolution Order, MRO)来解决这个问题。
-
MRO的规则
MRO决定了调用方法时的搜索顺序,Python 采用C3线性化算法来计算MRO,这是一种复杂的算法,但它遵循以下基本原则:
- 子类优先原则:在MRO中,子类会优先于父类被搜索。
- 父类从左到右:在MRO中,父类会按照在类定义中列出的顺序被搜索。
- 一次排除原则:每个类在MRO中只出现一次。
你可以使用
__mro__属性或mro()方法来查看一个类的MRO。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Parent1:
def method(self):
print("Parent1 method")
class Parent2:
def method(self):
print("Parent2 method")
class Child(Parent1, Parent2):
pass
print(Child.__mro__)
# 输出: (<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class 'object'>)
print(Child.mro())
# 输出: [<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class 'object'>]
-
-
示例:使用多重继承
以下是一个多重继承的例子,它展示了如何从多个父类继承方法并处理命名冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Parent1:
def greet(self):
return "Hello from Parent1"
class Parent2:
def greet(self):
return "Hello from Parent2"
class Child(Parent1, Parent2):
def greet(self):
return super().greet() + " and Child"
child = Child()
print(child.greet()) # 输出: Hello from Parent1 and Child在这个例子中,
Child类通过super()调用了Parent1的greet方法,因为Parent1在MRO中优先于Parent2。 -
注意事项
多重继承虽然强大,但也增加了代码的复杂性。以下是一些使用多重继承时需要注意的事项:
- 命名冲突:在多个父类中可能存在相同的方法或属性,容易引发命名冲突。在设计类时应尽量避免这种情况。
- MRO的复杂性:理解并正确使用MRO对于确保代码正确运行非常重要。
- 调试困难:多重继承使得代码的行为更加难以预测和调试,因此在使用多重继承时应尽量保持代码简洁和清晰。
Python 中的 zip 函数是什么?
-
概述
zip函数是 Python 中的一个内置函数,它用于将多个可迭代对象(如列表、元组等)“压缩”在一起,生成一个由元组组成的迭代器。每个元组包含来自每个可迭代对象的对应元素。 -
基本语法
1
zip(*iterables)
其中,
*iterables表示可以传入多个可迭代对象。 -
示例1:压缩两个列表
1
2
3
4
5
6
7list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
zipped = zip(list1, list2)
print(list(zipped))
# 输出: [(1, 'a'), (2, 'b'), (3, 'c')]在这个例子中,
list1和list2被压缩成一个由元组组成的迭代器,然后通过list()函数将其转化为列表。 -
示例2:压缩多个列表
1
2
3
4
5
6
7
8list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
list3 = [True, False, True]
zipped = zip(list1, list2, list3)
print(list(zipped))
# 输出: [(1, 'a', True), (2, 'b', False), (3, 'c', True)]在这个示例中,三个列表被压缩在一起,每个生成的元组包含来自三个列表的对应元素。
-
示例3:处理长度不等的列表
1
2
3
4
5
6
7list1 = [1, 2, 3]
list2 = ['a', 'b']
zipped = zip(list1, list2)
print(list(zipped))
# 输出: [(1, 'a'), (2, 'b')]当可迭代对象的长度不同时,
zip函数会以最短的可迭代对象为准,生成的迭代器的长度与最短的可迭代对象相同。 -
解压缩(Unzipping)
通过使用
zip函数的反操作,可以将压缩的可迭代对象解压缩回原来的形式。使用zip(*zipped)可以解压缩。1
2
3
4
5zipped = [(1, 'a'), (2, 'b'), (3, 'c')]
list1, list2 = zip(*zipped)
print(list1) # 输出: (1, 2, 3)
print(list2) # 输出: ('a', 'b', 'c') -
zip 与循环
zip函数常与for循环结合使用,以同时迭代多个可迭代对象。1
2
3
4
5
6
7
8
9
10list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
for num, char in zip(list1, list2):
print(f'Number: {num}, Character: {char}')
# 输出:
# Number: 1, Character: a
# Number: 2, Character: b
# Number: 3, Character: c这种使用方式使得处理成对数据更加方便和直观。
Python 中的 all 函数是什么?
-
概述
all函数是 Python 中的一个内置函数,用于判断一个可迭代对象中的所有元素是否都为真(True)。如果可迭代对象中的所有元素都为真,则返回True,否则返回False。如果可迭代对象为空,all函数返回True。 -
基本语法
1
all(iterable)
iterable:一个可迭代对象,如列表、元组、集合等。 -
示例1:所有元素都为真
1
2
3
4values = [True, True, True]
result = all(values)
print(result) # 输出: True在这个例子中,列表
values中的所有元素都为True,因此all函数返回True。 -
示例2:存在元素为假
1
2
3
4values = [True, False, True]
result = all(values)
print(result) # 输出: False在这个例子中,列表
values中存在一个False元素,因此all函数返回False。 -
示例3:空可迭代对象
1
2
3
4values = []
result = all(values)
print(result) # 输出: True在这个例子中,列表
values为空,根据all函数的定义,它返回True。 -
与逻辑运算结合使用
all函数常用于需要检查多个条件是否都为真的情况,特别是在过滤和验证数据时非常有用。1
2
3
4numbers = [2, 4, 6, 8, 10]
result = all(num % 2 == 0 for num in numbers)
print(result) # 输出: True在这个例子中,我们用列表推导生成一个布尔值列表,检查所有数字是否都是偶数。由于所有数字都是偶数,
all函数返回True。
Python 中的访问权限?
-
公有(Public)
在 Python 中,默认情况下所有属性和方法都是公有的。公有属性和方法可以在类的外部进行访问和修改。
1
2
3
4
5
6
7
8
9
10
11
12class MyClass:
def __init__(self, value):
self.value = value
def public_method(self):
print("This is a public method")
obj = MyClass(10)
print(obj.value) # 访问公有属性
obj.public_method() # 调用公有方法 -
受保护(Protected)
在 Python 中,通过在属性或方法名前加一个下划线
_来表示受保护的属性或方法。受保护的属性和方法通常不建议在类的外部访问,尽管从技术上讲,它们仍然是可访问的。这是一种约定,表示这些成员不应该被外部代码直接使用。1
2
3
4
5
6
7
8
9
10
11
12class MyClass:
def __init__(self, value):
self._value = value
def _protected_method(self):
print("This is a protected method")
obj = MyClass(10)
print(obj._value) # 虽然不建议,但还是可以访问受保护属性
obj._protected_method() # 虽然不建议,但还是可以调用受保护方法 -
私有(Private)
通过在属性或方法名前加两个下划线
__,可以使其成为私有成员。私有成员不能在类的外部直接访问。Python 实现私有成员的机制是名称改写(name mangling),即在内部将成员名称改写为_ClassName__memberName的形式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class MyClass:
def __init__(self, value):
self.__value = value
def __private_method(self):
print("This is a private method")
obj = MyClass(10)
# print(obj.__value) # 会报错,无法直接访问私有属性
# obj.__private_method() # 会报错,无法直接调用私有方法
# 通过名称改写可以访问私有成员(不建议)
print(obj._MyClass__value) # 正确访问私有属性
obj._MyClass__private_method() # 正确调用私有方法
Web 相关
Post 和 Get 的区别是什么?
-
数据传输方式
GET:
- URL 中传递参数:GET 请求将数据附加在 URL 的查询字符串中(即
?后面的部分)。 - 数据量限制:因为 URL 长度有限,GET 请求传输的数据量有限(具体限制取决于浏览器和服务器,但一般不超过 2000 字符)。
- 数据明文可见:由于数据在 URL 中明文传递,敏感信息不宜使用 GET 请求传输。
POST:
- 请求体中传递数据:POST 请求将数据包含在 HTTP 请求体中。
- 数据量较大:POST 请求传输的数据量没有严格限制,适合发送大量数据。
- 数据不明文可见:虽然数据仍可能被截获,但在 URL 中不可见,安全性相对较高。
- URL 中传递参数:GET 请求将数据附加在 URL 的查询字符串中(即
-
缓存机制
GET:
- 可缓存:GET 请求通常是幂等的(多次请求结果相同),浏览器和代理服务器通常会缓存 GET 请求的响应。
POST:
- 不可缓存:POST 请求可能会改变服务器的状态,浏览器和代理服务器通常不会缓存 POST 请求的响应。
-
数据长度限制
GET:
- 有限制:受限于浏览器和服务器对 URL 长度的限制,通常不超过 2000 字符。
POST:
- 无明显限制:可传输的数据量较大,受限于服务器配置和内存大小。
-
安全性
GET:
- 安全性较低:GET 请求的数据会显示在 URL 中,容易被截获和记录,不适合传输敏感信息。
POST:
- 安全性较高:虽然数据仍可能被截获,但在 URL 中不可见,适合传输敏感信息。配合 HTTPS 可以进一步提高安全性。
-
使用场景
GET:
- 获取数据:适用于从服务器获取数据,不会对服务器产生副作用。
- 查询操作:适用于查询操作,如搜索、读取数据等。
- 导航链接:适用于超链接导航,因为链接点击通常是幂等的。
POST:
- 提交数据:适用于向服务器提交数据,会对服务器产生副作用。
- 表单提交:适用于表单提交,如用户注册、登录、上传文件等。
- 修改操作:适用于涉及数据修改的操作,如添加、更新或删除数据。
-
幂等性
GET:
- 幂等:多次相同的 GET 请求,对服务器的影响是相同的,不会改变服务器的状态。
POST:
- 非幂等:多次相同的 POST 请求,可能会对服务器产生不同的副作用,如多次提交表单可能导致重复的数据创建。
Cookie 和 Session 的区别是什么?
-
存储位置
Cookie:
- 客户端存储:Cookie 存储在客户端浏览器中,客户端可以查看、修改和删除 Cookie。
Session:
- 服务器存储:Session 存储在服务器端,客户端浏览器只保存一个 Session ID,该 ID 用于标识服务器端的会话数据。
-
生命周期
Cookie:
- 持久性:Cookie 可以设置过期时间,持久化存储。即使关闭浏览器,Cookie 仍然存在,直到达到过期时间或被删除。
- 会话性:也可以不设置过期时间,这种 Cookie 称为会话 Cookie(Session Cookie),在浏览器关闭时自动删除。
Session:
- 会话性:Session 只在会话期间有效,默认情况下当用户关闭浏览器或会话超时后,Session 失效。
- 持久化:可以通过配置实现长时间有效的 Session(如持久化到数据库或文件系统),但这取决于服务器的具体实现。
-
安全性
Cookie:
- 安全性较低:由于 Cookie 存储在客户端,容易被截获、篡改和伪造。不建议在 Cookie 中存储敏感信息,可以使用 HTTP Only 和 Secure 标志来提高安全性。
- HTTP Only:防止客户端脚本(如 JavaScript)访问 Cookie。
- Secure:确保 Cookie 只能通过 HTTPS 传输。
Session:
- 安全性较高:Session 数据存储在服务器端,相对来说更安全。即使 Session ID 被截获,攻击者也很难获取到具体的会话数据。
- 安全性较低:由于 Cookie 存储在客户端,容易被截获、篡改和伪造。不建议在 Cookie 中存储敏感信息,可以使用 HTTP Only 和 Secure 标志来提高安全性。
-
数据存储容量
Cookie:
- 容量有限:每个 Cookie 的大小通常不能超过 4KB,不同浏览器对单个域名下 Cookie 的总数量有限制(通常为 20-50 个)。
Session:
- 容量较大:服务器端存储的 Session 数据没有严格的大小限制,可以存储大量的数据,受限于服务器的内存和存储空间。
-
用途
Cookie:
- 持久存储:适用于需要在客户端存储的持久数据,如用户偏好设置、购物车信息等。
- 跨请求数据共享:Cookie 可以在不同请求间共享数据。
Session:
- 会话管理:适用于需要在服务器端存储的临时数据,如用户登录状态、临时数据等。
- 敏感数据存储:适合存储敏感数据,因为数据不直接暴露给客户端。
-
性能
Cookie:
- 性能影响小:每次请求都会自动带上 Cookie,有时会增加请求和响应的大小,但通常影响较小。
- 适用于小数据量:因为存储在客户端,适用于较小的数据量。
Session:
- 性能依赖服务器:会话数据存储在服务器端,性能取决于服务器的处理能力和存储机制。并发用户较多时,服务器压力较大。
- 适用于大数据量:因为存储在服务器端,适用于较大的数据量。
HTTP 的三次握手和四次挥手
-
HTTP 的三次握手
三次握手(Three-Way Handshake)是客户端和服务器在建立TCP连接时所进行的三步过程。其目的是确保双方都能接收和发送数据,并且为数据传输的初始序列号进行同步。
- 第一次握手(SYN):
- 客户端向服务器发送一个SYN(同步序列号)包,请求建立连接。
- 报文段:SYN=1,Seq=x
- 客户端进入SYN_SENT状态。
- 第二次握手(SYN+ACK):
- 服务器收到客户端的SYN包后,确认连接请求,并向客户端发送一个SYN+ACK(同步序列号+确认序列号)包。
- 报文段:SYN=1,ACK=1,Seq=y,Ack=x+1
- 服务器进入SYN_RCVD状态。
- 第三次握手(ACK):
- 客户端收到服务器的SYN+ACK包后,向服务器发送一个ACK(确认序列号)包,表示已收到服务器的SYN+ACK包。
- 报文段:ACK=1,Seq=x+1,Ack=y+1
- 客户端进入ESTABLISHED状态,服务器也进入ESTABLISHED状态,TCP连接建立成功。
图示:
1
2
3
4
5
6
7
8
9
10
11客户端 服务器
| SYN=x |
|-------------------------->|
| |
| SYN=y, ACK=x+1 |
|<--------------------------|
| |
| ACK=y+1 |
|-------------------------->|
| | - 第一次握手(SYN):
-
HTTP 的四次挥手
四次挥手(Four-Way Handshake)是客户端和服务器在终止TCP连接时所进行的四步过程。其目的是确保双方都已完成数据传输,并且连接可以安全地关闭。
- 第一次挥手(FIN):
- 客户端向服务器发送一个FIN(结束标志)包,表示希望主动关闭连接。
- 报文段:FIN=1,Seq=u
- 客户端进入FIN_WAIT_1状态。
- 第二次挥手(ACK):
- 服务器收到客户端的FIN包后,向客户端发送一个ACK(确认序列号)包,确认已收到关闭请求。
- 报文段:ACK=1,Seq=v,Ack=u+1
- 服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
- 第三次挥手(FIN):
- 服务器向客户端发送一个FIN包,表示希望关闭连接。
- 报文段:FIN=1,Seq=w
- 服务器进入LAST_ACK状态。
- 第四次挥手(ACK):
- 客户端收到服务器的FIN包后,向服务器发送一个ACK包,确认已收到关闭请求。
- 报文段:ACK=1,Seq=u+1,Ack=w+1
- 客户端进入TIME_WAIT状态,等待一段时间(通常是2MSL,最长报文段生命周期)以确保服务器已收到ACK包。随后,客户端进入CLOSED状态。服务器接收到ACK包后,也进入CLOSED状态,连接关闭。
图示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14客户端 服务器
| FIN=u |
|-------------------------->|
| |
| ACK=u+1 |
|<--------------------------|
| |
| FIN=w |
|<--------------------------|
| |
| ACK=w+1 |
|-------------------------->|
| | - 第一次挥手(FIN):
-
总结
- 三次握手确保 TCP 连接的可靠建立,包括双方的同步和初始序列号的确认。
- 四次挥手确保 TCP 连接的可靠终止,允许双方完成数据传输并安全关闭连接。
AJAX 的优缺点?
-
概述
AJAX(Asynchronous JavaScript and XML)是一种用于创建快速动态网页的技术,它允许网页在不重新加载整个页面的情况下与服务器进行数据交换。
-
AJAX 的优点
- 用户体验更好:
- AJAX允许在不刷新页面的情况下更新页面的部分内容,这使得应用程序响应更加迅速,从而提高了用户体验。
- 减少带宽使用:
- 由于只更新页面的一部分,AJAX 请求通常比重新加载整个页面需要的带宽更少,这可以显著减少服务器和客户端之间的数据传输量。
- 提升性能:
- 由于数据传输量减少,并且请求是异步处理的,页面加载速度和响应时间显著提高,从而提升了整体性能。
- 异步处理:
- AJAX的异步特性允许用户在等待服务器响应时继续与页面进行交互,提高了用户的操作体验。
- 增强的交互性:
- AJAX使得创建动态、交互性强的网页应用成为可能,像电子邮件客户端、实时聊天应用、地图服务等都能得益于此。
- 分离前后端逻辑:
- AJAX请求可以将前端页面展示与后端数据处理逻辑分开,前后端可以独立开发和维护。
- 用户体验更好:
-
AJAX 的缺点
- 搜索引擎优化(SEO)问题:
- 由于内容是通过JavaScript动态加载的,而不是直接在页面HTML中,搜索引擎可能无法抓取这些动态内容,从而影响SEO效果。
- 浏览器兼容性问题:
- 尽管现代浏览器对AJAX支持较好,但早期的浏览器或某些特定环境可能不完全支持AJAX,导致兼容性问题。
- 可能增加复杂性:
- 实现和调试AJAX请求和响应可能需要更多的代码和复杂性,特别是在处理异步操作、错误处理和状态管理方面。
- 历史记录和书签问题:
- 由于AJAX动态更新页面内容而不改变URL,传统的浏览器历史记录和书签功能可能无法正常工作,这对用户导航体验产生一定影响。
- 安全问题:
- AJAX请求存在安全隐患,例如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。需要采取额外的措施来确保安全,如验证和授权机制。
- 依赖JavaScript:
- 如果用户禁用了JavaScript或使用了不支持JavaScript的浏览器,AJAX功能将无法正常工作,从而影响网页的功能和用户体验。
- 搜索引擎优化(SEO)问题:
-
总结
AJAX技术在提升用户体验、减少带宽使用和提升性能方面带来了诸多好处,使得网页应用更加动态和交互性强。然而,开发者在使用AJAX时也需要权衡其缺点,如SEO影响、浏览器兼容性、安全问题等,并采取相应的措施来优化和保护应用程序。合理使用AJAX可以大大提高网页的交互体验和性能,但也需要谨慎处理可能的复杂性和潜在的安全风险。
HTTP 和 HTTPS 的区别?
-
数据加密
- HTTP:HTTP 协议在数据传输过程中不进行加密。这意味着数据在客户端和服务器之间以纯文本形式传输,容易被中间人截获和篡改。
- HTTPS:HTTPS 在数据传输过程中使用 SSL/TLS 协议对数据进行加密。加密的数据在传输过程中无法被第三方解读,从而提供了更高的安全性。
-
端口号
- HTTP:默认使用端口号 80。
- HTTPS:默认使用端口号 443。
-
安全性
- HTTP:由于数据以纯文本形式传输,HTTP 协议容易受到各种类型的攻击,如中间人攻击(Man-in-the-Middle Attack)和窃听攻击。
- HTTPS:通过使用 SSL/TLS 加密数据传输,HTTPS 提供了数据完整性和隐私性,防止数据在传输过程中被截获或篡改。
-
SSL/TLS 证书
- HTTP:不需要 SSL/TLS 证书。
- HTTPS:需要一个由受信任的证书颁发机构(CA,Certificate Authority)签发的 SSL/TLS 证书。证书用于验证服务器的身份,并建立安全的加密连接。
-
性能
- HTTP:由于没有加密和解密的开销,HTTP 通常比 HTTPS 性能稍高。
- HTTPS:由于加密和解密数据需要额外的计算资源,HTTPS 的性能可能比 HTTP 略低。不过,现代硬件和优化技术已经使得这个性能差异变得非常小。
-
URL 结构
- HTTP:URL 以
http://开头。 - HTTPS:URL 以
https://开头。
- HTTP:URL 以
数据库相关
MySQL 分区有哪些?
-
范围分区(RANGE Partitioning)
- 定义:基于列值的范围进行分区。
- 适用场景:适用于基于日期、数字范围等情况的分区。例如,按年份、月份来分区。
1
2
3
4
5
6
7
8
9
10
11CREATE TABLE sales
(
id INT,
sale_date DATE,
amount DECIMAL(10, 2)
) PARTITION BY RANGE (YEAR(sale_date)) (
PARTITION p2018 VALUES LESS THAN (2019),
PARTITION p2019 VALUES LESS THAN (2020),
PARTITION p2020 VALUES LESS THAN (2021)
);
-
列表分区(LIST Partitioning)
- 定义:基于枚举值进行分区。
- 适用场景:适用于列值为离散、非连续的情况。例如,基于地理区域、类别等进行分区。
1
2
3
4
5
6
7
8
9
10
11CREATE TABLE user_profiles
(
id INT,
name VARCHAR(50),
country_code CHAR(2)
) PARTITION BY LIST (country_code) (
PARTITION pNorthAmerica VALUES IN ('US', 'CA', 'MX'),
PARTITION pEurope VALUES IN ('FR', 'DE', 'GB'),
PARTITION pAsia VALUES IN ('CN', 'JP', 'IN')
);
-
哈希分区(HASH Partitioning)
- 定义:基于哈希函数的值进行分区。
- 适用场景:适用于希望均匀分布数据的情况,不需要特定的分区规则。
1
2
3
4
5
6
7CREATE TABLE orders
(
id INT,
customer_id INT,
order_date DATE
) PARTITION BY HASH (customer_id) PARTITIONS 4;
-
键值分区(KEY Partitioning)
- 定义:类似于哈希分区,但使用MySQL提供的内部哈希函数进行分区。
- 适用场景:适用于希望数据均匀分布但不想指定哈希函数的情况。
1
2
3
4
5
6
7CREATE TABLE logs
(
id INT,
log_message VARCHAR(255),
log_date DATE
) PARTITION BY KEY(id) PARTITIONS 4;
-
线性哈希分区(LINEAR HASH Partitioning)
- 定义:改进的哈希分区,使用线性哈希函数。
- 适用场景:适用于需要处理大规模分区并且希望在分区增加时保持均匀分布的情况。
1
2
3
4
5
6
7CREATE TABLE sessions
(
id INT,
user_id INT,
start_time DATETIME
) PARTITION BY LINEAR HASH (user_id) PARTITIONS 6;
-
线性键值分区(LINEAR KEY Partitioning)
- 定义:类似于线性哈希分区,但使用MySQL提供的内部线性哈希函数。
- 适用场景:适用于大规模分区并希望均匀分布数据的情况。
1
2
3
4
5
6
7CREATE TABLE events
(
id INT,
event_name VARCHAR(50),
event_date DATE
) PARTITION BY LINEAR KEY(id) PARTITIONS 6;
-
分区优缺点
-
优点
- 提升查询性能:分区可以减少查询数据的范围,从而加快查询速度。
- 简化管理:分区使得管理大表更为简单,备份、恢复、删除数据等操作可以在单个分区上进行。
- 均衡IO负载:通过分区分布,可以均衡数据库的IO负载,提升整体性能。
-
缺点
- 复杂性增加:设计和维护分区表比普通表更复杂,需要额外的规划。
- 可能增加开销:在某些情况下,分区可能增加SQL的执行开销。
- 有限制:某些索引和操作在分区表上可能受限。
-
什么是 MySQL 事务?
-
概述
MySQL事务(Transaction)是指一组逻辑上的数据库操作,这些操作要么全部成功执行,要么全部回滚,从而保持数据库的一致性。事务是数据库管理系统(DBMS)中保证数据一致性和完整性的重要机制。
-
事务的基本操作
- START TRANSACTION:显式地开始一个事务。
- COMMIT:提交事务,将事务中的所有操作永久保存到数据库。
- ROLLBACK:回滚事务,撤销事务中的所有操作,使得数据库恢复到事务开始之前的状态。
-
示例
下面是一个简单的示例,展示了如何在MySQL中使用事务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14-- 开始事务
START TRANSACTION;
-- 执行一些SQL操作
INSERT INTO accounts (account_id, balance) VALUES (1, 1000);
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 如果一切正常,提交事务
COMMIT;
-- 如果出错,回滚事务
ROLLBACK;
数据库事务的 ACID 四大特性
-
原子性(Atomicity)
原子性保证了事务中的所有操作要么全部完成,要么全部不完成。换句话说,事务是一个不可分割的工作单元,即使在中途出现故障,事务也能确保其操作要么全部生效,要么全部撤销,不会出现部分生效的情况。
1
2
3
4
5
6
7
8
9
10
11START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 如果操作成功,提交事务
COMMIT;
-- 如果操作失败,回滚事务
ROLLBACK; -
一致性(Consistency)
一致性保证在事务开始和结束时,数据库都处于一致的状态。这意味着所有业务规则、约束和触发器在事务完成后仍然有效。在事务中进行的操作不会破坏数据库的完整性约束,如外键约束、唯一性约束等。
示例:假设有账户转账操作,如果在转账过程中一方账户扣款成功而另一方账户加款失败,那么数据库将回滚到事务开始前的状态,保持数据一致性。
-
隔离性(Isolation)
隔离性保证一个事务的执行不会因并发事务的操作而受到影响。不同的隔离级别提供了不同程度的隔离性,常见的隔离级别包括:
- READ UNCOMMITTED:最低的隔离级别,允许读取未提交的数据,可能导致脏读。
- READ COMMITTED:只能读取已提交的数据,避免脏读,但可能出现不可重复读。
- REPEATABLE READ:确保在一个事务中的多次读取操作结果一致,避免脏读和不可重复读,但可能出现幻读。
- SERIALIZABLE:最高的隔离级别,确保完全隔离,避免脏读、不可重复读和幻读,但性能较低。
示例:
1
2
3
4
5
6
7
8SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM accounts WHERE account_id = 1;
-- 其他事务在此期间对相同数据的更新不会影响当前事务
COMMIT; -
持久性(Durability)
持久性保证一旦事务提交,其对数据库的改变将永久保存,即使系统崩溃或重启,提交的事务也不会丢失。这通常通过将事务日志记录到稳定的存储(如磁盘)来实现。
1
2
3
4
5
6
7
8START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 提交事务后,数据更改将永久保存
COMMIT; -
总结
- 原子性:事务是不可分割的整体。
- 一致性:事务完成后,数据库状态依然满足所有约束。
- 隔离性:事务彼此独立执行,不受并发影响。
- 持久性:一旦提交,事务的改变永久保存。
MySQL 常用的聚合函数
| 聚合函数 | 说明 |
|---|---|
COUNT() |
统计某列的非空值的数量。 |
SUM() |
计算某列数值的总和。 |
AVG() |
计算某列数值的平均值。 |
MAX() |
返回某列的最大值。 |
MIN() |
返回某列的最小值。 |
1 | SELECT COUNT(*) FROM employees; |
MongoDB常用的聚合函数
MongoDB 提供了一系列常用的聚合操作符,用于在聚合管道(Aggregation Pipeline)中进行数据处理和计算。
| 表达式 | 描述 |
|---|---|
| $sum | 计算总和 |
| $avg | 计算平均值 |
| $min | 获取最小值 |
| $max | 获取最大值 |
| $push | 在结果文档中插入值到一个数组中 |
| $addToSet | 在结果文档中插入值到一个数组中,但不创建副本 |
| $first | 根据资源文档的排序获取第一个文档数据 |
| $last | 根据资源文档的排序获取最后一个文档数据 |
| $group | 将集合中的文档分组,可用于统计结果 |
| $sort | 将文档排序后输出 |
| $limit | 限制聚合管道返回的文档数 |
| $skip | 跳过指定数量的文档,并返回余下的文档 |
| $unwind | 将数组类型的字段进行拆分 |
| $lookup | 用于在同一数据库中的不同集合之间执行类似于SQL的左外部连接 |
1 | db.employees.aggregate([ |
MongoDB 管道
-
概述
MongoDB 的聚合管道(Aggregation Pipeline)是一个强大的数据处理框架,允许用户使用多个阶段来处理和转换集合中的文档。每个阶段(stage)执行一个操作,并将结果传递给下一个阶段,最终生成所需的输出。聚合管道的设计类似于 Unix 的管道(pipeline),可以链式调用多个操作。
-
管道阶段
以下是一些常见的管道阶段及其用途:
阶段 说明 $match过滤文档,仅保留符合条件的文档。 $group根据指定的字段对文档进行分组,并可以对每个分组应用聚合操作符。 $project修改输入文档的结构,例如包含、排除字段或添加计算字段。 $sort对文档排序。 $limit限制返回的文档数量。 $skip跳过指定数量的文档。 $unwind将数组字段拆分为多条文档。 $lookup进行表关联(类似 SQL 中的 JOIN 操作)。 $out将聚合管道的结果输出到一个新的集合。 $addFields添加新字段到文档。 $replaceRoot替换输入文档的根节点。
Redis 的数据类型有哪些?
Redis 是一个高性能的键值对存储系统,支持多种数据类型,能满足不同的应用需求。以下是 Redis 支持的几种主要数据类型及其用途:
| 数据类型 | 说明 |
|---|---|
| String | 最基本的类型,可以存储任意类型的字符串值,包括二进制数据、整数和浮点数。 |
| Hash | 键值对集合,适合存储对象,能够快速存取字段的值。 |
| List | 有序链表,可以用作队列(Queue)或堆栈(Stack)。 |
| Set | 无序集合,支持集合运算,如交集、并集和差集。 |
| Sorted Set | 有序集合,元素按分数排序,适用于排行榜等应用。 |
| Bitmap | 位数组,适用于需要位级别操作的场景。 |
| HyperLogLog | 基数估计算法,适用于大数据集的基数估计。 |
| Geospatial | 地理空间数据类型,支持地理位置存储和查询。 |
| Stream | 记录日志型数据,支持高效的消息队列操作。 |
MongoDB 和 MySQL 有什么区别?
-
数据模型
- MongoDB: 是一个 NoSQL 数据库,使用文档(Document)模型来存储数据。每个文档都是一个 BSON(类似 JSON)的对象,集合(Collection)是文档的集合。MongoDB 灵活的数据模型支持嵌套对象和数组,适合处理复杂的、半结构化或非结构化的数据。
- MySQL: 是一个关系型数据库(RDBMS),使用表(Table)来存储数据。数据以行和列的形式存储在表中,表之间可以通过外键(Foreign Key)进行关联。MySQL 的数据模型是结构化的,要求预先定义表结构(Schema)。
-
查询语言
- MongoDB: 使用 MongoDB 查询语言(MongoDB Query Language,MQL),支持 JSON 风格的查询和更新操作。MQL 提供了丰富的查询和聚合功能,支持复杂的数据处理任务。
- MySQL: 使用结构化查询语言(SQL),是一种标准化的查询语言,用于管理和操作关系型数据库。SQL 语法成熟且广泛应用,支持复杂的查询、更新和数据操作。
-
数据一致性和事务
- MongoDB: 默认提供最终一致性(Eventual Consistency),在分布式系统中可能有短暂的不一致。MongoDB 支持多文档 ACID 事务(从 4.0 版本开始),但事务的使用会带来额外的性能开销。
- MySQL: 提供强一致性(Strong Consistency),支持 ACID(原子性、一致性、隔离性、持久性)事务,确保数据的可靠性和一致性。MySQL 提供了各种隔离级别来控制并发事务的行为。
-
性能和扩展性
- MongoDB: 设计为分布式数据库,内置水平扩展(Sharding)功能,能够处理大规模数据和高吞吐量的应用。MongoDB 在处理高并发读写操作和大规模数据时表现良好。
- MySQL: 传统上是一个单节点系统,通过主从复制(Master-Slave Replication)和读写分离(Read-Write Splitting)来扩展。MySQL 的水平扩展(Sharding)通常需要额外的配置和应用层支持。
-
使用场景
- MongoDB: 适用于需要快速开发、数据模型灵活、高并发和水平扩展的应用场景,如内容管理系统、实时分析、物联网(IoT)数据存储和日志管理等。
- MySQL: 适用于数据结构固定、事务要求高、需要关系型数据模型的应用场景,如金融系统、电子商务平台、企业资源规划(ERP)系统和客户关系管理(CRM)系统等。
-
管理和工具
- MongoDB: 提供了丰富的管理工具,如 MongoDB Atlas(云数据库服务)、MongoDB Compass(图形化管理工具)和 MongoDB Shell(命令行工具)。MongoDB 社区版和企业版功能有所不同。
- MySQL: 提供了多种管理工具,如 MySQL Workbench(图形化管理工具)、phpMyAdmin(Web管理工具)和 MySQL Shell(命令行工具)。MySQL 社区版和企业版功能也有所不同。
-
总结
MongoDB 和 MySQL 各有优势,选择哪种数据库取决于具体的应用需求和场景。MongoDB 提供了灵活的数据模型和强大的扩展能力,适合快速开发和处理大规模数据;MySQL 提供了可靠的事务处理和成熟的关系型数据模型,适合需要高度一致性和复杂查询的应用。
机器学习相关
常见机器学习库有哪些?
- Scikit-Learn:提供了丰富的机器学习算法和工具,适合初学者和中级用户。
- TensorFlow:一个开源的机器学习框架,尤其适用于深度学习。
- Keras:一个高级神经网络 API,运行在 TensorFlow 之上,简化了深度学习的开发过程。
- PyTorch:另一个流行的深度学习框架,具有动态计算图的特点。
- XGBoost:一个优化的分布式梯度提升库。
- LightGBM:一个快速、高效、分布式的梯度提升框架。
常见机器学习算法有哪些?
-
监督学习算法
- 线性回归(Linear Regression)
- 用途:主要用于回归问题,预测连续型变量。
- 原理:建立变量之间的线性关系,通过最小化误差找到最佳拟合直线。
- 示例:房价预测。
- 逻辑回归(Logistic Regression)
- 用途:用于分类问题,特别是二元分类。
- 原理:使用 sigmoid 函数将输出映射到 (0, 1) 之间,判断概率大于 0.5 属于某一类。
- 示例:垃圾邮件分类。
- 支持向量机(SVM, Support Vector Machine)
- 用途:用于分类和回归问题。
- 原理:通过找到最佳的超平面来最大化类间距离(边界)。
- 示例:图像分类。
- 决策树(Decision Tree)
- 用途:用于分类和回归问题。
- 原理:通过递归分割数据集,构建树形结构,分裂依据信息增益或基尼系数。
- 示例:客户细分。
- 随机森林(Random Forest)
- 用途:用于分类和回归问题。
- 原理:集成多个决策树,通过投票或平均提高模型的泛化能力。
- 示例:信用评分。
- 梯度提升(Gradient Boosting)
- 用途:用于分类和回归问题。
- 原理:通过构建一系列弱学习器,每一步都试图修正前一步的误差。
- 示例:股票价格预测。
- K近邻(K-Nearest Neighbors, KNN)
- 用途:用于分类和回归问题。
- 原理:基于距离度量,将样本分类为邻近的 k 个点中最多的类别。
- 示例:推荐系统。
- 朴素贝叶斯(Naive Bayes)
- 用途:用于分类问题。
- 原理:基于贝叶斯定理,假设特征之间相互独立,计算每个类别的概率。
- 示例:文本分类。
- 线性回归(Linear Regression)
-
无监督学习算法
- k-means 聚类(k-means Clustering)
- 用途:用于聚类问题。
- 原理:将数据分为 k 个簇,通过迭代优化簇的中心。
- 示例:客户分群。
- 主成分分析(PCA, Principal Component Analysis)
- 用途:用于降维和特征提取。
- 原理:通过线性变换,将数据投影到主成分空间,保留最大方差。
- 示例:数据可视化。
- k-means 聚类(k-means Clustering)
深度学习算法有哪些?
- 卷积神经网络(CNN, Convolutional Neural Network)
- 用途:主要用于图像和视频处理。
- 原理:利用卷积层和池化层提取空间特征,最后通过全连接层进行分类或回归。
- 示例:图像识别。
- 循环神经网络(RNN, Recurrent Neural Network)
- 用途:处理序列数据,如时间序列和自然语言处理。
- 原理:通过循环连接,保留序列数据的时间依赖信息。
- 示例:文本生成。
- 长短期记忆网络(LSTM, Long Short-Term Memory)
- 用途:处理长序列数据,解决 RNN 中的长程依赖问题。
- 原理:引入记忆单元,通过门机制控制信息流动。
- 示例:语音识别。