Android NFC读取银行卡号的技术原理
如果你的手机支持 NFC,只需要将你的银行卡贴在手机背面,App 就可以获取到你的银行卡号和过期日期。
Android NFC读取银行卡号的技术原理
如果你的手机支持 NFC,只需要将你的银行卡贴在手机背面,App 就可以获取到你的银行卡号和过期日期。
引言
许多电商 App 中,都需要用户绑定银行卡用于支付。为了减少用户输入的麻烦,一般使用 OCR 提取银行卡上的信息做自动填充。如今部分 App 如 Grab、Shopee 会提示你使用 NFC 自动填充银行卡信息。
如果你的 Android 手机支持 NFC,同时你的银行卡、无论是储蓄卡还是信用卡、支持非接触式支付,你就可以体验这一神奇的功能,手机自动识别你的银行卡号和过期日期。
我做了一个 demo,其中一个功能就是使用 NFC 识别你的银行卡,你可以从 Google Play 下载体验:
https://play.google.com/store/apps/details?id=com.github.wonshaw.apilab
本文将介绍这一功能背后的技术原理和实现细节。
非接触式支付卡
如果你观察你的银行卡,上面往往会有一个放倒的 Wi-Fi 信号一样的标志:

右上角有 contactless 标志
这个就代表你的银行卡是一张非接触式支付卡,只需要将卡片靠近 POS 机就可以完成支付,不需要“刷”卡,也不需要将卡片插入 POS 机。
如果你的卡是银联卡,上面印有 QuickPass 的字样,则你的银联卡也是非接触式卡。
当你将你的银行卡靠近 POS 机以付款时,POS 机与你的卡通过 NFC(Near-field communication,近场通信)通信,以完成交易。
EMVCo
当你拿着你的银行卡,在一家餐馆的 POS 机上刷卡时,一切都很符合直觉,但这背后其实有一套相对复杂的流程。
你是持卡人;你的卡片所属的银行叫发卡行;商户也有自己的银行账户用来收款,一般商户的银行账号设立在收单行;收单行和发卡行之间需要通过卡组织网络连接;卡组织负责路由交易请求并协调清算服务,确保资金从发卡行安全地流向收单行并最终结算到商户账户。
你的银行卡之所以可以在全球大多数地区使用,并能被不同制造商的 POS 机正确识别,是因为主流卡组织联合建立了一个叫 EMVCo 的组织,EMVCo 推出了一系列技术规范。大多数银行卡,特别是印有主流卡组织标志的卡,都遵循 EMV 规范,这些规范统一了芯片卡的数据结构和安全协议,确保不同厂商的 POS 机和支付系统能够兼容使用,进一步提升了银行卡的全球适用性。
这些遵循 EMV 规范的卡,也可以叫 EMV 卡。
Android 设备与 EMV 卡通信
POS 机与 EMV 卡的非接触式通信是通过 NFC 进行的。
实际上,与 POS 机通信的并不需要真的是一张 EMV 实体卡,只要它能以 POS 机认可的协议与 POS 机交互即可,这就是 Apple Pay、Google Pay 等产品的原理。它们本质上就是用手机模拟一张 EMV 卡,通过 NFC 与 POS 机通信,完成支付流程。不过,它们并不直接模拟你添加的原始卡片,而是使用一个与设备绑定的虚拟卡进行支付。这个虚拟卡也有卡号和有效期,对 POS 机来说和普通卡并无区别,所以只要是支持刷非接触 EMV 卡的机器,你都能用 Apple Pay 或者 Google Pay。
手机 NFC 除了可以模拟卡片,还能以读卡器模式工作,此时 Android 设备类似一台 POS 机,会产生射频场给靠近的 EMV 卡供电并与其通信。
理论上,只要通过手机 NFC 实现 EMV 规范中定义的通信协议和数据交互流程,我们就可以用手机模拟一台 POS 机与 EMV 卡通信。为此我们必须了解 EMV 规范对非接触通信协议的具体定义。
相关的标准和规范文件
阅读本文足以让你了解其中的技术细节,但这里还是列举一下涉及的主要标准和规范文件。
ISO/IEC 14443
手机与 EMV 卡通过 NFC 通信,涉及 ISO/IEC 14443 系列标准,EMV 卡一般为 ISO/IEC 14443 标准中的 A 型卡。但好在 Android 提供的 NFC API 已经为我们实现了 ISO/IEC 14443 标准,我们只需要关注应用层协议即可,无需关心传输协议及以下的部分,因此我们无需阅读这个标准。
ISO/IEC 7816–4
EMV 卡与 POS 机的通信,应用层使用 APDU(Application Protocol Data Unit)命令与响应格式。不过 EMV 规范中本身对 APDU 已经有比较详尽的描述,我们也只需要关注 EMV 相关的 APDU,因此无需单独阅读 ISO/IEC 7816–4。
ISO/IEC 8825–1
EMV 卡通信中的数据部分大量使用 BER-TLV 编码规则,但 EMV 规范中已经对其用到的 TLV 编码格式有较为详尽的描述,因此无需单独阅读 ISO/IEC 8825–1。
EMV Contactless Specifications
与非接触式 EMV 卡的交互流程,需要参考 EMV 非接触式规范,主要参考如下几个规范文件:
- Book A: Architecture and General Requirements
- Book B: Entry Point Specification
- Book C-n: Kernel Specification
EMV Integrated Circuit Card Specifications
虽然手机和 EMV 卡是非接触式通信,但其应用层协议和接触式规范中定义的基本相同,因此我们还是需要读 EMV 接触式规范,主要阅读如下几个规范:
- Book 1: Application Independent ICC to Terminal Interface Requirements
- Book 3: Application Specification
- Book 4: Cardholder, Attendant, and Acquirer Interface Requirements
接下来介绍要实现 Android NFC 读取 EMV 卡号和过期日期的所需了解的理论知识。
POS 机与 EMV 卡交互流程
一台 POS 机与 EMV 卡的交互流程如下图所示:

