python-mini-projects 实战:基于 requests 与 BeautifulSoup 抓取 Medium 文章正文并保存为 TXT
2026/9/21 0:03:02 网站建设 项目流程
  • 示例工程

【免费下载链接】python-mini-projects

A collection of simple python mini projects to enhance your python skills

项目地址:https://gitcode.com/gh_mirrors/py/python-mini-projects
点击查看免费下载

导读

本文围绕 python-mini-projects 仓库中的「Scraping Medium Articles」小项目展开,讲解如何用 Python 写一个命令行脚本:用户输入一篇 Medium 文章的 URL,脚本自动抓取该页面 HTML,清洗掉标签后提取标题、作者、导语与正文,并保存为纯文本文件到本地scraped_articles目录。读完本文,你将掌握 requests 抓取网页、BeautifulSoup 解析 DOM、正则清洗文本以及文件自动落盘这一套完整的静态网页抓取流程,并能直接在本仓库目录中运行验证。

项目定位:一条命令把 Medium 文章变成本地 TXT

Medium 是全球程序员常用的技术写作平台,页面结构稳定、正文语义清晰,非常适合作为网页抓取的入门练习对象。该项目的核心目标非常聚焦:给定一篇 Medium 文章 URL,抓取其纯文本内容并落盘保存。项目入口文档位于 projects/Scraping Medium Articles/README.md,核心实现只有一个文件 scraping_medium.py,依赖仅两个第三方库,非常适合初学者读懂、改跑、再扩展。

项目目录结构如下:

projects/Scraping Medium Articles/ ├── README.md # 项目说明 ├── requirements.txt # 第三方依赖清单 ├── scraping_medium.py # 核心抓取脚本 └── scraped_articles/ # 抓取结果输出目录(已含 3 篇示例) ├── One_month_into_the_MLH_Fellowship.txt ├── One_stop_guide_to_Google_Summer_of_Code.txt └── The_Pros_and_Cons_of_Open_Source_Software.txt

scraped_articles目录中已经预置了 3 个抓取结果示例文件,用于直观展示脚本的输出格式,这也是验证脚本是否正常工作的现成参考。

环境准备与依赖安装

根据 README.md 的 Prerequisites 一节,运行前需要满足两个条件:

  1. 安装依赖:使用pip安装 requirements.txt 中列出的模块;
  2. 网络连通:设备上需要有可用的网络连接,因为脚本需要实时访问 Medium 页面。

requirements.txt 中固定了两个核心依赖及其版本:

beautifulsoup4==4.9.1 requests==2.23.0

安装命令:

pip install -r "projects/Scraping Medium Articles/requirements.txt"

说明:上述版本号是仓库中锁定的版本,若你本机 Python 环境较新,pip在安装时可能会自动升级到满足该版本约束的最新兼容版本;若遇到依赖冲突,可去掉版本号直接pip install requests beautifulsoup4

运行方式:像运行普通 Python 文件一样

README 明确指出,运行该脚本与运行任意 Python 文件无异:

python "projects/Scraping Medium Articles/scraping_medium.py"

启动后脚本会提示:

Enter url of a medium article:

粘贴一篇 Medium 文章链接并回车,脚本随即完成抓取、清洗与保存,并在终端打印保存位置,例如:

File saved in directory ./scraped_articles/One_month_into_the_MLH_Fellowship.txt

整个交互只有一次输入,属于典型的「一次性输入、自动处理」的命令行工具。

源码深度解析:四步完成一篇文章的抓取

虽然脚本只有 70 余行,却完整覆盖了网页抓取的四个核心环节。下面按 scraping_medium.py 的代码顺序逐段剖析。

1. 定位脚本目录,保证输出路径稳定

脚本开头有一行容易被忽略的关键代码(scraping_medium.py):

# switching to current running python files directory os.chdir('\\'.join(__file__.split('/')[:-1]))

它把当前工作目录切换到脚本自身所在目录,从而确保后续创建的./scraped_articles输出目录始终位于脚本旁边,而不是用户运行命令时所在的随机目录。这样做的好处是:无论从哪个路径调用脚本,输出位置都是确定且可预期的。

2.get_page():URL 校验 + 页面请求与解析

