☰
微博数据爬虫全链路:Cookie管理到百万级数据落库
2026/10/21 13:57:25 网站建设 项目流程

简介:面向Python爬虫初学者的完整微博数据采集项目,演示如何用requests库发送HTTP请求、BeautifulSoup解析页面并提取用户与博文信息,适合想系统掌握爬虫流程、反爬策略和数据存储的开发者。资源共77个文件,约106KB,以44个py脚本、14个md说明、11个html页面为主,另有yml、json、conf等配置依赖,目录按spider、parser、downloader、writer等模块拆分,结构清晰。已有1187人学习下载。项目从基础语法、HTTP协议原理讲起,覆盖动态User-Agent、代理IP、验证码处理等反爬思路,并结合CSV、JSON或SQLite等存储方案;附带的说明文档对Cookie配置、异常处理、Scrapy扩展也有提及。读者可通过阅读源码与配套文档,理解请求封装、页面解析、数据落库和异常兜底的完整链路;对这种小而完整的项目类型,适合用于课程设计或内部数据采集任务的实际起步模板。

1. 微博数据爬虫:从 Cookie 登录到百万级数据落库的完整链路

前阵子帮一个调研课题抓某话题下近三个月的微博数据,第一反应是找个现成的爬虫改改。翻了一圈发现,大部分项目要么依赖 Selenium 挂浏览器,要么登录态三天两头失效。最后用的这套方案很直接:用 Python 的 requests 请求微博移动端查询接口,按关键词和日期范围抓公开数据,HTML 回来以后按字段拆开,落到 CSV 和 MySQL,全程不用渲染页面,也不碰 JS 逆向。项目代码就是围绕这样一条链路组织的:Cookie 管理、请求调度、HTML 解析、结果存储,各占一层。适合要做舆情分析、热词趋势和样本采集的开发者,也适合想弄懂小型爬虫工程该有哪些模块的人,照着跑通一遍,基本就理解了这类爬虫通用的骨架。

2. 项目架构与设计:Cookie 管理、任务调度与解析存储的基础分工

2.1 目录结构:模块划分与职责边界

项目代码量不算大,但模块边界很清晰,没有把所有逻辑堆在一个 main.py 里。整个工程目录大致长这样:

weibo_spider/ ├── main.py # 命令行入口,接收参数、组装任务 ├── config.py # 全局配置:请求间隔、线程数、存储路径 ├── cookie_manager.py # Cookie 的读取、失效检测与刷新提示 ├── scheduler.py # 任务调度:把关键词+日期范围拆成抓取单元 ├── downloader.py # 请求下载层:构造 URL、发起请求、重试 ├── parser.py # 解析层:把 HTML/JSON 转成结构化字段 ├── storage.py # 存储层:写入 CSV、JSON 或 MySQL ├── utils.py # 时间格式化、日志输出、字符串清理 └── requirements.txt

每个文件承担的角色很单一。main.py 只做入口调度,拿到命令行参数之后丢给 scheduler,不直接碰请求和解析;downloader 只负责发请求和收响应,不管返回的内容是什么格式;parser 只处理文本到字段的转换,不关心数据最终落到哪里。这样分层的直接收益是替换成本低——你不想用 CSV 存数据,想直接写 MySQL,改 storage.py 一个文件就够了,其他模块根本不用动。另一个收益是问题好定位:抓过来的页面解析不出来,先去 parser 层查,而不是在下载层里翻代码。

这里有个常见误解:项目跑不起来就怪接口改了,实际上大部分情况是 Cookie 失效,这正是 Cookie 被拆成独立模块的原因。登录状态是这类爬虫最容易翻车的点,给它独立入口,后续换成扫码登录或者账号密码登录,都不影响主流程。config.py 单独存在也是同一个逻辑,请求间隔、线程数、存储路径这些高频调整项集中放在一起,每次跑数据只改配置,不动代码。