- Pre-processing(预处理):一般是配置 POS 机的参数,准备好数据,以便后续步骤使用,比如商户需要输入消费金额。
- Protocol Activation(协议激活):这一阶段,一方面会显示金额,提示消费者将卡片靠近读卡器,另一方面 POS 机也会使用读卡器来尝试发现 EMV 卡,与其建立连接。
- Combination Selection(组合选择):与卡片建立连接后,会选择卡片上的应用程序,以及决定使用什么内核进行交易。
- Kernel Activation(内核激活):激活对应的内核,由内核来处理交易。不同卡组织的交易逻辑不同,对应不同的内核。
- Kernel Processing(内核处理):由对应卡组织的内核来处理交易。
- Outcome Processing(结果处理):根据 Kernel Processing 的结果,决定下一步,例如是批准交易,还是拒绝。
在 POS 机与 EMV 卡的交易流程中,POS 机会通过读取芯片卡的数据,获取卡号(Primary Account Number,PAN)、过期日期等信息。获取过期日期是为了初步检查卡片是否仍在有效期内。获取卡号是因为它是交易的重要数据,用于路由交易到正确的卡组织和发卡行,同时标识交易的账户,并作为动态数据生成的重要输入。
NFC 获取 EMV 卡号的思路
POS 机和 EMV 卡的交互是在执行交易。我们要与 EMV 卡通信获取卡号、过期日期,本质上就是要控制 Android NFC 硬件模拟一台 POS 机与 EMV 卡交互。
我们不会像 POS 机一样发起交易,而是在我们获取到卡号、过期日期后,就直接终止了。我们也没有能力发起交易,一般情况下,只有经过 EMVCo 认证的 POS 机才能真正发起交易,尽管这些 POS 机大多也是运行在 Android 系统上,但这些设备在软硬件上均做了一些改动,以适应 EMVCo 的规范。
为了模拟 POS 机与 EMV 卡交互,POS 机交易流程的每个步骤的模拟方案如下:
- Pre-processing:我们直接将参数硬编码在代码里即可,无需单独模拟。
- Protocol Activation:Android NFC 硬件已经帮我们实现了和 EMV 卡的连接,因此我们无需特别关注这个步骤。
- Combination Selection:这一步需要我们实现,涉及两个选择命令,选择 PPSE 和 选择 AID。
- Kernel Activation:我们可以忽略这一步,因为我们并没有实现完整内核。
- Kernel Processing:这一步需要我们实现,但只需要获取到卡号、过期日期即可,无需实现无关的步骤。涉及 GPO 命令和 READ RECORD 指令
- Outcome Processing:这一步无需处理。
模拟 POS 机与 EMV 卡通信,传输层由 Android NFC 硬件负责,我们无需关心;应用层需要我们自行处理。下面介绍应用层所依赖的协议和约定,你也可以跳过这一部分,之后需要了解协议的时候再回过来看这一部分。
EMV 规范中,将 POS 机称之为终端(Terminal),下文也将使用这一称呼。
APDU
前面说到,终端与 EMV 卡的应用层协议,是基于 APDU(Application Protocol Data Unit)的。终端向 EMV 卡发出 APDU 命令,EMV 卡答复 APDU 响应。
APDU 命令
APDU 命令由 4 字节的命令头和一个可选的变长命令体组成,其结构如下:

