Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。

文章代表作者个人观点,少数派仅对标题和排版略作修改。


说明:本文的方案仅适用于 iPhone + Mac 双端用户。Android 机器未经调研,如果能够导出短信数据,那么本文后续对数据的处理和分析流程仍然适用。

我有两张 SIM 卡,一张是家乡的号码(下称「旧号」),另一张是到上海工作后新办理的号码(下称「新号」)。有了新号后,便把支付宝、淘宝、微信这些重要的账号都重新绑定了。最近想着旧号一直不用,还总是接到营销电话,不如直接注销了。正好这段时间,「注销手机号就是在出卖自己」这类话题引发了不少网友吐槽「我怎么知道自己的手机号都注册了什么服务?」所以来分享一下自己的解决方案。

工信部官方推出的「全国互联网账号『一证通查』」服务目前仅能查询到 16 个相当常见的互联网平台注册情况,覆盖面显然不足。于是,我转而开始思考,我和这些平台之间,有什么存在于本地的数据,能够揭示我们之间的关联吗?

是短信。当代社会,使用手机号注册平台账号,大概率会收到对方发来的各类短信。之前的工作曾涉及短信营销,一个印象深刻的点是,在给用户批量发短信前,必须先申请「短信签名」。一条常见的服务短信长这样:

【少数派】安全提醒:为了保障你的账号安全,我们建议你立即绑定邮箱。当你的手机丢失或手机号不可用时,你可以通过邮箱邮箱二次验证后来更换手机号。立即绑定:https://sspai.com/setting/account

短信开头的 少数派 就是所谓的「短信签名」,作为发信者的身份标识,一般可以被认为是公司名称。各个公司申请到短信资质和签名后,就可以在短信服务提供商购买额度,然后按照模板要求编写短信内容,最后一键发送。服务商则自动在短信内容前添加签名,并以方头括号  和 标记。通过对阿里云腾讯云华为云火山引擎这四家的短信服务规范取并集(以扩大短信识别范围),可以得到如下的表格(后文方案会用到短信类型,这里可以特别关注一下):

短信类型内容规范适用场景收信人类型案例
所有类型所有类型的短信都需要「短信签名」用于标识发送方属性,在短信内容中体现为短信开头的「【某某公司】不涉及不涉及见下
验证码短信短信内容必须包含「验证码、注册码、校验码、动态码、动态密码、CODE」中的一个需要安全验证的操作发起注册、登录等操作的用户【某某音乐】1234 您的某某音乐验证码,有效时间为 10 分钟,请尽快验证
通知类短信禁止发送营销推广类内容和链接系统相关信息,例如服务升级、服务开通、价格调整、 订单确认、物流动态、消费确认等注册用户【某某餐厅】您的会员卡 1234 在某某分店消费,剩余卡值 54.32,积分余额 46
营销类短信短信末尾必须携带退订方式,根据最新规定目前仅支持「拒收请回复R」新品宣传、会员关怀、商品促销、活动邀请、跨境营销等没有明确限制【某某旗舰店】新势力周囤货啦!全店叠加满减满 300 减 90!爆款下单赠惊喜好礼,冲! s.tb.cn/abcdefg 拒收请回复 R

于是,一个自然的想法是,我们可以逆向利用这些短信中的签名,来辅助判断自己的手机号绑定了哪些服务。只要导出这些短信,用正则表达式提取签名,按照出现频率倒序排列,一个个打钩确认就可以了。

这个方案需要同时使用 iPhone 和 Mac,并在 iPhone 上开启「短信转发」功能。在「设置 - 信息 - 短信转发」中,打开 Mac 设备的开关即可;读者也可参考 Apple 官方支持文档进行操作。等待一段时间,让历史短信同步到 Mac 上,完成之后就可以尝试使用诸如 TablePlus 之类的数据库管理工具访问位于 /Users/yourusername/Library/Messages/chat.db 路径的数据库了。这里建议将 chat.db 复制到一个普通权限的文件夹下,否则对数据库进行操作可能会被请求授予「完全磁盘访问权限」,同时可以避免误操作可能导致的其他问题。

