最近在折腾设备指纹相关的项目,需要把互联网上常见的协议标识、端口号和服务名整理成一份离线字典。刚开始我选择了一个最笨的办法:手动打开IANA的注册表页面,一个一个复制粘贴。几十个页面下来,眼睛都快看花了,数据还没整理完。后来我决定写一个Python爬虫,直接从IANA的注册表目录页采集数据,把"注册表名称、类型、更新时间、说明页"这些结构化字段统一抓下来。这篇文章就是这次实战的完整记录,包含我的踩坑经验,适合刚学爬虫想找真实项目练手的人,也适合需要批量获取IANA协议参数的开发者。我会把从页面分析、代码编写到问题排查的全过程都写清楚,代码可以直接复制去跑。
1. 项目整体设计与思路拆解
1.1 IANA注册表目录到底是什么
IANA(Internet Assigned Numbers Authority,互联网号码分配机构)是负责维护全球互联网协议参数的核心机构。它维护的注册表覆盖了TCP/UDP端口号、DNS参数、HTTP状态码、字符集、MIME类型、协议号等几乎所有网络底层数据。这些数据散落在大量独立页面中,比如TCP端口注册表、DNS参数注册表等。
在收集这些数据之前,需要先想清楚一件事:我是需要某一个注册表的完整内容,还是需要所有注册表的索引信息?大多数场景下,我们做数据字典,第一步先要弄清楚"有哪些注册表、每个注册表对应什么协议、最近更新时间是什么时候",这其实就是目录页的信息。目录页就像一本书的目录,先把整本书的章节结构抓下来,后续要深入某一章时再按需跳转。
IANA为了便于人工查阅,在 https://www.iana.org/assignments/ 和 https://www.iana.org/protocols 等目录页里,用统一的表格结构展示了各注册表的信息。仔细看会发现,表格里的列非常规整,通常包含注册表名称、协议名称或类型、更新时间,以及指向具体注册表详情页的链接。这种结构对爬虫极其友好,因为数据不是散落在自由文本里,而是有清晰的HTML标签边界。
1.2 为什么选择"目录页"作为切入点
做爬虫采集,最忌讳一上来就写一堆代码,把所有详情页全部抓一遍。先抓目录页有几个好处:
第一,目录页体积小,单次请求就能拿到大量注册表的元信息,单位请求的"信息密度"非常高。第二,目录页能帮你摸清整个站点下有哪些资源、每个资源的URL是什么格式,后续如果需要深入采集,可以根据目录页提供的说明页链接做二次请求。第三,目录页带有注册表的更新时间,这也是很多数据字典类应用特别看重的一个字段,没有它,你维护离线数据时根本不知道数据是否过期。
所以这次项目的核心思路就是:先采集目录页,提取注册表名称、类型、更新时间、说明页链接这四个核心字段,然后落成一份结构化的CSV文件。如果后续需要某个具体注册表的全量数据,再拼接说明页链接去抓详情页即可。这个方案的扩展性很好,不会把自己锁死在单一爬取任务上。
1.3 技术选型与取舍
技术栈我选的是Python 3 + requests + BeautifulSoup + lxml + pandas。为什么不用Scrapy?因为这次目标站点明确、页面数量少、不需要分布式调度,用轻量级库组合更灵活,出问题也好调试。
requests负责HTTP请求,它比Python自带的urllib写起来舒服太多,重定向、Session、请求头设置都非常直观。BeautifulSoup负责解析HTML,配合lxml解析器,速度和容错率都能接受。pandas用来做数据清洗和最后落盘,处理表格类数据比直接写CSV模块更省事,尤其是当你要去重、排序、统计的时候。
有一点要强调:这次不推荐用正则表达式解析。互联网页面结构经常会有微调,正则一写死就崩,而且可读性极差。BeautifulSoup基于标签的查找方式,即使页面加了一两列或者多了一层div,只要表格主体结构不变,代码基本不用改。
2. IANA页面结构分析与数据定位
2.1 确定目标页面与采集范围
IANA的注册表目录页有几个不同入口。本次采集的主入口是 https://www.iana.org/assignments/ 下的"Protocol Registries"目录,打开后能看到一个长表格,里面每一行都是一个注册表条目。每个条目的列内包含注册表名称(通常是一个超链接)、协议类型、状态/更新时间、说明等信息。
页面是服务端渲染的静态HTML,这意味着一开始就赢了一半,不需要处理JavaScript动态加载、不需要分析XHR接口,直接用requests就能拿到完整的HTML源码。这种页面在爬虫世界属于"开卷考试",比那些接口加密、前端渲染的现代Web应用友好太多了。
采集范围建议限定在目录页本身的表格数据,不自动跟踪每个注册表的详情页。原因有两个:一是部分注册表详情页数据量极大,比如"Service Name and Transport Protocol Port Number Registry"有上万行记录,如果全部抓取,耗时和存储都会变成新问题;二是本次项目的核心目标是建立索引,而不是复制整个IANA数据仓库。先把索引建好,后面按需抓详情。
2.2 用开发者工具确认字段位置
写爬虫前一定要先打开浏览器,按F12进入开发者工具,把目标页面翻一遍。看什么?主要看三点:表格在HTML里的结构是否规整、表头行是th还是td、每一行的链接是相对路径还是绝对路径。
以IANA的协议注册表目录为例,我在开发者工具里看到的是这样的大致结构:整个表格包在一个< table >标签里,表头是"Protocol Name""Last Updated""Description"这类字样。数据行是< tr >,每一行有若干< td >,其中第一个< td >里通常有一个< a >标签,注册表名称的文字就在链接里,链接的href属性就是说明页的URL。
这里有个细节:IANA页面的链接基本都是相对路径,比如"/assignments/dns-parameters/dns-parameters.xhtml"。直接把这个字符串存下来没法用,需要用urljoin函数和当前页面URL拼接成完整的绝对URL。这个坑我在第一次写的时候踩到了,后面在代码里会给出具体处理方式。
2.3 字段与HTML标签的对应关系
经过观察,目标页面表格列和我们需要的数据字段是逐列对应的。注册表名称主要出现在第一列的可点击链接中,说明页URL就是该链接的href属性,类型信息有时候在第二列,更新时间在最后一列。
但是需要提醒一点:IANA不同目录页的列并不是完全一致的。有的目录页更新时间列叫"Last Updated",有的叫"Updated",还有的干脆没有明显的更新时间,只有"Reference"或"Notes"列。所以解析代码里不能写死"第几列一定是XX",而是要结合表头来判断。简易做法是:先把表头行提取出来,转成小写并去掉空格,然后根据表头里的关键词来定位每一列。这个处理方式在遇到页面结构微调时能减少返工。
我建议在代码里先用一个字典把表头文字映射到字段名,比如"protocol name"映射到name,"last updated"映射到updated。这样即使列的排列顺序变了,只要表头文字还在,代码就还能正确解析。
3. 环境准备与基础代码实现
3.1 安装依赖库
写代码之前先把依赖装好。我习惯用pip直接安装,不额外搞虚拟环境的话也问题不大,但建议在项目目录下创建虚拟环境,避免和其他项目依赖冲突。
pip install requests beautifulsoup4 lxml pandas如果你在Windows环境下跑,后续保存CSV时建议选择utf-8-sig编码,否则Excel直接打开会中文乱码,这个我在后面会细说。
3.2 请求函数封装
不要一上来就用requests.get裸请求。IANA服务器本身没有很强的反爬机制,但默认的Python-requests User-Agent有可能被一些网关策略误伤。所以请求头要带上常见的浏览器User-Agent,并加一个Accept-Language表示自己接受英文内容。
同时要给请求设置超时时间并做一次重试。超时设置成10秒比较合适,太长会让程序在出现网络异常时卡死,太短又可能在网络波动时误伤。重试次数控制在1到2次即可,IANA这种老牌机构服务器稳定性很好,重试太多次反而显得笨重。
import requests from fake_useragent import UserAgent def fetch_page(url): ua = UserAgent() headers = { "User-Agent": ua.random, "Accept-Language": "en-US,en;q=0.9", } session = requests.Session() for attempt in range(2): try: resp = session.get(url, headers=headers, timeout=10) resp.raise_for_status() return resp except requests.RequestException as e: print(f"请求失败,尝试第 {attempt + 1} 次,错误:{e}") return None这里用Session而不是每次新建请求对象,是为了复用底层的TCP连接。虽然IANA目录页只有一两次请求,连接复用带来的性能提升不怎么明显,但如果是扩展成批量抓取详情页,Session的收益就体现出来了。
3.3 第一版页面解析代码
拿到页面响应后,把resp.text传给BeautifulSoup,用lxml解析器生成soup对象。接下来定位表格和行。
from bs4 import BeautifulSoup resp = fetch_page("https://www.iana.org/assignments/") if resp is None: print("页面获取失败,程序退出") exit(1) soup = BeautifulSoup(resp.text, "lxml") table = soup.find("table") rows = table.find_all("tr") print(f"表格总行数(包含表头):{len(rows)}")运行到这里,如果控制台输出了类似"表格总行数(包含表头):86"的结果,说明整个解析链路已经通了。很多新手卡在这一步——不是页面的请求的问题,而是用了错误的类名或者没有找到table标签。IANA页面里的表格没有特别明显的唯一id,所以我会建议再进一步,确认一下表格的上一级容器或者特征属性,再决定用哪个table。
3.4 表头识别与列索引映射
在正式提取数据之前,先把表头行解析出来,建立"表头文字 -> 列序号"的映射关系。这个步骤看着多余,但实际效果很好,尤其是IANA目录页偶尔会出现列顺序调整。
header_row = rows[0] headers = [th.get_text(strip=True).lower() for th in header_row.find_all(["th", "td"])] print("识别到的表头:", headers)表头提取出来之后,再看页面实际表格里有没有更新时间列。我遇到过一次,表头里根本没有"last updated",只有"updated",所以代码里匹配关键词时要把可能的情况都覆盖到。写一个小的映射函数,让代码去匹配"updated"、"date"等关键词,而不要死等某一个字符串。
4. 核心解析逻辑与数据清洗实现
4.1 遍历数据行的具体实现
表头映射做好后,下面就是遍历数据行,逐行提取字段。注意一个细节:表头行和真正的数据行需要分开处理,大部分情况下rows[0]是表头,直接跳过。
遍历每一行后,用find_all("td")取出当前行的所有单元格。这里要做一个长度校验,如果当前行的td数量明显少于表头列数,说明这一行可能是分隔行、注释行或者合并单元格导致的,直接跳过,不要强解析。还有一个小概率情况是某个单元格里没有文本,只有空白,get_text(strip=True)得到的是空字符串,清洗时需要把它处理成统一的缺省值。
import re from urllib.parse import urljoin base_url = "https://www.iana.org/assignments/" data = [] for row in rows[1:]: cells = row.find_all("td") if len(cells) < 2: continue name_tag = cells[0].find("a") registry_name = name_tag.get_text(strip=True) if name_tag else cells[0].get_text(strip=True) registry_url = urljoin(base_url, name_tag["href"]) if name_tag and name_tag.get("href") else "" # 类型 registry_type = cells[1].get_text(strip=True) if len(cells) > 1 else "" # 更新时间需要根据表头列索引定位 updated_idx = col_map.get("updated") updated = cells[updated_idx].get_text(strip=True) if updated_idx is not None and len(cells) > updated_idx else "" data.append({ "registry_name": registry_name, "registry_type": registry_type, "updated": updated, "detail_url": registry_url, })这里的urljoin是救命的函数。如果链接是相对路径,它会把base_url和href拼成一个完整URL;如果链接已经是绝对地址,它会原样保留。IANA页面的链接href基本都不带域名,所以这段代码必须写。
4.2 时间字段的格式化处理
IANA目录页的更新时间格式一般是"2024-08-01"这种ISO格式,也有部分页面显示为"August 2024"或者"2024-08"。为了后续能排序、筛选,建议统一成datetime.date对象或者"YYYY-MM-DD"字符串。
有些行时间字段为空或者只有一个"-"字符,要解析成时间对象前先判断非空,再去解析,否则pandas读进来会直接报错。我这次的处理策略是:不强行把日期转成时间对象,保存CSV时统一存字符串,这样后续如果要对日期排序,可以在加载数据时用pd.to_datetime再转。但前提是先把缺失值统一填成空字符串而不是"None"或"nan"。
时间字段还有一个坑:有的注册表更新时间不等于页面上的任何文字,而是藏在注册表详情页的XML或元数据里。目录页里没有的话就别硬抓,缺失就缺失,不要为了凑字段去编造。
4.3 用pandas完成去重与排序
数据解析完成后,把它装进pandas的DataFrame,方便做一轮清洗。去重是关键操作,因为有时候目录页会对同一个注册表出现两次引用,或者URL完全一致。去重时建议以detail_url为基准,保留第一条即可。
排序方面,我习惯按更新时间倒序,方便看最新的注册表有哪些。这也符合做数据字典的习惯:越新的数据越可能对线上系统产生影响。
import pandas as pd df = pd.DataFrame(data) df = df.drop_duplicates(subset=["detail_url"], keep="first") df = df.sort_values(by="updated", ascending=False).reset_index(drop=True) df["updated"] = df["updated"].replace({"-": "", "None": ""}) print(df.head(20))把数据打印出来看一眼,确认没有明显的缺漏,比如注册表名称全是空,或者URL全部都是空的,那就说明解析逻辑某个环节写错了。
4.4 保存数据到CSV
落盘时保存编码很重要。Windows下Excel用UTF-8直接打开CSV会乱码,所以编码要用utf-8-sig。保存时加上index=False,不要把DataFrame自带的索引列写进去。
df.to_csv("iana_registry_index.csv", index=False, encoding="utf-8-sig")如果后续想用Excel继续加工数据,utf-8-sig是最省心的选择。如果数据要给别人用或者写自动化脚本,直接存成JSON或者Parquet也可以,看需求。这次项目以通用性为主,CSV就足够了。
5. 常见问题与排查技巧整理
5.1 控制台显示"Process finished with exit code 0",但没有任何数据输出
这个问题在热搜里出现频率很高,也是很多新手刚跑万爬虫时最容易懵的一个现象。桌面端运行Python程序时,脚本正常结束了,但看不到任何打印内容,第一反应是"程序坏了"。
实际上exit code 0代表程序正常执行完毕。但为什么没有任何输出?常见原因有三个:一是主代码里忘了写print,或者解析结果的打印只放在某个分支里,代码正常跑完了但对结果什么都没输出;二是解析到的是个空表格,比如页面结构变化、class名称写错导致find_all返回空列表,程序默默跳过了;三是请求结果被某个中间层拦了,返回的是个错误页,但代码里没做print,也看不出异常。
排查思路很简单:在关键环节都加print。比如请求完后打印状态码、打印响应长度、打印soup解析出来的标题、打印表格行数。一层一层排查,很快就能定位是请求挂了还是解析挂了,而不是盯着exit code 0发呆。
print("HTTP状态码:", resp.status_code) print("响应内容长度:", len(resp.text)) print("页面标题:", soup.title.get_text() if soup.title else "无标题") print("识别到表格数:", len(soup.find_all("table")))有这些输出之后,是不是真的拿到了目标页面,一眼就能看出。
5.2 请求被拒或者返回403
IANA本身对爬虫不算苛刻,但如果User-Agent是默认的python-requests,个别网络出口或CDN策略可能直接拒绝。解决方式是把自己伪装成普通浏览器。
不要用一个固定的浏览器UA,因为同一浏览器UA反复请求同样可能被识别出是脚本。用fake-useragent库每次随机取一个UA,效果会好很多。同时把Accept、Accept-Language、Connection这些头也带上,让请求看起来更真实。
还有一个容易被忽略的点:requests默认不启用HTTP/2,而服务器如果强制HTTP/2,某些反爬组件可能会直接返回异常。不过IANA的情况还好,在单线程低频率下基本不会触发。
5.3 表格结构变化导致解析失败
页面改版是爬虫最大的敌人。IANA的目录页整体结构比较稳定,但还是出现过个别列名变化、表格外层套了新的div这类情况。
要降低改版对代码的冲击,有几条建议:
- 不要用极其具体的选择器,比如第3个table下的第5行这种,页面稍微一动就崩;
- 优先用表头文字定位列,而不是死记列序号;
- 对每一行数据的解析加try/except,单行失败时记录日志并跳过,不要因为某一行格式异常就让整个脚本崩溃;
- 保存数据时把原始页面HTML也留一份快照,方便出错后对比。
for row in rows[1:]: try: # 解析逻辑 except Exception as e: print(f"解析某一行失败:{e},该行内容:{row.get_text()}") continue5.4 CSV文件用Excel打开后中文乱码
很多人在Windows下用Excel打开CSV,中文全变成了一堆乱码。这是因为Excel默认用ANSI编码去解UTF-8文件。
解决方式就是保存时指定utf-8-sig编码。utf-8-sig会在文件开头写入BOM标记,Excel识别到BOM后就按UTF-8解码,乱码问题就消失了。代码就是前面写的那行:
df.to_csv("iana_registry_index.csv", index=False, encoding="utf-8-sig")如果你不是用Excel,而是用Python的pandas或数据库读取CSV,用标准utf-8也可以,但既然已经写成了utf-8-sig,pandas读的时候指定encoding="utf-8-sig"即可。
5.5 页面里有些表格内容不出现
有个别IANA注册表列表页面,在HTML里看不到全部数据,表格是分页的,或者通过"Show Inactive"这类按钮控制是否显示全部行。如果目录页里没有这个分页问题,但详情页有,就需要单独处理。
详情页一般会允许通过URL加参数来控制显示,比如partial或者per_page之类,或者提供CSV、JSON格式的导出接口。IANA很多注册表详情页都提供"Available Formats"下载,可以直接拿CSV格式,比解析HTML稳定得多。这也是我建议从目录页抓索引、从导出文件抓详情的原因。
6. 项目扩展与数据应用建议
6.1 从目录页升级为全量注册表抓取
目录页的数据结构拿下来之后,你手里就有了所有注册表详情页的URL。如果需要全量数据,可以循环请求这些URL,再解析详情页。
但这里一定要控制请求频率,不要一下子开100个并发去打IANA。礼貌爬虫的做法是一次请求后sleep 0.5到1秒,最多开两到三个线程。同时做好异常处理:某个详情页请求超时,记录下URL,等整体跑完后再补抓。
详情页的字段通常更多,比如注册表名称、注册说明(Registration Procedure)、参考文档(Reference)、可用格式(Available Formats)等。你可以把目录页的数据和详情页的数据拼接起来,形成一个更完整的表。拼接时用detail_url作为关联键,因为注册表名称偶尔会重复,URL不会。
6.2 定时同步与增量更新
IANA的注册表不是每天都会变,但每隔一段时间就会有更新,比如新增了某个HTTP状态码或协议参数。如果要维护一份常新的离线数据,建议加一个定时任务。
最简方案是写一个检查更新时间的脚本,每天运行一次,比较目录页里最新的更新时间字段和本地CSV里的值。有更新时才重新抓取并覆盖本地文件,没有更新时直接退出。这样既不会频繁打扰IANA服务器,又能保证本地数据相对新鲜。
import time if __name__ == "__main__": while True: run_sync() time.sleep(86400) # 每天一次用系统cron或Windows任务计划程序来跑这个脚本比while循环更可靠,重启后也能自动恢复调度。
6.3 数据可视化和类型分析
目录页抓下来后,用pandas做点统计是非常顺手的事。比如统计一下IANA维护的注册表一共有多少条,按类型分组看哪些协议分类数量最多。这些统计结果可以导出成图表,配合爬虫数据可视化,能直观看到整个互联网协议参数的分布。
比如用pyecharts或者matplotlib把注册表类型做成柱状图,一眼就能看出DNS参数、端口号、字符集这几个大类占比最高。这种图形用在技术汇报或者文档里非常提气。
import matplotlib.pyplot as plt type_counts = df["registry_type"].value_counts().head(10) type_counts.plot(kind="bar", figsize=(10, 6)) plt.title("IANA各类型注册表数量 Top 10") plt.tight_layout() plt.savefig("iana_type_counts.png", dpi=150)6.4 数据使用边界与合规提醒
采集IANA数据本身没有合规问题,因为这些都属于公开的协议参数,是互联网基础设施建设的一部分。但使用数据时还是有几点要注意:
不要高频全量抓取,尤其不要对详情页做极端的并发请求,礼貌采集才能长期稳定地获取数据。看清楚页面里是否标注了版权或使用条款,IANA有些注册表数据是通过第三方RFC定义的,使用时要保留出处。自己整理后的数据尽量标明抓取日期和数据来源,方便后续追溯。
爬虫解决的是"一次性获取"的问题,但要长期维护数据,重点还是要落在定时更新、质量监控和异常警报上。把这几个环节都补全,这份数据才真正具备生产环境的价值。
最后再分享一个小技巧
抓完目录页之后,建议把原始HTML存一份到本地,哪怕只是临时文件。原因很简单:IANA页面里的表格结构可能在我写完这篇教程之后某一天就变了,到时候如果你的脚本解析出了空数据,手头有原始HTML就能快速对比是页面改版了还是自己的代码写错了。这个小习惯帮我排查过很多次"看起来莫名其妙"的爬虫问题。
另外,如果你在解析时发现IANA某个页面的表格里有一部分行是在HTML中看不到的,不要急着写自动化点击按钮的逻辑,先去找找页面是否提供CSV、XML这类备用格式。IANA在很多详情页底部都有"Available Formats"区域,里面的CSV往往比HTML表格更好解析,数据字段也更全。能用官方导出格式解决的问题,就不要在HTML解析上耗时间。