☰
Python爬取网页导出Word:从抓取到排版完整指南
2026/10/15 9:34:53 网站建设 项目流程

简介:这份资源面向具备一定Python基础的爬虫学习者与Web数据管理初学者,围绕「用Python抓取网页内容并存入MongoDB」这一典型场景展开,帮助读者理解文档型数据库与传统关系型数据库的差异,掌握从请求网页、解析HTML到持久化存储的完整链路。包内共1个docx文档,压缩包约608KB,内容以图文与代码示例为主,涵盖requests发送请求、BeautifulSoup解析页面、pymongo连接数据库及insert_one写入文档等关键环节,并穿插MongoDB中文档、集合、数据库等核心概念的讲解。目前已有11252人学习下载,适合希望把爬取结果结构化保存、并借助MongoDB灵活模式与索引能力提升检索效率的读者参考,也可作为课程设计或小型数据采集项目的实践素材。

1. 用 Python 爬取网页并导出为 Word:从抓取到排版的完整落地路径

很多人第一次接到「把某个网页内容整理成 Word 文档」的需求时,第一反应是手动复制粘贴。页面少还能忍,一旦要处理几十上百个页面,或者需要每天定时更新,手工操作就会变成灾难。用 Python 爬取网页并导出为 Word 文档,本质上是一条「抓取 → 解析 → 清洗 → 排版 → 落盘」的流水线,核心工具组合通常是 requests 负责拿页面、BeautifulSoup 或 lxml 负责解析、python-docx 负责生成 .docx 文件。这套方案适合需要批量归档公告、整理行业资讯、生成周报素材的从业者,也适合刚学完 Python 基础语法想找一个完整练手项目的开发者。它不追求分布式爬虫的吞吐量,而是把重点放在「内容结构还原」和「文档可读性」上——这两点恰恰是复制粘贴最容易丢失的东西。

2. 抓取层选型:requests 与解析器的组合怎么定

2.1 静态页面与动态渲染的判断方法

在写任何代码之前,先要判断目标页面是静态返回还是依赖 JavaScript 渲染。最直接的办法是打开浏览器开发者工具的 Network 面板,禁用 JavaScript 后刷新页面,如果正文内容还在,说明是静态页面,requests 就能拿到;如果正文消失,说明内容由前端脚本注入,requests 拿到的 HTML 里只有空壳。这个判断决定了后续技术栈:静态页面用 requests + BeautifulSoup 足够,动态页面则需要考虑 Selenium 或 Playwright 这类能驱动真实浏览器的工具。我一般会先用 requests 试一次,看返回文本里有没有目标关键词,没有再做动态方案,避免过早引入重型依赖。

2.2 最小可运行抓取脚本与请求头配置

下面这段代码是一个最小可运行的抓取骨架,包含请求头伪装、超时设置和编码处理。很多新手拿到的 HTML 中文乱码,问题往往出在编码判断上,这里用 apparent_encoding 做兜底。

import requests from bs4 import BeautifulSoup # 请求头里带上 UA,避免被服务端直接拒绝 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-Language": "zh-CN,zh;q=0.9", } url = "https://example.com/article/123" # timeout 必须设,否则网络卡住时脚本会一直挂着 resp = requests.get(url, headers=headers, timeout=10) # 先看响应头里的编码,再用 apparent_encoding 兜底 resp.encoding = resp.apparent_encoding # 用 lxml 解析比 html.parser 快,容错也更好 soup = BeautifulSoup(resp.text, "lxml") # 先定位标题,不同站点标签不同,常见是 h1 或带特定 class 的 div title_tag = soup.find("h1") title = title_tag.get_text(strip=True) if title_tag else "未命名文档" print(title) print(resp.status_code)

这段代码里几个参数值得展开说。timeout=10 是连接和读取的总超时,生产环境建议拆成 connect timeout 和 read timeout 两个值,比如 timeout=(5, 15),连接阶段快速失败,读取阶段给足时间。resp.apparent_encoding 会调用 chardet 做编码探测,比直接写死 utf-8 更稳,但探测本身有开销,如果目标站点编码固定,直接指定更快。解析器选 lxml 而不是 html.parser,是因为 lxml 基于 C 实现,解析大页面时速度差距明显,而且对残缺 HTML 的修复能力更强。

2.3 正文提取的三种定位策略

拿到 soup 之后,怎么准确提取正文是第一个分水岭。常见做法有三种:按标签路径定位、按 class/id 定位、按文本密度启发式提取。按标签路径最直接,比如 soup.find("div", class_="article-content"),但一旦站点改版就会失效。按文本密度适合结构不固定的页面,思路是遍历所有 div,计算其中 p 标签文本长度占比,占比最高的那个通常就是正文容器。我一般会先看目标站点的 HTML 结构,如果 class 命名语义清晰就用第二种,结构混乱才上第三种。提取到的内容不要急着写 Word,先存成中间结构,比如一个包含 title、paragraphs、images 的字典,这样排版层和抓取层解耦,后面换输出格式也不用重写抓取逻辑。