另外,iOS 17 增加了一项功能新功能,可以自动删除使用过的验证码短信。如果你最近有注销手机号的需求,且想使用本文的方案,不妨先把这个功能关掉一段时间,以积累更多短信数据。设置路径为「设置 - 密码 - 密码选项 」,关闭「使用后删除」即可。

短信导出,有没有现成的解决方案?

本着「绝不重复造轮子」的原则,我确实找到了两个解决方案。

一是 iMazing 的短信备份功能,可以导出为 Excel 表格。国内经销商给出的最低价是 188 元,我没有使用此软件的需求,并不愿意单纯为了一次短信导出付出这个价格。同时,它导出的短信表格并没有区分手机号,这对我来说大大增加了排查的工作量,遂放弃。另外,我大约有 15K+ 条短信,从 iPhone 备份到 Mac 上耗费了将近 3 个小时,这个速度也让我有点不能接受。

另一个是用 Rust 语言编写的开源项目 imessage-exporter,免费且导出速度极快,但同样没有区分手机号。我给作者提了 issue,已经被加入下一版本的计划中。读者如果不想自己实现,可以等等作者发新版。可能是因为直接读取已经存在于 Mac 上的数据库(而不是经由 iPhone 备份的方式),同样是 15K+ 短信,导出仅消耗了几秒钟。

如果你只有一个手机号,并且已经是 iMazing 用户或者有一些技术背景,那这些其实是够用的。但对于双卡双待的用户,无法区分短信的收信号码,情况就有些复杂了。

粗糙的 MVP:先解决核心问题

一开始,我想修改一下前文中提的开源项目,毕竟只要添加数据库的一个字段就可以了。但我对 Rust 语言一无所知,即便有 AI 的加持,修改一个完整的项目也感到十分捉襟见肘,不知从何下手。折腾一番之后,最终还是开始自己造轮子了。

项目的核心问题是,我们的手机号到底注册/绑定了哪些服务?那么解决方案至少要把手机号、短信签名、频次这三列导出来。

先看一看,短信数据库长什么样?

经过一些探索,我整理出了 chat.db 中与我们问题相关的表格和字段。并且,同步到 Mac 的短信通过 destination_caller_id 这一字段保留了收信号码,这意味着我们的解决方案有了一些眉目。

表格名称内容
chat记录每个对话的属性,例如对话 ID、发信人号码、最后一次的联络时间等。
message记录每条信息的详细内容和属性,例如短信 ID、短信富文本内容、收信号码、收发时间、是否被过滤等。
chat_message_join记录对话 ID 和信息 ID 的对应关系,一个对话可能包含多条信息。

挑选出项目相关字段后,这三张表格关系如图所示。一个 chat (对话)可能包含多条 message (s) (信息),两者的对应关系,被记录于 chat_message_join 这张表里。一些需要解释的字段如下:

  • 自 macOS Ventura 某次更新后,短信文本不再直接存储于 text 列,而是以特殊格式放到了attributedBody 里。attributedBodyblob (Big Large Object) 类型,存储了包含短信文本在内的各项数据。由十六进制转化为文本后,在一堆乱码中勉强看到了短信内容和 NSAttributedStringstreamtyped等字眼,搜索资料后了解到与 iOS 和 Swift 有关。这方面我并不了解,还好搜到了一个开源项目(python-typedstream),可以通过 Python 直接读取并抽取其中的文本值。
  • date 是起始于 2001 年 01 月 01 日的时间戳(不同于默认的 1970 年 01 月 01 日),需要特别转换一下。
  • service 指短信的服务类型,亦即 SMS 或是 iMessage,这里我们只需要 SMS(因为 iMessage 是邮件地址发来的信息,不是我们需要的服务短信)。
  • is_from_me 代表「是否为本机发出的信息」,这里只需要收到的短信,所以加上一个过滤条件 is_from_me==0
  • destination_caller_id 就是我们需要的「收信号码」字段。
  • chat_identifier 是发信号码。