get_page()函数(scraping_medium.py)负责三个阶段的工作:

def get_page(): global url url = input('Enter url of a medium article: ') # handling possible error if not re.match(r'https?://medium.com/',url): print('Please enter a valid website, or make sure it is a medium article') sys.exit(1) res = requests.get(url) res.raise_for_status() soup = BeautifulSoup(res.text, 'html.parser') return soup
  • URL 合法性校验:用正则r'https?://medium.com/'检查用户输入,只接受以http://medium.com/https://medium.com/开头的地址,否则打印提示并以sys.exit(1)退出。这一步有效拦截了误输入的非 Medium 链接。
  • 发起 HTTP 请求:通过requests.get(url)拉取页面,随后调用res.raise_for_status():当响应状态码为 4xx/5xx 时该方法会抛出异常,避免把错误页面当正文解析。
  • 构造解析树:用BeautifulSoup(res.text, 'html.parser')把 HTML 文本解析成可遍历的文档树,供后续步骤提取内容。

3.purify():正则驱动的 HTML 标签清洗

purify()函数(scraping_medium.py)负责把带有标签的 HTML 片段还原为可读的纯文本:

def purify(text): rep = {"<br>": "\n", "<br/>": "\n", "<li>": "\n"} rep = dict((re.escape(k), v) for k, v in rep.items()) pattern = re.compile("|".join(rep.keys())) text = pattern.sub(lambda m: rep[re.escape(m.group(0))], text) text = re.sub('\<(.*?)\>', '', text) return text

它的处理逻辑分两步:

  1. 特殊标签替换为换行:先把<br><br/><li>这三种标签替换成\n,使得换行语义被保留下来。实现上用re.escape对替换键做转义后拼接成正则,再通过pattern.sub配合 lambda 完成映射替换,避免替换结果被后续正则再次误伤。
  2. 删除其余所有标签:用re.sub(r'\<(.*?)\>', '', text)把剩下的所有尖括号标签(如<p><a><strong>等)整体移除,只保留纯文本内容。

这种「先保留换行语义、再删除标签」的两段式清洗,是静态页面文本抽取的常用手法,比简单粗暴地删除所有标签能得到更接近原文排版的结果。

4.collect_text():按 Medium 页面结构组装正文

collect_text()(scraping_medium.py)是整个脚本的信息组织核心,它依据 Medium 页面的 DOM 结构规律来提取并拼装内容:

def collect_text(soup): fin = f'url: {url}\n\n' main = (soup.head.title.text).split('|') global title title = main[0].strip() fin += f'Title: {title.upper()}\n{main[1].strip()}' header = soup.find_all('h1') j = 1 try: fin += '\n\nINTRODUCTION\n' for elem in list(header[j].previous_siblings)[::-1]: fin += f'\n{purify(str(elem))}' except: pass fin += f'\n\n{header[j].text.upper()}' for elem in header[j].next_siblings: if elem.name == 'h1': j+=1 fin += f'\n\n{header[j].text.upper()}' continue fin += f'\n{purify(str(elem))}' return fin

这一段暗含了对 Medium 页面结构的几个假设,结合仓库中 scraped_articles 的示例输出可以一一印证:

  • 标题与作者来源:Medium 页面<title>的格式通常是「文章标题 | 出版物名称」。脚本用split('|')拆分,main[0]作为文章标题并转大写,main[1]作为作者/出版物署名行,拼成Title: xxx与署名两行。示例文件开头的Title: ONE MONTH INTO THE MLH FELLOWSHIPby Kunal Kushwaha正是这一逻辑的直接产物。
  • 导语(INTRODUCTION)提取:取页面中所有<h1>标签,把第二个<h1>之前的所有兄弟节点反向遍历并清洗,作为 INTRODUCTION 段——即文章头部作者信息、阅读时长等元数据。
  • 正文分节提取:以第二个<h1>为起点向后遍历兄弟节点:遇到新的<h1>就换行输出该小节标题(转大写),普通节点则经purify()清洗后追加为正文。Medium 正文中的小标题本身就是<h1>,所以脚本天然地把文章切成多个「大写标题 + 段落」的小节。

示例输出中INTRODUCTIONWEEK 1WEEK 2等小节标题全部大写、段落之间以空行分隔的排版,正是上述组装逻辑的运行结果。

