前言

写 CTF EXP 时最常打交道的就是正则提取、HTTP 请求、HTML 解析和并发。这篇文章把 Python 写 EXP 最常用的六个模块整理成速查手册——正则、文件操作、requests、BeautifulSoup、多进程、多线程——每个都有可复制的代码片段。


一、正则表达式(re 模块)

1.1 特殊符号速查

符号 含义
\s 空白符号(空格、制表符、换行等)
\S 除空白符号以外的任意符号
\w 字母、数字、下划线 [a-zA-Z0-9_]
\W 除字母、数字、下划线以外的符号
\d 数字 [0-9]
\D 除数字以外的任意符号
. 除换行符以外的任意字符(re.S 模式下匹配换行)
^ 匹配字符串开头
$ 匹配字符串结尾
\b 单词边界

1.2 限定符(量词)

限定符 含义
X+ X 至少出现 1 次,最多无限次
X* X 出现 0 次或无限次
X? X 出现 0 次或 1 次
X{m} X 恰好出现 m 次
X{m,} X 至少出现 m 次
X{m,n} X 出现 m 到 n 次

1.3 范围符(字符类)

1
2
3
4
5
6
7
8
9
10
[abc]        任选其一(a 或 b 或 c)
[^abc] 排除 a、b、c
[a-z] 所有小写字母
[A-Z] 所有大写字母
[0-9] 所有数字,等价于 \d
[^0-9] 非数字,等价于 \D
[0-9a-zA-Z_] 等价于 \w
[^0-9a-zA-Z_] 等价于 \W
[\t\r\n\f] 空白符集合,等价于 \s
[^\t\r\n\f] 非空白符,等价于 \S

1.4 实战示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re

# 手机号
text = "13787782556"
pattern = r"^(\+86\-)?1[3-9][0-9]{9}$"
print(re.match(pattern, text))

# URL
text = "http://www.baidu.com"
pattern = r"^(((http)|(https)|(ftp))\:\/\/)(w{3}\.)[A-Za-z0-9\-]{2,18}?(\.([a-zA-Z0-9\-]){2,18})$"
print(re.match(pattern, text))

# IP 地址
text = "127.0.0.1"
pattern = r"(\d{1,3}\.){3}\d{1,3}"
print(re.match(pattern, text))

1.5 常用匹配函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import re

text = "hello world 123"

# search —— 在任意位置搜索,返回第一个匹配对象
result = re.search(r"\d+", text)
print(result.group()) # 123

# match —— 必须从字符串开头匹配
result = re.match(r"hello", text)
print(result.group()) # hello

# findall —— 返回所有匹配,列表形式
results = re.findall(r"\w+", text)
print(results) # ['hello', 'world', '123']

# finditer —— 返回迭代器,每个元素是 match 对象
for item in re.finditer(r"\w+", text):
print(item.group())

# sub —— 替换
result = re.sub(r"\d+", "456", text)
print(result) # hello world 456

match vs search match 必须从文本开头匹配,search 可以在任意位置匹配。写 EXP 时大多数情况用 search

1.6 flags 参数

1
re.search(pattern, text, flags=re.I)
修饰符 描述
re.I / re.IGNORECASE 不区分大小写
re.M / re.MULTILINE 多行模式,^$ 匹配每行的开头和结尾
re.S / re.DOTALL . 匹配包括换行在内的所有字符
re.U / re.UNICODE Unicode 模式(Python 3 默认)
re.X / re.VERBOSE 允许写带注释和空白的正则,可读性更好
re.A / re.ASCII ASCII 模式,\w \b 等只匹配 ASCII 字符

1.7 贪婪模式 vs 非贪婪模式

1
2
3
4
5
6
7
8
9
text = "<div>hello</div><div>world</div>"

# 贪婪模式:尽可能多匹配
print(re.findall(r"<div>.*</div>", text))
# ['<div>hello</div><div>world</div>']

# 非贪婪模式:尽可能少匹配
print(re.findall(r"<div>.*?</div>", text))
# ['<div>hello</div>', '<div>world</div>']

记忆:* + ? {m,n} 后面加 ? 变成非贪婪。