把短信数据拿出来

此处使用 Python 语言,在 Terminal 中运行 pip install pytypedstream 以安装上文提到的包。完成后,我们先定义两个函数,分别用来从 attributedBody 中提取短信文本和转换时间戳。

# 用于从blob数据类型中抽取短信文本内容
import typedstream
def get_value_from_bytesobj(hex_bytes):
    try:
        ts_obj = typedstream.unarchive_from_data(hex_bytes)
        return ts_obj.contents[0].value.value
    except Exception as e:
        return None
# 用于将时间戳转换为人类可读的日期时间格式
from datetime import datetime, timedelta
# iOS messages的timestamp开始于2001-01-01,且需要转化为北京时间
def timestamp_converting(timestamp, custom_epoch = datetime(2001, 1, 1)):
    try:
        timestamp_seconds = timestamp / 1e9
        resulting_date = custom_epoch + timedelta(seconds=timestamp_seconds) + timedelta(hours=8)
        return resulting_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]
    except Exception as e:
        return None

接下来我们连接到数据库 chat.db,取出需要的数据,并且应用刚才定义的两个函数,对数据做初步处理,并写入 Excel 表格。

import sqlite3
import pandas as pd

# 建立数据库链接,取数据
database_path = 'path/to/chat.db'
connection = sqlite3.connect(database_path)

query = '''SELECT a.ROWID AS message_id, c.ROWID AS chat_id, c.chat_identifier, a.attributedbody, a.date, a.destination_caller_id FROM message AS a
LEFT OUTER JOIN chat_message_join AS b ON a.rowid = b.message_id
LEFT OUTER JOIN chat AS c ON b.chat_id = c.rowid
WHERE a.is_from_me = 0 AND a.service = "SMS";'''

df = pd.read_sql_query(query, connection)
connection.close()

# 初步处理数据,抽取短信内容、转换时间戳,排除空文本短信,写入Excel表格
df['sms_text'] = df['attributedBody'].apply(get_value_from_bytesobj)
df['sms_date'] = df['date'].apply(timestamp_converting).apply(lambda x: pd.to_datetime(x, format='%Y-%m-%dT%H:%M:%S.%f'))
# 去除没有文本内容的短信
df = df[~df['sms_text'].isnull()]
df = df[['message_id', 'chat_identifier', 'destination_caller_id', 'sms_date', 'sms_text']].sort_values(by = 'sms_date', ascending = False)
df.to_excel("sms_content.xlsx",index=None)

这里解释一下为何选择较为「笨重」的 .xlsx 格式存储,而不选择 .csv?因为不确定短信文本内容中是否包含了和分隔符(英文逗号 , )一样的符号,导致后续读数时错列。同时数据量也不大, 如果数据量大,可以尝试用 arrow 包写 .parquet 文件。

最终,我们可以得到数据字典结构如下的表格,并进行下一步分析。

字段名称数据类型含义及备注样例
message_idinteger短信 ID5337
chat_identifiertext发信人号码106905109757920
destination_caller_idtext本机号码,可能不止一个(双卡双待)+8613912345678
sms_datedatetime收信时间2024-04-01 12:34:56
sms_texttextattributedBody 字段中提取的短信文本内容【某某音乐】1234 是您某某音乐的验证码,有效时间为 10 分钟,请尽快验证

清洗数据,得到检查清单

注:从这里开始,编程语言变成了 R。在解决个人生活问题的项目上,我的编程习惯比较随意,什么语言方便用什么。我在本文最后提供了一个一键导出的应用,供不太熟悉 R 语言读者使用。另外,少数派编辑器的代码块模块还不支持 R 语言,所以语言类型我选择了纯文本,没有高亮。

library(data.table)
library(tidyverse)
library(readxl)
library(writexl)

# 读取刚才保存的短信数据表格
sms_df = read_xlsx("sms_content.xlsx") %>% 
  setDT()

