☰
Python爬虫简单运用爬取代理IP的实现
2026/10/10 13:39:05 网站建设 项目流程

前言


这篇讲的是页面解析的技术面:给定一个内容里含有ip:port的页面,怎么把地址抽出来。这是一个很典型的「结构化抽取」练习,和「去哪找代理」是两个问题。标题里说「爬取代理IP」,很容易让人以为重点是「找到免费代理站然后抓下来」——本文不这么做,也不会给出任何真实站点的地址:示例里的 HTML 全部是我自己构造的本地字符串,你在自己的编辑器里就能跑。


有必要把来源问题说清楚,因为它直接决定这篇要不要写。公开的「免费代理」来源不可信,原因不是「质量差」这么简单:



  • 你根本不知道流量经过谁。一个代理的运营者可以看到你通过它发出的全部明文请求,包括你带的 Cookie、Token、以及请求内容,这等于把一个陌生人插进了你的请求链路。

  • 有些「免费代理」本身就是蜜罐,专门用来收集谁在用代理、以及通过代理访问了什么。

  • 有些地址根本不做转发,而是把请求直接发出去再回来,你的真实来源 IP 就这么泄露了。

  • 还有一类会在响应里注入内容,做中间人式的篡改。


所以即便你只是想练手,也请把练习范围限定在自己构造的页面上;真要用于生产,来源必须是你有权使用的、可以追溯的(自建代理、公司出口、付费代理服务)。


本机没有 Python 解释器,也没有装requests等第三方库,本文代码未在本机运行验证,只作逐行人工推演;用到的标准库(html.parser、re、ipaddress)都是随 Python 一起分发的,第三方库的部分会注明需要先pip install。


一、先定任务边界


先说清楚这篇的输入输出:




项内容



输入一段含有地址列表的 HTML 文本(本文中为本地字符串)

输出一个去重后的host:port字符串列表

不做的事不做真实网络请求、不访问任何真实站点、不校验地址是否可用

前提页面来源是你有权解析的内容



这里要特别强调「不做的事」:本文只到「把文本里的地址抽出来」为止。抽出来的地址一律视为不可信,能不能用、要不要用,是另一个必须单独决策的问题。


二、用标准库html.parser解析本地 HTML


标准库自带一个 HTML 解析器html.parser,它的用法是:继承HTMLParser,重写handle_starttag、handle_data、handle_endtag这几个钩子,解析器在扫到对应结构时回调你。它不需要装任何东西,这是它最大的优点。


先构造一段页面。这是我自己写的演示 HTML,表格里混了正常地址和几个不合法的值,用来检验后面抽取逻辑的健壮性:


# 适用于 Python 3.8+
DEMO_HTML = """
<html><body>
<table>
<tr><th>IP</th><th>端口</th><th>位置</th></tr>
<tr><td>192.0.2.10</td><td>8080</td><td>演示机房 A</td></tr>
<tr><td>192.0.2.11</td><td>3128</td><td>演示机房 B</td></tr>
<tr><td>198.51.100.7</td><td>8888</td><td>演示机房 C</td></tr>
<tr><td>999.1.1.1</td><td>80</td><td>非法 IP,应被丢弃</td></tr>
<tr><td>203.0.113.9</td><td>70000</td><td>端口越界,应被丢弃</td></tr>
</table>
</body></html>
"""

192.0.2.0/24、198.51.100.0/24、203.0.113.0/24是 RFC 5737 专门保留给文档示例的网段,不会指向真实主机——这正是你写示例时该用的地址段。


接下来写解析器,把表格的每一行读出来:


# 适用于 Python 3.8+
from html.parser import HTMLParser

class TableRows(HTMLParser):
"""把表格解析成 [[单元格, 单元格, ...], ...]"""
def __init__(self):
super().__init__()
self.rows = []
self._row = None
self._cell = None

def handle_starttag(self, tag, attrs):
if tag == "tr":
self._row = []
elif tag in ("td", "th") and self._row is not None:
self._cell = []

def handle_data(self, data):
if self._cell is not None:
self._cell.append(data)

