简介:这份PDF文档系统整理了国际航空运输协会(IATA)为全球航空公司指定的两字母代码,并附有对应的三字母代码参考,面向航空从业者、票务代理、航班数据分析人员及航空爱好者。内容按代码首字符从A到Z及数字0-9排列,逐条列出代码、航空公司名称与所属国家或地区,涵盖琥珀航空、四川航空、宿雾太平洋航空等众多航司,也收录了伽利略、Sabre等全球分销系统及部分铁路运营商的识别代码。资源包共1个PDF文件,约690KB,页面结构清晰,便于按字母顺序快速检索与对照查阅。目前已有413人学习浏览。读者可借此掌握IATA 762号决议下的代码分配规则,理解两字代码在预订、时刻表、票务、征税、航空提单及无线电通讯中的实际用途,并区分唯一英文号、数字加英文号与可控重复码三类编码形式,适合作为日常查询与业务核对的实用工具手册。
1. 从一份 IATA 航空公司代码 PDF 说起:它到底能解决什么问题
手里拿到一份IATA航空公司代码.pdf,多数人的第一反应是「查个两字码而已」。真到落地场景里,事情没这么简单:航司两字码(2-letter code)和三字码(3-letter code)、结算代码、ICAO 三字呼号、国家/地区归属、联盟归属、是否仍在运营、是否被合并或更名——这些字段经常被混在一张表里,来源不同、口径不同、更新节奏也不同。PDF 这种载体天生适合人读,不适合程序读,一旦要拿它去对接订座系统、做运价匹配、清洗航班数据、给 BI 报表补维度,问题就集中爆发:列对不齐、换行断字、合并单元格、脚注混进正文、历史航司和现役航司混排。
这份 PDF 真正能解决的问题,是把「航司代码」从一个模糊的字符串,变成一张可被程序消费的维表。适合谁:做航班数据清洗的数据工程师、做机票/差旅系统的后端、做航空数据分析的分析师,以及需要把 IATA 代码映射到内部主数据的产品同学。它不适合谁:只想临时查一个代码的人,直接搜在线查询页更快。下面按「先立住概念 → 再动手抽取 → 再讲坑 → 最后讲进阶校验」的顺序,把这份 PDF 从静态文档变成可用数据资产的全过程讲清楚。
2. 先分清 IATA 两字码、三字码和 ICAO 呼号:选错字段后面全白干
2.1 四种「航司代码」的真实区别与使用场景
很多人把「航司代码」当成一个东西,实际至少有四套并行体系,混用会直接导致数据对不上。
| 代码类型 | 长度 | 典型示例形态 | 主要用途 | 谁在用 |
|---|---|---|---|---|
| IATA 两字码 | 2 位字母/数字 | 两位大写 | 订座、票务、运价、行李规则 | 航司、GDS、OTA |
| IATA 三字码(会计代码) | 3 位数字 | 三位数字 | 结算、开账、BSP 对账 | 财务、结算系统 |
| ICAO 三字码 | 3 位字母 | 三位大写字母 | 空管、飞行计划、航行情报 | 空管、运行控制 |
| IATA 三字呼号 | 3 位字母 | 三位大写字母 | 无线电呼号、航班号前缀 | 运行、签派 |
关键点:IATA 两字码是业务主键,ICAO 三字码是运行主键,两者不能互相替代。比如同一家航司,两字码用于卖票,ICAO 码用于飞。做数据清洗时,如果 PDF 里同时出现这两列,必须分别建字段,不能合并成一列「航司代码」。
提示:IATA 两字码存在复用历史。某家航司停运后,其两字码可能被另一家航司重新启用。所以「代码 → 航司」不是永久一对一,必须带生效时间维度。
2.2 为什么 PDF 是最难啃的载体:结构特征先摸清
在动手写解析脚本前,先花十分钟把 PDF 的「物理结构」看清楚,能省掉后面几小时的返工。常见特征有这几类:
- 表头跨页重复,但列宽在不同页可能微调;
- 航司名称过长时自动换行,导致一行数据在文本层被拆成两行;
- 国家/地区列有时用全称、有时用两字国家码;
- 脚注用上标数字,抽取后混进名称字段;
- 已停运航司用删除线或灰色标注,文本层无法体现。
判断方法:用pdfplumber先 dump 第一页的字符坐标,看列是否稳定;再用pdftotext -layout看纯文本版是否可读。两种结果对比,决定走「坐标抽取」还是「文本流解析」。
# 先看文本层是否可用,-layout 保留原始排版 pdftotext -layout IATA航空公司代码.pdf out_layout.txt # 再看字符级坐标,判断列是否对齐 python -c "import pdfplumber; pdf=pdfplumber.open('IATA航空公司代码.pdf'); print(pdf.pages[0].extract_words()[:5])"逻辑说明:pdftotext -layout输出的是「人眼版」,如果列对得整齐,说明文本流解析可行;extract_words()返回每个词的坐标,如果同一列的 x0 值波动很小,说明坐标抽取更稳。参数上,-layout不加会丢失列关系,加了会引入多余空格,后续要统一处理。
3. 用 pdfplumber 把 PDF 抽成结构化表:最小可跑通脚本
3.1 环境准备与依赖版本边界
我一般用 Python 3.9+,核心依赖三个:pdfplumber负责抽取,pandas负责清洗,re负责正则。不推荐用PyPDF2做表格抽取,它对合并单元格和跨页表头支持很差,容易在「航司名称换行」上翻车。
pip install pdfplumber==0.10.3 pandas==2.1.4版本上不用追最新,pdfplumber0.10.x 对extract_table的table_settings支持已经够用。如果 PDF 是扫描件(文本层为空),这套方案直接失效,需要先做 OCR,那是另一条路径,本文不展开。
3.2 逐页抽取表格并处理跨页表头
import pdfplumber import pandas as pd import re def extract_airline_table(pdf_path): all_rows = [] header = None with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 用线条策略抽表,适合有边框的 PDF table = page.extract_table({ "vertical_strategy": "lines", "horizontal_strategy": "lines", "snap_tolerance": 3, }) if not table: continue for row in table: # 清理 None 和多余空白 cleaned = [re.sub(r"\s+", " ", (c or "")).strip() for c in row] # 判断是否表头行:包含「代码」「名称」等关键词 if any(k in "".join(cleaned) for k in ["两字码", "三字码", "航司名称"]): if header is None: header = cleaned continue all_rows.append(cleaned) df = pd.DataFrame(all_rows, columns=header) return df df = extract_airline_table("IATA航空公司代码.pdf") print(df.shape) print(df.head())逻辑说明:vertical_strategy="lines"表示按 PDF 里的竖线切列,适合有完整边框的表;snap_tolerance=3是坐标吸附容差,PDF 里线条常有 1-2 像素偏移,设太小会漏切,设太大会把相邻列合并。表头判断用关键词匹配,因为跨页后表头会重复出现,只保留第一次。re.sub(r"\s+", " ", ...)把换行和连续空格压成单空格,解决「航司名称被拆行」的问题。
参数怎么改:如果 PDF 没有边框线,把vertical_strategy改成"text",并配合min_words_vertical控制列识别;如果列被错误合并,调大snap_tolerance到 5;如果表头识别失败,把关键词列表换成 PDF 里实际出现的列名。
3.3 字段规范化:两字码、三字码、国家码分开存
抽出来只是第一步,字段类型必须显式定义,否则后面 join 必出问题。
def normalize(df): df = df.rename(columns=lambda x: x.strip()) # 两字码:强制大写,去除非字母数字 df["iata_2code"] = df["两字码"].str.upper().str.replace(r"[^A-Z0-9]", "", regex=True) # IATA 三字码:纯数字,补零到 3 位 df["iata_3code"] = df["三字码"].astype(str).str.extract(r"(\d+)")[0].str.zfill(3) # 国家/地区:统一成两字码,这里假设 PDF 给的是全称,需映射 df["country"] = df["国家/地区"].str.strip() # 标记是否现役:名称里含「已停运」「合并」等关键词 df["is_active"] = ~df["航司名称"].str.contains("已停运|合并|更名", na=False) return df[["iata_2code", "iata_3code", "航司名称", "country", "is_active"]] df_clean = normalize(df) df_clean.to_csv("airlines_clean.csv", index=False)逻辑说明:两字码用str.replace去掉 PDF 里可能混入的括号和空格;三字码用str.extract(r"(\d+)")只取数字部分,再zfill(3)补零,防止「012」被读成「12」。is_active用关键词反向标记,这是最省事的做法,但边界在于:如果 PDF 用删除线而非文字标注停运,这个方法会漏,需要结合坐标或颜色信息,属于进阶处理。
注意:
zfill(3)必须在astype(str)之后,如果列里混了浮点数(如 12.0),要先转 int 再转 str,否则会得到「12.」这种脏值。
4. 避坑与排查:IATA 代码抽取里最容易翻车的 5 个点
4.1 现象:抽出来的行数比 PDF 目视行数少
原因:PDF 里存在合并单元格,extract_table把跨行单元格只填在第一行,后续行该列为空,被误判为无效行丢弃。解决:不要用「整行非空」做过滤条件,改成「两字码列非空」作为有效行判断;或者在table_settings里加"intersection_tolerance": 5,让合并单元格的边界更宽松。
4.2 现象:航司名称里混进了脚注数字
原因:PDF 脚注用上标,文本层里上标和正文在同一行,抽取后变成「某某航空1」。解决:在清洗阶段用正则去掉名称末尾的孤立数字re.sub(r"\d+$", "", name),但要注意有些航司名称本身以数字结尾(如「XX 航空 1」这种极少见情况),所以只去末尾且长度小于 3 的数字串。
4.3 现象:两字码大小写不一致,join 时对不上
原因:PDF 排版里有些代码是小写,有些是大写,文本层原样保留。解决:所有代码字段统一str.upper(),并且在入库前加CHECK约束或 pandas 的assert,确保没有小写残留。这是血泪经验,曾经因为一个「aa」和「AA」对不上,排查了一下午。
4.4 现象:同一航司出现多行,代码相同但名称不同
原因:PDF 里同时列了现役名称和历史名称,或者同一航司在不同国家/地区有不同注册实体。解决:以两字码 + 生效日期为联合主键去重,保留最新一条;如果没有日期列,按 PDF 页码顺序保留最后一条,并在数据里加source_page字段便于回溯。
4.5 现象:扫描版 PDF 抽出来全是空
原因:PDF 没有文本层,pdfplumber读不到字符。解决:先用pdfplumber检查page.chars是否为空,为空则走 OCR 路径(如pytesseract),但 OCR 对表格结构还原差,建议 OCR 后仍用坐标法重建列,不要直接信任 OCR 的文本流。
5. 进阶:把代码表变成可校验的主数据,并做交叉验证
5.1 用 ICAO 码做交叉校验,发现错行和错列
单靠 IATA 两字码无法发现「列错位」问题,因为两字码本身看起来都合法。引入 ICAO 三字码做交叉校验,能抓出大部分错行:正常航司的 IATA 两字码和 ICAO 三字码在公开资料里有固定对应关系,如果抽出来的组合在已知映射里查不到,大概率是列错位或行错位。
# 假设有一份已知的 iata->icao 映射(可从公开航司数据整理) known_map = {"CA": "CCA", "MU": "CES", "CZ": "CSN"} # 示例,实际需补全 def cross_check(df, icao_col="icao_3code"): def check(row): expected = known_map.get(row["iata_2code"]) if expected and row[icao_col] != expected: return "MISMATCH" return "OK" df["check"] = df.apply(check, axis=1) return df[df["check"] == "MISMATCH"]逻辑说明:known_map是人工整理的基准映射,只覆盖常见航司,但足以发现系统性错位。apply逐行比对,返回不匹配的行。参数上,icao_col要指向 PDF 里实际抽出的 ICAO 列名,如果 PDF 没有这一列,这一步跳过,改用「国家/地区 + 航司名称关键词」做弱校验。
5.2 建立更新机制:PDF 会变,代码表不能只抽一次
IATA 代码表不是静态的,航司更名、合并、停运都会导致代码变化。我一般会做三件事:第一,把每次抽取的 CSV 按snapshot_date存档,保留历史版本;第二,用iata_2code做主键做增量对比,新增和消失的代码单独输出一份 diff 报告;第三,在数据库里给is_active字段加更新时间戳,查询时默认只取现役,需要历史时显式带上时间条件。
import datetime def snapshot(df, path="airlines_clean.csv"): df["snapshot_date"] = datetime.date.today().isoformat() df.to_csv(path, index=False) # 与上一版对比 try: prev = pd.read_csv(path.replace(".csv", "_prev.csv")) new_codes = set(df["iata_2code"]) - set(prev["iata_2code"]) gone_codes = set(prev["iata_2code"]) - set(df["iata_2code"]) print("新增:", new_codes) print("消失:", gone_codes) except FileNotFoundError: pass逻辑说明:snapshot_date让每次抽取可追溯;diff 用集合差集,输出新增和消失的代码。参数上,path指向当前版本,_prev.csv是上一版,实际使用时可以用日期命名文件,避免覆盖。
5.3 一个具体技巧:用「代码 + 名称首词」做模糊去重
有些 PDF 里同一航司因为名称换行被拆成两条记录,两字码相同但名称一条是「中国国际航空」,另一条是「中国国际航」。用difflib.SequenceMatcher对同一两字码下的名称做相似度比对,相似度高于 0.8 的合并,保留较长的那条。这个技巧帮我省掉过大量手工核对,但要注意:相似度阈值不要设太低,否则会把「XX 航空」和「XX 航空货运」这种真实不同的实体合并。
from difflib import SequenceMatcher def dedup_by_similarity(df, threshold=0.8): result = [] for code, group in df.groupby("iata_2code"): names = group["航司名称"].tolist() merged = [] for name in names: if not merged: merged.append(name) continue if SequenceMatcher(None, merged[-1], name).ratio() > threshold: # 保留较长的名称 if len(name) > len(merged[-1]): merged[-1] = name else: merged.append(name) for m in merged: result.append({"iata_2code": code, "航司名称": m}) return pd.DataFrame(result)逻辑说明:groupby按两字码分组,组内用SequenceMatcher逐条比对,相似则合并保留长名。threshold=0.8是经验值,低于 0.7 会误合并,高于 0.9 会漏合并。这个方法的边界在于:它假设同一两字码下不会有多家真实不同的航司,如果 PDF 里存在代码复用(历史航司和现役航司同码),需要先按is_active分组再执行。
我自己的习惯是:每次拿到新的 IATA 代码 PDF,先跑一遍抽取脚本,把结果和上一版 diff 一遍,重点看「消失的代码」——那往往意味着航司停运或合并,是业务上最需要关注的变化。这套流程跑顺之后,一份 PDF 从拿到到变成可 join 的维表,大概二十分钟。希望帮到你。
本文还有配套的精品资源,点击获取