2.2 任务调度:关键词与日期范围如何拆分成抓取单元

scheduler 不直接去请求接口,而是先做任务拆分。它拿到的原始输入是关键词和日期范围,如果把整个范围当成一个黑盒整体去抓,中间任意一次断网或者被限制,之前的数据就白抓了,重新来一遍代价很高。所以项目把任务拆成“某关键词在某一天的第几页”这种最小单元,按天去推进。核心逻辑大概是这样的:

# scheduler.py 核心逻辑:把关键词+时间范围拆成天粒度任务 from datetime import datetime, timedelta def build_tasks(keyword: str, start_date: str, end_date: str): tasks = [] start = datetime.strptime(start_date, "%Y-%m-%d") end = datetime.strptime(end_date, "%Y-%m-%d") while start <= end: tasks.append({ "keyword": keyword, "date": start.strftime("%Y-%m-%d"), "page": 1, "finished": False, }) start += timedelta(days=1) return tasks

这里 keyword 是搜索关键词,start_date 和 end_date 是字符串形式的日期范围。每次循环生成一个字典,代表“某关键词在某一天的第 1 页”这个抓取单元。finished 字段先置为 False,后续在断点续抓的时候会被改成 True,标记这一天已经抓完。

为什么按天拆而不是在构造 URL 时直接传一个大的时间范围?两个现实考虑。第一,接口对时间范围的处理经常不是精确到天的,按天拆能保证单次请求的返回结果相对完整;第二,断点续抓方便,某一天抓失败,重试时只需要重跑那一天,不用整个任务从头再来。你也可以在这个函数里继续扩展,比如加一个 hours 参数把天拆成小时粒度,适合对高峰时段的数据做更细的切片,代价是任务数量会成倍增加,限速策略需要同步调整。

2.3 下载层与 Cookie 层:一个请求从被构造到返回的完整链路

任务拆好之后,进入 downloader 的领地。downloader 要做的事情很单一:拿到一个任务单元,构造出对应的请求 URL,带上配置好的请求头和 Cookie,发出去,再把响应内容返回。这里有一个关键实现细节:微博移动端的接口,有的返回 JSON,有的返回 HTML,所以 downloader 在返回内容之前要先判断响应类型。常见做法是看 Content-Type 头,如果是 JSON 就调用 resp.json(),否则就保留 resp.text:

# downloader.py 基础请求逻辑 def fetch(task: dict, cookie: str): url = build_search_url(task["keyword"], task["page"]) headers = { "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)", "Cookie": cookie, "Referer": "https://m.weibo.cn/", } # 控制请求频率,避免触发风控 time.sleep(random.uniform(0.5, 1.5)) resp = requests.get(url, headers=headers, timeout=10) if resp.status_code != 200: return None # 非 200 一律不走解析,进入重试队列 if "application/json" in resp.headers.get("Content-Type", ""): return resp.json() return resp.text

build_search_url 放在 utils.py 里,按关键词和页码拼接参数,具体实现会在第 3 章展开。这里 time.sleep 不是可有可无的,即使任务拆得很细,单位时间内的请求频率还是会被服务端记录。随机间隔比固定间隔更接近真实用户行为,项目默认在 0.5 到 1.5 秒之间浮动。如果你只抓几万条数据,这个频率问题不大;要抓更大的量,需要把这个间隔放到 config 里,让不同场景可以单独调。

Cookie 的读取和失效检测由 cookie_manager 负责。项目约定把当前有效的 Cookie 字符串复制到 cookie.txt,程序启动时读入内存。每次请求返回后,如果发现状态码是 401,或者响应内容里出现了登录跳转标志,就认为 Cookie 已经过期:

# cookie_manager.py Cookie 失效检测 COOKIE_FILE = "cookie.txt" def load_cookie(): with open(COOKIE_FILE, "r", encoding="utf-8") as f: return f.read().strip() def is_expired(status_code: int, content: str) -> bool: return status_code == 401 or "login" in content[:300].lower()