def handle_endtag(self, tag):
if tag in ("td", "th") and self._cell is not None:
if self._row is not None:
self._row.append("".join(self._cell).strip())
self._cell = None
elif tag == "tr" and self._row is not None:
self.rows.append(self._row)
self._row = None

parser = TableRows()
parser.feed(DEMO_HTML) # 解析本地字符串,没有任何网络请求
parser.close()
for row in parser.rows:
print(row)

这里有几个容易踩的点,都在代码里处理了:td出现在tr之外时self._row是None,所以每次用到它都先判空;handle_data可能被调用多次(单元格里嵌了别的标签时,文字会被拆成几段回调),所以单元格内容要先收集到一个列表再join,不能直接赋值覆盖;strip()必须在join之后做,否则拿到的可能是带换行的碎片。


feed()只负责喂数据,建议最后调用一次close(),让解析器把缓冲里的内容处理完。


三、用ipaddress校验,而不是只靠正则


表格只是搬运工,真正的校验在抽取这一步。最直观的做法是用正则匹配ip:port:


# 适用于 Python 3.8+
import ipaddress
import re

ADDR_RE = re.compile(r"(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})")

def to_endpoint(host, port_text):
"""校验并规范化一个 host:port,非法则返回 None"""
try:
ipaddress.IPv4Address(host) # 非法 IPv4 会抛 AddressValueError
except ipaddress.AddressValueError:
return None
try:
port = int(port_text)
except ValueError:
return None
if not (1 <= port <= 65535):
return None
return f"{host}:{port}"

要理解正则和校验的分工:正则只负责「找出长得像的片段」,ipaddress才负责判断「它是不是合法的 IPv4」。上面那个\d{1,3}允许999.1.1.1,正则完全无法识别;而ipaddress.IPv4Address("999.1.1.1")会直接抛AddressValueError,把它挡掉。端口同理——70000能通过正则的\d{1,5},但超出1..65535的范围,必须自己检查。


把两段接起来,从解析出的表格行里抽地址:


# 适用于 Python 3.8+
# ADDR_RE 与 to_endpoint 沿用上一段代码里的定义
def extract_endpoints(rows):
seen = set()
result = []
for row in rows:
if len(row) < 2:
continue
# 演示页面把 IP 和端口放在两列里,先用冒号拼回 "ip:port" 再匹配
joined = ":".join(row[:2])
m = ADDR_RE.fullmatch(joined)
if not m:
continue
endpoint = to_endpoint(m.group(1), m.group(2))
if endpoint and endpoint not in seen:
seen.add(endpoint)
result.append(endpoint)
return result

print(extract_endpoints(parser.rows))
# 预期只保留 3 条合法地址,非法 IP 与越界端口各被丢一条

这里用的是fullmatch而不是search:因为joined已经被我们拼成了恰好两段,用fullmatch要求整串都符合ip:port的形状,能挡掉「IP 列里混了别的东西」的情况。如果页面里是「一格里写着1.2.3.4:8080」这种形态,直接用search找子串更合适——用哪个取决于文本的形状。


用set去重是必要的:同一批来源里重复地址很常见,不去重会让下游的校验做无用功。


四、第三方解析器:思路与注意事项


如果你的页面结构比表格复杂(嵌套层很深、有大量属性、需要按 CSS 选择器或 XPath 定位),标准库的html.parser会写得很啰嗦,这时通常会用lxml或BeautifulSoup(bs4)。它们是第三方库,需要先pip install lxml/pip install beautifulsoup4,本机没有安装。


用它们的思路是一模一样的,只是把「手写状态机」换成「选择器」:定位到承载地址的那批节点,取出文本,再走上面同一套正则加ipaddress的校验。本机既没有这些库,我也无法运行验证,所以这里不写出具体到参数名的调用——find_all也好、xpath也好,参数名和返回类型请以这两个库的官方文档为准,不要照抄任何你没核对过的签名。