# 这里去除了短信首尾的空白符号和占位符,并且将所有英文符号转为小写
sms_df[, sms_text := sms_text %>% str_squish() %>% str_remove_all(., "\uFFFC") %>% str_to_lower()]

# 此处为了排除一些无关联的IDFA而进行过滤
# 请将"new"和"old"替换为自己的手机号
sms_df = sms_df[destination_caller_id %chin% c("new", "old")]

首先,我们需要将手机号发来的日常短信过滤掉。手机号是 11 位,在 chat_identifier 字段中会增加前缀 +86。在查阅资料获得手机号段特征后,我们可以通过以下正则表达式将其过滤(关于正则表达式可以参考 regex101少数派这篇文章,这里不再赘述)。

phone_number_pattern = "^\\+86((135|136|137|138|139|150|151|152|157|158|159|178|182|183|184|187|188|195|197|198|130|131|132|145|155|156|166|175|176|185|186|196|133|149|153|173|177|180|181|189|190|191|193|199|192|162|165|167|170|171)[0-9]{8}|134[0-8]{8})$"

sms_df = sms_df[!str_detect(chat_identifier, phone_number_pattern)]

然后,把 【】 内部的文本抽取出来,选择旧号相关的短信签名,按照出现频率倒序排列,写入 Excel 文件,一一去排查即可。注意,这里我们的正则表达式匹配了位于开头或结尾的方头括号,因为有些公司会把短信签名放在末尾(比如交通银行),也许是有自己的独立渠道,不需要受短信服务商模板的约束。

此外,还需将首尾均未出现签名的短信过滤掉,除了由一些公共机构、政府部门下发的公益短信外,全部都是售假、赌博、色情类内容(不清楚它们是如何绕过审核系统下发的)。

# 匹配短信开头和末尾的签名
sms_df[, `:=`(sms_sign_start = sms_text %>% str_extract(., "^【(.*?)】", group = 1), sms_sign_end = sms_text %>% str_extract(., "【([^】]*)】$", group = 1))]

# 将首尾均未出现签名的短信过滤掉
sms_df = sms_df[!(is.na(sms_sign_start) & is.na(sms_sign_end))]

# 将首尾签名两列数据合并到一列中
sms_df = sms_df %>%
  pivot_longer(
    cols = starts_with("sms_sign"),
    values_to = "sms_sign",
    values_drop_na = T
  ) %>% 
  setDT()

# 按照出现频率从高到低对短信签名进行排序,并写入文件
# 这里请将"old"替换为你的手机号  
sms_sign_cnt = sms_sign_cnt[destination_caller_id == "old", .N, .(sms_sign)] %>% 
  setorder(-N)
sms_sign_cnt %>% 
  write_xlsx(., "path/to/file.xlsx")

我的旧号中有 368 个短信签名,出现频率前五的签名如下(出现频次进行了数据脱敏),包含了当时仍属于网易的「网易考拉」,也许算得上是一滴时代的眼泪了。不过这么多签名,排查起来工作量不少,能不能简化一点呢?

sms_signN(出现频次)
交通银行402
中国移动287
网易109
网易考拉98
招商银行72

一个简单的思路是,将低频签名过滤掉。让我们先看一下数据的分位点。

sms_sign_cnt[, quantile(N)]
sms_sign_cnt[N >= quantile(N, 0.75)]
0%25%50%75%100%
1114402

有超过一半的短信签名只出现过 1 次,前 25% 的短信签名出现次数在 4 次及以上。如果我们以「至少出现 4 次」为基准进行切割,需要排查的签名就只剩下了 109 个。快刀斩乱麻,直接减少了 70% 的工作量——但会不会有点担心,那些低频签名中偏偏有我们很看重的呢?

精细化分析:减少排查工作量

从这里开始,项目的目标从「找到所有注册/绑定的服务」变成了「找到值得被排查的服务」。因此,我们需要增加一些数据分析的维度,尽量排除不需要检查的签名。