手动复制 Cookie 虽然土,但稳定。用账号密码登录的方案要处理验证码和短信验证,反而更容易被风控盯上。项目默认保留手动 Cookie 方案,同时把 is_expired 判断做成了独立函数,方便后续接扫码登录或者更自动化的登录态刷新——这是为二次开发留的口子,后面第 6 章会提到怎么接。

2.4 并发调度:线程池消费任务队列

爬虫工程的并发和普通业务并发思路不一样。普通业务并发追求每个请求都尽量快,爬虫并发追求的是在单位时间内稳定地完成请求,避免触发风控。项目默认是单线程顺序执行的,这是保守但也稳妥的起步方式;当任务量大了以后,可以在 main.py 里用线程池把任务分发出去:

# main.py 中开启并发消费任务 from concurrent.futures import ThreadPoolExecutor, as_completed MAX_WORKERS = 4 def run_tasks(tasks, cookie): with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool: futures = [pool.submit(process_task, task, cookie) for task in tasks] for future in as_completed(futures): try: future.result() except Exception as e: log_error(e)

process_task 必须是纯函数,它只接收一个任务字典和 Cookie,内部完成请求、解析、存储,最后返回当天是否完成的状态。因为每个任务之间没有共享状态,所以可以安全地并发。全局的请求频率要小心:每个线程各自 sleep,整体频率等于单线程的 MAX_WORKERS 倍,这也是为什么下载间隔参数在 config.py 里单独抽出来的原因。我一般先把线程数定在 4,间隔 1 秒,跑十几分钟日志没有异常再往上升,不靠感觉拍脑袋。

3. 核心模块拆解:搜索 URL 构造、JSON 解析与双通道存储

3.1 搜索 URL 构造:关键词编码与分页参数

从 downloader 里调用的 build_search_url 开始看。移动端搜索接口最关键的参数是 containerid,它决定了搜索类型:type=1 对应综合搜索,type=2 是用户搜索,type=3 是视频搜索。项目默认用综合搜索,也就是 type=1:

# utils.py 构造搜索 URL from urllib.parse import urlencode def build_search_url(keyword: str, page: int, search_type: int = 1): params = { "containerid": f"100103type={search_type}&q={keyword}", "page_type": "searchall", "page": page, } return "https://m.weibo.cn/api/container/getIndex?" + urlencode(params)

urlencode 来自 urllib.parse,它会把中文关键词转成百分号编码,也把 & 和 = 这些特殊字符正确处理掉。如果你搜的关键词里带了引号或者逗号,没有这层编码,请求地址很可能直接断掉或者返回空结果。page 参数控制翻页,搜索接口一页大概能返回二十条左右的博文卡片,具体数量要看当天的数据量,不能靠页码反推总量。

如果你想搜某个特定用户的内容而不是全站关键词,可以在这个函数里加 if 分支,把 containerid 换成用户详情页的格式。同一个 downloader 不用改,只是 URL 构造函数里多了分支。这里也是接口最可能变化的地方,如果某天请求返回的数据结构和预期完全不同,优先抓包看接口参数,而不是去解析层折腾。

3.2 JSON 响应解析:从卡片里抠出九个标准字段

移动端搜索接口的响应是一个嵌套 JSON,主体在 data 字段下面。data.cards 是卡片数组,每张卡片代表一个博文展示单元;卡片里的 mblog 对象,才是真正的微博数据。项目在 parser.py 里用一层函数做这个转换:

# parser.py 卡片结构转扁平字段 def parse_card(card_obj: dict) -> dict: mblog = card_obj.get("mblog", {}) user = mblog.get("user", {}) created_at = parse_time(mblog.get("created_at", "")) return { "weibo_id": mblog.get("id"), "user_id": user.get("id"), "nickname": user.get("screen_name"), "content": mblog.get("text", ""), "created_at": created_at, "reposts_count": mblog.get("reposts_count", 0), "comments_count": mblog.get("comments_count", 0), "attitudes_count": mblog.get("attitudes_count", 0), "source": mblog.get("source", ""), }