1.8 多列表并行遍历(zip

从页面中提取多个并列字段时的常用写法:

1
2
3
4
5
names = re.findall(r'<td class="name">(.*?)</td>', html)
grades = re.findall(r'<td class="grade">(.*?)</td>', html)

for name, grade in zip(names, grades):
print(f"{name}: {grade}")

二、文件操作

2.1 基本读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 读文件
with open("./host.txt", mode="r", encoding="UTF-8") as f:
content = f.read() # 整个文件读成一个字符串
# for char in content: ... # 逐字符遍历

# 逐行读
with open("./host.txt", mode="r", encoding="UTF-8") as f:
for line in f:
print(line.strip()) # strip() 去掉行尾换行符

# 写文件
with open("./output.txt", mode="w", encoding="UTF-8") as f:
f.write("hello world\n")

# 追加写
with open("./output.txt", mode="a", encoding="UTF-8") as f:
f.write("append line\n")

2.2 模式说明

模式 含义
r 只读(默认)
w 写入,清空原内容再写
a 追加,在原内容末尾添加
r+ 读写,不截断
w+ 读写,清空原内容
a+ 读写,追加模式
b 二进制模式(如 rbwb

EXP 常用: 读用 r,写用 wa。一般场景不用 w+/a+


三、requests 模块——HTTP 请求

3.1 GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = "http://target.com/api"
headers = {
"User-Agent": "Mozilla/5.0",
"Cookie": "session=xxx",
}

resp = requests.get(url, headers=headers, timeout=5, verify=False)
print(resp.status_code) # 200
print(resp.text) # 响应体(字符串)
print(resp.content) # 响应体(bytes)
print(resp.headers) # 响应头
print(resp.cookies) # Cookie

verify=False:忽略 HTTPS 证书验证(自签名证书时使用)。

3.2 POST 请求

1
2
3
4
5
6
7
# JSON 方式
data = {"key1": "value1", "key2": "value2"}
resp = requests.post(url, json=data, headers=headers)

# 表单方式
data = {"username": "admin", "password": "123456"}
resp = requests.post(url, data=data, headers=headers)

3.3 保持会话(Session

1
2
3
4
5
6
7
sess = requests.Session()

# 登录
sess.post("http://target.com/login", data={"user": "admin", "pass": "xxx"})

# 后续请求自动带 Cookie
resp = sess.get("http://target.com/admin")

3.4 代理

1
2
3
4
5
proxies = {
"http": "http://127.0.0.1:8080",
"https": "http://127.0.0.1:8080",
}
resp = requests.get(url, proxies=proxies)

3.5 常见 HTTP 状态码

状态码 含义
200 成功
301 永久重定向
302 临时重定向
401 未授权(需登录)
403 禁止访问
404 不存在
500 服务器内部错误

四、BeautifulSoup 模块——HTML 解析

4.1 初始化

1
2
3
4
5
from bs4 import BeautifulSoup
import requests

resp = requests.get("http://target.com/page")
soup = BeautifulSoup(resp.text, "html.parser") # 指定解析器

4.2 select —— CSS 选择器(最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 标签选择
soup.select('a') # 所有 <a> 标签
soup.select('div a') # <div> 下所有 <a>(后代)
soup.select('div > a') # <div> 下直接子元素 <a>
soup.select('div > ul > li > a') # 逐级选择

# class 选择
soup.select('.juejin') # class="juejin"

# id 选择
soup.select('#sp') # id="sp"

# 属性选择
soup.select('a[id]') # 有 id 属性的 <a>
soup.select('a[id="a1"]') # id="a1" 的 <a>

# 获取文本
for item in soup.select('.cn-box > p'):
print(item.get_text())

4.3 find —— 查找单个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
# 按标签 + 属性查找
meta = soup.find("meta", {"name": "description"})
print(meta["content"]) # 取属性值

# 按 class 查找(注意 class_ 带下划线)
link = soup.find('a', class_='juejin')

# 按自定义属性查找
link = soup.find('a', title='home')

# 取属性值
img = soup.find("img")
print(img["src"]) # 直接拿 src 属性

4.4 find_all —— 查找所有元素

1
2
3
4
5
6
7
8
9
10
11
# 获取所有匹配元素
links = soup.find_all('a', {"name": "aaa"})
for link in links:
print(link["href"])

# 嵌套使用
container = soup.find_all('div', {"class": "container"})
for div in container:
inner = div.find('span', {"class": "title"})
if inner:
print(inner.get_text())

4.5 实战:爬取 HTML 表格数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bs4 import BeautifulSoup
import requests

resp = requests.get("http://target.com/table")
soup = BeautifulSoup(resp.text, "html.parser")

rows = soup.select('table.data-table tr')
for row in rows[1:]: # 跳过表头
cols = row.select('td')
if len(cols) >= 3:
name = cols[0].get_text().strip()
value = cols[1].get_text().strip()
flag = cols[2].get_text().strip()
print(f"{name} | {value} | {flag}")

五、多进程(multiprocessing

5.1 进程三大状态

1
2
3
就绪 (Ready) ──→ 运行 (Running) ──→ 阻塞 (Blocked)
↑ │
└─────────────────────────────────────┘
  • 就绪: 一切准备就绪,等待 CPU 调度
  • 运行: 正在 CPU 上执行
  • 阻塞: 等待 I/O、sleep、锁等

5.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from multiprocessing import Process
import time


def worker(name, num):
print(f"[{name}] 开始, num={num}")
time.sleep(2)
print(f"[{name}] 完成")


if __name__ == '__main__':
p1 = Process(target=worker, args=("进程1", 42))
p1.start() # 启动子进程
p1.join() # 等待子进程结束后再继续
print("主进程结束")

__name__ == '__main__' 是必须的! Windows 上不加这个会无限递归创建子进程。

5.3 Process 参数

1
2
3
4
5
6
7
p = Process(
target=func, # 目标函数
args=(), # 位置参数
kwargs={"key": "value"}, # 关键字参数
name="my_process", # 进程名
daemon=True, # 守护进程:主进程退出时自动终止子进程
)

5.4 继承 Process 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from multiprocessing import Process
import time


class Test(Process):
def __init__(self, name):
super(Test, self).__init__()
self.name = name

def run(self):
print(f"{self.name} running")
time.sleep(3)


if __name__ == '__main__':
t = Test("aaa")
t.start()
t.join()

5.5 进程关键特性

特性 说明
不共享全局变量 子进程复制父进程的内存空间,修改互不影响
能拿到全局变量 子进程启动时会拷贝一份父进程的全局变量
进程间通信 需要 QueuePipeManager 等机制

5.6 守护进程 vs 孤儿进程

类型 定义
守护进程 子进程跟随父进程——父进程死,子进程自动关daemon=True
孤儿进程 父进程先死,子进程被 init(PID 1)收养,继续运行

六、多线程(threading

6.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading
import time


def worker(name, num):
print(f"[{name}] 开始, num={num}")
time.sleep(2)
print(f"[{name}] 完成")


t1 = threading.Thread(target=worker, args=("线程1", 42))
t1.start() # 启动线程
t1.join() # 等待线程结束
print("主线程结束")

用法与 Process 几乎相同,target args kwargs daemon 等参数通用。

6.2 继承 Thread 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import threading
import time


class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name

def run(self):
print(f"{self.name} running")
time.sleep(3)


t = MyThread("worker")
t.start()
t.join()

6.3 线程 vs 进程对比

多线程 (threading) 多进程 (multiprocessing)
全局变量共享 共享(同一内存空间) 不共享(独立内存空间)
GIL 影响 CPU 密集型受限(同一时刻只有一个线程执行 Python 字节码) 不受 GIL 限制
适用场景 I/O 密集型(网络请求、文件读写) CPU 密集型(计算、爆破)
创建开销
内存占用 共享进程内存 各自独立内存

6.4 EXP 中多线程的典型用法

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
import threading
import requests

results = []
lock = threading.Lock()


def brute(uid):
"""并发爆破用户 ID"""
url = f"http://target.com/user/{uid}"
resp = requests.get(url, timeout=5)
if "flag" in resp.text:
with lock:
results.append((uid, resp.text))
print(f"[+] Found: {uid}")


threads = []
for uid in range(1, 1000):
t = threading.Thread(target=brute, args=(uid,))
t.start()
threads.append(t)

for t in threads:
t.join()

print(f"Results: {len(results)}")

lock 的作用: 多线程共享 results 列表时,append 虽然是原子操作但你仍需要 lock 保护对共享资源的读写,特别是 print 和写文件的场景。


七、综合实战——一个完整的爬虫 EXP 骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env python3
"""CTF 爬虫 EXP 骨架"""

import re
import threading
import requests
from bs4 import BeautifulSoup

# ====== 配置 ======
TARGET = "http://target.example.com"
THREADS = 10
lock = threading.Lock()
results = []


def fetch_page(path):
"""抓取页面"""
sess = requests.Session()
resp = sess.get(f"{TARGET}{path}", timeout=5)
return resp.text


def parse_data(html):
"""解析 HTML,提取数据"""
soup = BeautifulSoup(html, "html.parser")
# CSS 选择器提取
items = soup.select('.data-item')
for item in items:
name = item.select_one('.name').get_text().strip()
value = item.select_one('.value').get_text().strip()
# 正则二次提取
flag_match = re.search(r'flag\{[^}]+\}', value)
if flag_match:
with lock:
results.append(flag_match.group())
print(f"[+] Flag: {flag_match.group()}")
return items


def brute_param(param):
"""并发爆破参数"""
url = f"{TARGET}/check?key={param}"
try:
resp = requests.get(url, timeout=3)
if resp.status_code == 200 and "exists" in resp.text:
with lock:
print(f"[+] Valid: {param}")
except:
pass


def run():
# 阶段1: 爬取主页
html = fetch_page("/index")
items = parse_data(html)

# 阶段2: 多线程爆破
threads = []
for i in range(1000):
t = threading.Thread(target=brute_param, args=(f"key_{i}",))
t.start()
threads.append(t)
# 控制并发数
if len(threads) >= THREADS:
for t in threads:
t.join()
threads = []

for t in threads:
t.join()

# 阶段3: 输出结果
print(f"\n=== 共找到 {len(results)} 个 flag ===")
for r in results:
print(r)


if __name__ == "__main__":
run()

参考