它的各个字段如下所示:
CLA:1 字节,指令类别 INS:1 字节,指令代码 P1:1 字节,指令参数 1 P2:1 字节,指令参数 2 Lc:0 字节或者 1 字节,代表 Data 的字节长度 Data:指令中的数据部分,可变长度,长度为 Lc Le:0 字节或 1 字节,希望响应返回的数据的最大字节数。如果不需要返回数据,则省略。如设置为 0,则允许返回的数据的最大长度为 256 字节。
APDU 响应
APDU 响应由一个可选的响应体和 2 字节的尾部组成,其结构如下:

它的各个字段如下所示:
Data:响应中的数据,长度为变长,传输层不会明确返回它的长度,但由于传输层提供给应用层的响应的长度是明确的,减去尾部的长度 2 字节,即可得到 Data 的长度。如果没有数据,这一部分可以省略 SW1:1 字节,命令处理状态,表明命令处理的总体状态 SW2:1 字节,命令处理限定符,表明命令处理的具体状态,是对 SW1 的补充说明
SW1 和 SW2 共同决定了 APDU 响应的状态。
当 SW1 = 0x90,SW2 = 0x00 时,代表指令执行是成功的,解析任何 APDU 响应前,都需要先检查 SW1 和 SW2 是成功的。
BER-TLV
APDU 中数据大部分都是 BER-TLV(Basic Encoding Rules — Tag-Length-Value)数据对象。其由两个或三个连续字段组成:
- 标签(T):标签字段由一个或多个连续字节组成,表示一个类、类型和编号。EMV 规范中标签字段编码占 1 或 2 个字节。
- 长度(L):长度字段由一个或多个连续字节组成,表示后续字段,也就是值的长度。EMV 规范中,卡片和终端间传输的数据,长度字段编码占 1 或 2 个字节。
- 值(V):值字段表示数据对象的值。如果长度字段 L = ‘00’,则不存在值字段。
BER-TLV 数据对象可以分为两类:
- 基本数据对象:它的值就是一个数据元素。结构为 T-L-V。
- 构造型数据对象:它的值包含一个或多个基本数据对象或构造型数据对象。构造型数据对象的值字段称为模板。简单来说就是 TLV 的值部分可以是多个连续 TLV 组成的。结构为 T-L-TLV1-TLV2-…-TLVn。
Tag 编码方式
Tag 可以由一个或多个字节组成,如果 Tag 的第一个字节的低 5 位全为 ‘1’(0bXXX11111),则代表它并不是一个单字节的 Tag,还需要查看后续字节,共同组成 Tag。
Tag 的非第一个字节的部分:
- 如果最高位为 ‘1’(0b1XXXXXXX),则代表它后面还有组成这个 Tag 的字节。
- 如果最高位为 ‘0’(0b0XXXXXXX),则代表它是这个多字节 Tag 的最后一个字节。
Length 编码方式
Length 可以由一个或多个字节组成,如果 Length 的第一个字节的最高位为 ‘0’ (0b0XXXXXXX),则为单字节编码的长度,值字段的长度由其低 7 位直接表示,范围 [0, 127]。
如果 Length 的第一个字节的最高位为 ‘1’ (0b1XXXXXXX),则为多字节编码的长度,第一个字节的低 7 位表示后续还有多少个字节属于这个 Length。后续的字节组成一个大端表示的二进制数,表示值字段的长度。
EMV 规范中的数据类型
EMV 规范中数据有其数据类型,在解析、组装数据时需要按照数据类型来操作。
a(字母型数据)
每个字节包含一个字符。允许的字符仅为字母(a 到 z 和 A 到 Z)。
an(字母数字型数据)
每个字节包含一个字符。允许的字符为字母(a 到 z 和 A 到 Z)和数字(0 到 9)。
例外情况:Payment Account Reference(支付账户参考)仅允许大写字母(A 到 Z)和数字(0 到 9)。
ans(字母数字特殊型数据)
每个字节包含一个字符。允许的字符及其编码见 Book 4 Annex B 的 Common Character Set table(通用字符集表)。
例外情况:Application Preferred Name(应用首选名称)允许的字符为 ISO/IEC 8859 标准中定义的非控制字符,具体由与应用首选名称相关的 Issuer Code Table Index(发卡机构代码表索引)指定。
b(二进制型数据)
无符号二进制数,或位组合数据。
示例: 二进制数:应用事务计数器(ATC)定义为“b”,长度为两字节。如果 ATC 值为 19,存储为十六进制 00 13。 位组合数据:有一些数据是把一个或多个字节的每一位当作状态指示用的,这种称为位组合数据。
cn(压缩数字型数据)
每个字节包含两个数字字符(值范围为十六进制 ‘0’–’9')。 这些数据元素左对齐,并用尾随的十六进制 ‘F’ 填充。
示例: 值 1234567890123 存储为十六进制 12 34 56 78 90 12 3F FF,长度为 8 字节。
n(数字型数据)
每个字节包含两个数字字符(值范围为十六进制 ‘0’–’9')。 这些数字右对齐,并用前导的十六进制零填充。
示例: Amount, Authorised(授权金额)定义为“n 12”,长度为 6 字节。值 12345 存储为十六进制 00 00 00 01 23 45。
var.(可变型数据)
可变长度,可以包含任何位组合。
DOL
有时,终端需要按照 EMV 卡的要求构建一个数据元素列表,并将其传递给卡片。为了尽量减少 EMV 卡内部的处理,这种列表不是 TLV 编码的,而是将多个数据元素串联起来构建的单一字段。
由于没有使用 TLV 编码,终端和 EMV 卡之间必须通过某种约定来组装和解析字段,这种情况下 EMV 卡会提供 DOL(Data Object List,数据对象列表) ,DOL 指定了要包含在字段中的数据和数据的长度。
其格式如下:
Tag1-Length1-Tag2-Length2-…-TagN-LenghN
其中 Tag 长度为 1 字节或 2 字节,遵循前述 TLV 的编码方式,代表 EMV 卡需要终端提供的数据的 Tag。
每个 Tag 后面的 Length 统一为一个字节,代表 EMV 卡需要终端提供的该数据的长度,单位为字节。
通过 DOL,EMV 卡可以要求终端提供 EMV 卡想要知道的数据对象,但仅限基本数据对象。
终端会根据 DOL 组装数据,直接编码为如下格式: Value1(长度为Length1)-Value2(长度为Length2)-…-ValueN(长度为LengthN)
因为这里需要按照 EMV 卡的要求给出数据,顺序和长度必须严格匹配,所以终端在根据 DOL 组装数据时,需要遵循如下规则:
Tag 未知或为构造型数据对象
如果 DOL 中标识的数据对象的 Tag 对终端未知,或者它表示一个构造型数据对象,终端应根据指定的长度提供全为十六进制零的占位数据元素。
可选静态数据缺失
如果列表中的数据对象对终端有意义,但它表示的可选静态数据在终端中缺失,则命令字段中对应该数据对象的部分应填充全十六进制零。
长度小于实际数据长度
如果 DOL 条目中指定的长度小于实际数据对象的长度
- 对于 数字型数据(n),截断左侧字节,保留右侧字节。
- 对于其他格式的数据,截断右侧字节,保留左侧字节。
长度大于实际数据长度
如果 DOL 条目中指定的长度大于实际数据对象的长度:
- 数字型数据(n):使用前导十六进制零填充。
- 压缩数字数据(cn):尾部填充十六进制 FF。
- 其他格式数据(an、ans 或 b):尾部填充十六进制零
数据不适用于当前交易
如果列表中的数据对象对终端有意义,但它表示的数据不适用于当前交易,则命令字段中对应该数据对象的部分应填充全十六进制零。
有了前面的知识储备和思路,现在我们可以着手实现 NFC 读取 EMV 卡号、过期日期的功能。
选择 PPSE
选择 PPSE(Proximity Payment System Environment)属于 Combination Selection 步骤。这是第一个命令,用来获取 EMV 卡上可用的应用列表。
构造 SELECT 命令,选择名称为 2PAY.SYS.DDF01 的文件,这个文件名对非接触式支付环境是固定的。
CLA:’00' INS:’A4' P1:’04',根据名称选择 P2:’00',选择首个或唯一一个 Lc:取决于 Data Data:‘2PAY.SYS.DDF01’ 的 ByteArray Le:’00'
选择 PPSE 的响应中数据部分结构如下:

这里我们并不需要关心 9F3E 和 9F3F,出现它们时,需要做额外处理,但银行的借记卡、信用卡响应里不会有它们。
我们的目标是选择一个 Directory Entry 中的 ADF Name(Application Definition File Name)。
Directory Entry 中 Application Priority Indicator 决定了它的优先级。非接触式下,Application Priority Indicator 的高 4 位可以忽略,低 4 位视为一个 16 进制数,其不为 0x0 时,0x1 代表最高优先级,0xF 代表最低优先级;为 0x0 时视为 0xF;如果没有这个字段,视为 0xF。
这样我们可以选择一个最高优先级的 Directory Entry,如果存在多个并列的,则可以根据情况选择其中一个。它的 ADF Name 是我们 选择 AID 命令的输入。
选择 AID
选择 AID(Application Identifier)也属于 Combination Selection 步骤。在选择 PPSE 后,继续选择 EMV 卡上的一个应用。
这里需要构造 SELECT 命令,其中一个参数为上一个指令中获得的 ADF Name:
CLA:’00' INS:’A4' P1:’04',根据名称选择 P2:’00',选择首个或唯一一个 Lc:取决于 Data Data:选择 PPSE 获得的 ADF Name 的 ByteArray Le:’00'
选择 AID 的响应中数据部分结构如下:

选择 AID 响应中我们需要解析 PDOL(0x9F38),全称 Processing Options Data Object List,它其实就是一个 DOL,只不过是用在下面的 GPO 指令中,所以叫 PDOL。
此外,需要将 DF Name(Dedicated File Name)的值以 0x9F06 为 Tag 暂存下来,后续步骤构造 PDOL 数据可能会用到。
GPO 指令
GPO(GET PROCESSING OPTIONS)指令属于 Kernel Processing 步骤,主流卡组织 Kernel 第一条命令都是 GPO。从现在开始都是在模拟 Kernel Processing 步骤。
根据上一条指令获取到的 PDOL,我们可以构造一个 PDOL 数据,构造方法和 DOL 一节中描述的相同。如果缺少 PDOL 数据,或者未提供 EMV 卡所要求的 PDOL 数据,后续的 GPO 指令将直接失败。
如果想知道 PDOL 中可能出现什么数据,可以查看 EMV 规范中,数据来源为 Terminal 的 Tag。这些数据中除了 0x9F06,是在选择 AID 后获取到的,其他的都可以提前构造好。
在构造 PDOL 数据时,需要根据 EMV 卡给出的 PDOL 的顺序,以及不同数据类型的截断、填充规则正确构造。
构造 GPO 命令。
CLA:’80' INS:’A8' P1:’00' P2:’00' Lc:取决于 Data Data:构造的 PDOL 数据 Le:’00'
GPO 响应数据有两种格式,两种格式都需要正确处理,因为我们并不知道 EMV 卡会以哪种格式返回。
格式 1
格式 1 为一个 Tag 为 0x80 的基本数据对象,即一个 TLV。

其中 AIP(Application Interchange Profile)占用两个字节,我们不关心。我们要解析的是 AFL(Application File Locator)。
格式 2
格式 2 为一个 Tag 为 0x77 的构造型数据对象,即一个 TLV 里面可能有多个 TLV,但一定包括 AIP(Application Interchange Profile)和 AFL(Application File Locator)
在格式 2 的情况下,响应中还可能带有 Track 2 Equivalent Data(0x57),Application Primary Account Number(0x5A),Application Expiration Date(0x5F24),可以直接解析出卡号和过期日期。
READ RECORD
如果通过 GPO 指令直接获取到 Track 2 Equivalent Data(0x57)或 Application Primary Account Number(0x5A)和 Application Expiration Date(0x5F24),则可以跳过这个步骤。
在没有直接通过 GPO 指令获取到上述信息的情况下,我们必须通过 READ RECORD 指令来遍历读取 EMV 卡上的文件,以找到这些信息。READ RECORD 指令的参数需要解析 AFL 来得到。
解析 AFL
AFL 为一个列表,这个列表中每个元素的内容都是 4 个字节。这 4 个字节的内容如下:
- 第 1 字节:高 5 位表示 SFI(Short File Identifier,短文件标识符),低 3 位可以忽略。
- 第 2 字节:表示该 SFI 下要读取的第一个或唯一一个记录号(Record number)。这个字节不能设置为零。
- 第 3 字节:表示该 SFI 下要读取的最后一个记录号。其值应大于或等于第 2 个字节的值。当第 3 个字节的值大于第 2 个字节时,应读取从第 2 个字节记录号到第 3 个字节记录号(包含它们)的所有记录。当第 3 个字节的值等于第 2 个字节时,仅读取第 2 个字节编码的记录号。
- 第 4 字节:对我们来说用不到,可以忽略。
这样我们就获得了 AFL 列表,列表中每个元素包含一个 SFI,一个起始记录号,一个结束记录号。起始和结束记录号可以相等。
对于 AFL 列表中的每一个元素,以及每一个元素的每一个记录号,都执行 READ RECORD 指令。
READ RECORD 指令
构造 READ RECORD 命令,注意它没有 Lc 和 Data。
CLA:’00' INS:’B2' P1:记录号 P2:高 5 位对应 SFI 高 5 位,低 3 位为 100,代表 P1 为记录号 Le:’00'
READ RECORD 响应为 Tag 为 0x70 的构造型数据对象,即它的值为一系列 TLV,在这一系列 TLV 中查找我们要的数据 Track 2 Equivalent Data(0x57)或 Application Primary Account Number(0x5A)和 Application Expiration Date(0x5F24)。
在这个不断读取解析的过程中,如果收集到了 EMV 卡号和过期日期,则终止这个 READ RECORD 的遍历读取动作。
假设现在我们已经拿到了 Track 2 Equivalent Data(0x57)或 Application Primary Account Number(0x5A)和 Application Expiration Date(0x5F24),接下来是解析它们。
Track 2 Equivalent Data
Track 2 Equivalent Data(0x57)中同时包含卡号和过期日期,可以一次性得到我们想要的结果。
其格式为:
卡号(n, var. (up to 19)) 十六进制 ‘D’ 过期日期(n 4) 其他数据
卡号和过期日期均为数字型数据,卡号最多有 19 个数字,中间有一个十六进制的‘D’分割,过期日期为 4 个数字,格式为 YYMM。
Application Primary Account Number
Application Primary Account Number(0x5A)仅包含卡号数据。
其格式为:
cn var(. up to 19)
卡号为压缩数字型数据,卡号本身数字最多 19 个,数据长度最大为 10 字节,前面说过,压缩数字型数据,数字会左对齐,所以其末尾可能会有一个额外的 0xF,需要丢弃掉。
Application Expiration Date
Application Expiration Date(0x5F24)仅包含过期日期数据。
其格式为:
n 6(YYMMDD)
过期日期为数字型数据,6 个数字,日期格式为 YYMMDD。
到这里,你已经明白了通过 NFC 读取 EMV 卡号的技术原理。上述步骤,对主流卡组织的非接触式 EMV 卡均有效。
English version:
메타데이터
- post_id
- 5d9750f2a172
- slug
- android-nfc读取银行卡号的技术原理-5d9750f2a172
- url
- https://medium.com/@wanxiao1994/android-nfc%E8%AF%BB%E5%8F%96%E9%93%B6%E8%A1%8C%E5%8D%A1%E5%8F%B7%E7%9A%84%E6%8A%80%E6%9C%AF%E5%8E%9F%E7%90%86-5d9750f2a172
- canonical_url
- https://medium.com/@wanxiao1994/android-nfc%E8%AF%BB%E5%8F%96%E9%93%B6%E8%A1%8C%E5%8D%A1%E5%8F%B7%E7%9A%84%E6%8A%80%E6%9C%AF%E5%8E%9F%E7%90%86-5d9750f2a172
- author_url
- https://medium.com/@wanxiao1994
- status
- ok
- fetched_at
- 2026-06-14 17:09:17