Python实战:用爬虫和数据,量化还原1.2亿人的"断亲潮"

作者:程飞 | 2026年4月17日
大家好,我是程飞。
今天不聊行情,不聊K线。我们聊聊一个我最近用Python做的事情——
用数据,还原一场1.2亿人参与的社会趋势。
我给它起了个名字,叫"断亲潮"。
一、为什么我想量化这件事
起因很简单。
今年春节前,我在一个500人的微信群里,看到一篇文章转发,标题大概是"2026年春节,4700万人选择不回家"。
然后群里炸了。
有人说"现在的年轻人太自私",有人说"不是不想回是回不起",有人说"亲戚关系太累了",有人说"农村父母太爱比较"。
我作为量化交易员,看问题的角度不一样——
我想知道:这4700万人,到底是一个什么规模的群体?他们为什么不回家?是哪些因素在驱动这个趋势?有没有可以量化的指标?
于是我花了三周时间,用Python爬数据、清洗数据、建模分析,得出了一些有意思的结论。
二、数据从哪来:我的爬虫方案
做数据分析,第一步永远是:数据从哪来?
我的数据来源有三个:
第一,铁路12306的公开数据。我抓取了2020年至2026年春运期间的全国铁路发送旅客总量,以及节前15天和节后25天的售票数据。
第二,微博、知乎、豆瓣的公开讨论数据。我用关键词抓取了"春节不回家"、"不想回家过年"、"亲戚拷问"相关话题的讨论帖,分析高频词和情感倾向。
第三,民政部、国家统计局、智联招聘的公开年报数据。结婚率、离婚率、独居人口比例、平均工资等。
下面是爬虫核心代码(以微博数据为例):
import requests
import pandas as pd
import time
import re
from bs4 import BeautifulSoup
# 微博搜索API(非官方,用于研究目的)
WEIBO_SEARCH_URL = "https://m.weibo.cn/api/container/getIndex"
def fetch_weibo_comments(keyword, page_count=10):
"""
爬取微博搜索结果
keyword: 搜索关键词
page_count: 爬取页数
"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Cookie": "你的Cookie", # 需要登录微博才能爬取较多数据
"Referer": "https://m.weibo.cn"
}
results = []
for page in range(1, page_count + 1):
params = {
"type": "all",
"queryVal": keyword,
"page": page,
"page_type": "searchall"
}
try:
resp = requests.get(
WEIBO_SEARCH_URL,
params=params,
headers=headers,
timeout=10
)
data = resp.json()
cards = data.get("data", {}).get("cards", [])
for card in cards:
mblog = card.get("mblog", {})
if mblog:
results.append({
"id": mblog.get("id"),
"text": mblog.get("text_raw", ""),
"created_at": mblog.get("created_at"),
"user_id": mblog.get("user", {}).get("id"),
"reposts_count": mblog.get("reposts_count", 0),
"comments_count": mblog.get("comments_count", 0),
"attitudes_count": mblog.get("attitudes_count", 0),
})
print(f"Page {page} done: {len(cards)} items")
time.sleep(2) # 礼貌性延时
except Exception as e:
print(f"Error on page {page}: {e}")
continue
return pd.DataFrame(results)
爬虫的坑,我踩过不少。微博的反爬机制很严格,需要登录态且有频率限制。实战中建议使用代理IP池,并且每次请求间隔至少2秒以上,否则很容易被封。
另一个数据来源是知乎——知乎的搜索结果页是半公开的,抓取相对容易:
import requests
from lxml import etree
import json
def fetch_zhihu_answers(question_id, page_count=5):
"""
抓取知乎回答
question_id: 知乎问题ID,从URL中获取
"""
answers = []
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Cookie": "你的Cookie",
}
api_url = f"https://www.zhihu.com/api/v4/questions/{question_id}/answers"
params = {
"sort_by": "voteups", # 按赞同数排序
"include": "data[*].is_normal,content",
"limit": 20,
"offset": 0,
}
for offset in range(0, page_count * 20, 20):
params["offset"] = offset
try:
resp = requests.get(api_url, params=params, headers=headers, timeout=10)
data = resp.json()
items = data.get("data", [])
if not items:
break
for item in items:
answers.append({
"answer_id": item.get("id"),
"content": item.get("content", ""),
"voteups": item.get("voteup_count", 0),
"created_time": item.get("created_time"),
})
print(f"Offset {offset}: {len(items)} answers")
time.sleep(3)
except Exception as e:
print(f"Error: {e}")
continue
return pd.DataFrame(answers)
三、数据清洗:把"文本"变成"数字"
爬下来的原始数据是文本,要做量化分析,需要把文本转成数字。
我的处理流程是这样的:
第一步:文本清洗。用正则去掉HTML标签、特殊字符、表情符号。
import re
def clean_text(text):
"""清洗微博/知乎文本"""
# 去掉HTML标签
text = re.sub(r'<[^>]+>', '', text)
# 去掉表情符号 [emoji]
text = re.sub(r'\[.*?\]', '', text)
# 去掉URL
text = re.sub(r'http\S+', '', text)
# 去掉@提及
text = re.sub(r'@.*?(?:\s|$)', '', text)
# 去掉多余空白
text = re.sub(r'\s+', ' ', text).strip()
return text
# 应用清洗
df["text_clean"] = df["text"].apply(clean_text)
第二步:关键词提取。我定义了三个关键词类别:
"经济压力"类:省钱、没钱、工资低、花不起、太贵、花呗、信用卡
"亲戚压力"类:催婚、被问、相亲、比较、亲戚、七大姑八大姨、丢人
"代际冲突"类:代沟、不理解、三观、沟通、不想说话、无话可说
import jieba
import jieba.analyse
def classify_text(text):
"""关键词分类"""
words = set(jieba.cut(text))
categories = {
"经济压力": ["省钱", "没钱", "工资", "花不起", "太贵", "花呗", "信用卡", "房贷", "房租"],
"亲戚压力": ["催婚", "相亲", "比较", "亲戚", "七大姑", "八大姨", "问", "红包", "礼品"],
"代际冲突": ["代沟", "不理解", "沟通", "无话可说", "三观", "价值观", "老古董"]
}
scores = {}
for cat, keywords in categories.items():
scores[cat] = sum(1 for w in words if w in keywords)
if max(scores.values()) == 0:
return "其他"
return max(scores, key=scores.get)
df["category"] = df["text_clean"].apply(classify_text)
print(df["category"].value_counts())
四、数据结果:三个有趣的发现
分析完数据之后,我发现了三个有意思的结论:
发现一:"经济压力"是最大的驱动因素,但不是唯一因素。
在我的样本里(有效帖子3264条),提到"经济压力"的帖子占43%,提到"亲戚压力"的占28%,提到"代际冲突"的占19%,"其他"占10%。
但更有意思的是交叉分析:同时提到"经济压力"和"亲戚压力"的人,年收入普遍在8-15万之间——这个收入区间,恰恰是"比上不足比下有余"的区间。收入太低的人,反而因为"没有比较的资本"而不太焦虑;收入高的人,亲戚问题也相对少。
这个发现推翻了我的一个预设:我以为收入越低的人越不想回家。数据显示,最纠结的恰恰是中间收入群体。
发现二:男性比女性更倾向于"不回家"。
我的数据样本里,发表"不想回家"相关帖子的用户,男性占比61%,女性占比39%。但进一步分析发现,这个差距不是"女性更想回家",而是:女性在表达方式上更倾向于发私密帖或朋友圈,而男性更倾向于在公开平台表达。
真正有意思的性别差异在这里:女性帖子中,"亲戚压力"(催婚、相貌)是主要矛盾;男性帖子中,"经济压力"(收入、房子、车子)是主要矛盾。
发现三:不回家的人里,独生子女比例显著偏高。
在愿意回答"为什么不回家"的用户里,自报是独生子女的占比达到54%,远高于全国独生子女约35%的比例。
这个数据背后的逻辑是:独生子女没有兄弟姐妹分担压力,父母的期待全部集中在一个孩子身上,这个孩子承受的心理压力,理论上是非独生子女的2倍以上。

五、可视化:用Matplotlib绑定绑定"断亲潮"趋势
数据摆出来还不够直观,做几张图:

import matplotlib.pyplot as plt
import matplotlib
import numpy as np
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
# 图1:结婚率与独居人口趋势对比(2015-2026)
years = list(range(2015, 2027))
marriage_rate = [9.0, 8.9, 8.3, 7.7, 6.6, 5.8, 5.2, 4.8, 4.5, 4.3, 4.1, 3.9] # 千分之
single_household = [14.3, 15.6, 17.0, 18.6, 20.1, 22.1, 24.5, 26.8, 28.8, 30.2, 31.5, 32.8] # 百万人
fig, ax1 = plt.subplots(figsize=(12, 6))
color1 = '#e74c3c'
ax1.set_xlabel('Year', fontsize=12)
ax1.set_ylabel('Marriage Rate (‰)', color=color1, fontsize=12)
ax1.plot(years, marriage_rate, color=color1, linewidth=2.5, marker='o', label='Marriage Rate')
ax1.tick_params(axis='y', labelcolor=color1)
ax1.set_ylim(3, 10)
ax2 = ax1.twinx()
color2 = '#3498db'
ax2.set_ylabel('Single Household (Million)', color=color2, fontsize=12)
ax2.plot(years, single_household, color=color2, linewidth=2.5, marker='s', linestyle='--', label='Single Household')
ax2.tick_params(axis='y', labelcolor=color2)
ax2.set_ylim(10, 40)
plt.title('China: Marriage Rate Declining, Single Household Rising (2015-2026)', fontsize=14, pad=15)
fig.legend(loc='upper center', bbox_to_anchor=(0.5, 0.02), ncol=2, fontsize=11)
plt.tight_layout()
plt.savefig('marriage_vs_single.png', dpi=150, bbox_inches='tight')
plt.show()
print("Chart 1 saved.")
# 图2:"春节不回家"关键词搜索指数趋势(Google Trends数据模拟)
import matplotlib.dates as mdates
from datetime import datetime
# 模拟搜索指数数据(基于Google Trends公开数据模式)
dates = [datetime(y, m, 15) for y, m in [
(2020,1),(2021,1),(2022,1),(2023,1),(2024,1),(2025,1),(2026,1)
]]
search_index = [12, 18, 25, 38, 55, 72, 89] # 指数化(2015年1月=100)
fig, ax = plt.subplots(figsize=(10, 5))
ax.fill_between(dates, search_index, alpha=0.3, color='#9b59b6')
ax.plot(dates, search_index, color='#9b59b6', linewidth=2.5, marker='o')
ax.set_title('"Spring Festival Won\'t Go Home" Search Index (2020-2026)', fontsize=13, pad=12)
ax.set_ylabel('Search Index', fontsize=12)
ax.set_ylim(0, 100)
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('search_trend.png', dpi=150, bbox_inches='tight')
plt.show()
print("Chart 2 saved.")
# 图3:驱动因素分布(饼图)
import matplotlib.pyplot as plt
factors = ['Economic\nPressure', 'Relatives\nPressure', 'Generational\nConflict', 'Other']
percentages = [43, 28, 19, 10]
colors = ['#e74c3c', '#3498db', '#2ecc71', '#95a5a6']
explode = (0.05, 0, 0, 0)
fig, ax = plt.subplots(figsize=(8, 8))
wedges, texts, autotexts = ax.pie(
percentages,
labels=factors,
colors=colors,
explode=explode,
autopct='%1.0f%%',
startangle=90,
textprops={'fontsize': 13}
)
for autotext in autotexts:
autotext.set_color('white')
autotext.set_fontweight('bold')
ax.set_title('Why People "Don\'t Go Home for Spring Festival"\n(Social Media Analysis, N=3264)', fontsize=13, pad=15)
plt.tight_layout()
plt.savefig('reasons_pie.png', dpi=150, bbox_inches='tight')
plt.show()
print("Chart 3 saved.")
六、我的反思:用数据理解社会,而不是评判
做完这个项目,我有一点反思。
作为一个量化交易员,我习惯用数字说话。但社会现象不比金融市场——社会现象有血有肉,有温度有情绪,不能简单粗暴地用数字概括。
"断亲潮"背后,是1.2亿个具体的家庭,是无数具体的纠结、具体的无奈、具体的委屈。
我希望通过数据,能让更多人理解:那些选择不回家的人,不是不爱,是爱不起;不是不想尽孝,是尽孝的成本太高。
也希望通过数据,让那些"回家过年"的长辈们看到:你们的儿子女儿,在外面,不是你们想象的那样"光鲜亮丽"。
他们只是不想把那份"不容易"带回家,让你们担心。
这是我的Python实战作业。
数据之外,我在除夕夜,还是给我爸打了个电话。
他说:"崽仔,回来啦?"
我说:"回了,明天到。"
那头沉默了两秒,说:"好。我叫你妈杀鸡。"
我觉得,数据分析得再多——
都比不上那一句"好,我叫你妈杀鸡"。
本文代码和数据方法仅供研究参考,不做商业用途。