3. 用 python-docx 生成文档:样式、段落与图片的写入

3.1 文档骨架的初始化与样式继承

python-docx 的核心对象是 Document,它默认基于一个空白模板。如果你需要公司或实验室的统一格式,可以准备一个 .docx 模板文件,用 Document("template.docx") 加载,这样标题样式、页边距、字体都会继承模板设置。下面这段代码演示了从零创建文档并写入标题和段落的基本流程。

from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH # 加载模板可以继承预设样式,没有模板就用默认 doc = Document() # 添加一级标题,level=1 对应 Heading 1 样式 doc.add_heading("抓取结果汇总", level=1) # 添加正文段落 para = doc.add_paragraph("这是从目标页面提取的第一段正文内容。") # 对段落里的 run 做精细控制,比如字号和颜色 run = para.runs[0] run.font.size = Pt(12) run.font.color.rgb = RGBColor(0x33, 0x33, 0x33) # 设置段落对齐方式 para.alignment = WD_ALIGN_PARAGRAPH.LEFT doc.save("output.docx")

这里的关键点是理解 paragraph 和 run 的关系。一个段落可以包含多个 run,每个 run 是一段连续的同格式文本。add_paragraph 返回的是段落对象,直接设置 paragraph.font 会影响整个段落,但更推荐对 run 操作,因为实际排版中经常需要一段话里部分加粗、部分变色。add_heading 的 level 参数对应 Word 里的标题级别,导出后可以直接生成导航目录,这对长文档很实用。

3.2 图片下载与嵌入的完整链路

网页正文里的图片不能直接引用 URL,Word 文档需要把图片数据嵌入进去。流程是先用 requests 下载图片二进制流,再用 BytesIO 包装成文件对象,最后通过 add_picture 插入。下面这段代码处理了图片下载失败和尺寸限制两个常见问题。

import requests from io import BytesIO from docx.shared import Inches def add_image_from_url(doc, img_url, max_width=Inches(5.5)): try: # 图片请求也要带 UA,部分站点会校验来源 resp = requests.get(img_url, timeout=15) resp.raise_for_status() img_stream = BytesIO(resp.content) # 先按最大宽度插入,高度自动等比缩放 doc.add_picture(img_stream, width=max_width) except Exception as e: # 图片失败不能中断整个文档生成,记录占位即可 doc.add_paragraph(f"[图片加载失败: {img_url}]") print(f"图片下载异常: {e}")

max_width 设成 5.5 英寸是因为 A4 纸默认页边距下,正文区域宽度大约就是这个值,超过会导致图片被裁切或溢出页面。add_picture 只指定 width 时,python-docx 会自动按原始宽高比计算高度,不用手动算。异常处理里把失败图片替换成文字占位,是为了保证文档结构完整,不至于因为一张图挂掉整个任务。

3.3 表格数据的结构化写入

如果抓取的内容包含表格,比如统计数据或参数列表,用 add_table 比手动拼段落更合适。下面是一个把二维列表写入 Word 表格的例子。

def add_table_to_doc(doc, data): if not data: return rows, cols = len(data), len(data[0]) table = doc.add_table(rows=rows, cols=cols) # 使用内置的网格样式,导出后带边框 table.style = "Table Grid" for i, row in enumerate(data): for j, cell_text in enumerate(row): table.cell(i, j).text = str(cell_text)

table.style 设为 "Table Grid" 会应用带边框的样式,这是 python-docx 内置样式名,不用额外定义。注意 cell.text 赋值会覆盖单元格原有内容,如果需要保留格式要操作 cell.paragraphs。表格数据量大的时候,建议先做一次列宽估算,避免导出后列宽不均影响阅读。

4. 避坑与排查:编码、反爬与文档损坏的常见问题

4.1 中文乱码:现象是导出文档里全是问号或方块

原因通常有两个:一是 requests 没有正确设置 encoding,二是 python-docx 写入时字符串本身已经是错误编码。解决方法是抓取阶段打印 resp.encoding 和 resp.apparent_encoding 对比,如果两者不一致且页面 meta 里声明了 charset,以 meta 声明为准手动设置。写入 Word 前,确保所有字符串都是 Python 的 str 类型,不要传入 bytes。

4.2 请求被拒:现象是返回 403 或验证码页面

原因是服务端识别出请求不是来自真实浏览器。解决方法是补全请求头,除了 User-Agent,还要带上 Referer、Accept、Accept-Encoding 等字段。如果仍然被拒,检查请求频率是否过高,适当加 time.sleep 随机延迟。注意不要用多线程猛刷同一个站点,这既容易触发封禁,也不是这套方案的设计目标。