此后都是基于作者个人短信数据情况而进行的非标准化分析,主要是为了提供一些解决思路,读者如果感兴趣,可以参考自行实现。

Step1 排除移动服务商和公共机构

大体扫一眼短信内容,我们会发现不少短信来自于移动服务商:话费余额提醒、宽带推广、流量包优惠等等。手机号和移动服务商天然绑定,注销手机号自然不需要考虑它们。以我的移动号码为例,所有以 10086 和 10085 开头的号段都是移动下发各类通知的号码,可以将其直接过滤。同时,再增加一个判断条件,短信文本中出现了类似「中国移动」「上海移动」「江苏移动」等模式的签名也会被过滤掉。

另外,有些公共机构的公益短信(例如天气预警)也会通过 10086 的端口下发,可以将其一并过滤。

sms_df[destination_caller_id == "old" & (str_detect(chat_identifier, "^1008[56]") | str_detect(sms_text, "(中国|江苏|上海)移动")), .N, sms_sign] %>% 
  arrange(., desc(N)) %>% {
    print(.)
  } -> plot_stats_mobile_operators
sms_signN
中国移动287
中国移动心级服务,让爱连接73
余额不足提醒55
流量提醒42
江苏省文旅厅13
# 这里anti_join()函数的功能是从X中排除出现在Y的内容
sms_sign_cnt_step1 = sms_sign_cnt %>% 
  anti_join(., plot_stats_mobile_operators, by = join_by(sms_sign))

plot_stats_step1 = nrow(sms_sign_cnt_step1)

这一步排查出了 87 个移动运营商和公共机构签名,总排查量还剩 281 个,占比 76%。

Step2 排除电商旗舰店

每逢各类电商活动,商家们的推广信息就纷至沓来。我们的号码和电商平台绑定,不需要额外对这些旗舰店进行解绑操作。因此,包含「旗舰店」字样的短信签名可以被过滤掉。

sms_df[destination_caller_id == "old" & str_detect(sms_sign, "旗舰店"), .N, sms_sign] %>% {
  print(.)
} -> plot_stats_ecommerce

sms_sign_cnt_step2 = sms_sign_cnt_step1 %>% 
  anti_join(., plot_stats_ecommerce, by = join_by(sms_sign))

plot_stats_step2 = nrow(sms_sign_cnt_step2)

这一步排查出了 14 个电商商家签名,总排查量还剩 267 个,占比 73%。

Step3 排除已经解绑过的服务

因为我有两个手机号,且一部分账号已经重新绑定到新号上,所以短信签名在新旧号的活跃状态就成了一个额外的分析维度。例如,如果旧号自某天(记为 T)起再也没有收到过某个短信签名,而新号第一次收到此签名的短信在 T 之后,就可以认为我已经将此签名的服务绑定到新号上了,从而在检查列表中将其排除。

签名新号状态签名旧号状态含义检查列表策略
活跃活跃两个号码混用保留此签名
活跃不活跃已迁移到新号过滤此签名
不活跃活跃未迁移到新号保留此签名
不活跃不活跃不存在此情况不涉及

我们利用全部数据集(包含新号和旧号的短信)构建一个表格,对每一个短信签名,都列出以下四个属性:

  • 旧号最远接收的日期 min_date_old
  • 旧号最近接收的日期 max_date_old
  • 新号最远接受的日期 min_date_new
  • 新号最近接收的日期 max_date_new

基于对日期大小的比较,可以判断短信签名在新旧号的活跃状态(过去的日期距离当下越远,则在数值上越小)。

sms_minmax_date = sms_df[, .(min_date = min(sms_date), max_date = max(sms_date)), .(destination_caller_id, sms_sign)]

sms_minmax_date = sms_minmax_date %>% 
  pivot_wider(., names_from = destination_caller_id, values_from = c(min_date, max_date)) %>% 
  setDT()

生成的表格字段如下:

sms_signmin_date_newmin_date_oldmax_date_newmax_date_old
某服务2020-06-25 18:17:362018-11-18 21:54:342021-07-05 17:54:162019-10-15 12:12:46
「新号活跃、旧号不活跃」模式时间关系示意

