尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python爬虫实战:合规采集QQ空间数据,构建交互式个人关系网
去年有段时间我需要把自己在一个社交平台上的多年互动记录整理成一张可视化的关系图谱。搜了一圈网上的“python爬虫获取关系网”教程发现大部分讲的是怎么模拟登录、怎么遍历好友的好友——也就是去抓取那些你根本无权访问的第三方数据。这种思路放在前几年可能还行但放到现在风险高到不值得碰。于是我换了个思路把项目范围收敛到“只采集我自己的账号登录后可见的数据”用官方页面提供的接口把数据拿下来再做清洗、构图、可视化。整个流程跑完之后我不光拿到了一张能交互查看的关系网还把爬虫工程的接口定位、参数处理、分页策略、数据建模这些环节完整过了一遍。这篇就把全过程写出来如果你也想做类似的个人社交数据分析或者想系统学习爬虫项目从零到一怎么落地这篇文章可以当一份详细的参考资料。1. 关系网数据的合规边界先想清楚能拿什么再动手1.1 三种数据形态每一种的分量都不一样所谓“关系网”在QQ体系里其实可以拆成三种完全不同的数据形态它们的获取难度和法律地位也截然不同第一种是你自己账号直接拥有的数据。比如你自己的好友列表、你空间的访客记录、你发的说说下面谁点了赞、谁留了言。这些数据你本来就拥有访问权限只是通常没有一个方便导出的入口。爬虫在这里扮演的角色其实就是一个“自动化的导出工具”帮你把自己可见的数据整理成结构化文件。这一块是完全可以做的。第二种是半公开的推测性数据。比如你通过共同好友推测出的“谁可能认识谁”或者通过互动频率算出来的“你和谁关系最近”。这类数据是你在第一种数据基础上二次加工出来的结果属于分析产物用于个人用途问题不大。第三种是他人账号的私有数据。比如遍历某个人的好友列表、读取别人空间的访客记录甚至去抓取非公开的聊天关系。这类操作属于典型的越权访问不管披着什么技术外衣都迈不过合规这条红线。网上很多号称“爬取QQ关系网”的教程坑就埋在这里——它们演示的往往是第三种。我最后确定的项目范围是只采集自己账号能看到的页面数据。说直白点就是把自己空间的好友列表、访客记录、说说的点赞和评论这些交互数据抓下来用来画一张“以我为中心”的个人关系网。这既达到了做图谱分析的目的又不触碰别人的数据边界。1.2 技术选型为什么我坚持“接口直连”而不是浏览器自动化很多人在做这类项目时第一反应是上 Selenium 或 Playwright 去模拟浏览器操作。不是说不行而是没必要。原因有三点第一效率差距悬殊。浏览器自动化要加载完整的页面、执行全部JS、渲染各种资源一个请求出去背后是几百KB的HTML和几十个静态文件。而直接调接口拿到的是干净的JSON数据一个响应可能只有几十KB同样的数据量采集时间能差一个数量级。第二稳定性更好。浏览器自动化受页面改版影响很大今天按钮换了个class名脚本就挂了接口层面的字段虽然也会变但变动频率低得多而且字段语义通常更稳定。第三学习价值不同。接口直接调用要求你去抓包、分析参数、理解签名逻辑这套能力才是爬虫技术的核心。浏览器自动化更像是“用技术手段模拟人手操作”对你理解数据流转帮助有限。我最终的选型组合很简单Python requests 做会话管理和请求发送Pandas 做数据清洗NetworkX 做图建模Pyecharts 做可视化。全程不需要重型框架一台普通笔记本就能跑完所有步骤。环境准备就三行命令pip install requests pip install pandas pip install networkx pip install pyechartsrequests 和 pandas 不用多说networkx 是 Python 生态里最常用的图分析库pyecharts 则能输出浏览器里可交互的HTML图。整套技术栈都是轻量级方案方便后续替换成你更熟悉的库。2. 抓包定位接口把浏览器开发者工具当成第一生产力2.1 观察流量找到返回JSON的数据接口整个项目的第一步是搞清楚页面上的数据到底是从哪个接口来的。这一步的核心工具就是浏览器开发者工具的 Network 面板。操作路径很简单打开QQ空间页面按F12进入开发者工具切到 Network 标签页在筛选框里输入 XHRXMLHttpRequest——因为我们要找的是异步请求的数据接口不是页面本身的HTML文档。然后刷新页面或者手动触发一次数据加载动作比如翻到好友列表下一页、打开访客记录弹窗观察新增的请求。这里有个很有效的对比技巧连续翻两页对比多出来的请求。翻页前后都出现的请求往往是框架或统计类的翻页时才出现的请求大概率就是承载数据的主接口。点开这类请求在 Response 标签里看返回内容如果是一大段 JSON 结构里面有类似friends、visitors、like这样的字段那基本就是它了。我这次要采集的数据分散在几个不同的功能区块里所以用了几个小时逐项点击页面功能把每个模块对应的接口都登记下来。整理出来的接口清单大致长这样数据模块请求方式核心返回字段说明好友列表GET好友昵称、备注名、分组分页加载一次大约返回固定数量访客记录GET访客昵称、访问时间按时间倒序带游标分页说说点赞GET点赞者昵称、说说ID、时间需要先拿到每条说说的ID说说评论GET评论者昵称、评论内容、时间和点赞逻辑类似接口的具体域名和路径这里就不放出来了。不是因为什么神秘原因而是这类地址的变动比较频繁而且不同账号拿到的参数格式可能不一样我建议你自己动手抓一次顺便也能锻炼定位接口的能力。要强调的是你抓到的这些接口都是你自己账号登录后才能访问的返回的数据也仅限于你账号可见的范围——这个边界从第一步就要守住。2.2 用 requests.Session 保持会话把请求头做好找到接口之后下一步就是用 requests 复现这些请求。这里最关键的一个动作是让 requests 使用你在浏览器里登录好的身份凭证。具体做法是从浏览器开发者工具里找到当前页面请求的 Cookie复制下来设置到 requests 的请求头里。注意 Cookie 是有时效的而且登录态有触发风控的风险所以实际项目中应该把 Cookie 单独存放方便过期后快速更新。我习惯先把公共请求头整理成一个字典import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://******/, # 实际地址请自己抓包确认 Cookie: 这里填你浏览器里复制的Cookie, }然后创建 Session 对象之后的每个请求都复用它session requests.Session() session.headers.update(HEADERS)为什么要用 Session因为 Session 会自动帮你管理底层连接、维持某些请求参数的一致性而且在连续请求时能减少重复握手的时间消耗。更重要的是你可以在 Session 上统一设置超时时间、重试策略后续代码会清爽很多。这里要特别提醒一个新手常踩的坑有的接口除了 Cookie还需要额外的动态令牌参数。这个参数通常藏在页面的 JavaScript 变量里或者由某个前置请求返回。如果你发现请求返回的状态码正常但业务字段是空的很可能就是漏了这个参数。解决办法是在 Network 面板里搜索该参数名看看它是在哪个请求里第一次出现的顺藤摸瓜就能找到来源。# 一个典型的请求过程先拉取页面拿到动态令牌再请求真正的数据接口 def fetch_friend_list(session, start_page0): # 第一步获取动态令牌示意流程 token get_dynamic_token(session) # 第二步携带令牌请求数据接口 params { page: start_page, token: token, } resp session.get(FRIEND_API_URL, paramsparams, timeout10) return resp.json()这个get_dynamic_token我特意留成了示意函数因为它的实现取决于你抓到接口的具体逻辑。给个方向大部分令牌都是页面脚本里用某个固定算法算出来的你可以在开发者工具的 Sources 面板里搜索令牌的关键字段名把算法还原成 Python 函数即可。3. 参数签名和分页游标采集环节的两块硬骨头3.1 签名参数不是用来防你的是用来区分请求来源的很多社交平台的数据接口都会带一个签名参数。它的原理大同小异客户端把请求参数、时间戳、一部分Cookie值按照约定的算法拼起来算出一个哈希字符串跟着请求一起发给服务器。服务器拿到后用同样的算法算一遍比对结果是否一致就能确认这个请求确实来自它自己的客户端逻辑。理解这一点很重要签名参数本质上不是用来“防爬虫”的而是用来区分请求来源、防止接口被恶意刷请求的。我们写爬虫的时候完全可以用合法的身份凭证正常调用这些接口——前提是采集范围在自己账号可见的数据内并且控制好频率。还原签名算法的过程就像做一道带答案提示的填空题。我在抓包的时候发现同一个参数请求两次的值不一样于是去页面脚本里搜索这个参数名定位到一段JS函数再从函数里逆推Python实现。整个定位过程不涉及破解任何加密通信只是在复现前端页面本来就会执行的逻辑。# 签名参数生成思路示意代码非真实算法 import hashlib import time from urllib.parse import quote def make_sign(params: dict, secret: str) - str: # 1. 将参数按key排序后拼接成字符串 pairs [] for key in sorted(params.keys()): pairs.append(f{key}{quote(str(params[key]))}) base_str .join(pairs) # 2. 混合时间戳和固定字符串 mixed f{base_str}{int(time.time())}{secret} # 3. 取哈希 return hashlib.md5(mixed.encode()).hexdigest()这段代码只是一个思路模板真实场景里你可能遇到 SHA1、HMAC甚至叠加盐值。核心方法论是固定的先去页面脚本里找到签名函数读明白它对参数做了什么变换然后一比一复现成Python。别忘了验证——拿着浏览器里请求的参数原样带入代码算出来的签名值必须和抓包抓到的一致不一致就是算法还没复现准确。3.2 游标分页的逻辑不是页号是“从哪条数据接着拉”社交平台的数据接口很少用传统的page1分页更多是用游标分页。游标这个词听起来玄乎其实就是一个“数据位置标记”。服务器不会告诉你一共多少页而是告诉你这一批数据的最后一条是什么下次你带着这个标记来我给你它后面的数据。这种设计的好处是即使用户数据实时增长也不会出现翻页时数据重复或漏掉的情况。对爬虫来说我们需要做的就三件事第一从第一页响应的固定字段拿到第一个游标值。第二每次请求带上当前游标。第三判断服务器返回的结束标记如果为空或者特定值说明数据拉完了。def fetch_all_visitors(session): cursor results [] while True: params { cursor: cursor, count: 20, } data session.get(VISITOR_API_URL, paramsparams, timeout10).json() # 提取当前批次数据 batch data[data][list] results.extend(batch) # 提取下一批游标 cursor data[data].get(next_cursor, ) # 终止条件游标为空或列表为空 if not cursor or not batch: break # 控制频率避免短时间请求过多 time.sleep(1.5) return results这里最容易被忽略的是终止条件的判断。我一开始只判断了列表是否为空结果因为某次请求返回了一个空列表但游标仍然有值程序就提前退出了丢了不少数据。后来改成“游标为空或者列表为空二者满足其一就结束”问题才解决。建议你在调试阶段打印每一批次的数量和游标值确认数据是连续拉取的再进行全量采集。另外一个常见问题就是死循环。如果服务器在极端情况下一直返回同一个游标程序就会无限跑下去。稳妥的做法是设置一个最大迭代次数比如最多拉500轮到了直接报错退出。控制循环次数这个细节能在很多突发情况下保住你的程序。3.3 数据落地先存文件再想怎么用采集到的数据不要直接丢给后面的分析流程一定要先落盘。我通常的做法是每拉完一个模块就把原始JSON按行写入一个.jsonl文件。这样有几个好处程序中断了已采的数据不丢分析环节有bug了可以重新读文件不用重新爬后续想看某个字段的原始结构直接查文件就行。import json def save_to_jsonl(path: str, records: list): with open(path, a, encodingutf-8) as f: for record in records: f.write(json.dumps(record, ensure_asciiFalse) \n)存储命名也值得讲究一下。我会用“模块名_日期”的格式建文件比如visitors_20250601.jsonl。这样即使采集过程跨了多天也不会把新旧数据混在一起。数据落盘之后采集这一层就算完工了后面所有操作都可以在本地文件上进行不会再打扰线上接口。4. 从JSON到图谱数据清洗与关系图建模4.1 把原始数据转成“关系三元组”从接口拿到的原始JSON是面向页面展示的字段命名和结构都比较“散”。要画关系网首先得把数据转换成统一的“关系”模型。我的定义很粗暴所有关系都可以表示成“谁 - 通过什么行为 - 与谁产生联系”的三元组。举个例子一条访客记录转换后就是(访客A, 访问, 我)带有时间属性。一条说说点赞记录就是(用户B, 点赞, 说说作者)如果再关联到说说内容就多了一个中间节点。评论类似。拿到这些三元组后统计每一对节点之间的交互频次频次就是边的权重。这个权重是后续所有分析的基础比如判断谁是“高频联系人”、哪个社群内的互动更密集全都靠它。# 关系数据模型示例 relation_records [ { source: 用户A, relation: 访问, target: 我, timestamp: 2025-03-01 12:30:00, }, # ... ]这里要强调一个实践习惯在转换成模型之前先花点时间把原始字段完整看一遍不要急着丢掉“多余”字段。比如访客记录里的来源页面、点赞记录里的说说的点赞总数这些字段当时看着没用后面做特征分析时可能就派上大用场。数据是资产清洗是在整理资产不是在消灭资产。4.2 用 Pandas 规整数据去重、补全、统一时间格式关系三元组生成之后只是结构化了还没到能直接建模的质量。中间至少要做三道规整第一道是去重。同一个用户可能同时出现在访客列表和点赞列表里需要合并实体。用 Pandas 的drop_duplicates按关键列去重即可。这里要注意去重前先定义好“什么算重复”是昵称相同就算还是昵称加ID两个字段都相同才算真实环境里昵称可能重复所以如果原始数据有用户ID去重应该以ID为准。第二道是补全空值。点赞记录里偶尔会缺失时间字段访客记录里偶尔会缺昵称。处理原则是能推断的尽量推断比如缺失时间可以按页面返回的排序位置猜一个批次时间实在无法补全的就标记为未知而不是直接删行——删除会破坏统计口径。第三道是统一时间格式。接口返回的时间格式五花八门有的是时间戳有的是2025-03-01 12:30:00。统一转成标准的YYYY-MM-DD HH:MM:SS后面做时间序列分析才能顺畅。import pandas as pd df pd.DataFrame(relation_records) # 统一时间格式 df[timestamp] pd.to_datetime(df[timestamp], formatmixed) # 按用户ID去重假设有uid字段 df df.drop_duplicates(subset[uid, relation, target]) # 空值处理 df[nickname] df[nickname].fillna(未知用户)做完这三步数据基本就是干净的了。我习惯把清洗后的结果输出成一个relations.csv一行一条关系记录作为所有后续分析的标准输入。4.3 用 NetworkX 建图节点、边、权重一个都不能少NetworkX 的使用逻辑非常直观添加节点、添加边、设置属性。关键是把问题定义清楚——你要画的关系网节点代表什么边代表什么权重怎么计算。在我这个项目里节点就是所有和我产生过交互的用户外加我自己。边表示“两个用户之间存在互动”权重是互动次数。这里的互动可以是A访问了我也可以是我和A在同一个帖子里有过评论往来多种互动行为汇总成一个总频次。import networkx as nx G nx.Graph() # 用Pandas清洗好的数据构建图 for _, row in df.iterrows(): u, v row[uid], row[target] relation row[relation] if not G.has_edge(u, v): G.add_edge(u, v, weight0) G[u][v][weight] 1构建完图之后就可以做很多有意思的分析了。最基础的是度中心性——统计每个节点的连接数连接越多说明这个人在关系网中越活跃。也可以用社区发现算法把联系紧密的节点自动聚成一个个小组有时候能还原出“同事圈”“同学圈”“家人圈”。# 计算连接数最高的前10个节点 importance sorted(G.degree(weightweight), keylambda x: x[1], reverseTrue) for node, degree in importance[:10]: print(node, degree)这里有个容易犯的错degree()默认只按邻居数量算不会把边的权重算进去。如果两个用户互动了100次和互动了1次在图结构上都是“一条边”但关系强度天差地别。所以要用weightweight参数让权重参与计算。5. 把图谱渲染成可交互页面以及还能往下挖什么5.1 用 Pyecharts 输出可交互的关系网页面图建模完成之后最后一步就是把图变成看得见的可视化页面。Pyecharts 的 Graph 类型专门干这事支持节点拖拽、悬停查看详情、缩放等交互功能。from pyecharts import options as opts from pyecharts.charts import Graph nodes [] for node in G.nodes(): # 节点大小映射到连接数 nodes.append(opts.GraphNode( namenode, symbol_sizemin(10 G.degree(node) * 2, 60), )) links [] for u, v in G.edges(): links.append(opts.GraphLink( sourceu, targetv, valueG[u][v][weight], )) graph Graph() graph.add(, nodes, links, repulsion500, edge_symbol[circle, arrow]) graph.set_global_opts(title_optsopts.TitleOpts(title个人关系网可视化)) graph.render(relationship_graph.html)可视化这一步千万别只是把图画出来就看一眼要带着问题去看。比如图里有没有明显的聚簇有没有一些节点明明连接数不高却连接了多个互不相连的圈子——这种节点往往是关键的“桥梁人物”。我在实际数据里就看到过几个这样的节点平时互动不多但起着串联两个社群的作用。交互页面的输出格式是HTML双击就能在浏览器打开发给别人看也不需要额外装软件。如果想要更炫酷的3D效果可以试试 Gephi 或者 ECharts GL但作为个人项目Pyecharts 的交互性已经完全够用了。5.2 时间维度分析和社群发现关系网不只是“一张网”关系网如果只有一版静态图看完其实有点浪费。数据里还藏着时间维度画完静态图之后我用两个方向做了延展。第一个方向时间切片。把互动数据按月切分每个月单独画一张关系网连起来看就能发现哪些人是从始至终都在的哪些人是某段时间活跃后来消失的。这个视角特别适合复盘自己的社交活跃周期。实现也不复杂Pandas 里按月份groupby然后对每个月的数据单独建图、单独出图。第二个方向社群发现。NetworkX 内置了贪心模块度最大化算法用起来非常省事from networkx.algorithms.community import greedy_modularity_communities communities greedy_modularity_communities(G) for i, community in enumerate(communities): members [node for node in community] print(f社区{i 1}: {len(members)}人, 成员示例: {members[:5]})这个算法会把联系紧密的节点自动分组。我跑出来的结果挺有意思基本还原出了几个生活圈子连我自己都没想到某些互动不多的人会被归到同一个社区里。做社群发现时建议多跑几次因为贪心算法的初始顺序会影响结果得到的是一个近似解。可以固定随机种子保证结果可复现。5.3 数据与代码解耦这个习惯帮我省了无数时间整个项目做完回过头来看最值得说的反而是一个工程习惯数据与代码解耦。当时的做法很简单每个采集模块产出的原始JSONL文件、清洗后的CSV文件都放在独立的data/目录里分析脚本只负责读文件不直接依赖爬虫代码的中间变量。这意味着我可以今天爬一批数据、明天写分析代码、后天重新可视化三个环节完全解耦互不干扰。这个习惯在二次迭代时帮了大忙。后来接口改了字段名我只需要改采集模块清洗和分析代码完全不用动。如果当初把所有步骤塞在一个脚本里字段变动可能要牵连整条链路。给接口留出容错空间给数据留出稳定的中间格式是能让你少加班的实在经验。最后再分享一个小技巧采集到的数据定期做一次本地备份不要只留一份。我有一次清理临时文件时手滑删掉了采集了两小时的原始数据重新跑一遍浪费了大半天。从那以后所有采集数据都会同步一份到备份目录这种代价极低的习惯能在关键时刻给你兜底。
RELATED