4.3 文档打不开:现象是生成的 .docx 用 Word 打开提示文件损坏

原因多半是图片流写入时没有正确关闭或定位。BytesIO 对象在 add_picture 之后不需要手动关闭,但如果同一个流被多次读取,指针会停在末尾导致后续读取为空。解决方法是每次插入图片都新建一个 BytesIO,不要复用。另外,doc.save 之前确保没有未捕获的异常中断了文档对象的状态。

4.4 段落顺序错乱:现象是导出后文字和图片顺序与网页不一致

原因是抓取阶段把文本和图片分开存储,写入时按类型批量处理,丢失了原始顺序。解决方法是在解析阶段就按 DOM 顺序遍历,遇到文本节点记文本,遇到 img 标签记图片 URL,生成一个有序列表,写入时按列表顺序逐项处理。这个改动不大,但能避免后期大量返工。

4.5 样式不生效:现象是设置了字体但导出后没变化

原因是 python-docx 的样式继承有优先级,直接对 run 设置 font 通常有效,但如果模板里定义了更具体的样式规则,可能会覆盖。解决方法是先用 doc.styles 查看可用样式名,优先通过样式名控制,而不是逐个 run 设置。如果必须精细控制,确保在 add_run 之后立即设置属性,不要等段落写完了再回头改。

5. 进阶技巧:批量任务编排与文档质量校验

单页抓取跑通之后,真正的挑战在于批量处理和结果校验。我一般会把整个流程拆成三个独立函数:fetch_page 负责拿 HTML 并返回解析后的结构化数据,build_docx 负责把结构化数据写成文档,validate_docx 负责检查生成结果。这样做的好处是每一层都可以单独测试,出问题时能快速定位是抓取挂了还是排版挂了。

批量任务编排上,不建议一上来就上多线程。先用 for 循环串行跑十个页面,观察总耗时和失败率。如果瓶颈确实在网络 IO,再考虑用 concurrent.futures 的 ThreadPoolExecutor,把并发数控制在 3 到 5 之间。下面是一个带重试和断点记录的批量骨架。

import time import json from pathlib import Path def batch_process(url_list, output_dir="output"): Path(output_dir).mkdir(exist_ok=True) # 用 json 记录已完成的 URL,支持断点续跑 progress_file = Path(output_dir) / "progress.json" done = set() if progress_file.exists(): done = set(json.loads(progress_file.read_text(encoding="utf-8"))) for idx, url in enumerate(url_list): if url in done: continue try: data = fetch_page(url) # 抓取与解析 doc = build_docx(data) # 生成文档对象 doc.save(Path(output_dir) / f"doc_{idx}.docx") done.add(url) # 每完成一个就落盘进度,避免中途崩溃丢状态 progress_file.write_text( json.dumps(list(done), ensure_ascii=False), encoding="utf-8" ) except Exception as e: print(f"处理失败 {url}: {e}") # 随机延迟,降低对目标站点的压力 time.sleep(1.5)

这段代码里 progress.json 是关键,它让批量任务具备断点续跑能力。实际跑几十个页面时,中途遇到网络抖动或某个页面结构异常是常态,没有进度记录就得从头再来。延迟设 1.5 秒是经验值,既不会太慢,也能明显降低被限流的概率。

文档质量校验这块,我习惯在生成后做三项检查:段落数是否为零、图片占位符数量是否超过阈值、文档能否被 python-docx 重新打开。第三项尤其重要,因为有些损坏在保存时不会报错,只有重新读取才会暴露。下面这个校验函数可以直接复用。

from docx import Document def validate_docx(file_path, min_paragraphs=3, max_failed_images=2): try: doc = Document(file_path) except Exception as e: return False, f"文档无法打开: {e}" paras = [p.text for p in doc.paragraphs if p.text.strip()] if len(paras) < min_paragraphs: return False, f"有效段落过少: {len(paras)}" failed = sum(1 for p in paras if "图片加载失败" in p) if failed > max_failed_images: return False, f"失败图片过多: {failed}" return True, "校验通过"

min_paragraphs 和 max_failed_images 这两个阈值要根据实际页面调整。资讯类页面正文段落通常不少于 5 段,设 3 是保守下限;图片失败容忍度设 2 是因为偶发网络问题不可避免,但超过 2 张就说明抓取环节有系统性问题,需要回头检查图片 URL 的提取逻辑。

最后说一个我踩过的坑:早期版本里我把所有页面的内容写进同一个 Word 文档,结果文档越来越大,打开越来越慢,一次失败还导致整个文件损坏。后来改成每个页面独立生成一个 .docx,再用一个汇总脚本按需合并。合并时用 docxcompose 这类库比手动复制段落更可靠,因为它能保留样式和图片关系。这个改动让单点故障不再影响全局,也方便单独重跑失败页面。希望帮到你。

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

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

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

立即咨询