min_date_new > max_date_old 为过滤条件,找到满足「新号活跃、旧号不活跃」模式的短信签名,可以视作已经解绑旧号并重新绑定了新号。

sms_minmax_date[min_date_new > max_date_old] %>% {
  print(.)
} -> plot_stats_shifted_to_new

sms_sign_cnt_step3 = sms_sign_cnt_step2 %>% 
  anti_join(., plot_stats_shifted_to_new, by = join_by(sms_sign))

plot_stats_step3 = nrow(sms_sign_cnt_step3)

在旧号的短信中,我们找到了 81 个满足指定模式的签名,总排查量还有 193 个,占比 52%。注意,本小节是对整个旧号数据集进行操作,前面已经过滤的签名仍然有可能重复出现在这里,所以数值上并不是直接相减的关系,而是排除(亦即 anti_join() )后计数。

还有一种情况是新旧号在混用,例如我在 B 站和当当网都有两个账号,可能一个是通过微信登录直接授予了旧号,另一个是用手机验证码登录了新号,导致一会新号收到短信,一会旧号收到短信。这种模式体现在时间上的特征为,新旧号的时间段是相互交错的。这类签名也需要作为排查对象,先存下来,在下一步分析时使用。

「新旧号混用」模式的时间关系示意
sms_minmax_date[(max_date_old > max_date_new & max_date_new > min_date_old) | (max_date_new > max_date_old & max_date_old > min_date_new)] %>% {
  print(.)
  } -> plot_stats_mixed

Step4 排除只发过营销类短信的服务

在文章开头,我们提到了服务短信的三种分类。细想一下,「验证码短信」涉及到注册、登录等需要安全验证的场景,频率低但重要性高;「通知类短信」则标识着用户在此公司有相关业务,频率越高则关联程度越高,主动操作的可能性就越高,注销手机号的影响也就越大;「营销类短信」则不是用户主动发起的,也没有强制规定不能发送给未注册的用户,无论其频率如何,重要性都是最低。

因此,那些出现在营销类短信的签名,可以认为是获客类操作,反证了我们的手机号并没有注册过它们的服务。

我们明确知道验证码和营销类短信的模板要求,因此可以用正则表达式为短信打上标签。但是通知类短信没有明显的文本规律,为了扩大安全边界,我们可以将不满足验证码和营销类模式的短信都归为通知类短信。

另外,由于营销类短信的拒收话术有过更新,所以为了定位此类历史短信,对应的正则表达式也扩大了识别范围。

三类短信的文本特征
# 这里的OTP代表One Time Password
verification_code_pattern = "验证码|注册码|校验码|动态码|动态密码|动态验证码|code|otp" 
promotion_pattern = "拒收|退订|td|回[a-z]+退?"

# 通过正则表达式为短信打上类型标签
sms_df[, sms_category :=
               ifelse(
                 str_detect(sms_text, verification_code_pattern),
                 "verification",
                 ifelse(
                   str_detect(sms_text, promotion_pattern),
                   "promotion",
                   "notification"
                 )
               )
]

接下来,将旧号中只发送过营销类短信的签名找出来。

sms_df[destination_caller_id == "old", .N, .(sms_sign, sms_category)] %>% 
  pivot_wider(., names_from = "sms_category", values_from = "N", values_fill = 0) %>% 
  filter(., notification == 0 & verification == 0) %>%
  as.data.table() %>% {
    print(.)
  } -> plot_stats_only_promotion

这里我们需要特别注意一下,「旧号仅发送过营销短信的签名」也有可能后续给新号发了验证码和通知类短信(存在新旧号混用的情况),所以我们要把这个待过滤列表中「新旧号混用的签名」先排除掉,剩下的才是应该被过滤的签名。

sms_sign_cnt_step4 = sms_sign_cnt_step3 %>% 
  anti_join(
    plot_stats_only_promotion %>% 
      anti_join(., plot_stats_mixed, by = join_by(sms_sign)),
    by = join_by(sms_sign)
  )

