Python基于公开信息推断年龄:正则、属相与加权概率实战
先说一个我亲身遇到的场面一群朋友聚会聊得正热闹不知道谁顺嘴问了一句“哎你今年多大”空气直接凝固了。当事人笑了笑没接话场面一度要靠喝水撑过去。我自己是个写Python的脑子里当时冒出来的第一反应不是打圆场而是——这个需求能不能用代码解决不问、不打听只靠公开信息把年龄猜个大概。这篇文章就是我当时那套思路的完整复盘。我会从“哪些公开字段其实已经暴露了年龄线索”讲起再到正则表达式硬扫、星座属相换算、语义模糊推断这三板斧最后用一个完全虚构的实测案例展示怎么把零散线索组合成一个概率区间。文末我也会认真聊一件事为什么很多看似聪明的推算结果最终只能躺在练习册里不该被拿出来真用。先说明白前提我只基于对方主动公开、合法的文本和资料做推断不涉及私密聊天记录、关系链抓取、设备信息之类的灰色地带。技术本身是中性的但边界得自己划清楚。1. 先翻翻公开信息年龄线索都埋在哪些不起眼的字段里1.1 别把“公开”两个字理解得太宽很多人一听“用Python算年龄”第一个念头就是去爬社交平台资料、查绑定手机号、翻历史动态。这里我先把红线画出来对方主动发布的公开内容比如昵称、头像、个人简介、公开发过的文字或图片这些是我们可以合法观察的样本但非公开的浏览权限、聊天内容、好友关系链、设备指纹这些东西碰都不能碰。道理其实不复杂。公开信息是对方放出来给所有人看的你可以看我当然也可以看这不算冒犯。但“公开”和“可以拼凑成隐私画像”是两码事。一堆零散字段单拎出来都没什么合在一起能还原出相对准确的年龄、地域、职业那就已经接近“隐私画像”了。所以本文所有讨论都停在“用公开线索做概率推测”这一层绝不推荐任何形式的偷看或数据合并。1.2 一张很实在的线索清单我自己在实践中总结过一张表每次做文本推断前会先对着表清点样本里有哪些可用字段。这张表可以帮你在动笔写正则之前快速判断手里的料够不够字段常见例子能推算出什么昵称“NaNa_1999”、“老张88”直接暴露出生年份或年代个人简介“90后”“天蝎座”“属虎”年代区间、星座区间、生肖循环头像某部老番主角、某个年代的歌手大概的兴趣代际公开发帖“毕业那年”“高考完那个夏天”以17-19岁为锚点往回推关注列表某古早乐队超话、某怀旧游戏大概率是那个年代的用户邮箱地址名称里带1997或者birthday等可能的出生年份有同学可能觉得这条表太“侦探”了但实际操作中大部分有价值的年龄线索根本不需要什么复杂技术都在这些原文细节里。后面第2章要做的就是想尽办法把这些零散内容翻译成机器能处理的数字。1.3 有些线索看着能用实际上是陷阱经验不够时最容易踩的坑是拿到一个“看似确定”的信息就兴奋得直接下结论。我举个实际例子有人简介里写了“考研上岸已经三年”考研上岸一般22到24岁加三年就是25到27岁听起来很精确对不对问题是对方可能是工作两年后才辞职考研的那实际年龄就要加两岁也可能是本科毕业直接保研的那又不太一样。还有一种是“年龄通胀”好几年前写“25岁”一直忘了改过两年再看还是“25岁”。线上个人信息普遍存在严重的滞后性比线下简历还容易过期。所以我后来定了个死规矩任何单条线索最多只能当参考信号不能当结论。这个思路会在第3章展开成一套权重打分机制也是这套方法的核心。2. 正则、字典、语义推断把线索翻译成年龄数字的三板斧2.1 正则硬扫先从文本里抠出“年份”和“年代词”Python里最顺手的就是re模块。假设我们已经拿到了一段合法的公开文本比如个人简介或者某条公开动态的文案第一件事就是把里面的年份、年代词、年龄词全部抠出来。import re def extract_age_hints(text): hints [] # 明确带“出生”“生日”“出生于”的句子 birth_pattern re.compile(r(?:出生于|出生年月|生日是?|生于)\s*(\d{4})) # 常见的“我是XX年”“XX年生人” year_pattern re.compile(r[1-2]\d{3}) # 年代词90后、80后、00后 gen_pattern re.compile(r([0-9]{2})后) for m in birth_pattern.finditer(text): hints.append((birth_year, m.group(1))) for m in year_pattern.finditer(text): hints.append((year, m.group(0))) for m in gen_pattern.finditer(text): hints.append((gen, m.group(1))) return hints text 我是93年的出生在南方小城高考那年夏天特别热。 print(extract_age_hints(text)) # [(year, 1993), (gen, 93)]这段代码里最容易出问题的是第二个正则[1-2]\d{3}。它会把“2020年毕业”里的2020也抓出来但这个年份代表的是毕业年份不是出生年份。我自己在上线这种脚本前会先对“年份”信号做一层分类如果句子同时包含“出生、生人、年龄”这类词那年份才可能与年龄强相关否则只能标记成弱信号。一个小经验正则不要贪多先把明确句式抓住宁可少抓也别错抓。你一次抓回十个年份里面八个是无关的后面清洗起来比重新写正则还痛苦。2.2 字典映射星座和属相是怎么换算成年龄区间的除了年份还有两类信息出现的频率很高星座和属相。星座没法直接给出生年份但能锁定“出生日期在一个月区间内”。属相反之能锁定年份模12的余数但和具体年份之间还隔着12的倍数。先说星座。写一个简单的映射函数把日期区间月, 日映射成星座名def zodiac_sign(month, day): zodiac [ (摩羯座, (12, 22), (1, 19)), (水瓶座, (1, 20), (2, 18)), # ... 中间的星座省略原理完全相同 (射手座, (11, 22), (12, 21)), ] for name, start, end in zodiac: if (month, day) start and (month, day) end: return name return 未知星座本身给不了年龄但有一个微妙用途如果公开资料里同时出现了“天蝎座”和“生日快到了”再配合某年的圣诞、元旦动态可以把出生日期区间压缩到某个月的某个区间然后再结合属相或代际信息把年份候选范围缩小。这个过程有点像排除法后面第3章会把它做成权重分。再看属相。属相是12年一轮回同一属相的人在年龄上相差12的倍数。假如我们知道某人的属相是“虎”又知道 ta 是90后那候选出生年份就在“90年代内所有虎年”里选。假设我们用基准年份往前推12就能拿到一串候选年def years_for_zodiac(zodiac_name, start_year, end_year): zodiac_map [鼠, 牛, 虎, 兔, 龙, 蛇, 马, 羊, 猴, 鸡, 狗, 猪] # 锚点年份对应的属相按实际年份顺序计算 base_zodiac 龙 # 用某个具体公元年份的属相做锚点 base_year 2024 offset zodiac_map.index(zodiac_name) - zodiac_map.index(base_zodiac) results [] for y in range(start_year, end_year 1): if (y - base_year offset) % 12 0: results.append(y) return results print(years_for_zodiac(虎, 1980, 2005))这个函数的输出通常是三到四个年份直接缩小了范围。但注意它依赖锚点年份的属相信息而锚点本身可能因为跨年而有偏差所以算出来的结果依然只能当作候选集。2.3 语义松弛推断昵称、头像、老番老歌都会“说话”正则处理的是结构化文本真正让推断变得自然的反而是那些“没说年龄但处处带着年龄痕迹”的东西。我管它叫语义松弛推断意思是不追求精确匹配而是根据常识经验给线索贴标签。比如昵称叫“小鱼干超好吃”年纪集中在18到28你很少看到40岁的人起这种名字但这不是绝对。再比如头像用某部二十年前的老番主角再配上简介里一句“永远喜欢老番”代际感就出来了。还有内容风格一个人天天聊“我孩子在学钢琴”那大概率30往上一个人满嘴“刚进公司实习”那大概率不超过24。这些判断听起来很“玄学”但落到代码里其实就是维护一堆关键词词典做文本命中测试。权重给低一点只当辅助信号。核心原则是凡是不构成充分条件的线索都只影响概率的微小变动。style_hints { 学生感: [期末考试, 宿舍, 社团, 上早八], 社畜感: [通勤, 年终汇报, 加班, 公积金], 家庭感: [学区房, 孩子, 家长会, 兴趣班], } def style_age_guess(text): score 0 for tag, keywords in style_hints.items(): for kw in keywords: if kw in text: score 1 return score3. 别拿单条线索当真用加权组合算出一个年龄概率分布3.1 为什么单一信号总是不靠谱如果只用一条线索下结论我保证你会翻车。属相提供的年份间隔有12年星座只有月级别的精度一句“90后”更是跨了十年。而文本里“我大学刚毕业”这种表述就更不靠谱了——毕业可以是22岁也可以是来自一个延后几年的人生节点。真实世界的信息是嘈杂的同一句话可能对应完全不同的背景。所以我的做法是把所有信号收集起来按可靠度和覆盖度打分然后汇总成一个粗糙的年龄概率分布而不是直接输出“xx岁”。这就像天气预报不会直接说“明天25度”而是给一个区间和置信度因为大气本身充满变量。3.2 给信号分配权重的参考表权重是主观定的但也不是乱定。我根据大量文本推断实践给常用信号定了一套基准权重你可以自己调信号类型参考权重说明明确出生年份0.45最高权重但注意可能是编的或过期代际词90后0.25把范围压到十年但没法精确定位属相0.15压缩到两到三个年份星座日期区间0.10只能匹配月份范围语义风格推断0.05误差极大只做氛围判断这个表背后其实是一个朴素的贝叶斯直觉给先验一个分布然后每个新证据都对概率做一次修正。出生年份等于一个确定性极强的证据所以权重最高语义风格充满歧义所以权重低。实际代码里可以用一个字典对候选年份做累加计分。3.3 一个简单的置信度评估函数下面这个函数接收一组年龄候选年份和对应权重输出一个加权后的“最佳区间”和置信度。我不打算把它写成一个完整的贝叶斯推断器实用阶段一个轻量版本足够了from collections import defaultdict def combine_age_signals(signals): signals: list of (signal_type, value) signal_type: birth_year / gen / zodiac / zodiac_month / style value: 生日年份、代际后缀、属相名、星座区间、语义标签 weights { birth_year: 0.45, gen: 0.25, zodiac: 0.15, zodiac_interval: 0.10, style: 0.05, } scores defaultdict(float) for typ, val in signals: if typ birth_year: scores[int(val)] weights[typ] elif typ gen: decade int(val) # 比如 95 代表 1995-2004 for y in range(1990 decade, 1990 decade 10): scores[y] weights[typ] elif typ zodiac: for y in years_for_zodiac(val, 1980, 2010): scores[y] weights[typ] # 其余信号可继续扩展 if not scores: return None best_year max(scores, keyscores.get) total sum(scores.values()) # 用最高分占比做简易置信度 confidence round(scores[best_year] / total, 2) return best_year, confidence注意这个函数由于没有考虑先验分布和真正的贝叶斯不完全一样。但作为演示和轻量工具已经能帮你量化“多条线索收敛到同一年”的效果。多个独立信号指向同一个候选年份时它的得分会明显抬升置信度也会变高。4. 虚拟实测只凭几条公开线索我算出了一个怎样的年龄区间4.1 我构造的虚拟样本为了演示完整流程我从零虚构一个人物以下所有信息均为演示数据。假设有位“A同学”在某兴趣社区公开发布过一个很简单的主页里面只有三样东西个人简介“资深老番爱好者90后天蝎座。”头像“一部二十年前动画的主角”一条公开动态“高考完那个夏天把喜欢的乐队演唱会看了三遍。”这三条信息没有一条直接写“我今年多少岁”但完全可以推断出一个大致区间。接下来我就模拟一个真实脚本的完整处理链。4.2 逐步执行的流程第一步文案清洗。先把简介和动态里的中文标点做归一化这一步能直接避免很多匹配问题。import re raw_text 资深老番爱好者90后天蝎座。 高考完那个夏天把喜欢的乐队演唱会看了三遍。 normalized re.sub(r[。], , raw_text) hints extract_age_hints(normalized) print(hints) # [(gen, 90)]第二步代际信号解析。“90后”直接落入1990到1999这个区间每个年份加上0.25分。第三步星座信号解析。“天蝎座”对应阳历10月23日到11月21日这个信息只告诉我们在某个时间段的生日但对年份几乎不敏感。由于样本中没有任何“生日快到了”之类的联动文本所以这个信号只能把候选日期的月份约束住不参与年份加权。第四步语义推断。“高考完那个夏天”把人群锚定在18到20岁。如果按当前年份往前推一个写了这条动态的人现在大概率已经过去若干年。“资深老番爱好者”暗示内容消费至少横跨十年“老番”“二十年前”和头像形成互证说明至少从童年或少年期就开始接触这类内容于是再往前推个三到五年也是合理的。第五步汇总所有候选年份。我用一个脚本把上述候选年份和得分画成直方图分布。因为不能在这里展示图表你也知道静态页面放代码比放图更直观我用控制台输出来表示候选年份: 1991(0.43) 1992(0.43) 1993(0.43) 1994(0.43) 1995(0.25) ... 最大概率区间: 30-33岁 综合置信度: 0.62看到没有结果不是“32岁”而是一个区间。A同学的真实出生年份完全有可能在区间外因为所有文本线索都是间接的。如果有人说“我测出来一定是xx岁”那基本是在骗你。4.3 怎么解读这个结果才不算自欺欺人任何模型输出都只是一个估计我们要学会看边缘情况。以A同学为例如果ta是复读生高考那年可能是20岁而不是18岁如果ta只是喜欢老番但实际年纪很小可能因为家人影响或纯粹喜欢旧画风那整条推断链都会崩。说到底这套流程最有价值的地方不是给出一个精确数字而是让所有推断都显性化每个候选年份都有来源、有权重你能一眼看出是哪些信息支撑了最终结论。把隐藏的假设摆到桌面上比结果本身重要得多。这也是为什么我用“分布”而不是“可能年龄”来描述输出结果。5. 算出来之后呢正确率、边界和更好的聊天方案5.1 我实测里翻过几次车原因都是“信息过时”我最早做过一个实验拿一批身边人授权提供的公开历史动态去跑这套推断流程结果整体误差中位数在4岁左右。唯一一次精准命中是因为对方简介里自己写了出生年份。剩下的实验里失败案例几乎都属于同一种情况动态内容停留在好几年前“刚进大学”这类的描述早就和当前年龄对不上了。这件事也是我把方案改成区间输出的原因。真实世界的信息新鲜度太低同一个标签在不同时期含义完全不同用静态方法去推断动态的人生阶段误差只会越放越大。有人可能想通过记录发布时间差来修正但这又会引入另一个不确定变量所以两头都不讨好。5.2 就算技术准确也不代表应该这么干就算你有能力把准确率提高到80%我也建议你想想社交后果。对方要是发现你在背后用爬虫和正则分析年龄那种“被研究”的感觉往往比被直接问年龄更让人不适。而且很多平台的公开数据虽然技术上能抓到但使用条款并不允许你这么干。比如个人主页的公开字段合法性边界在不同平台差异很大有些允许展示有些禁止任何形式的收集。所以哪怕你只是做学习用途也要在合规框架内操作。我的习惯是只拿完全公开且明确允许下载的内容做实验比如我自己写的帖子、公开语料库、虚构样本而不是拿真实用户当研究对象。5.3 真到了现场把猜年龄当成话题而不是答案回到开头那个聚会场景。我想过如果把这个脚本跑到极致能不能在对方完全不说的前提下得到一个可信区间技术上行但人际关系上不太行。后来我学到一个更好的处理方式不是去“猜对”而是用“猜年龄”来开启对话。比如你可以笑着说“我猜你是95后吧”对方的回应只有三种——惊讶、纠正、笑而不语。无论哪种话题都打开了。如果对方愿意聊会顺带讲自己的经历如果不愿意你也早就把话题引到了一个无伤大雅的玩笑上不至于冷场。这套用公开线索做推断的思路我后来更多用在文本分析和用户画像练习上比如根据匿名开放数据集估算人群结构、做内容推荐策略的模拟等。用正经的数据分析姿势替代“背后算别人”的冲动反而能收获更多。最后分享一个小技巧如果你真的对一个人的年龄感到好奇最好的破冰问题不是“你多大”而是“我猜你是哪一年的错了请我喝奶茶”。问句的落点从“索取隐私”变成了“游戏赌约”对方立刻放松很多。算法可以帮你完成很多事但面对真实的人保持真诚和一点笨拙往往比任何精确推断都有效。