
简介本资源是一份面向Python初学者与Web数据采集实践者的完整技术文档聚焦于网页爬取与非关系型数据库存储的端到端实现。内容以深圳市规划和自然资源局公示页面为真实案例系统讲解如何使用requests发起请求、lxml与BeautifulSoup解析HTML、正则表达式提取关键字段并将结构化结果存入MongoDB——突出其文档型特性对网页内容如嵌套公示文本、动态字段的天然适配优势。资源为单个608KB的Word文档.docx内含MongoDB核心概念详解文档/集合/数据库、环境配置、分步代码示例、字段设计逻辑及常见连接排错说明便于离线研读与代码复现。目前已有11259人学习下载适合希望掌握“爬虫NoSQL”轻量级数据采集方案、规避传统关系型数据库建模约束的开发者快速上手并理解工程落地要点。1. 爬完就丢不这是一套能直接进毕设答辩、课设验收、甚至交付给导师看的「网页内容归档方案」你是不是也试过用 requests BeautifulSoup 抓完几十页公示存成一堆 txt 或 csv结果导师问“数据怎么查怎么复现附件怎么跟进标题乱码怎么解”——当场卡壳。这不是代码写得不对是缺一套闭环设计从目标网站结构反推爬取逻辑用文档型数据库天然匹配 HTML 内容的嵌套性再把非结构化文本元信息附件线索打包成可检索、可导出、可回溯的完整单元。本方案以深圳市规划和自然资源局公示页为真实靶场非模拟全程基于 Python 实现MongoDB 存储结构完全贴合网页 DOM 层级最后导出 Word 文档时保留标题层级、段落缩进、超链接可点击——不是截图不是纯文本 dump是真正能当工作成果交出去的交付物。适合毕设/课设需要体现“数据获取→存储→组织→呈现”全链路能力的同学也适合想快速验证一个政务类网页归档流程的工程师。2. 总体架构设计三层流水线与四个核心模块的职责划分本方案采用清晰的三层流水线架构采集层 → 存储层 → 导出层每层解耦、可独立测试、失败不阻塞后续。其中采集层又细分为链接发现、页面解析、附件映射三个子模块存储层严格遵循 MongoDB 文档模型特性不做关系型建模导出层则绕过“先转 Excel 再粘贴到 Word”的玄学操作直连 python-docx API 构建语义化文档结构。整套流程不依赖任何 GUI 工具如 Robo 3T 可视化客户端仅作验证用所有操作均可通过命令行触发、日志可追溯、配置可参数化。2.1 链接发现模块动态生成分页 URL 并提取公示详情页链接原始代码中硬编码了index_{}.html的页码范围1–33但实际政务网站分页常有跳变、断页、或末页无index_N.html而是index.html?pageN。我们改用更鲁棒的方式先请求首页解析页码导航栏提取所有有效分页链接再统一处理。这样即使网站改版只需调整 XPath 表达式无需修改循环逻辑。def get_all_pagination_urls(base_url: str) - list: 从首页自动提取全部分页链接兼容无序/跳页/末页无数字后缀等情况 res requests.get(base_url, headersheaders, timeout10) res.encoding res.apparent_encoding selector etree.HTML(res.text) # 尝试匹配常见分页结构数字页码、下一页文字链接、末页按钮 page_links selector.xpath(//div[classpagination]//a/href | //ul[classpagination]//a/href) # 过滤掉空链接、#锚点、javascript:void(0) valid_urls [] for link in page_links: if not link or link.startswith(#) or javascript in link.lower(): continue full_url urljoin(base_url, link) if full_url not in valid_urls: valid_urls.append(full_url) # 始终包含首页 if base_url not in valid_urls: valid_urls.insert(0, base_url) return list(set(valid_urls)) # 去重 # 使用示例 base_page http://pnr.sz.gov.cn/xxgk/gggs/index.html all_pages get_all_pagination_urls(base_page) print(f共发现 {len(all_pages)} 个有效分页URL)提示urljoin()是urllib.parse模块中的函数用于安全拼接相对路径与基础 URL避免手动字符串拼接导致的双斜杠或协议丢失问题。务必导入from urllib.parse import urljoin。该模块输出的是标准化后的绝对 URL 列表后续所有请求均基于此列表发起不再依赖预设页码范围。这是整个采集链路稳定性的第一道防线——链接发现不准后面全白干。2.2 页面解析模块结构化提取标题、来源、日期、正文、附件五要素原始代码中对//div[classxxym]/h5/text()的两次索引[0]和[1]极易因 HTML 结构微调而越界报错比如某页少了一行来源信息。我们改为用命名提取 容错兜底确保每个字段都有明确语义和 fallback 值def extract_page_content(selector: etree.HTML) - dict: 结构化提取单页公示内容返回带默认值的字典 content { title: , source: 未知来源, date: 未知日期, article: , appendix_links: [] } # 标题优先取 xxym 下 h4若无则尝试 title 标签 title_nodes selector.xpath(//div[classxxym]/h4/text()) if title_nodes: content[title] title_nodes[0].strip() else: title_tag selector.xpath(//title/text()) content[title] title_tag[0].strip() if title_tag else 无标题 # 来源 日期统一从 xxym 下 h5 提取用正则分离 h5_nodes selector.xpath(//div[classxxym]/h5/text()) if h5_nodes: combined_text .join([t.strip() for t in h5_nodes]) # 匹配“来源XXX”和“日期YYYY-MM-DD”等常见格式 source_match re.search(r来源[:]\s*(.?)(?[\n\t]|$), combined_text) date_match re.search(r(?:日期|时间|发布)[:]\s*(\d{4}[-年\.]\d{1,2}[-月\.]\d{1,2}), combined_text) if source_match: content[source] source_match.group(1).strip() if date_match: content[date] date_match.group(1).replace(年, -).replace(月, -).replace( , ) # 正文取 TRS_Editor 下所有 p同时兼容 div、section 等容器 article_nodes selector.xpath(//div[classTRS_Editor]//p/text() | //div[classTRS_Editor]//div/p/text() | //div[classTRS_Editor]//section/p/text()) content[article] \n.join([p.strip() for p in article_nodes if p.strip()]) # 附件提取所有 fa-caret-right 图标后的 a 标签并补全 URL appendix_anchors selector.xpath(//i[classfa fa-caret-right]/following-sibling::a[href]/href | //span[contains(class,icon-attach)]/following-sibling::a[href]/href) for href in appendix_anchors: full_href urljoin(selector.base_url, href.strip()) if full_href not in content[appendix_links]: content[appendix_links].append(full_href) return content这段代码的关键设计点在于字段解耦每个字段独立提取、独立容错一个字段失败不影响其他字段语义优先用正则匹配“来源”“日期”等中文标识符而非依赖固定索引位置容器泛化正文提取覆盖p在div、section等不同父容器下的嵌套情况附件健壮不仅抓fa-caret-right后的链接还兼容icon-attach等常见附件图标类名。这才是生产级页面解析该有的样子——不是靠人眼数第几个h5而是让代码自己读懂网页的语义结构。2.3 存储层设计MongoDB 文档结构严格对齐网页信息粒度原始代码中insert_one()直接写入扁平字典但实际政务公示存在多附件、多段落、多责任人等嵌套需求。我们升级为嵌套文档结构既保持 BSON 原生支持又为后续导出 Word 的多级标题、附件列表预留扩展性# MongoDB 文档结构定义Python dict 形式 mongo_doc { _id: ObjectId(), # 自动生成唯一ID url: http://pnr.sz.gov.cn/xxgk/gggs/201911/t20191114_489608.html, crawl_time: datetime.now(), # 爬取时间戳用于版本控制 metadata: { title: 关于XX项目规划许可的公示, source: 深圳市规划和自然资源局, publish_date: 2019-11-14, update_date: 2019-11-14, # 若页面有更新时间则填入 status: 已发布 # 后续可扩展为“已撤回”“已过期” }, content: { raw_html: , # 可选存原始 HTML 片段需控制长度建议 1MB text_blocks: [ {type: paragraph, text: 根据《城乡规划法》...}, {type: paragraph, text: 一、项目基本情况}, {type: list_item, text: 1. 项目名称XX大厦}, {type: list_item, text: 2. 建设单位YY公司} ], attachments: [ { name: 附件1项目总平面图.pdf, url: http://pnr.sz.gov.cn/xxgk/gggs/attach/201911/t20191114_489608_1.pdf, size_kb: 1245, mime_type: application/pdf } ] } }这个结构的价值在于metadata层封装所有可检索元信息方便按publish_date建索引、按source聚合统计content.text_blocks显式区分段落类型paragraph/list_item/heading为 Word 导出时自动应用样式提供依据content.attachments是数组天然支持多附件且每个附件含name、url、size_kb导出 Word 时可生成可点击超链接文件大小标注crawl_time是关键时间戳同一公示若多次爬取可通过该字段判断最新版本避免脏数据覆盖。MongoDB 不是“把 JSON 往里塞就行”而是用文档结构表达业务语义。这个设计让后续所有操作——查、导、展、验——都有据可依。2.4 导出层实现python-docx 构建语义化 Word 文档支持标题层级与超链接原始需求是“导出为 word 文档”但很多方案只是把所有文本.add_paragraph()一遍结果 Word 里全是正文样式无法生成目录、无法折叠章节、附件链接不可点击。我们用python-docx精确控制样式from docx import Document from docx.shared import Pt, Inches from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml.ns import qn from docx.oxml import OxmlElement def create_word_document(doc_data: dict, output_path: str): 根据 MongoDB 文档生成结构化 Word 文件 doc Document() # 设置默认字体解决中文显示问题 style doc.styles[Normal] font style.font font.name SimSun font.size Pt(12) # 设置中文字体关键否则宋体不生效 rpr style.element.rPr rfonts rpr.find(qn(w:rFonts)) if rfonts is None: rfonts OxmlElement(w:rFonts) rpr.append(rfonts) rfonts.set(qn(w:eastAsia), SimSun) # 标题一级标题 title_para doc.add_heading(doc_data[metadata][title], level0) title_para.alignment WD_PARAGRAPH_ALIGNMENT.CENTER # 元信息区二号小标题 表格布局 doc.add_heading(公示基本信息, level1) table doc.add_table(rows3, cols2, styleTable Grid) table.cell(0,0).text 来源 table.cell(0,1).text doc_data[metadata][source] table.cell(1,0).text 发布日期 table.cell(1,1).text doc_data[metadata][publish_date] table.cell(2,0).text 爬取时间 table.cell(2,1).text doc_data[crawl_time].strftime(%Y-%m-%d %H:%M:%S) # 正文内容按 text_blocks 类型应用不同样式 doc.add_heading(公示内容, level1) for block in doc_data[content][text_blocks]: if block[type] heading: doc.add_heading(block[text], level2) elif block[type] list_item: p doc.add_paragraph(block[text], styleList Bullet) else: # paragraph p doc.add_paragraph(block[text]) p.paragraph_format.first_line_indent Inches(0.3) # 首行缩进2字符 # 附件列表生成可点击超链接 if doc_data[content][attachments]: doc.add_heading(附件下载, level1) for att in doc_data[content][attachments]: p doc.add_paragraph() runner p.add_run(f● {att[name]} ({att[size_kb]} KB)) # 添加超链接关键步骤 add_hyperlink(p, att[url], att[url]) doc.save(output_path) print(fWord 文档已生成{output_path}) def add_hyperlink(paragraph, url: str, text: str): 向段落添加超链接python-docx 官方未直接支持需手动构造 XML part paragraph.part r_id part.relate_to(url, hyperlink, is_externalTrue) hyperlink OxmlElement(w:hyperlink) hyperlink.set(qn(r:id), r_id) new_run OxmlElement(w:r) r_pr OxmlElement(w:rPr) new_run.append(r_pr) new_run.text text hyperlink.append(new_run) r paragraph.add_run() r._r.append(hyperlink) r.font.color.theme_color MSO_THEME_COLOR_INDEX.HYPERLINK r.font.underline True这段代码解决了三个 Word 导出的核心痛点中文字体失效通过rFonts强制设置eastAsia字体为 SimSun宋体避免默认 Times New Roman 显示乱码样式失控用styleList Bullet等内置样式替代手动加符号保证 Word 目录自动生成附件不可用add_hyperlink()函数手动注入w:hyperlinkXML 节点使附件链接在 Word 中真正可点击、可跳转。这不是“能导出”而是导出即可用、即专业、即符合政务文档规范。3. 关键技术选型与避坑指南为什么选 requestslxmlpymongopython-docx而不是其他组合3.1 为什么不用 BeautifulSouplxml 的 XPath 在政务网站中更稳BeautifulSoupBS4语法简洁但面对政务网站常见的不规范 HTML如缺失闭合标签、嵌套错乱、JS 动态插入内容其树形解析容易错位。而 lxml 基于 libxml2对 malformed HTML 容错更强且 XPath 表达式可精准定位“第 N 个 class 为 xxym 的 div 下的第 2 个 h5”这种确定性在结构稳定的政务公示页中是刚需。实测对比同一页面BS4 解析h5文本时因标签错位导致find_all(h5)返回空列表而 lxml 的xpath(//div[classxxym]/h5/text())仍能稳定命中。3.2 为什么 MongoDB 不用 _id 当 Word 文件名用 publish_datetitle 更可靠原始代码中_id是 ObjectId形如5f8b1a2c3d4e5f6789012345毫无业务意义。若导出 Word 用此命名用户根本无法从文件名判断内容。我们改用publish_date title组合生成文件名清洗非法字符后例如2019-11-14_关于XX项目规划许可的公示.docx。这样既保证唯一性日期标题足够区分又具备可读性方便人工归档、邮件发送、导师抽查。3.3 为什么附件链接要urljoin()而非字符串拼接政务网站附件路径极不规范可能为./attach/1.pdf、/xxgk/attach/1.pdf、http://xxx.gov.cn/attach/1.pdf甚至//cdn.gov.cn/attach/1.pdf协议相对。requests.get()会自动处理前三种但python-docx的超链接必须是完整 URL 才能点击。urljoin(base_url, href)能正确处理所有情况urljoin(http://pnr.sz.gov.cn/xxgk/gggs/, ./1.pdf)→http://pnr.sz.gov.cn/xxgk/gggs/1.pdfurljoin(http://pnr.sz.gov.cn/xxgk/gggs/, /attach/1.pdf)→http://pnr.sz.gov.cn/attach/1.pdfurljoin(http://pnr.sz.gov.cn/xxgk/gggs/, //cdn.gov.cn/1.pdf)→https://cdn.gov.cn/1.pdf自动补协议手写字符串拼接在遇到/开头路径时会丢掉域名这是翻车高发区。3.4 常见问题排查5 条血泪经验总结现象 1爬取时大量页面返回 403 Forbidden原因目标网站启用了 User-Agent 黑名单或检测到非浏览器请求头。原始代码中 UA 字符串虽长但已被部分 WAF 识别为爬虫模板。解决使用fake-useragent库动态生成 UA每次请求随机切换from fake_useragent import UserAgent ua UserAgent() headers {User-Agent: ua.random}同时添加Accept、Accept-Language、Referer头模拟真实访问Referer 设为上级列表页 URL。现象 2MongoDB 插入后 Robo 3T 查不到数据或数据乱码原因未设置res.encodingres.text默认用ISO-8859-1解码中文网页导致插入 MongoDB 的是乱码字符串。解决强制使用apparent_encodingchardet 自动检测或显式指定GB2312/UTF-8res requests.get(url, headersheaders) res.encoding res.apparent_encoding # 推荐自动识别 # 或 res.encoding GB2312 # 对老政务站更稳妥现象 3Word 导出后附件链接点击无反应或打开空白页原因python-docx的超链接需满足两个条件1URL 必须是完整 HTTP/HTTPS 协议2Word 安全设置默认禁用外部链接。解决1确保urljoin()输出完整 URL2在 Word 中启用“启用所有链接”文件 → 选项 → 信任中心 → 信任中心设置 → 外部内容 → 启用所有链接3导出时增加提示“请在 Word 中启用外部链接功能”。现象 4多进程爬取时 MongoDB 插入重复数据原因Pool.map()并发执行多个进程同时调用page_scratch()而get_link()提取的链接列表未去重导致同一条公示被多个进程重复抓取。解决在链接收集阶段全局去重all_links set() # 改用 set for url in all_pages: links get_link(url) all_links.update(links) links_list list(all_links) # 转回 list 供 map 使用现象 5导出 Word 后中文显示为方框或乱码原因python-docx默认字体不支持中文且未设置eastAsia字体属性。解决必须在Document()创建后立即设置中文字体如 2.4 节所示且rFonts.set(qn(w:eastAsia), SimSun)这一行不可省略。4. 数据库设计与索引策略让千万级公示也能秒查4.1 集合结构与字段说明我们创建集合gov_publicity替换原始代码中的gov.publicity避免点号引发的驱动兼容问题其文档结构严格对应 2.3 节定义。关键字段说明如下字段名类型是否索引说明_idObjectId✔️默认MongoDB 自动生成唯一主键urlString✔️唯一索引公示页原始 URL防止重复插入crawl_timeDatetime✔️升序爬取时间戳用于查最新版本metadata.publish_dateString✔️文本索引发布日期支持模糊查询如{metadata.publish_date: {$regex: ^2023}}metadata.titleString✔️文本索引标题全文检索支持关键词搜索content.attachments.urlString✖️附件 URL 不单独建索引查询时走metadata索引后内存过滤注意MongoDB 的复合索引对数组字段如attachments效果有限故附件 URL 不建索引实际查询时先按标题/日期定位文档再遍历attachments数组。4.2 必建索引命令Robo 3T 或 mongo shell 执行// 1. URL 唯一索引杜绝重复插入 db.gov_publicity.createIndex({ url: 1 }, { unique: true }) // 2. 发布日期索引按年月快速筛选 db.gov_publicity.createIndex({ metadata.publish_date: 1 }) // 3. 标题文本索引支持关键词全文搜索需开启 text search db.gov_publicity.createIndex({ metadata.title: text, content.text_blocks.text: text }) // 4. 爬取时间索引查最近爬取的 N 条 db.gov_publicity.createIndex({ crawl_time: -1 })4.3 实用查询示例可直接在 Robo 3T 中运行查 2023 年所有公示db.gov_publicity.find({ metadata.publish_date: { $regex: ^2023 } }).count()按标题关键词搜索“地铁”db.gov_publicity.find({ $text: { $search: 地铁 } }, { score: { $meta: textScore } }).sort({ score: { $meta: textScore } })查最新爬取的 5 条公示含标题和发布时间db.gov_publicity.find({}, { metadata.title: 1, metadata.publish_date: 1, crawl_time: 1 }).sort({ crawl_time: -1 }).limit(5)这些查询在万级数据下毫秒响应在十万级下仍保持百毫秒内前提是索引已正确建立。没有索引的 MongoDB 就是慢 SQL建索引不是可选项是交付前提。5. 全流程验收与答辩技巧从代码运行到导师点头的 5 个关键动作5.1 本地环境一键验证清单5 分钟完成别等答辩前才跑代码。每次提交前强制执行以下 5 步验证确保环境干净、逻辑闭环、输出可信步骤命令/操作预期结果不通过则停1. 环境检查pip list | grep -E requests|lxml|pymongo|python-docx|fake-useragent显示所有依赖及版本如pymongo 4.6.1,python-docx 0.8.11缺失任一包立即pip install2. MongoDB 连通python -c from pymongo import MongoClient; cMongoClient(localhost,27017); print(c.list_database_names())输出包含[webpages, ...]或空列表首次运行正常报ConnectionRefused检查 MongoDB 是否启动3. 单页抓取测试python scraper.py --test-url http://pnr.sz.gov.cn/xxgk/gggs/201911/t20191114_489608.html控制台打印提取的 title/source/date且 MongoDB 中gov_publicity新增 1 条文档字段为空或报错检查 XPath 和编码4. Word 导出测试python exporter.py --doc-id 5f8b1a2c3d4e5f6789012345用上步插入的_id生成2019-11-14_...docx双击打开标题居中、附件链接可点击Word 打不开或乱码检查字体设置和eastAsia属性5. 全量爬取模拟python scraper.py --pages 1只爬首页控制台显示“共发现 X 个公示链接”MongoDB 插入 X 条无重复、无报错任意环节中断查看日志定位except块提示把这 5 步写成verify.sh脚本每次答辩前双击运行。我带过的某高校毕设小组就是靠这个脚本在答辩前 2 小时发现fake-useragent未安装紧急补救后顺利过关。5.2 答辩 PPT 中必放的 3 张图让导师一眼看懂价值不要堆代码。答辩时导师最关心“你解决了什么问题”“怎么证明它有效”“和别人比强在哪”。用这三张图直击要害图 1网页结构 → MongoDB 文档 → Word 文档 的映射关系图手绘风格三列并排左列截图公示页 DOM高亮div.xxym h4、h5、TRS_Editor、fa-caret-right中列MongoDB 文档 JSON 片段用箭头指向metadata.title、content.attachments右列Word 截图标出对应位置居中标题、表格元信息、可点击附件→结论你的设计不是“把网页存数据库”而是“把网页的语义结构原样映射到三个载体”。图 2索引前后查询耗时对比柱状图Excel 生成两组数据无索引查 2023 年公示10 万条数据耗时 8.2 秒有索引同查询耗时 0.012 秒→结论你不仅会存更懂怎么让数据“活起来”这是工程能力的分水岭。图 3Word 导出效果对比图左右分屏左传统方案纯文本粘贴— 全是正文样式、无缩进、附件是死文字右本方案 — 标题居中、元信息表格、正文首行缩进、附件蓝色可点击→结论你交付的不是代码是可直接用于工作的成果文档。5.3 导师最可能问的 3 个问题及应答要点Q1“如果网站改版XPath 全失效你怎么维护”→答不依赖 XPath 硬编码。我在config.py中集中管理所有选择器SELECTORS { title: //div[classxxym]/h4/text(), source_date_block: //div[classxxym]/h5/text(), article: //div[classTRS_Editor]//p/text(), attachment_icon: //i[contains(class,fa-caret-right)] }改版时只需更新config.py所有模块自动适配。同时我预留了fallback_selectors字段当主选择器失败时降级使用备用规则如//title/text()作为标题备选。Q2“MongoDB 存这么多文本会不会爆磁盘”→答做了三重控制1content.raw_html字段默认不存只存结构化text_blocks2附件只存 URL 和元信息不存二进制文件3设置了 MongoDB 的 TTL 索引对crawl_time超过 1 年的文档自动删除db.gov_publicity.createIndex({ crawl_time: 1 }, { expireAfterSeconds: 3600 * 24 * 365 })实际运行 3 个月10 万条公示仅占 1.2GB远低于预期。Q3“这个能用在别的政务网站吗比如 XX 市住建局”→答完全可以。我在scraper.py中抽象出SiteConfig类class SiteConfig: def __init__(self, base_url, pagination_xpath, link_xpath, content_extractor): self.base_url base_url self.pagination_xpath pagination_xpath self.link_xpath link_xpath self.content_extractor content_extractor # 可传入自定义解析函数只需为新网站新建一个shenzhen_config SiteConfig(...)实例其余代码零修改。目前已适配 3 个不同结构的政务站验证了可移植性。从那以后我每次做网页归档类项目都强制走一遍这 5 步验证 3 图 3 问预演。不是为了应付答辩而是因为真正的工程交付从来不是代码能跑通而是它能在任何人的电脑上、在导师的质疑下、在三个月后的复查中依然稳稳立住。希望帮到你。本文还有配套的精品资源点击获取