plot_stats_step4 = nrow(sms_sign_cnt_step4)

这一步我们在旧号所有短信中确定了 110 个仅发送过营销类短信的服务,大致是一些借贷、网游类我未使用过的服务。

精细化分析小结

步骤剩余短信签名数量每一步排除的签名数量(去重)
开始3680
Step1 过滤移动运营商28187
Step2 过滤旗舰店26714
Step3 过滤已迁移19374
Step4 过滤纯营销12766
结束127241

最终,待排查的短信签名从 368 个降低到了 127 个,减少了 65% 的排查工作。相比较于以「出现频率>=4」为标准快刀斩乱麻砍掉约同等工作量(70%)的方式,后续的分析复杂了一些,但是对结果准确度的信心却大大提升了。

经过四个步骤的清洗,排查工作量降低至 35%

一键导出工具下载

链接:https://pan.quark.cn/s/bfe088195639

提取码:wHV6

这个工具用 Python 的 tkinter 包做了一个十分简陋的 GUI,并使用 PyInstaller 进行了打包。我对非 Web 应用程序的开发十分不熟悉,所以这个工具仅实现了基本功能:选择数据库、判断数据库是否存在指定的表格和字段、选择保存路径、抽取和输出数据为 Excel 表格。同时,因为是在本文内放出的下载链接,默认用户都浏览过本文,所以界面上没有提供其他辅助信息,这可能会让从其他渠道接触的用户一头雾水。

如果读者无法打开此工具,可能需要到「设置 - 安全和隐私」中选择允许本应用运行。另外,如果打开了,却发现它好像闪退了,请稍微等待一会儿,它似乎在解压自己到某个临时文件夹,解压好了这个窗口就会弹出来。或者到活动监视器中杀掉进程后重试。

如果数据库通过检验,将会输出两个 .xlsx 文件:

  1. 历史收到的所有 SMS 类型短信(相当于一次短信导出)。
  2. 分表单存储的「运营商和公共机构签名」「电商旗舰店签名」「分短信类别统计过频次的其他签名」(已排除前两者),均有收信号码、短信签名和频次标注。

这个产品后续会有什么改进吗?

一开始我想把它做成一次性买断产品,价格大概在 3-6 元。毕竟,谁不希望拥有属于自己且还能赚钱的产品呢?网络上的讨论确实让我意识到,这个需求虽然是超低频的一次性需求,但也真实存在,并且我的解决方案确实告诉了用户「你的手机号到底在哪些平台注册过」,理论上存在 PMF。

但涉及到对短信内容的分析,有多少用户会允许一个不知名开发者的小工具去访问自己的敏感数据呢?即便我声明了这个工具不会上传用户数据、纯在本地分析、可以断网使用,想必还是很难打消用户的疑虑。尤其是还没有申请签名,可能被 macOS 的安全策略阻拦,而直接选择系统路径的 chat.db 又会被提示需要「授予完全磁盘访问权限」,总归令人不安。其实换我是用户,我也不太会愿意使用这个工具。

另外,虽然尽可能列出了排查对象,也尽可能减少了排查工作量,但毕竟不能帮助用户一键解绑。所有绑定的服务,都需要用户手动一个个去寻找解绑方式。因此,这个工具的竞争对手,也许并不是其他相同思路、界面更美观、用户体验更好、开发者更知名的同类产品,而是用户面对上百个待排查项目最终冒出的「算了好烦,还是办个 8 块钱保号套餐吧」想法。

但不管怎么说,我也算是有了第一款属于自己的产品了,我会尝试在小红书上营销一下此产品,看看市场反馈。后续还有其他项目需要用 React 开发,所以也想尝试用 React + Electron + Python 的方式把它完善一下,当做练习作品,至少 UI 不能看起来像诈骗软件(笑)。

> 关注 少数派小红书,找到数字时代更好的生活方式 🎊

> 实用、好用的 正版软件,少数派为你呈现 🚀