当前位置:首页>python>Python实战:用爬虫和数据,量化还原1.2亿人的"断亲潮"

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

  • 2026-09-08 10:53:33
Python实战:用爬虫和数据,量化还原1.2亿人的"断亲潮"

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实战作业。

数据之外,我在除夕夜,还是给我爸打了个电话。

他说:"崽仔,回来啦?"

我说:"回了,明天到。"

那头沉默了两秒,说:"好。我叫你妈杀鸡。"

我觉得,数据分析得再多——

都比不上那一句"好,我叫你妈杀鸡"。


本文代码和数据方法仅供研究参考,不做商业用途。

最新文章

随机文章