九个字段里,weibo_id 是这条微博的唯一标识,后续去重靠的就是它;user_id 是发博用户的数字 ID,nickname 是显示名;content 取自 text 字段,注意这里带回 HTML 标签,包括链接和表情标签,存储前需要清洗;created_at 是发布时间;reposts_count、comments_count、attitudes_count 分别对应转发数、评论数和点赞数;source 是发布设备,类似客户端名称或网页版标识的包装。

有一个容易忽略的点:text 字段里既有纯文本,也有包在 HTML 标签里的链接和表情。清洗逻辑可以作为 parse_card 的后置步骤,比如用正则把标签剥掉,同时把转义字符还原成可读文本。我不建议把这些逻辑直接塞进 parse_card,因为字段拆分应该保持纯净,清洗是独立职责。这符合前面架构分层的思路,后面做二次开发时也更容易定位问题。

3.3 时间解析:相对时间与标准时间的换算

微博返回的 created_at 是相对时间,不是固定格式的时间戳。常见的几种形态是“刚刚”“5分钟前”“今天 08:30”“昨天 23:50”“10-08”这类短格式,偶尔也会有完整日期。直接把这串东西丢进存储,后面做趋势分析时按时间排序就是一场灾难。项目里针对这段的解析逻辑是逐级匹配:

# utils.py 相对时间转标准时间 import re from datetime import datetime, timedelta def parse_time(raw: str) -> str: if not raw: return "" raw = raw.strip() now = datetime.now() if raw == "刚刚": return now.strftime("%Y-%m-%d %H:%M:%S") if "分钟前" in raw: minutes = int(re.search(r"\d+", raw).group()) return (now - timedelta(minutes=minutes)).strftime("%Y-%m-%d %H:%M:%S") if "今天" in raw: return now.strftime("%Y-%m-%d") + " " + raw.replace("今天 ", "") if "昨天" in raw: yesterday = now - timedelta(days=1) return yesterday.strftime("%Y-%m-%d") + " " + raw.replace("昨天 ", "") if len(raw) == 5 and "-" in raw: return f"{now.year}-{raw} 00:00:00" return raw

这段代码的核心是优先级匹配。这里有一个没处理完的边界情况:跨年的时候,“12-31 23:50”这种格式会被解析成今年的 12 月 31 日,如果你抓的数据跨越了元旦,那部分数据的年份就会错位。我在实际改这个项目时加了一层校验:如果解析出的时间比当前时间晚了一天以上,就自动把年份减一。这个坑在第 5 章会单独拎出来讲,跑真实数据时真的会出现。

3.4 数据落库:CSV 与 MySQL 双通道

存储层是独立模块,项目默认支持 CSV 和 MySQL 两种通道。CSV 的写入要注意编码,用 utf-8-sig 而不是 utf-8,否则表格软件打开直接乱码:

# storage.py 写入 CSV import csv def save_to_csv(rows: list[dict], file_path: str): with open(file_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows)

MySQL 通道的关键是字符集。连接参数里不写 charset,或者写了 utf8 而不是 utf8mb4,存进去的 emoji 表情会变成问号。项目默认用 utf8mb4,同时用 executemany 批量写入来减少连接开销:

# storage.py MySQL 批量写入 import pymysql def save_to_mysql(rows: list[dict], conn, table_name="weibo_posts"): fields = list(rows[0].keys()) sql = f"INSERT INTO {table_name} ({','.join(fields)}) VALUES ({','.join(['%s'] * len(fields))})" data = [tuple(r[field] for field in fields) for r in rows] cursor = conn.cursor() cursor.executemany(sql, data) conn.commit()