还有个层次问题值得单独提醒:requests也是第三方库(pip install requests),它负责把页面取回来。但本文的所有示例都刻意不做网络请求——把「取」和「解析」分开,既方便测试(解析函数可以喂本地字符串),也避免让示例默认指向某个真实站点。


另外,如果你要解析的是 HTML 里嵌的ip:port列表,还要注意不要用正则去解析整个 HTML 文档。HTML 不是正则语言,一个跨标签的正则匹配很容易在复杂页面上失控(回溯、误匹配、把注释里的内容也抓出来)。正则只应该作用在已经由解析器取出的文本节点上,这是上面流程里先解析、后正则的原因。


五、抽出来之后:一律当作不可信


抽到地址不等于能用。至少要再走这几步,且每一步都要意识到「这些地址可能本身就是陷阱」:



  1. 去重(上面已经做了);

  2. 逐个校验(另写一个真实探测环节,探测目标必须是你自己控制、且允许被探测的服务);

  3. 加来源标记,记录每个地址来自哪一批数据,出问题时能追溯;

  4. 绝不把来源不明的地址用于携带 Cookie、Token 或任何身份凭据的请求——这等于把凭据交给一个陌生人。


第 4 条最要紧。用免费代理去访问需要登录的站点,等于把你自己的会话凭据直接送给了代理运营者。


常见坑点



  1. ❌ 直接对一整篇 HTML 用正则匹配ip:port,跳过解析器。


✅ 先用html.parser之类的解析器取出文本节点,再对文本节点用正则;HTML 不是正则语言。



  1. ❌ 只用正则\d{1,3}(\.\d{1,3}){3}判断地址合法性,于是999.1.1.1被当成有效地址。


✅ 正则找候选,ipaddress.IPv4Address做合法性校验,端口再单独检查1..65535。



  1. ❌ 在handle_data里直接self._cell = data,结果一段文字被拆成多次回调时只剩最后一片。


✅ 先把片段收集进列表,handle_endtag时再join并strip。



  1. ❌ 把td标签写在tr外面,代码里self._row还是None就直接append,抛AttributeError。


✅ 每次使用前判空,self._row is not None才写入。



  1. ❌ 从公开免费代理站抓地址,直接拿来用,还带着自己的登录 Cookie。


✅ 公开免费代理可能是蜜罐或中间人;绝不用来源不明的代理发送任何凭据,来源必须可追溯。



  1. ❌ 相信「地址在页面上出现过就是可用的」,不做去重也不做校验。


✅ 去重、记录来源、独立校验三步都要做;抽出来只是候选,不是可用资源。



  1. ❌ 为了解析复杂页面,凭印象写一个bs4或lxml的调用,参数名都没核对。


✅ 这些是第三方库,需先安装;接口以各自官方文档为准,不确定就不要写出具体参数名。



  1. ❌ 在示例里写死一个真实站点地址,读者一运行就去打了别人的服务器。


✅ 示例一律用具名保留网段(如192.0.2.0/24)和本地构造的 HTML 字符串。


总结




步骤用什么关键点



准备输入自己构造的本地 HTML 字符串用 RFC 5737 保留网段,不指向真实站点

解析结构html.parser.HTMLParser处理tr/td嵌套、多次handle_data、上下文判空

抽取候选re正则只对文本节点匹配,不在整篇文档上跑

校验ipaddress.IPv4Address+ 端口范围正则管「像」,ipaddress管「是不是合法」

收尾set去重、来源标记抽出来的地址一律视为不可信



「爬取代理IP」这件事,技术上的难点其实很有限——解析一段结构化的 HTML、抽字段、做校验,都是基础操作。真正需要投入判断力的是来源和用途:这段 HTML 从哪来、抽出的地址能不能信、拿它们去干什么。示例用本地字符串,不只是因为本机没有网络环境,更是因为在教学场景里,让读者把注意力放在解析逻辑而不是「哪个站还能抓」上,才是对的。


参考:html.parser、re、ipaddress的接口以 Python 官方文档为准;lxml、BeautifulSoup、requests为第三方库,需另行安装,接口以其官方文档为准;本文代码未在本机运行,仅作人工推演。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询