相关推荐

从符文到数学:教育游戏如何用规则设计点燃孩子的学习内驱力

从符文到数学:教育游戏如何用规则设计点燃孩子的学习内驱力

前天整理项目文件,翻到 Numera Quest: Kingdom of Math Runes 的第一个可玩原型。这个以数学为内核、用符文做表意系统的教育游戏,最初不过是我对“孩子看见数字就头大”这件事的一次反抗。它现在依然算不上什么爆款,但从立项、开发再到上线三…

📅 2026/10/14 9:00:35
LiteLLM统一接入多模型:从SDK到代理网关的工程实践

LiteLLM统一接入多模型:从SDK到代理网关的工程实践

做LLM应用最烦的一件事,不是模型效果不够好,而是“接模型”这件事本身太零碎。前阵子一个朋友跟我聊他的AI助手项目:默认对话想用一个模型,复杂推理想切到另一个平台的长上下文模型,批量总结又要走便宜一点的模型。听起…

📅 2026/10/14 9:00:35
AI辅助App开发全流程:从想法到上架的实战指南

AI辅助App开发全流程:从想法到上架的实战指南

1. 从想法到上架:AI辅助App开发的全景拆解这两年我身边不少朋友都在问同一个问题:现在AI这么强,是不是一个人就能把一款App从想法做到上架?我的回答一直是——能,但前提是你得搞清楚AI到底能帮你做什么、不能帮你做什么…