这里用字符串拼接表名和字段列表,因为这个场景的字段在代码里写死,不涉及用户输入,所以没有问题。如果你要把这个函数开放给外部参数,那就必须改成白名单校验。两条通道可以同时开,CSV 用来快速预览抽样,MySQL 用来存全量,后面的分析脚本直接连库读。

3.5 断点续抓:进度文件怎么记录

断点是爬虫工程里特别容易被忽略、但真正跑总量时救命的机制。项目的做法是维护一个 progress.json,以“关键词+日期”为键记录完成状态:

# utils.py 进度记录 import json, os def load_progress(): if not os.path.exists("progress.json"): return {} with open("progress.json", "r", encoding="utf-8") as f: return json.load(f) def mark_finished(progress, key): progress[key] = True with open("progress.json", "w", encoding="utf-8") as f: json.dump(progress, f, ensure_ascii=False)

调用时机比实现本身更重要。mark_finished 必须在数据真正落盘之后调用,而不是在请求成功之后就标记。如果你把标记放在落盘之前,进程在写入过程中崩了,下次启动会跳过这一天,直接造成数据空洞。第 5 章里有一条就是专门讲这个顺序问题。

4. 从零跑通:环境准备、Cookie 配置与命令行入口

4.1 环境准备:Python 版本与依赖安装

项目用到的第三方库很少,核心只有一个 requests,剩下的都是辅助。requirements.txt 内容大致是这样的:

requests>=2.28.0 beautifulsoup4>=4.11.0 lxml>=4.9.0 pandas>=1.5.0 pymysql>=1.0.0

如果你只用 CSV 输出,pymysql 可以不装,直接注释掉对应行。beautifulsoup4 和 lxml 不是所有解析路径都用得上,但保留着不会有坏处,因为获取到的内容有 HTML 清洗的场景,后面二次开发很可能用到。Python 版本建议 3.8 到 3.11,3.12 的 typing 语法差异在个别第三方库上还有兼容问题,没必要在这个项目里给自己增加变量。安装命令就一行:

pip install -r requirements.txt

装完以后可以先跑一句导入确认环境没有问题。这一步看着多余,但能帮你把“环境问题”和“代码问题”分开,后面调试的时候少走弯路。如果你有多个 Python 环境,记得先激活对应的虚拟环境再装依赖,否则安装位置和运行位置不一致,会带来一种“明明装了却找不到模块”的玄学问题。

4.2 Cookie 获取:从浏览器开发者工具复制

项目不提供账号密码登录,Cookie 需要自己从浏览器拿。操作路径是这样:用浏览器打开微博移动站,登录账号,然后打开开发者工具,切到 Network 面板,随便点开一个搜索请求,在 Headers 里找到 Cookie 这一行,全选复制,粘贴到项目根目录的 cookie.txt 里。注意不要带上引号,也不要有多余的换行,一段干净的 Cookie 字符串就可以工作。

这里有两点要提醒。第一,复制完 Cookie 之后,短时间内有效,但登录态会随着账号活跃度慢慢失效。长期跑数据的话,建议准备一个专门用来采集的小号,平时不用它发内容,只在需要的时候刷新登录,尽量避免账号被风控限制。第二,Cookie 里包含账号相关的标识信息,不要把这个文件提交到公开仓库,就算只是练手项目,也应该把 cookie.txt 写进忽略列表,这个习惯从项目一开始就要养成,不然后悔药没地方买。

4.3 命令行入口:三种启动方式

main.py 把参数暴露成三个维度:关键词、日期范围、任务文件。最常用的启动方式如下:

# 方式一:抓最近 7 天的数据 python main.py --keyword 新能源 --days 7 # 方式二:指定精确日期范围 python main.py --keyword 新能源 --start 2025-01-01 --end 2025-01-07 # 方式三:批量任务文件 python main.py --taskfile tasks.json

