使用Python手搓一個(gè)生產(chǎn)級(jí)限流器
“限流不是拒絕服務(wù),而是保護(hù)服務(wù)——讓系統(tǒng)在壓力下優(yōu)雅地活著,而不是突然崩塌。”
一、為什么你的服務(wù)需要限流
2023 年某知名 AI 平臺(tái)在新品發(fā)布當(dāng)晚,因未做好限流防護(hù),短短 15 分鐘內(nèi)涌入的請(qǐng)求量超過(guò)正常峰值的 80 倍,數(shù)據(jù)庫(kù)連接池耗盡,整個(gè)服務(wù)雪崩。事后復(fù)盤(pán),技術(shù)負(fù)責(zé)人苦澀地說(shuō):“如果當(dāng)時(shí)有一個(gè)限流器,最多損失部分用戶體驗(yàn),而不是全站癱瘓。”
限流(Rate Limiting),是系統(tǒng)穩(wěn)定性設(shè)計(jì)中最重要的防線之一。它的本質(zhì)是:在時(shí)間維度上控制資源的消費(fèi)速率,保護(hù)下游系統(tǒng)不被壓垮,同時(shí)對(duì)用戶提供公平的服務(wù)保障。
三種經(jīng)典限流算法——令牌桶(Token Bucket)、漏桶(Leaky Bucket)、滑動(dòng)窗口(Sliding Window)——各有側(cè)重,適用場(chǎng)景不同。本文將帶你從原理到代碼,逐一實(shí)現(xiàn)它們,并在文末給出選型建議。
二、固定窗口:最樸素的限流(以及它的致命缺陷)
在深入三大算法之前,先了解最簡(jiǎn)單的固定窗口計(jì)數(shù)器,理解它的不足,才能體會(huì)后續(xù)算法的價(jià)值。
import time
import threading
from collections import defaultdict
class FixedWindowRateLimiter:
"""
固定窗口限流器
缺陷:窗口邊界處可能出現(xiàn) 2 倍流量突刺
"""
def __init__(self, max_requests: int, window_seconds: int):
self.max_requests = max_requests
self.window_seconds = window_seconds
self._counts = defaultdict(int)
self._window_starts = defaultdict(float)
self._lock = threading.Lock()
def is_allowed(self, key: str = "default") -> bool:
now = time.time()
with self._lock:
window_start = self._window_starts[key]
# 當(dāng)前時(shí)間超過(guò)窗口,重置計(jì)數(shù)
if now - window_start >= self.window_seconds:
self._window_starts[key] = now
self._counts[key] = 0
if self._counts[key] < self.max_requests:
self._counts[key] += 1
return True
return False
# 演示固定窗口的邊界問(wèn)題
limiter = FixedWindowRateLimiter(max_requests=10, window_seconds=60)
# 假設(shè)窗口為 [0s, 60s),在第 59s 發(fā)送 10 個(gè)請(qǐng)求(全部通過(guò))
# 然后在第 61s(新窗口開(kāi)始)再發(fā)送 10 個(gè)請(qǐng)求(全部通過(guò))
# 實(shí)際上在 [59s, 61s] 這 2 秒內(nèi)通過(guò)了 20 個(gè)請(qǐng)求——是上限的 2 倍!
print("固定窗口在邊界處存在 2 倍流量突刺風(fēng)險(xiǎn)")
這個(gè)"邊界突刺"問(wèn)題在高并發(fā)場(chǎng)景下可能讓下游系統(tǒng)瞬間承受雙倍壓力。這正是滑動(dòng)窗口要解決的問(wèn)題。
三、令牌桶算法:允許突發(fā),平滑限速
原理圖解
令牌桶示意圖:
┌─────────────────┐
以固定速率 │ ?? ?? ?? ?? ?? │ 桶容量上限
補(bǔ)充令牌 → │ ?? ?? ?? ?? │ (burst capacity)
└────────┬────────┘
│
請(qǐng)求到來(lái)時(shí)取令牌
↓
有令牌 → 放行請(qǐng)求
無(wú)令牌 → 拒絕/等待
令牌桶的核心特性:允許一定程度的流量突發(fā)(桶里積累的令牌可以被一次性消費(fèi)),但長(zhǎng)期平均速率不超過(guò)令牌補(bǔ)充速率。這非常適合 API 調(diào)用:用戶可能偶爾有突發(fā)需求,但不能持續(xù)高頻。
Python 實(shí)現(xiàn)
import time
import threading
from typing import Optional
class TokenBucketRateLimiter:
"""
令牌桶限流器
特性:
- 允許短時(shí)突發(fā)(桶滿時(shí)可消費(fèi)全部令牌)
- 長(zhǎng)期速率受 rate 控制
- 線程安全
"""
def __init__(self, rate: float, capacity: int):
"""
:param rate: 令牌補(bǔ)充速率(個(gè)/秒)
:param capacity: 桶的最大容量(突發(fā)上限)
"""
self.rate = rate
self.capacity = capacity
self._tokens = float(capacity) # 初始滿桶
self._last_refill = time.monotonic()
self._lock = threading.Lock()
def _refill(self):
"""根據(jù)時(shí)間流逝補(bǔ)充令牌(惰性計(jì)算,不需要后臺(tái)線程)"""
now = time.monotonic()
elapsed = now - self._last_refill
new_tokens = elapsed * self.rate
self._tokens = min(self.capacity, self._tokens + new_tokens)
self._last_refill = now
def acquire(self, tokens: int = 1) -> bool:
"""
嘗試獲取令牌
:param tokens: 需要的令牌數(shù)(支持批量消費(fèi))
:return: True=放行, False=拒絕
"""
with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
return False
def acquire_with_wait(self, tokens: int = 1,
timeout: float = None) -> bool:
"""
等待直到獲取到令牌(阻塞版本)
:param timeout: 最長(zhǎng)等待時(shí)間(秒),None 表示永久等待
"""
start = time.monotonic()
while True:
with self._lock:
self._refill()
if self._tokens >= tokens:
self._tokens -= tokens
return True
# 計(jì)算需要等待多久才能獲得足夠令牌
with self._lock:
deficit = tokens - self._tokens
wait_time = deficit / self.rate
if timeout is not None:
elapsed = time.monotonic() - start
if elapsed + wait_time > timeout:
return False
time.sleep(min(wait_time, 0.01)) # 最多睡 10ms 避免過(guò)度等待
@property
def current_tokens(self) -> float:
"""查看當(dāng)前令牌數(shù)(調(diào)試用)"""
with self._lock:
self._refill()
return self._tokens
# ── 使用示例 ──────────────────────────────────────────────
def demo_token_bucket():
# 每秒補(bǔ)充 5 個(gè)令牌,桶容量 10
limiter = TokenBucketRateLimiter(rate=5, capacity=10)
print("=== 令牌桶演示 ===")
print(f"初始令牌數(shù):{limiter.current_tokens:.1f}")
# 模擬突發(fā)請(qǐng)求:一次性消費(fèi) 8 個(gè)令牌
results = []
for i in range(12):
allowed = limiter.acquire()
results.append("?" if allowed else "?")
print(f"請(qǐng)求 {i+1:2d}:{'放行' if allowed else '拒絕'} "
f"(剩余令牌:{limiter.current_tokens:.2f})")
# 等待 2 秒后令牌恢復(fù)
print("\n等待 2 秒,令牌恢復(fù)中...")
time.sleep(2)
print(f"2 秒后令牌數(shù):{limiter.current_tokens:.1f}(補(bǔ)充了約 10 個(gè))")
print(f"再次請(qǐng)求:{'放行' if limiter.acquire() else '拒絕'}")
demo_token_bucket()
四、漏桶算法:絕對(duì)平滑,削峰填谷
原理圖解
漏桶示意圖:
請(qǐng)求涌入(任意速率)
↓↓↓↓↓↓↓↓
┌──────────┐
│ │ ← 桶滿則溢出(拒絕請(qǐng)求)
│ 隊(duì)列 │
│ 緩沖 │
│ │
└────┬─────┘
│ 以固定速率漏出(處理請(qǐng)求)
↓
恒定速率輸出
漏桶與令牌桶的核心區(qū)別:漏桶的輸出速率是嚴(yán)格恒定的,不允許突發(fā)。無(wú)論入流量多大,處理速率始終如一。這對(duì)下游服務(wù)的保護(hù)最為徹底,但用戶體驗(yàn)上感知更明顯——即使桶里有空間,也要排隊(duì)等待。
Python 實(shí)現(xiàn)
import time
import threading
from collections import deque
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
class Request:
"""封裝請(qǐng)求及其元數(shù)據(jù)"""
data: Any
arrive_time: float
key: str = "default"
class LeakyBucketRateLimiter:
"""
漏桶限流器
特性:
- 嚴(yán)格恒定的輸出速率
- 超出桶容量的請(qǐng)求直接丟棄
- 天然實(shí)現(xiàn)請(qǐng)求整形(Traffic Shaping)
"""
def __init__(self, rate: float, capacity: int):
"""
:param rate: 漏出速率(請(qǐng)求數(shù)/秒)
:param capacity: 桶容量(最大排隊(duì)數(shù))
"""
self.rate = rate
self.capacity = capacity
self._interval = 1.0 / rate # 兩次漏出之間的間隔
self._queue: deque = deque()
self._lock = threading.Lock()
self._last_leak_time = time.monotonic()
# 啟動(dòng)后臺(tái)漏出線程
self._running = True
self._worker = threading.Thread(target=self._leak_worker, daemon=True)
self._worker.start()
def _leak_worker(self):
"""后臺(tái)工作線程:以固定速率處理請(qǐng)求"""
while self._running:
time.sleep(self._interval)
with self._lock:
if self._queue:
request = self._queue.popleft()
wait_time = time.monotonic() - request.arrive_time
print(f" ?? 漏出處理:key={request.key}, "
f"等待={wait_time*1000:.1f}ms")
def submit(self, data: Any, key: str = "default") -> bool:
"""
提交請(qǐng)求到漏桶
:return: True=入隊(duì)成功, False=桶已滿(丟棄)
"""
with self._lock:
if len(self._queue) >= self.capacity:
print(f" ?? 桶已滿,丟棄請(qǐng)求:key={key}")
return False
request = Request(data=data, arrive_time=time.monotonic(), key=key)
self._queue.append(request)
print(f" ?? 請(qǐng)求入隊(duì):key={key}, 隊(duì)列深度={len(self._queue)}")
return True
def stop(self):
self._running = False
# ── 使用示例 ──────────────────────────────────────────────
def demo_leaky_bucket():
print("\n=== 漏桶演示(2 req/s,桶容量 5)===")
limiter = LeakyBucketRateLimiter(rate=2, capacity=5)
# 模擬突發(fā):瞬間提交 8 個(gè)請(qǐng)求
for i in range(8):
limiter.submit(f"request-{i}", key=f"user_{i % 3}")
# 等待漏桶處理完畢
time.sleep(4)
limiter.stop()
print("漏桶處理完成,輸出速率嚴(yán)格為 2 req/s")
demo_leaky_bucket()
五、滑動(dòng)窗口算法:精準(zhǔn)統(tǒng)計(jì),消除邊界突刺
原理圖解
固定窗口(存在邊界問(wèn)題):
|← 窗口1(0-60s)→|← 窗口2(60-120s)→|
| 9 requests | 9 requests |
↑
邊界時(shí)刻:前后 2 秒內(nèi)實(shí)際有 18 個(gè)請(qǐng)求!
滑動(dòng)窗口(精確統(tǒng)計(jì)):
當(dāng)前時(shí)間 t=75s,窗口大小 60s
統(tǒng)計(jì) [15s, 75s] 內(nèi)的請(qǐng)求數(shù) → 精確到每一秒
滑動(dòng)窗口分為兩種實(shí)現(xiàn):滑動(dòng)日志(精確但內(nèi)存大)和滑動(dòng)計(jì)數(shù)器(近似但高效)。
Python 實(shí)現(xiàn)
import time
import threading
from collections import deque
from typing import Dict
class SlidingWindowLogLimiter:
"""
滑動(dòng)窗口日志限流器(精確版)
記錄每個(gè)請(qǐng)求的時(shí)間戳,精確統(tǒng)計(jì)窗口內(nèi)的請(qǐng)求數(shù)
適合:低流量、高精度要求的場(chǎng)景
缺陷:內(nèi)存占用隨請(qǐng)求量線性增長(zhǎng)
"""
def __init__(self, max_requests: int, window_seconds: float):
self.max_requests = max_requests
self.window_seconds = window_seconds
# key → deque of timestamps
self._logs: Dict[str, deque] = {}
self._lock = threading.Lock()
def is_allowed(self, key: str = "default") -> bool:
now = time.monotonic()
window_start = now - self.window_seconds
with self._lock:
if key not in self._logs:
self._logs[key] = deque()
log = self._logs[key]
# 清除窗口外的舊記錄
while log and log[0] <= window_start:
log.popleft()
# 判斷窗口內(nèi)請(qǐng)求數(shù)
if len(log) < self.max_requests:
log.append(now)
return True
return False
def current_count(self, key: str = "default") -> int:
"""當(dāng)前窗口內(nèi)的請(qǐng)求數(shù)"""
now = time.monotonic()
window_start = now - self.window_seconds
with self._lock:
if key not in self._logs:
return 0
return sum(1 for t in self._logs[key] if t > window_start)
class SlidingWindowCounterLimiter:
"""
滑動(dòng)窗口計(jì)數(shù)器限流器(高效版)
將窗口劃分為多個(gè)小格子,用計(jì)數(shù)器近似統(tǒng)計(jì)
適合:高流量、對(duì)精度要求不苛刻的場(chǎng)景
Redis 分布式限流的主流方案
"""
def __init__(self, max_requests: int, window_seconds: float,
precision: int = 10):
"""
:param precision: 將窗口劃分為多少個(gè)子格子(越多越精確,越耗內(nèi)存)
"""
self.max_requests = max_requests
self.window_seconds = window_seconds
self.precision = precision
self.slot_duration = window_seconds / precision
# 每個(gè) key 的滑動(dòng)計(jì)數(shù)器:{slot_index: count}
self._counters: Dict[str, Dict[int, int]] = {}
self._lock = threading.Lock()
def _current_slot(self) -> int:
"""當(dāng)前時(shí)間對(duì)應(yīng)的格子索引"""
return int(time.monotonic() / self.slot_duration)
def is_allowed(self, key: str = "default") -> bool:
current_slot = self._current_slot()
# 有效的格子范圍
valid_slots = set(range(current_slot - self.precision + 1,
current_slot + 1))
with self._lock:
if key not in self._counters:
self._counters[key] = {}
counter = self._counters[key]
# 清除過(guò)期格子
expired = [s for s in counter if s not in valid_slots]
for s in expired:
del counter[s]
# 統(tǒng)計(jì)窗口內(nèi)總請(qǐng)求數(shù)
total = sum(counter.values())
if total < self.max_requests:
counter[current_slot] = counter.get(current_slot, 0) + 1
return True
return False
# ── 對(duì)比演示 ──────────────────────────────────────────────
def demo_sliding_window():
print("\n=== 滑動(dòng)窗口演示(10 req/10s)===")
log_limiter = SlidingWindowLogLimiter(max_requests=10, window_seconds=10)
counter_limiter = SlidingWindowCounterLimiter(max_requests=10,
window_seconds=10,
precision=10)
# 模擬在 5 秒內(nèi)發(fā)送 15 個(gè)請(qǐng)求
results_log = []
results_counter = []
for i in range(15):
results_log.append(log_limiter.is_allowed("user_1"))
results_counter.append(counter_limiter.is_allowed("user_1"))
time.sleep(0.2)
print(f"滑動(dòng)日志結(jié)果:{['?' if r else '?' for r in results_log]}")
print(f"計(jì)數(shù)器結(jié)果: {['?' if r else '?' for r in results_counter]}")
print(f"日志版窗口內(nèi)請(qǐng)求數(shù):{log_limiter.current_count('user_1')}")
demo_sliding_window()
六、整合實(shí)戰(zhàn):帶限流的 FastAPI 中間件
將三種限流器整合為一個(gè)統(tǒng)一接口,集成到 FastAPI 中:
from abc import ABC, abstractmethod
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import time
class RateLimiter(ABC):
"""限流器抽象基類"""
@abstractmethod
def is_allowed(self, key: str) -> bool:
pass
@abstractmethod
def get_info(self, key: str) -> dict:
"""返回限流狀態(tài)信息(用于響應(yīng)頭)"""
pass
class RateLimitMiddleware:
"""
FastAPI 限流中間件
支持按 IP、用戶 ID、API Key 等維度限流
"""
def __init__(self, app, limiter: RateLimiter,
key_func=None, exclude_paths: list = None):
self.app = app
self.limiter = limiter
self.key_func = key_func or self._default_key_func
self.exclude_paths = exclude_paths or ["/health", "/docs"]
@staticmethod
def _default_key_func(request: Request) -> str:
"""默認(rèn)按客戶端 IP 限流"""
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
return request.client.host
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
request = Request(scope, receive)
# 排除不限流的路徑
if request.url.path not in self.exclude_paths:
key = self.key_func(request)
if not self.limiter.is_allowed(key):
response = JSONResponse(
status_code=429,
content={
"error": "Too Many Requests",
"message": "請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試",
"retry_after": 1
},
headers={
"Retry-After": "1",
"X-RateLimit-Limit": "100",
}
)
await response(scope, receive, send)
return
await self.app(scope, receive, send)
# 構(gòu)建應(yīng)用
app = FastAPI(title="限流演示 API")
# 選擇限流算法(可熱切換)
# 方案1:令牌桶(允許突發(fā))
rate_limiter = TokenBucketRateLimiter(rate=10, capacity=20)
# 方案2:滑動(dòng)窗口(精確統(tǒng)計(jì))
# rate_limiter = SlidingWindowLogLimiter(max_requests=100, window_seconds=60)
app.add_middleware(RateLimitMiddleware, limiter=rate_limiter)
@app.get("/api/data")
async def get_data(request: Request):
client_ip = request.client.host
return {
"message": "請(qǐng)求成功",
"client": client_ip,
"timestamp": time.time()
}
@app.get("/health")
async def health():
return {"status": "ok"}
七、Redis 分布式限流:生產(chǎn)環(huán)境的標(biāo)準(zhǔn)方案
單機(jī)限流器在多實(shí)例部署時(shí)會(huì)失效——每個(gè)實(shí)例各自維護(hù)狀態(tài),總請(qǐng)求數(shù)可能超出預(yù)期。生產(chǎn)環(huán)境必須用 Redis 實(shí)現(xiàn)分布式限流:
import redis
import time
class RedisTokenBucketLimiter:
"""
基于 Redis 的分布式令牌桶限流器
使用 Lua 腳本保證原子性
"""
# Lua 腳本:原子化地補(bǔ)充令牌并判斷是否放行
LUA_SCRIPT = """
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local last_tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)
local last_refill = tonumber(redis.call('hget', key, 'last_refill') or now)
-- 補(bǔ)充令牌
local elapsed = now - last_refill
local new_tokens = math.min(capacity, last_tokens + elapsed * rate)
local allowed = 0
if new_tokens >= requested then
new_tokens = new_tokens - requested
allowed = 1
end
redis.call('hset', key, 'tokens', new_tokens)
redis.call('hset', key, 'last_refill', now)
redis.call('expire', key, math.ceil(capacity / rate) * 2)
return {allowed, math.floor(new_tokens)}
"""
def __init__(self, redis_url: str, rate: float, capacity: int):
self.redis = redis.from_url(redis_url)
self.rate = rate
self.capacity = capacity
self._script = self.redis.register_script(self.LUA_SCRIPT)
def is_allowed(self, key: str, tokens: int = 1) -> tuple[bool, int]:
"""
:return: (是否允許, 剩余令牌數(shù))
"""
now = time.time()
result = self._script(
keys=[f"ratelimit:{key}"],
args=[self.rate, self.capacity, now, tokens]
)
allowed = bool(result[0])
remaining = int(result[1])
return allowed, remaining
# 使用示例(需要 Redis 服務(wù))
# redis_limiter = RedisTokenBucketLimiter(
# redis_url="redis://localhost:6379",
# rate=10,
# capacity=50
# )
# allowed, remaining = redis_limiter.is_allowed("user:12345")
# print(f"放行:{allowed},剩余令牌:{remaining}")
八、三大算法橫向?qū)Ρ?/h2>
| 維度 | 令牌桶 | 漏桶 | 滑動(dòng)窗口 |
|---|---|---|---|
| 突發(fā)流量 | ? 允許(桶容量?jī)?nèi)) | ? 嚴(yán)格排隊(duì) | ? 有限允許 |
| 輸出平滑度 | 中 | 極高 | 高 |
| 實(shí)現(xiàn)復(fù)雜度 | 低 | 中 | 中 |
| 內(nèi)存占用 | 極低 | 中(隊(duì)列) | 低(計(jì)數(shù)) |
| 精確度 | 高 | 高 | 近似(計(jì)數(shù)器版) |
| 適用場(chǎng)景 | API 調(diào)用限額 | 流量整形 | 精細(xì)化統(tǒng)計(jì) |
| 典型應(yīng)用 | GitHub API | 網(wǎng)絡(luò)帶寬限速 | Nginx limit_req |
選型建議:
如果你的場(chǎng)景是對(duì)外 API 服務(wù),用戶可能有合理的突發(fā)需求(如批量導(dǎo)出),選令牌桶,既能控制長(zhǎng)期速率,又不會(huì)讓正常突發(fā)請(qǐng)求體驗(yàn)太差。
如果你的場(chǎng)景是保護(hù)下游服務(wù)(如數(shù)據(jù)庫(kù)、第三方 API),需要嚴(yán)格控制請(qǐng)求速率避免過(guò)載,選漏桶,它能將任意突發(fā)流量整形為恒定輸出。
如果你需要精確統(tǒng)計(jì)時(shí)間窗口內(nèi)的請(qǐng)求次數(shù)(如"每分鐘最多 100 次"的 SLA 保障),選滑動(dòng)窗口,配合 Redis 可輕松實(shí)現(xiàn)分布式精確限流。
九、總結(jié)
限流是系統(tǒng)穩(wěn)定性設(shè)計(jì)的基石,三種算法各有側(cè)重:令牌桶平衡突發(fā)與限速,漏桶追求絕對(duì)平滑,滑動(dòng)窗口精確感知流量。在 Python 實(shí)戰(zhàn)中,本地場(chǎng)景用線程安全的純 Python 實(shí)現(xiàn)即可;多實(shí)例部署時(shí),務(wù)必引入 Redis + Lua 腳本的分布式方案,保證全局一致性。
好的限流器不只是"拒絕請(qǐng)求",它還應(yīng)該做到:返回清晰的 429 Too Many Requests、在響應(yīng)頭中告知 Retry-After 時(shí)間、對(duì)不同用戶等級(jí)設(shè)置差異化配額。這些細(xì)節(jié),才是用戶友好型 API 的體現(xiàn)。
限流的本質(zhì)是保護(hù)——保護(hù)你的服務(wù),也保護(hù)你的用戶。把這道門(mén)守好,系統(tǒng)才能在風(fēng)雨中巋然不動(dòng)。
以上就是使用Python手搓一個(gè)生產(chǎn)級(jí)限流器的詳細(xì)內(nèi)容,更多關(guān)于Python限流器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python安裝numpy&安裝matplotlib& scipy的教程
下面小編就為大家?guī)?lái)一篇python安裝numpy&安裝matplotlib& scipy的教程。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
使用Python構(gòu)建Markdown轉(zhuǎn)Word文檔轉(zhuǎn)換器
在當(dāng)今的文檔處理中,Markdown因其簡(jiǎn)潔的語(yǔ)法和易讀性而廣受歡迎,而Microsoft Word(DOCX格式)則因其廣泛的兼容性和專業(yè)的排版效果成為商業(yè)文檔的標(biāo)準(zhǔn),本文將介紹如何使用Python構(gòu)建一個(gè)帶有圖形界面的Markdown轉(zhuǎn)Word文檔轉(zhuǎn)換器,需要的朋友可以參考下2025-02-02
初學(xué)者學(xué)習(xí)Python好還是Java好
在本篇文章里小編給大家分享的是關(guān)于初學(xué)者學(xué)習(xí)Python好還是Java好的相關(guān)內(nèi)容,需要的朋友們可以學(xué)習(xí)下。2020-05-05
Python實(shí)現(xiàn)批量識(shí)別銀行卡號(hào)碼以及自動(dòng)寫(xiě)入Excel表格步驟詳解
這篇文章主要介紹了使用Python實(shí)現(xiàn)高效摸魚(yú),批量識(shí)別銀行卡號(hào)碼并且自動(dòng)寫(xiě)入Excel表格,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01
python+opencv實(shí)現(xiàn)移動(dòng)偵測(cè)(幀差法)
這篇文章主要為大家詳細(xì)介紹了python+opencv實(shí)現(xiàn)移動(dòng)偵測(cè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
Python實(shí)現(xiàn)對(duì)PPT文件進(jìn)行截圖操作的方法
這篇文章主要介紹了Python實(shí)現(xiàn)對(duì)PPT文件進(jìn)行截圖操作的方法,涉及Python操作幻燈片的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04