📅 2026/10/14 9:00:35
MORE NEWS

更多资讯

📰

智能体记忆系统横评与治理

智能体记忆系统横评与治理 生成式人工智能正从单轮问答迈向具备长期交互能力的自主智能体。在企业级落地中,跨会话的偏好沉淀、上下文召回与数据治理,直接决定 Agent 的任务完成率与合规边界。 本文聚焦以下四个核心问题: 主流智能体记忆系…

📰

Hermes agent 贡献指南:从环境搭建到 PR 合并的完整实操

1. 从零理解 Hermes agent 的贡献体系1.1 这个项目到底在解决什么问题第一次接触 Hermes agent 的人,大概率会经历一个相同的困惑:文档看了一圈,知道它能做任务编排、能接工具、能跑多轮对话,但真到了想给它提个 PR 或者补个功能的…

📰

WPF中ObservableCollection排序的正确姿势:ICollectionView实战指南

1. 项目概述:为什么WPF里对ObservableCollection排序会让人抓狂? 在WPF开发中, ObservableCollection 是绝大多数人绑定列表数据的默认选择——它自带通知机制,UI能自动响应增删改。但一旦你尝试给它加个“按姓名升序”或“按时间…

📰

SAR点目标指标计算:sinc函数到分辨率、PSLR与ISLR的工程实践

简介:面向SAR成像、雷达成像与遥感信号处理学习者的一份MATLAB配套资料包,聚焦sinc函数这一理想点扩散函数,系统梳理分辨率、峰值旁瓣比(PSLR)、积分旁瓣比(ISLR)三个核心点目标聚焦指标。压缩包…

📰

课堂行为识别系统实战拆解:YOLO检测、行为分类与评价规则

简介:一份基于深度学习的学生课堂行为识别评价综合系统源码,属于导师指导并通过评审的毕业设计项目,评审得分98分,适合计算机相关专业正在准备毕业设计的学生,也适合需要完整项目实战练习的深度学习入门与进阶学习者。…

📰

【AI大模型进阶】微调中的过拟合表现:训练集 loss 降了,测试集降了吗?

【AI大模型进阶】微调中的过拟合表现:训练集 loss 降了,测试集降了吗? 集 loss 降了,测试集降了吗? 写在前面 大模型微调不管是全参数微调、LoRA 微调、QLoRA 低秩微调,大家最直观的监控指标就是损失 loss。很多人训练的时候看到训练 loss 一路持续下降,就默认模型效…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