5.save_file():自动建目录并按标题命名落盘

save_file()(scraping_medium.py)负责持久化:

def save_file(fin): if not os.path.exists('./scraped_articles'): os.mkdir('./scraped_articles') fname = './scraped_articles/' + '_'.join(title.split()) + '.txt' with open(fname, 'w', encoding='utf8') as outfile: outfile.write(fin) print(f'File saved in directory {fname}')
  • scraped_articles目录不存在则自动创建;
  • 文件名取自文章标题,把标题中的空格替换为下划线('_'.join(title.split())),如One_month_into_the_MLH_Fellowship.txt
  • utf8编码写入,兼容文章中的中文、emoji 等非 ASCII 字符;
  • 落盘后打印保存路径,方便用户确认结果。

6. 驱动入口

文件末尾的标准入口(scraping_medium.py)把上述函数串成完整流水线:

if __name__ == '__main__': fin = collect_text(get_page()) save_file(fin)

get_page()返回解析树 →collect_text()组装成纯文本 →save_file()写入磁盘,一条主线完成全部抓取任务。

输出文件格式规范

以仓库自带的 One_month_into_the_MLH_Fellowship.txt 为例,输出文件的固定结构为:

url: https://medium.com/<...>/<slug> Title: 文章标题(大写) by 作者署名 INTRODUCTION 导语与元信息段落 小节标题(大写) 小节正文段落 ...

即:首行保留原始 URL 便于溯源;随后是标题与署名;INTRODUCTION段收纳文章头部信息;之后每个<h1>小标题独立成节、大写展示。这种「保留来源 + 结构化分节」的格式,让抓取结果既适合直接阅读,也方便后续做文本分析、语料库构建等二次加工。

边界条件与已知限制(基于源码实现)

根据代码逻辑,脚本存在以下客观边界,使用时需要留意:

  1. 仅接受https?://medium.com/前缀的 URL:正则校验决定了它只能处理 Medium 主域下的文章,诸如子域名、自定义域或非 Medium 链接都会被拒绝并退出。
  2. 强依赖 Medium 的 DOM 结构<title>的「标题 | 出版物」格式、<h1>作为正文小标题等假设均针对 Medium 页面设计。若 Medium 调整页面结构,INTRODUCTION的提取可能因异常被try/except静默跳过,正文分节也可能出现偏差——脚本本身没有对这些分支做显式告警。
  3. 页面结构假设通过但内容缺失时会静默处理collect_text()header[j]的索引依赖页面至少存在两个<h1>,极端情况下可能触发IndexError或异常分支。
  4. 纯文本输出,不保留图片与链接purify()会删除所有标签,因此文章中的图片、代码块高亮、超链接等富文本信息都会丢失,只留下线性文字。

这些限制并非缺陷,而是小型抓取脚本「够用即可」的典型取舍,也为读者预留了改造空间。

结合源码的扩展思路

在理解现有实现后,可以基于同一骨架做如下扩展(均不涉及修改仓库,仅作为思路参考):

  • 批量抓取:把input()改成从文件或参数列表循环读取多个 URL,复用get_page → collect_text → save_file流水线即可;
  • 增加请求头与限速:为requests.get添加User-Agenttime.sleep,降低对目标站点的访问压力;
  • 保留 Markdown 结构:在purify()的替换字典中加入<h1>#<strong>**等映射,直接输出 Markdown;
  • 编码健壮性:对main列表长度做防御性判断,避免<title>格式变化时越界。

小结

「Scraping Medium Articles」是一个小而完整的爬虫教学样例:requests负责取回页面,BeautifulSoup负责解析结构,正则负责清洗标签,最后以 UTF-8 落盘成结构化 TXT。通过本仓库的 README.md、scraping_medium.py 与 scraped_articles 示例三者对照,读者可以完整复现「输入 URL → 输出 TXT」的抓取链路,并以此为模板迁移到其他静态内容网站的文本抽取任务中。

  • 示例工程

【免费下载链接】python-mini-projects

A collection of simple python mini projects to enhance your python skills

项目地址:https://gitcode.com/gh_mirrors/py/python-mini-projects
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询