方式一适合日常增量抓取,days 参数从今天往回推,不用手算日期。方式二适合补历史数据,start 和 end 都是 YYYY-MM-DD 格式,两个参数可以只传一个,如果只传 start,end 默认取今天。方式三适合一次性处理多个关键词,tasks.json 里是一个数组,每个元素包含 keyword、start、end 三个字段:

[ {"keyword": "新能源", "start": "2025-01-01", "end": "2025-01-07"}, {"keyword": "人工智能", "start": "2025-01-01", "end": "2025-01-07"} ]

启动之后,控制台会滚动输出日志,包含当前处理的关键词、日期、页码和已抓取条数。第一次跑的时候建议只传一个关键词和一天的范围,确认链路通了再扩大范围。跳过这个步骤直接全量抓,一旦出问题,你分不清是代码问题还是参数问题,排查成本会高很多。

4.4 跑通后的快速验证:数据对不对要看这四个点

项目跑完一批任务后,先别急着去分析,用下面四个检查点快速过一遍。第一个,看 progress.json 里对应日期的任务是不是都标记成了 finished;第二个,看输出 CSV 的文件大小和行数是否和数据量匹配;第三个,抽查几行数据,重点看 created_at 是不是标准日期格式,而不是“昨天 23:50”这种相对时间;第四个,用 pandas 扫一下整体范围,确认没有跳天和明显的异常时间:

import pandas as pd df = pd.read_csv("output/新能源.csv") print(df.head(5)) print("时间范围:", df["created_at"].min(), "→", df["created_at"].max()) print("总条数:", len(df))

这四个检查点能过滤掉大部分采集中产生的质量问题。特别是时间范围这一项,如果你要的是最近 7 天的数据,但 max 时间戳比今天早了好几天,说明任务调度或者时间解析某个环节出了问题,这时候就该回去看日志,而不是继续加量抓。

5. 避坑与排查:Cookie 失效、返回空页与进度记录顺序

5.1 Cookie 失效但程序不退出,日志一直在刷

现象:跑了一段时间后,所有请求都返回 401,但程序没有停下来,日志继续一页一页地滚,最后 CSV 文件里只有表头,没有数据行。第一次遇到还以为接口改了,费了半天劲去查 URL 构造,后来才发现问题出在失效检测上。

原因:失效判断只检查了状态码 401 这一种情况,但实际上移动端接口在登录态失效时,有的请求会照常返回 200,只是内容变成了登录跳转页或空壳数据。程序不知道登录已经失效,任务队列照常推进,数据自然落不下来。

解决:把失效判断分成两层。第一层看状态码,第二层看响应内容,连续出现三次可疑的空页面就主动终止任务,而不是继续空转。同时把失效时的日志输出做得醒目些,避免盯着日志看半天才发现不对。

5.2 请求返回 200,但 cards 数组是空的

现象:某一天的搜索,请求成功返回 200,响应结构也正常,cards 数组却是空的,程序认为这一天没有数据,直接标记完成。

原因:有两类常见的触发因素。一类是关键词里带了特殊字符,比如引号、逗号、百分号,构造 URL 时没有被正确编码;另一类是短时间内的请求频率触发了风控,服务端返回了空结果来降低风险。这两种情况的表现一样,都是“无数据”,但原因完全相反,处理方式也不一样。

解决:先把关键词放到浏览器里手动搜一遍,确认有数据;然后在构造 URL 的环节做编码检查,特殊字符必须走 urlencode;最后给 downloader 加上空结果计数,连续出现多次空卡片时自动进入冷静期,比如暂停一段时间再继续。把这三步都做了,这个坑基本不会再出现。

5.3 相对时间解析在跨年时错位

现象:抓取去年 12 月的数据时,部分“12-31 23:50”被解析成了今年的 12 月 31 日 23:50,时间排序全乱。你观察到的规律是,越靠近跨年节点的数据,时间越奇怪。

