ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python爬虫实战:历史事件时间线数据采集与清洗存储

Python爬虫实战:历史事件时间线数据采集与清洗存储 历史是一条奔流不息的长河而数字化的今天我们终于可以用 Python 爬虫把这条河里的浪花一朵朵捞起来变成结构化的数据表格。这个“历史事件时间线数据爬取”项目听起来像是历史系学生的作业但真正动手做一遍你会发现它几乎涵盖了爬虫入门到进阶的所有核心知识点请求发送、HTML 解析、翻页处理、动态加载识别、数据清洗、存储与增量更新。我更愿意把它当成一个练手项目爬什么不重要重要的是通过历史数据这个载体把 Python 爬虫的完整链路跑通。这篇文章我会从项目设计、环境准备、核心实现到问题排查一步步拆解适合刚学完 Python 基础、想找一个真实项目练手的读者也适合想系统梳理爬虫技术栈的开发者参考。1. 项目整体设计与思路拆解1.1 爬虫项目的本质把网页变成结构化数据很多人一听到爬虫就想到“爬取整个互联网”这种宏大叙事实际上日常 90% 的爬虫任务都简单得多找到一个包含目标数据的网页把里面有价值的信息提取出来存成 CSV、JSON 或者数据库。历史事件时间线爬虫就是这么个典型场景。我们需要做的事情可以拆成四步第一找到可靠的历史事件数据源比如按年份排列的中外历史大事记页面第二分析页面结构确定事件条目在 HTML 中的位置第三批量发送请求并解析把“公元前221年秦始皇统一六国”这种半结构化文本变成时间、事件、朝代、人物、地区等字段第四清洗并存储最终形成一份可以检索、排序、统计的历史知识库。这个项目的独特之处在于时间字段的处理。普通爬虫处理数字或者字符串容易但历史时间线里充满“公元前”“公元”“约”“初”“末”这类模糊表达清洗环节的难度一下子拉高了。这也是我推荐大家用历史数据练手的原因——它能逼着你认真思考数据规范化的问题而不是抓下来就完事。1.2 技术选型为什么用 requests BeautifulSoup什么时候需要 Selenium技术选型没有标准答案只有最合适的方案。我这个项目的主干用了 requests 加 BeautifulSoup原因很直接目标页面是服务器直接渲染出来的静态 HTML数据都老老实实躺在源代码里用 requests 一把梭就行又快又省资源。至于网上铺天盖地讲“bs4 爬取动态页面”的教程那是针对页面内容由 JavaScript 动态加载的情况。比如豆瓣电影 Top250虽然数据也在初始 HTML 里出现过但很多仿站或数据聚合页面其实是前端异步渲染的直接 requests 拿不到内容这时候就要考虑两条路一是打开浏览器开发者工具找到真正的数据接口很多是 JSON 格式直接请求接口二是实在找不到接口或者页面交互太复杂再上 Selenium 模拟浏览器操作。就历史时间线网站来说我见过不少用 Table 标签排版的老式页面纯静态非常适合练手。但也有一些做得比较现代的站点用 Vue/React 渲染列表连翻页都是前端路由切换——这种情况就必须会抓接口或上 Selenium。所以我的建议是先分析再动手能静态就静态能接口就接口Selenium 永远是兜底手段不是首选。1.3 目标网站分析与数据字段规划动手写代码之前先花半小时把目标网站摸清楚。我习惯从三个角度分析第一内容结构是否稳定是否分页、分章节第二URL 有没有规律比如?page1、/year/2023/这种第三数据字段有哪些能不能直接映射成表格列。对于历史时间线项目我规划的字段是event_id唯一ID方便去重、year年份存整数如果是公元前就存负数、era原始时间文本比如“公元前221年”、event事件描述、dynasty所属朝代、location发生地点可选、person关键人物可选、source_url数据来源链接方便追溯。这些字段已经足够支撑后续的排序、筛选和简单的历史知识问答。为什么一定要保留era原始字段因为爬下来的时间表达五花八门正则清洗时难免丢失信息留一个原始文本字段等于留了后悔药后续发现问题还能重新处理。这也是我做了这么多年爬虫的一个经验清洗过的字段要留原始字段也要留永远不要只存加工后的结果。2. 环境准备与基础工具配置2.1 Python 环境安装别看基础踩坑的人真不少这个项目对 Python 版本没有硬性要求3.8 以上都能跑但我建议直接装最新的稳定版。Windows 用户去官网下载安装包时一定要记得勾选“Add Python to PATH”这一步漏了后面在命令行敲python就会提示“不是内部或外部命令”。Mac 用户可以用 Homebrew 装也可以官网下载 pkg 包。Linux 用户一般系统自带 Python3但版本可能偏旧建议用apt install python3-pip先把 pip 装上。装好之后打开终端Windows 用 PowerShell 或 CMD输入python --version和pip --version确认环境正常。这里我要特别提醒网上很多“Python 安装教程”会教你直接pip install全局安装第三方库我强烈建议先创建虚拟环境。虚拟环境就像给你的项目单独开了一个小房间不会把系统全局环境搞乱也不用担心不同项目之间依赖冲突。用python -m venv venv创建Windows 下激活命令是venv\Scripts\activateMac/Linux 是source venv/bin/activate。2.2 安装依赖库requests、BeautifulSoup4、pandas 一个不能少激活虚拟环境后用 pip 安装以下库pip install requests beautifulsoup4 lxml pandasrequests 负责发送 HTTP 请求beautifulsoup4 负责解析 HTMLlxml 是解析器比 Python 自带的 html.parser 快很多pandas 用来做数据清洗和存储虽然不是必须但处理表格数据确实方便。另外建议装一个fake_useragent库它可以随机生成浏览器 User-Agent可以有效降低被反爬虫识别为脚本的概率pip install fake_useragent安装完成后可以用一行命令验证import requests from bs4 import BeautifulSoup import pandas as pd print(环境OK)我在这个项目里还装了openpyxl因为最后要把数据存成 Excel 做展示。如果你只存 CSV可以不装。总之依赖越精简越好爬虫项目的原则是能不用框架就不用框架Scrapy 虽然强大但为了一个几千条数据的页面去上分布式爬虫纯属杀鸡用牛刀。2.3 请求头、Cookie 与基本的反爬意识爬虫的本质是模拟浏览器访问服务器区分正常用户和爬虫主要靠请求头里的信息。最基本的必须带上User-Agent表明你是哪个浏览器其次是Referer告诉服务器你从哪个页面跳转过来某些站点还需要Cookie来维持登录态或会话。我在代码里习惯这样组织请求头from fake_useragent import UserAgent ua UserAgent() headers { User-Agent: ua.random, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }fake_useragent 每次调用ua.random会随机生成一个浏览器标识让请求看起来像不同用户在访问。不过要注意有些网站会校验 User-Agent 的合法性如果遇到一直返回 403可以手动写死一个真实的 UA 字符串再试。关于反爬我的经验是尊重网站的 robots.txt控制请求频率不要把自己的 IP 玩进黑名单。历史数据这种公开信息只要访问频率合理一般不会触发风控。但如果对方明确设置了反爬策略那就需要增加延时、使用代理池等方式解决我在后面问题排查部分再展开讲。3. 历史事件时间线爬虫核心实现3.1 静态页面爬取requests BeautifulSoup 提取事件条目假设我们已经找到目标页面结构大概是这样的简化示例div classtimeline div classevent-item span classyear公元前221年/span span classdesc秦始皇统一六国建立秦朝/span /div div classevent-item span classyear公元前202年/span span classdesc西汉建立/span /div /div第一步用 requests 获取页面源码import requests from bs4 import BeautifulSoup from fake_useragent import UserAgent url https://example.com/history/timeline ua UserAgent() headers {User-Agent: ua.random} resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 解决中文乱码后面详细讲 soup BeautifulSoup(resp.text, lxml)第二步通过选择器定位事件条目。BeautifulSoup 支持 CSS 选择器比 find_all 直观得多。比如上面这个结构我可以这样提取items soup.select(div.event-item) for item in items: year_ele item.select_one(span.year) desc_ele item.select_one(span.desc) if year_ele and desc_ele: era year_ele.get_text(stripTrue) event desc_ele.get_text(stripTrue) print(era, event)这里有个细节get_text(stripTrue)会把标签里的换行和多余空格去掉得到干净的文本。如果页面里的年份和事件是分多个标签嵌套的比如年份里还有a链接那get_text也能把内部所有文本拼出来非常好用。3.2 处理翻页与时间线跨度URL 规律与循环采集大多数历史时间线站点会按年份分页比如/history/2023、/history/2024。遇到这种情况只需要把年份作为变量循环拼接 URL 就行。但也有些站点用的是“加载更多”按钮每次点击返回一页 JSON 数据这时候需要找到接口。假设 URL 规律是https://example.com/history/{year}年份从公元前 3000 年到公元 2026 年我们可以这样写循环import time for year in range(-3000, 2027): if year 0: url fhttps://example.com/history/BC{abs(year)} else: url fhttps://example.com/history/AD{year} resp requests.get(url, headersheaders, timeout10) soup BeautifulSoup(resp.text, lxml) # 解析当前页的事件 # parse_events(soup) time.sleep(1) # 控制频率避免给服务器造成压力注意两点第一负数年份转 URL 时要考虑站点自己的规则有的用BC221有的用-221一定要先手动访问一页确认第二time.sleep(1)是必须的哪怕数据量很大也要加这是爬虫的基本礼仪也是保护自己 IP 的方式。如果对方网站没有明确禁止爬取每秒一个请求是相对安全的频率。3.3 动态加载内容用浏览器开发者工具找接口selenium 兜底如果你的目标站点是动态渲染的直接requests.get拿到的 HTML 里没有数据那怎么办第一步永远是打开浏览器Chrome 或 Edge按 F12 进入开发者工具切到 Network网络面板刷新页面观察 XHR/Fetch 请求。很多动态页面其实是前端调后端接口接口返回 JSON 里面就有你要的全部数据。找到那个返回 JSON 的接口后直接请求它解析 JSON 比解析 HTML 还爽。比如接口返回可能是{ code: 0, data: { list: [ {year: 公元前221年, event: 秦始皇统一六国}, ... ] } }那直接用requests.get(api_url).json()就行根本不需要 BeautifulSoup。这也是我反复强调“能接口就接口”的原因JSON 结构清晰还省去了解析 HTML 的麻烦。但如果接口加密、参数复杂或者页面必须滚动才能加载那就只能上 Selenium 了。Selenium 会启动一个真实的浏览器自动执行 JavaScript拿到的就是完整的渲染后页面。使用 Selenium 需要安装selenium库和对应的浏览器驱动代码示例from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 无头模式不弹出浏览器窗口 driver webdriver.Chrome(optionsoptions) driver.get(https://example.com/history) html driver.page_source soup BeautifulSoup(html, lxml) # 后续解析逻辑一样 driver.quit()用 Selenium 的时候如果是“加载更多”按钮可以用driver.find_element(...).click()触发也可以模拟滚动到底部。但注意 Selenium 性能开销大如果一万个页面都用它跑速度会非常感人所以能接口就接口Selenium 只用来攻坚个别无法静态获取的页面。3.4 数据清洗与结构化把“公元前221年”转成可排序的时间字段花大力气爬下来的数据如果不做清洗就是一堆乱糟糟的字符串比如“公元前221年”“约1600年”“五代十国时期”“汉武帝时期”等排序和统计根本无从谈起。所以清洗是让项目有价值的关键一步。我的清洗思路是先在era字段里保留原始文本再写一个解析函数把类似“公元前221年”转成整数-221import re def parse_year(era): 将中文历史时间文本转换为整数年份公元前为负数。 示例 公元前221年 - -221 公元2026年 - 2026 约1600年 - 1600 1644年 - 1644 无法识别返回 None if not era: return None text era.strip() # 处理“公元前” match_bc re.search(r公元前\s*(\d{1,6})\s*年, text) if match_bc: return -int(match_bc.group(1)) # 处理“公元”前缀如“公元2026年” match_ad re.search(r公元\s*(\d{1,6})\s*年, text) if match_ad: return int(match_ad.group(1)) # 处理裸年份如“221年” match_year re.search(r(\d{1,6})\s*年, text) if match_year: return int(match_year.group(1)) # 处理“约”或“左右”等模糊词兜底提取数字 match_num re.search(r-?\d{1,6}, text) if match_num: return int(match_num.group()) return None这个函数不复杂但覆盖了绝大多数情况。测试几个例子print(parse_year(公元前221年)) # -221 print(parse_year(公元2026年)) # 2026 print(parse_year(约1600年)) # 1600 print(parse_year(1644年)) # 1644 print(parse_year(五代十国时期)) # None对于None的结果我会单独标记出来后续人工核对。历史文本的模糊性没办法用正则完全解决但保留原始文本后至少可以追查。朝代信息的提取也类似。很多事件描述里会提到“秦朝”“唐朝”可以维护一个朝代关键词映射表用简单的包含匹配来打标签。这个操作不追求 100% 准确能做到 80% 就已经能支撑后续很多分析了。4. 数据存储与后续分析4.1 保存为 CSV / JSON建立本地历史知识库清洗完成后的数据最直接的就是存成 CSV 或者 JSON。CSV 用 Excel 就能打开方便人工浏览JSON 适合程序读取后续做 API 或者数据分析都很顺手。用 pandas 可以一行代码写 CSVimport pandas as pd data [] # 每个元素是包含字段的字典 for item in all_items: data.append({ event_id: item[id], era: item[era], year: item[year_int], event: item[event], dynasty: item[dynasty], location: item[location], person: item[person], source_url: item[source_url], }) df pd.DataFrame(data) df.to_csv(history_timeline.csv, indexFalse, encodingutf-8-sig)这里有一个新手特别容易踩的坑to_csv默认用 UTF-8 编码但 Excel 打开 UTF-8 的 CSV 会乱码。解决办法是编码参数用utf-8-sig它会在文件开头加一个 BOM 头Excel 就能正确识别。如果你只是想用 pandas 继续分析用encodingutf-8就行。同时也可以存一份 JSONdf.to_json(history_timeline.json, orientrecords, force_asciiFalse)force_asciiFalse很重要否则中文会被转成\u开头的 Unicode 转义序列可读性极差。4.2 简单可视化与检索按时期/关键词筛选有了结构化的 DataFrame检索就变得很简单。比如我想看看唐朝发生过哪些事件tang_dynasty df[df[dynasty] 唐朝] print(tang_dynasty)我想找到所有包含“战争”的事件wars df[df[event].str.contains(战争)] print(wars)如果想把公元前的事件单独拎出来bc_events df[df[year] 0].sort_values(year)这些操作在 Excel 里也能做但用 pandas 的筛选、统计、切片更为灵活。当数据量达到几千上万条时pandas 的优势就非常明显。我还会顺手生成一个简单的年份分布直方图看看哪个时期的历史事件记录最密集——这其实已经有点历史大数据分析的意思了。4.3 增量爬取与更新策略历史事件数据一直在增长明年你可能会重新跑一遍爬虫获得 2027 年的新事件。但重新爬全量数据浪费资源所以增量爬取是必须考虑的。最简单的方案在存储表里加一个source_url字段每次爬取前把已经存在的 URL 加载到一个集合里新数据进来时先判断 URL 是否已经存在存在就跳过。这个方案也叫“基于 URL 去重”实现简单效果也很好import pandas as pd try: old_df pd.read_csv(history_timeline.csv) existing_urls set(old_df[source_url].tolist()) except FileNotFoundError: existing_urls set() new_records [] for url in all_urls: if url in existing_urls: continue item crawl_page(url) if item: new_records.append(item)当然更专业的做法是用 SQLite 或 MySQL 存储利用数据库的唯一索引去重。但对于这种个人项目CSV 集合去重已经足够了。关键是把更新逻辑想清楚别每次都全量覆盖否则后续维护成本很高。5. 常见问题与排查技巧实录5.1 编码问题中文乱码怎么治爬中文网页最常遇到的就是乱码。解决方案分两步第一在resp对象上检查编码print(resp.encoding) # 可能是 ISO-8859-1、GBK、UTF-8第二设置正确的编码。最保险的方式是用resp.apparent_encoding它是 requests 根据页面内容自动推测的编码大多数时候都很准resp.encoding resp.apparent_encoding但也有例外比如某些页面声明了 GB2312但实际内容包含 GBK 扩展字符apparent_encoding可能会推测错。这时候可以手动指定resp.encoding gbk如果你用的是requests.get(...).text中文已经乱码了还可以通过解码原始字节救回来raw resp.content # 原始字节 text raw.decode(gbk, errorsignore)错误处理参数errorsignore会跳过无法解码的字节避免程序崩溃。这类问题多试几次就能总结出规律老站点用 GBK 多新站点基本都是 UTF-8。5.2 反爬虫机制User-Agent、频率限制、IP封锁应对反爬是爬虫绕不开的话题。历史时间线网站的反爬一般不会太严格但我也遇到过几个典型的坑。第一种是返回 403 Forbidden这通常说明 User-Agent 被识别了或者请求频率太高。先试试换一个浏览器 UA再增加延时如果还是 403可以补一个Referer头模拟从首页点进来的正常路径headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example.com/, }第二种是正常返回但内容里没有数据比如弹出一个验证码页面。这就要考虑更高级的应对策略比如使用代理 IP、打码平台或者干脆降低爬取强度。我个人的原则是如果目标网站明确通过验证码拦截那就停止爬取转用官方 API 或手动整理遵守网站的意愿比拿数据更重要。第三种是请求频率导致 IP 被封表现是一段时间内所有请求都超时或返回 403。解决方式是加随机延时避免固定间隔import time import random time.sleep(random.uniform(0.5, 1.5))这个随机延时可以让请求模式更像人的行为而不是精确到毫秒的机器行为。5.3 定位不到元素先看是不是 iframe 或动态渲染用 BeautifulSoup 解析时经常出现select_one返回None的情况。新手容易懵其实原因不外乎三种选择器写错、内容在 iframe 里、内容是动态加载的。如果是 iframe比如有些页面把时间线表格嵌在子页面里主页面只有iframe srccontent.html。这时候需要先拿到 iframe 的src再单独请求这个地址iframe soup.select_one(iframe) iframe_url iframe[src] # 继续请求 iframe_url 解析内容如果是动态渲染参考我前面说的去 Network 里找接口或者用 Selenium 渲染后再解析。还有一种隐蔽情况页面里确实有数据但被script包在 JavaScript 变量里比如script window.historyData [{year: 公元前221年, event: 秦始皇统一六国}]; /script这时候可以用正则匹配提取window.historyData ...;再 json.loads 解析。这种情况在不少老站点也常见多一种思路就多一条路。5.4 数据缺失与异常处理让爬虫更健壮爬虫跑着跑着突然中断是常态原因可能是网络超时、服务器返回 5xx、某个页面结构特殊导致解析出错。所以代码里一定要有异常处理和重试机制。最基本的是用try...except包裹请求import time def fetch_html(url, headers, retries3): for i in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text else: print(f状态码异常: {resp.status_code}, 重试 {i1}/{retries}) except requests.RequestException as e: print(f请求异常: {e}, 重试 {i1}/{retries}) time.sleep(1) return None如果某个页面解析时出现AttributeError就是找不到某个元素可以把对应的 URL 记录到错误日志里跳过当前页继续跑后面的。不要因为一个脏数据就中断整个任务实时输出进度也能帮你掌握跑批情况success_count 0 fail_count 0 for url in urls: html fetch_html(url, headers) if html is None: fail_count 1 continue # 解析逻辑 ... success_count 1 if success_count % 100 0: print(f已完成 {success_count} 页失败 {fail_count} 页)这种“先跑通再完善”的思路特别重要。我第一版爬虫只写了核心逻辑跑一半崩了但已经有了大量数据后面加上异常处理和断点续爬才逐渐变得稳定。6. 踩坑体会与扩展建议6.1 我的几个经验教训这个项目让我重新打磨了不少爬虫细节有几个印象特别深的点。第一永远不要过分相信页面结构的稳定性。历史类网站改版不算频繁但只要改版CSS 类名和 DOM 结构可能全变选择器全部失效。所以我在代码里尽量用语义化的选择器比如div.event-item而不是用div:nth-child(3)这种绝对位置改版后还能容错一些。第二时间清洗真的没有想象中简单。你以为所有年份都是“公元前/公元”开头实际上数据里还有“约在公元前 256 年”、“春秋时期公元前770年—公元前476年”、“明洪武元年1368年”这种带括号、带区间的表达。如果要精确到年份我是先用正则提取括号内的第一个年份取不到再用模糊匹配。如果目标是做年份区间统计那还要设计起止年份两个字段复杂度上一个台阶。第三保存数据时一定要记录source_url。有一次我发现数据里混入了重复事件一查是同一条历史在不同页面出现了两遍如果没有来源 URL我根本没法定向排查是哪两个页面带来的重复。有了 URL我可以快速写脚本按 URL 去重保留第一个问题几分钟就解决。6.2 让这个项目更有意思的扩展方向如果你跑通了这个基础版爬虫后面可以玩的花样还有很多。比如做一个简单的 Flask Web 应用把历史数据做成 API支持按年份范围查询、按朝代筛选、按关键词搜索也可以结合jieba分词对事件描述做词频统计看看哪个历史时期“战争”“改革”“灾害”出现得最多甚至可以把年份分布用pyecharts做成动态时间轴让“历史的长河在指尖流淌”这个标题真正视觉化。更实际一点的扩展是给爬虫加一个计划任务每天定时运行一次自动抓取新增的历史事件并追加到数据库。这个过程会涉及请求调度、数据校验、日志记录等工程化技能对找工作面试也有帮助。如果你现在正刷“boss直聘python数据爬取”相关岗位把这样一个带数据清洗、增量更新、异常处理的项目写进简历绝对比只写“会使用 requests/BeautifulSoup”有说服力得多。我个人在实际操作中的一点体会是爬虫项目的核心从来不是“爬”而是“整理”。历史数据尤其如此同一个事件在不同的记载里可能年份不一样事件描述详略也不同。这个项目做下来你对数据质量的理解会比只会爬豆瓣电影 Top250 的开发者深得多。最后再分享一个小技巧如果目标站点提供了robots.txt花一分钟去看一眼这既是对站长的尊重也能让你提前知道哪些路径不允许爬取。我的原则很简单——数据是为了学习和研究不是用来糟蹋别人服务器的。守住这个底线爬虫这条路才能走得远。
返回列表