做NBA数据分析这段时间,我最大的感触是:真正难的不是后面跑模型、调参数,而是最开始能不能拿到一份干净、完整、让你放心往里灌的分析数据。很多人学Python数据分析,一上来就学pandas、matplotlib,结果真到自己动手做个项目,卡在第一步——数据从哪来?手工复制粘贴Excel,累到怀疑人生;找现成数据集,又经常碰到字段不全、更新不及时。这时候,用Python写一个小爬虫去抓公开的NBA数据源,就成了性价比最高的选择。
这篇文章会顺着一条完整的实操线走下来:用requests获取网页,用BeautifulSoup解析HTML表格,把球员的场均数据整理成结构化DataFrame,最后落成CSV文件。整个流程围绕“数据分析前置”这个定位展开,不搞花哨的分布式爬虫,也不碰需要登录和复杂反爬的网站,就是老老实实从静态网页里把数据抠出来、洗干净、存下来。无论你是刚看完Python基础语法想找练手项目,还是已经跑过几个分析demo但没碰过爬虫,这篇都值得读完。
顺便说一句,文中所有示例都以公开可访问的NBA统计页面为目标,抓取频率也控制在合理范围,这不仅是技术上的自觉,也是做数据采集这行最基本的规矩。
1. 项目定位与整体思路拆解
1.1 为什么“数据源采集”是数据分析的前置环节
多数人理解的数据分析,是画图、是建模、是跑回归,但实际做起来,数据获取和数据清洗通常要占掉整个项目七成以上的时间。NBA数据尤其如此:球员每场比赛会产生几十项统计,再加上赛季、球队、对手、主客场等维度,数据源的选择直接决定了后续分析能走多远。
举个例子,你想分析“得分后卫的年龄和场均得分有没有关系”。这个命题听起来简单,但落地时立刻要面对几个问题:场均得分去哪里查?是查官方口径还是第三方口径?要不要过滤只有几场出场记录的边缘球员?伤病缺席的场次算不算分母?这些问题如果不在采集阶段就考虑清楚,后面所有图表和结论都站不住脚。这就是“前置”二字的含义:爬虫不只是把网页存下来,而是在采集阶段就带着分析目标去设计字段、清洗规则和存储结构。
目前获取NBA公开数据的常见渠道有三类:官方或第三方API、现成数据集仓库、自己写爬虫。没有绝对的好坏,但各自的适用场景差别很大。
| 渠道 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 官方API(如stats.nba.com) | 数据结构化好、字段全 | 有访问限制、需要研究接口规则 | 做长期稳定的数据管道 |
| 现成数据集(Kaggle/GitHub CSV) | 拿来即用、适合练手 | 滞后严重、字段不可定制 | 快速验证分析思路 |
| 自己写爬虫(requests+BeautifulSoup) | 完全可控、能取到最新数据、想抓什么抓什么 | 需要处理反爬和页面结构变化 | 定制化需求、学习爬虫和网页解析 |
我的建议是:练手阶段,三种都用一遍。先拿现成数据集跑通分析逻辑,再用爬虫去拿最新数据做对比,你会直观感受到“自己拿到的数据”和“别人整理好的数据”在工作量上的差距,也能更好的理解API和网页爬取各自的边界。
1.2 技术选型:为什么是 requests + BeautifulSoup 这个组合
市面上Python爬虫工具一抓一大把,Scrapy、Selenium、Playwright、pandas.read_html,各有各的用武之地。这个项目选择requests加BeautifulSoup,并不是因为它最强,而是因为它恰好卡在“数据分析前置任务”的最优点上:轻量、可控、学习成本低,同时足够解决绝大多数静态表格的抓取需求。
先看Scrapy。它是个完整的爬虫框架,有并发调度、中间件、管道、日志系统,非常适合大规模采集。但正因为完整,它的学习曲线也比较陡,一个简单的单页面抓取任务,要理解项目结构、spider类、item管道,对只想拿数据做分析的人来说有点杀鸡用牛刀。Selenium和Playwright则解决的是动态渲染页面的问题,启动浏览器、等待元素、截图调试,资源开销大、速度慢。如果目标页面里数据是明文HTML就能拿到,完全没必要让浏览器参与进来。
那pandas.read_html呢?我承认它很诱人,一行代码就能把页面里所有表格读成DataFrame。但它的短板有两个:一是可控性差,页面上一堆无关表格时你得手动指认目标表格;二是它对HTML结构容错能力一般,遇到复杂的嵌套表头、合并单元格,解析出来的数据经常是歪的,反而不如自己写循环解析来得稳。对于一个数据最终要进分析流程的场景,字段位置的精准性比对代码行数更值钱。
所以我的选择逻辑很简单:目标页面是静态HTML表格,页面规模不算夸张,用requests拿到文本,交给BeautifulSoup按DOM结构提取,全程自己掌控,出了错也好排查。等你跑通了这套流程,再去看Scrapy、Playwright这些工具,会发现很多概念是相通的,那时候再按需切换也不迟。
2. 数据源分析与页面结构拆解
2.1 目标数据源与字段设计
这个项目的目标数据源选的是Basketball-Reference网站上的赛季场均数据页。选择这个页面的原因很直接:它的URL规律清晰、数据表是标准HTML结构、没有登录墙,对我们这类低频率、教学性质的抓取非常友好。更重要的是,这个页面的数据宽表几乎覆盖了篮球数据分析的基础维度。
所谓“场均数据”,对应的是NBA官网每场比赛统计按出场次数归一化后的结果。页面的核心表格里,每一行代表一名球员在一个赛季的场均贡献,主要字段包括:
| 字段缩写 | 含义 | 分析价值 |
|---|---|---|
| Player | 球员姓名 | 主键之一,注意有同名的可能 |
| Pos | 位置(PG/SG/SF/PF/C) | 位置维度的群体分析 |
| Age | 年龄 | 年龄与表现关系研究 |
| Tm | 球队缩写 | 球队维度筛选 |
| G / GS | 出场数 / 首发数 | 判断球员样本量是否充足 |
| MP | 场均上场时间 | 负荷与效率分析 |
| FG / FGA / FG% | 命中数 / 出手数 / 命中率 | 投篮产量与效率 |
| 3P / 3PA / 3P% | 三分命中数 / 出手数 / 命中率 | 外线能力评估 |
| FT / FTA / FT% | 罚球命中 / 出手 / 命中率 | 制造犯规能力 |
| TRB / AST / STL / BLK | 篮板 / 助攻 / 抢断 / 盖帽 | 基础全面性指标 |
| TOV / PF | 失误 / 犯规 | 负面行为指标 |
| PTS | 场均得分 | 最常用的进攻产出指标 |
在开始写爬虫之前,先把这些字段列出来是有必要的。因为后续做分析时你会发现,同样叫“命中率”,投篮命中率、三分命中率、罚球命中率是三套完全不同的数据,如果采集的时候字段名称没理清,后面画图时很容易张冠李戴。
2.2 看懂表格的HTML结构:Chrome开发者工具怎么用
拿到网页之后,第一步不是写代码,是先搞清楚目标数据长在HTML的哪个位置。打开Chrome,在目标页面右键选择“检查”,或者直接按F12进入开发者工具,用左上角的选取按钮点一下数据表格,你会在Elements面板里看到对应的HTML片段。
Basketball-Reference这个站的表格结构非常典型,大致长这样:
<table id="per_game_stats" class="stats_table sortable"> <thead> <tr> <th>Player</th> <th>Pos</th> <th>Age</th> ... </tr> </thead> <tbody> <tr> <th><a href="/players/a/abc123.html">Alex Abrines</a></th> <td>SG</td> <td>22</td> ... </tr> </tbody> </table>注意两个关键细节:第一,表格有个固定的id="per_game_stats",这就相当于给表格起了个唯一名字,代码里可以直接用这个id定位,精准且不容易误伤页面上的其他表格;第二,每一行的第一个单元格是th而不是td,里面放的是球员姓名。这个细节很多人会忽略,如果用统一的方式去找td,球员名字列就会漏掉,字段数就会对不齐。我在第一次解析时就是在这里栽了跟头,看了半天输出总觉得少一列。
另外这个页面的表头其实是半透明的“sticky header”,就是滚动时固定在页面顶部那种效果。虽然视觉上它是悬浮的,但DOM结构里 表头仍然在thead标签里,所以不用在爬虫上特殊处理,直接按thead和tbody分开解析即可。
3. 核心实现:从请求到干净的DataFrame
3.1 请求头设置与请求频率控制
先搭环境。Python版本建议3.9以上,用到的库就四个:requests、beautifulsoup4、pandas、lxml。安装命令一条搞定:
pip install requests beautifulsoup4 pandas lxmllxml是BeautifulSoup的解析引擎,比默认的html.parser速度快不少,解析复杂表格时页更稳,强烈建议一起安装。
然后是请求部分。很多爬虫新手最容易踩的坑就是直接requests.get(url)一把梭,然后收到一个403或者被重定向到验证页面。这不是你代码写错了,而是网站服务器看到请求头里写着Python-urllib/3.x,直接判定为爬虫,在入口就把你挡掉了。解决方式很朴素:把请求头伪装成正常浏览器的样子。
import requests 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive", } url = "https://www.basketball-reference.com/leagues/NBA_2024_per_game.html" try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() print("请求成功,状态码:", resp.status_code) except requests.RequestException as e: print("请求失败:", e)timeout=10一定要加。爬虫跑起来最怕的不是返回失败,而是请求挂在那里不响应,整个程序卡死。设置超时后,超过10秒没回应就抛异常,你就可以在异常处理里做重试或者跳过,不至于让整个任务停摆。
请求频率控制放到后面批量爬取的部分细说,这里先记住一个大原则:抓取公开数据不是竞赛,用最慢的节奏拿全数据,比用最快的速度被封IP强一百倍。
3.2 使用 BeautifulSoup 解析球员数据表格
请求拿到的是整页HTML字符串,下一步是让BeautifulSoup把它变成可以查询的DOM树。
from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "lxml") # 通过 id 定位到目标数据表 table = soup.find("table", id="per_game_stats") if table is None: raise ValueError("未找到 per_game_stats 表格,页面结构可能发生了变化")定位到表格之后,先不要着急一股脑去遍历所有行。这个页面的表格包含表头行和数据行,如果直接table.find_all("tr"),表头会被混进数据里。正确做法是分开处理thead和tbody。
tbody = table.find("tbody") rows = tbody.find_all("tr") print("数据行数量:", len(rows))每一行的单元格提取也要注意那个细节:球员姓名在第一个th里,其余字段在td里。为了统一,我习惯用row.find_all(["th", "td"])一次性提取所有单元格,这样不管它是th还是td,顺序都不会乱。
import pandas as pd column_names = [ "Player", "Pos", "Age", "Tm", "G", "GS", "MP", "FG", "FGA", "FG%", "3P", "3PA", "3P%", "2P", "2PA", "2P%", "eFG%", "FT", "FTA", "FT%", "ORB", "DRB", "TRB", "AST", "STL", "BLK", "TOV", "PF", "PTS" ] records = [] for row in rows: cells = row.find_all(["th", "td"]) if len(cells) == 0: continue values = [cell.get_text(strip=True) for cell in cells] records.append(values) # 转成 DataFrame,去掉表头行和可能出现的分隔行 df = pd.DataFrame(records, columns=column_names[:len(records[0])]).dropna(how="all") df = df[df["Player"] != "Player"] print(df.head())多提一句,records[0]这个位置在实际解析时有可能是空的或者只有少量单元格,因为网页里偶尔会出现合计行或分隔行。所以我在转换成DataFrame之前会先做一次非空过滤,保证后面的清洗不会处理到空行。
3.3 数据清洗与类型转换:别急着画图
现在拿到的是“看起来挺像样”的DataFrame,但它的每个单元格依然是字符串。比如PTS列里存的是"19.4",Age列里是"23",年龄和得分无法参与任何数值计算。这一步必须做类型转换,顺带也要处理缺失值。
数据清洗时我一般分三步走。第一步,去掉噪音字符,比如某些球员名字前面带星号,代表他入选了当季全明星,这个符号对分析来说不是字段内容,直接清理掉。第二步,把统计列从字符串转成数值,转换失败的统一置成NaN。第三步,检查每一列的空值数量,决定是填充还是删除。
# 1. 清理球员姓名中的全明星标记 df["Player"] = df["Player"].str.replace("*", "", regex=False).str.strip() # 2. 定义数值列(百分比列转成浮点,后续分析时按小数使用) numeric_cols = [ "Age", "G", "GS", "MP", "FG", "FGA", "FG%", "3P", "3PA", "3P%", "2P", "2PA", "2P%", "eFG%", "FT", "FTA", "FT%", "ORB", "DRB", "TRB", "AST", "STL", "BLK", "TOV", "PF", "PTS" ] for col in numeric_cols: df[col] = pd.to_numeric(df[col], errors="coerce") # 3. 查看空值情况 print(df.isna().sum()) print(df.dtypes)关于百分比列的取值,这里要特别提醒:Basketball-Reference页面上的百分比是以47.6这种形式展示的,意思是47.6%,不是0.476。如果你直接拿去做回归分析,量纲会和别的字段不一致。所以要么在清洗阶段统一除以100,要么在分析阶段时刻记得它的单位。我习惯在清洗阶段就统一成小数形式,省得后面反复确认。
做完清洗之后,你会发现数据对比刚才已经干净了很多。此时再用df.describe()看一遍各列的均值、最值、分位数,如果发现PTS最大值是36这种合理的数值,说明解析基本没问题。
3.4 输出CSV文件:编码是个隐形坑
数据清洗干净之后就该落盘了。CSV是数据分析最常见的数据交换格式,写出来之后,后续的pandas读取、Excel查看、甚至导入数据库都十分方便。
df.to_csv( "nba_2024_per_game.csv", index=False, encoding="utf-8-sig" )注意这个utf-8-sig。如果直接写utf-8,生成的CSV用Excel打开时,球员姓名里包含英文没什么大问题,但一旦有中文内容,就很容易乱码。加-sig会在文件开头写入BOM标记,Excel就能正确识别编码,这是处理CSV输出时非常实用的小细节。
除了CSV,我还会同时导一份JSON或者其他格式存档。大数据分析场景下,数据源最终大概率要进数据库,所以下面这段存SQLite的代码也一并附上。SQLite不需要单独安装服务端,Python标准库自带,适合作为本地分析仓库。
import sqlite3 conn = sqlite3.connect("nba_stats.db") df.to_sql("per_game_2024", conn, if_exists="replace", index=False) conn.close() print("数据已写入 SQLite")数据存进SQLite之后,以后做多赛季对比分析就不用每个赛季都重新爬,一次采集、多次复用,这才是“数据源”该有的样子。
4. 批量抓取多赛季:让数据源活起来
4.1 多赛季URL规律与循环抓取
只抓单个赛季的数据,有点浪费这套爬虫。很幸运的是,Basketball-Reference的URL规律非常清晰,赛季年份直接体现在路径里:
https://www.basketball-reference.com/leagues/NBA_2024_per_game.htmlhttps://www.basketball-reference.com/leagues/NBA_2023_per_game.htmlhttps://www.basketball-reference.com/leagues/NBA_2022_per_game.html
规则就是NBA_{年份}_per_game,括号里的年份指赛季结束年份。比如2024页面对应的是2023-2024赛季。有了这个规律,批量抓取就是一次循环+组装URL的事情。
这一段代码,我建议你一定要加上两个保护措施:随机睡眠时间和异常捕获。前者是为了不给目标服务器造成压力,后者是因为网络请求这东西永远不可能100%稳定,不能因为一个赛季失败就让整个任务停下来。
import random import time import requests from bs4 import BeautifulSoup import pandas as pd def fetch_season(year, headers, retries=3): url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html" for attempt in range(retries): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f"{year} 赛季第 {attempt+1} 次请求失败:{e}") time.sleep(10) return None all_frames = [] for year in range(2020, 2025): html = fetch_season(year, headers) if html is None: print(f"跳过 {year} 赛季") continue soup = BeautifulSoup(html, "lxml") table = soup.find("table", id="per_game_stats") if table is None: continue tbody = table.find("tbody") if tbody is None: continue rows = tbody.find_all("tr") records = [] for row in rows: cells = row.find_all(["th", "td"]) if not cells: continue values = [cell.get_text(strip=True) for cell in cells] records.append(values) df = pd.DataFrame(records, columns=column_names[:len(records[0])]) df = df[df["Player"] != "Player"].dropna(how="all") df["Season"] = f"{year-1}-{str(year)[2:]}" all_frames.append(df) print(f"{year} 赛季抓取完成,共 {len(df)} 行") time.sleep(random.uniform(3, 6))random.uniform(3, 6)的含义是每次请求结束后随机等3到6秒。别小看这几秒钟,它让整个请求序列变得不那么“机器”,更接近于人类浏览节奏。我自己用这个节奏抓过2000多个页面,从来没有出过问题。
合并所有赛季数据时,记得加一列Season用来区分年份,否则几个赛季的数据堆在一起就没法做时间序列分析了。
final_df = pd.concat(all_frames, ignore_index=True) print(final_df.shape) final_df.to_csv("nba_per_game_multi_seasons.csv", index=False, encoding="utf-8-sig")4.2 遇到JS渲染页面的处理思路
静态页面用requests直接抓是理想情况,但如果你换一个数据源,发现requests.get拿回来的HTML里找不到数据表格,那十有八九是目标页面的数据是通过JavaScript异步加载的。这时候先用requests硬抓就不管用了,得换思路。
我的排查顺序是这样的:先在浏览器里打开目标页面,按F12切到Network面板,刷新页面,然后在筛选栏里选Fetch/XHR,看数据请求都发到了哪里。很多所谓“动态页面”,实际上是在你打开页面时通过一个后台接口拿到JSON数据,再渲染到表格里的。你只要从Network面板里找到这个JSON接口的URL,直接用requests去请求它,解析JSON,反而比解析HTML还省事。
举个例子,有些NBA数据分析平台的数据面板,表格对应的真实数据源是一个返回JSON的接口,接口地址和参数都藏在Network面板里。找到它之后,用resp.json()拿到Python字典或列表,剩下的数据清洗逻辑基本不用变。这个“先找接口,再上浏览器自动化”的思路,能让你的爬虫性能提升一个数量级。
只有一种情况我才会考虑上Selenium或者Playwright:数据是页面加载后通过复杂交互才渲染出来的,而且完全找不到对应接口。但即便如此,我也建议先把页面往下滚动一遍,看看是否有加载更多按钮,很多分页内容其实也是接口请求。浏览器自动化是最后的兜底方案,不是默认选择。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
爬虫写完之后,运行阶段才是真正的问题爆发期。我把这几年遇到过的典型问题整理成一张速查表,每个问题都标注了现象、可能的原因和解决办法,你跑代码的时候大概率会遇到其中一两个。
| 现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 返回403 | 缺少User-Agent / 被限流 | 加完整headers;降低请求频率;检查是否被临时封禁 |
| 返回200但找不到table | 页面改版 / 表id变了 | 打印HTML前2000字符;重新定位表格的class或id |
| 数据行数量对不上 | 有合计行、分隔行参与解析 | 只遍历tbody下的行;按Player列过滤无效行 |
| 球员名字后面带星号 | 全明星标记混入 | 用str.replace清洗,不影响分析 |
| 数值列无法计算 | 字符串类型没转换 | 用pd.to_numeric(..., errors="coerce")强制转换 |
| 中文输出乱码 | 编码不统一 | 写CSV用utf-8-sig;控制台输出前设置PYTHONIOENCODING=utf-8 |
| 抓取几个页面后报错 | IP被临时限制 | 立即停止任务;截图记录时间;等待较长时间再继续 |
| 请求超时挂起 | 未设置timeout | 所有请求统一加timeout=10,必须处理异常 |
5.2 字段错位和复杂表头的坑
最让人头疼的排错场景,是表格解析完之后字段串位。比如球员得分和年龄段位了,第一列变成年龄,第二列变成球员名。遇到这种情况,别急着改代码,先把原始HTML打印出来,认真看一行数据的单元格结构。
复杂网页表头会超出这个项目的范畴,这里只提醒一种常见结构:双行表头。有些网站为了让长表格更易读,会在一行表头之上再加一行分类表头,比如先写“投篮”,下面再拆成“命中”、“出手”、“命中率”。如果解析时直接把所有th当成一列来读,字段就全乱了。解决办法也简单:要么用thead里的最后一组th作为实际列名,要么在代码里手动定义列名,让DataFrame结构和预期完全对齐。
我在代码里用的就是手动定义column_names的方式。虽然是硬编码,但从稳定性角度看,它反而是最不容易出错的选择。爬虫这东西,追求的不只是能跑,而是长期能跑、跑挂了容不容易修。
5.3 反爬与访问伦理:别把自己弄进小黑屋
聊爬虫绕不开反爬话题,但这个项目的目标是公开数据、低频率采集,所以我只说和它相关的几条经验。
第一条,严格遵守请求间隔,别贪快。同一个人太快地重复请求,服务器会直接判定为异常行为。我在多个数据站点的经验是,单次访问间隔保持3秒以上,批量任务加随机浮动,基本不会触发风控。第二条,别拿大规模并发去测试别人的服务器,这不只是技术问题,也是基本的网络礼仪。第三条,遵重网站的公共条款和服务限制,如果你发现目标站点在条款里明确禁止爬取,就不要强行采集;尊重别人服务器的承载能力,也是在保护你自己。
最后一条经验,也是我个人的习惯:抓下来的HTML原样保存一份。爬虫和数据分析不一样,页面结构是随时可能变化的。你把原始HTML存成文件,哪怕过两天解析逻辑写错了,还能重新解析,不需要重新请求。这个小习惯在排错时能省掉大量重复请求,既快又稳。
6. 让爬完的数据直接进入分析
6.1 用pandas做第一次探索性分析
数据到手,终于到了整个项目“前置”二字兑现的时候。用pandas读CSV,做一次快速探索,验证一下数据能不能支撑起后续的分析。
import pandas as pd df = pd.read_csv("nba_per_game_multi_seasons.csv") print(df.shape) print(df.columns.tolist()) # 筛选本赛季出场数超过30场的球员,按场均得分排序 valid = df[(df["Season"] == "2023-24") & (df["G"] >= 30)] top_scorers = valid.nlargest(10, "PTS")[["Player", "Pos", "Age", "Tm", "G", "PTS", "TRB", "AST"]] print(top_scorers.to_string(index=False))这段代码把球员数据变成了真正的分析结论雏形。你可以看到场均得分榜前十名里,哪些是经验丰富的老将、哪些是正值当打之年的中生代,位置分布怎么样,助攻和篮板是不是跟着得分走。这些观察虽然没有跑复杂的模型,但已经是在做数据分析,而且每一步都基于你自己采集、清洗过的数据,感觉完全不同。
再进一步,可以做一个年龄和得分的简单相关性检查:
sample = valid[["Age", "PTS", "MP"]].dropna() print(sample.corr())出来的结果虽然只是线性相关,但在高年级号的球员往往出场时间也更多,得分跟着上涨,这个趋势用数据验证了之后,你后续再去做更复杂的多元回归,地基就是稳的。
6.2 数据源的扩展方向与增量更新
这套爬虫跑通之后,它可以向几个方向延展。
第一个方向是字段扩展。Basketball-Reference同一套URL体系下还有投篮热区、高阶统计、每36分钟数据、每百回合数据等多个页面,解析逻辑几乎一样,唯一需要改的是表格id和字段清单。抓一次多抓几种口径,分析时的视角就会丰富很多。
第二个方向是时间维度的扩展。把批量抓取从5个赛季扩展到20个赛季,就可以做球员职业生涯轨迹分析、联盟打法和节奏演进分析等长周期课题。只要保证一次一次地爬,频率控制好,数据量换来的分析价值是几何级上升的。
第三个方向是增量抓取。NBA赛季进行中,数据页面每天都在更新。你可以设计一个每天运行一次的定时任务,只抓最新日的比赛数据,追加到现有CSV或者SQLite里。这个思路就是把爬虫从一个一次性脚本升级成可持续运行的数据管道,和标题里的“数据源”定位完全吻合。
我个人在实际操作里最想提醒你的,其实还是我在前面反复说过的那句话:爬虫永远只是第一步,真正决定一个分析项目上限的,永远是数据质量。而这个质量,在你写第一行requests.get的时候就已经开始被决定了。先把数据源管好了,分析工具再多也只是锦上添花。