原因:时间解析里处理“MM-DD HH:mm”这种格式时直接使用了当前年份,没有做跨年校验。12 月 31 日如果在今年还没到,那它必然是去年的数据,解析逻辑需要把这个推断做进去。

解决:在时间解析函数外面再包一层校验,如果解析出的时间晚于当前时间一天以上,就把年份减一。做这件事的时候注意别把正常当天数据改掉,阈值条件必须严格大于一定范围,而不是大于几小时就回退,否则会影响真实当天的数据。

5.4 进度标记过早,数据直接缺失

现象:断点续抓之后发现,某一天的 CSV 文件里只有几条数据,但当时明明跑完了。检查进度文件,那天的标记确实是 finished。

原因:进度标记放在了请求成功之后、数据落盘之前。进程如果恰好在落盘中途退出,进度已经写了,数据却没写全。下次启动时,程序看到 finished 就跳过这一天,这个空洞永远不会被补上。

解决:把标记完成这步挪到数据写完 CSV 或 MySQL 之后。规定顺序是:拿到数据、写存储、标记完成。另外在存储层对 weibo_id 做去重,即使任务重跑,重复写数据也不会污染结果。从那以后我每次跑数据采集项目,都强制把这块的调用顺序按这个标准走一遍。

5.5 MySQL 里中文正常但表情变成问号

现象:CSV 文件里 emoji 正常,MySQL 表格里同一段内容变成了问号。

原因:数据库连接字符集没有设置为 utf8mb4。旧版字符集定义不覆盖四字节的 emoji 字符,只有 utf8mb4 才能完整保存。

解决:连接参数里显式写 charset 为 utf8mb4,同时确认表结构的默认字符集也是 utf8mb4 开头。这两个地方保持一致,表情和生僻字就都不会乱码。如果你用的是非关系型数据库,这一步不需要,但字段里的 HTML 标签清洗仍然要做。

6. 二次开发:从通用爬虫到可复用的定制化数据管线

6.1 新增字段:短链接展开与设备来源清洗

项目默认已经抓了发布设备字段,但它常带一层 HTML 标签包装,存库之前可以加一个正则清洗,把标签剥掉得到纯净的客户端名称。如果你需要进一步追踪博文里的外链,可以在解析层加一个短链展开函数,对内容里的短链做一次请求拿到跳转目标,把目标域名作为一个新字段存下来,这个字段是识别营销内容的基础素材。注意展开请求要放在下载层的限速逻辑之后,否则会额外增加请求频率。

6.2 营销号过滤:基于行为特征而不是关键词黑名单

直接过滤关键词黑名单很容易误伤,也更难维护。更稳的方案是基于项目已有字段做行为判断:比如发布设备集中在某个固定客户端、发文频率高于阈值、内容里短链占比偏高、评论数长期为零。这些特征都来自现有字段,你只需要在存储之前加一个过滤函数,把符合特征组合的样本丢弃或单独打标记,就完成了基础的质量清洗。我一般会把过滤规则放进 config.py,方便每次跑数据时按话题调整阈值,而不是每次改代码。

6.3 定时增量抓取:把任务调度交给系统

项目本身适合做一次性任务。如果你要长期追踪某个话题,可以用系统自带的调度器每天跑一次增量任务。以类 Unix 系统自带的定时任务为例,每天凌晨两点执行当天的增量抓取,命令可以写成这样:

0 2 * * * cd /path/to/weibo_spider && python main.py --keyword 新能源 --days 1

增量抓取的关键仍然是去重。与其在存储层反复插入掩盖问题,不如在调度层做好进度持久化:progress.json 本身就是跨天重跑的锚点,启动时先读进度文件,跳过已经完成的任务单元,而不是每次都从头抓一遍。这个项目的代码我改过两轮,最大的教训是不要等项目跑挂了才想起补进度记录,第一次跑之前就把断点续抓设计好,后面所有增量任务都会省心很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询