ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python网络爬虫工程化:请求解析、数据入库与异常恢复实战

Python网络爬虫工程化:请求解析、数据入库与异常恢复实战 简介这是一份基于Python的网络爬虫设计与实现开题报告适合计算机、软件工程等专业学生用于开题答辩也可作为爬虫项目前期论证的参考。报告系统梳理了国内外在动态网页抓取、聚焦爬虫及验证码识别方面的研究现状并结合课题任务完成可行性分析从反爬应对、模拟登录、数据存储与搜索优化等角度提出关键问题的解决思路同时对Windows开发环境、Firefox调试工具、MySQL与Elasticsearch等软件条件做了具体说明。资源为单个PDF文件压缩包大小约59KB全文结构完整、内容紧凑。目前已有816人在CSDN学习下载。对准备撰写爬虫开题报告、设计课程项目或了解爬虫技术选型的读者而言这份材料能够提供清晰的框架与实用策略可直接参考其模块划分和论述方式。1. 开题报告里的 Python 网络爬虫先想清楚这三件事准备写“基于Python的网络爬虫”方向的开题报告最容易出现的状况是花了大半篇幅描述爬虫能抓多少数据、页面有多好爬却在答辩时被一句“数据拿到了之后怎么办”卡住。我接触过的爬虫入门课题大多不是死在技术上而是死在范围定义不清楚。开题报告真正回答的问题是你要采集什么数据、通过什么链路拿到、拿到之后怎样验证。这里的核心指标是“可复现”。一台机器、一个 Python 环境、一条命令行就能跑通的最小闭环比画十页架构图都管用。下面按我平时给课题搭爬虫原型的顺序来展开。目标不是做一个能跑一次的脚本而是做一个中断了能续、源站改了能查、结果能被审计的采集流程。这套流程包含抓取、解析、落库、调度与验收五个环节对新手友好对老手也有值得对标的工程边界。2. Python 网络爬虫的抓取层选型requests、httpx 与 aiohttp 的取舍很多人把爬虫的起点定在“拿到 HTML”这没错但容易忽略两个前置问题请求用什么库发响应用什么解析。Python 第三方库生态里这几个选型直接决定了开发效率和排错难度。2.1 请求库怎么选requests 够用httpx 留给异步改造单机、单线程的爬虫requests 依然是默认选择。它的 API 简单、错误信息明确、社区示例多遇到 TLS 握手失败或者编码乱码这类基础问题网上几乎都能搜到对应解法。httpx 的优势在于提供同步和异步两套接口如果你打算后续用 asyncio 改造可以先统一用 httpx。aiohttp 适合完全异步的场景但排查问题时心智负担会明显变大新人不建议从它起步。对比项requestshttpxaiohttpAPI 易用性高高中同步调用原生原生需额外处理异步支持需线程池配合原生支持原生支持HTTP/2不支持支持有限支持典型场景中小型爬虫、原型验证同步异步混合项目高并发抓取服务实际写请求时最小闭环长这样import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 } resp requests.get( https://books.toscrape.com/, headersheaders, timeout(3.05, 10), ) resp.raise_for_status() print(resp.status_code, len(resp.text))代码里的timeout(3.05, 10)是元组写法前一个值表示连接超时后一个表示读取超时。只写一个数值的话两个阶段会共用同一个超时遇到慢接口容易误判。raise_for_status()的作用是让 4xx、5xx 状态码直接抛出异常避免后续把错误页当成正常内容解析。这一步的关键不是“能打印出网页长度”而是验证两件事目标站点是否在接受无 Cookie 请求时仍返回内容以及返回内容的编码是否需要额外处理。常见的坑是resp.text拿到的是乱码因为站点声明的字符集和实际内容不一致。可以用resp.encoding resp.apparent_encoding兜底但这会带来一次额外的内容嗅探开销只在少量页面时使用。2.2 解析层BeautifulSoup 负责可读性lxml 负责速度拿到 HTML 后割裂的字符串处理方式不推荐。BeautifulSoup 配合 lxml 解析器是多数人的第一选择它支持 CSS 选择器写起来直观。很多免费 python 源码大全里的爬虫例子都用find_all逐个查节点容易写出又长又慢的循环。我习惯优先用select将定位逻辑收敛成一条选择器表达式from bs4 import BeautifulSoup soup BeautifulSoup(resp.text, lxml) for book in soup.select(article.product_pod)[:5]: title book.h3.a.get(title) price book.select_one(p.price_color).get_text(stripTrue) print(title, price)article.product_pod是页面里每本书的外层容器book.h3.a借用了 BeautifulSoup 的属性便捷访问方式select_one则用于获取容器内第一个匹配节点。这里没有用正则提取信息是因为 HTML 结构一旦微调正则会碎得一塌糊涂而 CSS 选择器的容错性相对好一些。如果页面规模到了几万页以上解析耗时就开始变得可感知。此时可以把BeautifulSoup(resp.text, lxml)换成lxml.html.fromstring(resp.text)再通过.cssselect()做同类操作处理速度通常能提升一倍以上。代价是错误信息不如 BeautifulSoup 友好定位问题需要多打日志。2.3 合规边界robots.txt、访问频率与公开数据这一节不讨论法律条文只讲工程上默认要做的三件事。第一请求前查看目标站点的robots.txt它定义了哪些路径允许自动抓取哪些不允许。第二控制访问频率单一来源的短时间高频请求会给对方服务造成压力。第三尊重登录态需要账号权限才能看到的数据不要通过破解验证逻辑的方式获取。常见的实现是把 robots 检查封装成一个小函数from urllib.robotparser import RobotFileParser rp RobotFileParser() rp.set_url(https://books.toscrape.com/robots.txt) rp.read() if rp.can_fetch(*, url): print(允许抓取, url) else: print(跳过, url)can_fetch接收两个参数第一个是 User-Agent 标记第二个是待抓取 URL。这里没有做复杂的逻辑但足以在开题报告里表达一个明确的工程态度爬虫不是把请求发出去就结束了而是知道自己该采什么、不碰什么。后面所有调度设计都建立在这个边界之上。3. 基于 Python 的网络爬虫工程骨架请求、去重与异常恢复原型的最大问题是“能跑”和“能一直跑”之间隔着一整层异常处理。开题阶段如果把精力全放在页面解析上等数据量上来就会面临大量重复请求、半截失败、断点丢失。因此在写具体抓取逻辑之前先给爬虫搭一个尽量通用的骨架类。3.1 用类把状态组织起来而不是写一长串脚本脚本式爬虫的流程是线性执行改一处逻辑就要从头跑一遍。把状态收拢到类里有几个直接收益请求重试次数可配置已访问 URL 集合可复用日志里能带上当前上下文。下面是我常用的目录型爬虫骨架import logging import time import requests from urllib.parse import urljoin logger logging.getLogger(__name__) USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15, ] class CatalogSpider: def __init__(self, base_url, delay1.0, max_retries3): self.base_url base_url self.delay delay self.max_retries max_retries self.session requests.Session() self.seen set() def fetch(self, url): last_exc None for attempt in range(self.max_retries): headers {User-Agent: USER_AGENTS[attempt % len(USER_AGENTS)]} try: resp self.session.get(url, headersheaders, timeout10) if resp.status_code in (429, 503): backoff 2 ** attempt logger.warning(请求受限 status%s 等待%s, resp.status_code, backoff) time.sleep(backoff) continue resp.raise_for_status() return resp except requests.RequestException as exc: last_exc exc logger.error(第%s次尝试失败 url%s error%s, attempt 1, url, exc) time.sleep(2 ** attempt) raise RuntimeError(f抓取失败: {url}) from last_exc def parse(self, doc): raise NotImplementedError def run(self, start_url): queue [start_url] while queue: current queue.pop(0) if current in self.seen: continue self.seen.add(current) resp self.fetch(current) for item in self.parse(resp.text): print(item) time.sleep(self.delay)fetch方法里做了指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒。attempt % len(USER_AGENTS)是为了在多轮重试之间轮换请求头降低因重复 UA 被限流的概率。run方法维护了一个最朴素的队列seen集合确保同一 URL 不会被重复抓取。这个骨架不是最优性能方案但它把“抓取一个页面”和“处理一个页面”彻底拆开了。后续不管是接数据库、接消息队列还是改成并发都只需要替换run内部的调度部分解析逻辑不受影响。3.2 中断恢复把种子和已访问集合落盘上面的骨架放在进程里没问题一旦程序崩溃seen集合就丢了。常见的做法是每抓完一个页面就把 URL 追加到本地文件或者用 SQLite 存一张 visited 表。我会在开题报告里直接给出第二种方案因为它附带时间戳后面做增量更新更方便。import sqlite3 conn sqlite3.connect(crawl_state.db) conn.execute(CREATE TABLE IF NOT EXISTS visited (url TEXT PRIMARY KEY, crawled_at TEXT)) def mark_visited(url): conn.execute( INSERT OR IGNORE INTO visited (url, crawled_at) VALUES (?, datetime(now)), (url,), ) conn.commit()INSERT OR IGNORE依靠主键去重重复插入会静默跳过不会报错。恢复时只需要在主程序启动阶段执行SELECT url FROM visited把结果加载进seen即可。这套方案对单机爬虫足够稳妥也容易在开题报告中解释清楚。3.3 异常体系的三个层次第一个层次是网络异常requests.RequestException捕获连接错误、超时等处理方式是重试。第二个层次是解析异常页面结构可能因为改版而临时变化用try/except捕获并记录原始 HTML 片段便于事后定位。第三个层次是数据层异常比如数据库写入时唯一约束冲突这种通常不需要重试但要打印完整上下文。建议不要把三个层次全部混在一个except里。网络异常重试解析异常跳过数据异常抛错分别对应三种不同的现场处理方法。日志里至少要能区分出哪一类错误居多这直接影响后续排查方向。4. 抓取结果落库与增量更新从 CSV 到 SQLite 的平滑过渡数据落库是开题报告里最容易被低估的模块。很多示范教程把结果打印到终端就算结束可一旦需要统计采集量、验证覆盖率、做数据可视化没有结构化存储会非常被动。我的做法是先用 SQLite 起步等表结构和查询稳定后再迁移到 MySQL避免前期被数据库环境配置拖住。4.1 表结构设计从信息字段到抓取状态字段以书籍目录型站点为例表结构除了书名、价格、URL 之外还要预留抓取时间和更新时间字段。这样既能回答“今天抓了多少本”也能做按天增量对比。CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price TEXT, url TEXT UNIQUE, crawled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );url加唯一约束是最基础的去重手段。写入时使用INSERT OR IGNORE重复 URL 会被直接忽略不会因为主键冲突中断整个采集任务。这个设计非常朴素但在目录型数据上比先查询再插入的方式更省事。对应的写入代码sql INSERT OR IGNORE INTO books (title, price, url) VALUES (?, ?, ?) rows [ (item[title], item[price], item[url]) for item in parse_result ] conn.executemany(sql, rows) conn.commit()executemany批量执行插入吞吐量远高于逐条执行。实际使用中一次提交几百条是合理的过多会导致事务过大执行失败时回滚成本也高。4.2 增量抓取的两种思路第一种思路是只抓新增 URL。从某个列表页解析出候选 URL 后先用SELECT url FROM books WHERE url IN (...)过滤掉已存在的只对剩下的发起网络请求。这个思路适合新增数据集中在首页或最近页的场景。第二种思路是按时间戳增量。把每次采集会话标记为一次批次提取本轮所有 URL 与上一轮的crawled_at做对比找出新增和消失的记录。这种方式能统计页面下架情况适合做长期数据分析但实现复杂度略高。开题报告阶段我通常建议采用第一种思路理由很直接实现代码量少验证速度快评委更容易在演示中看到效果。第二种思路可以在展望部分提一句作为后期优化方向。4.3 调度与可视化埋点定时调度最简单的方案是系统自带的 cron 或任务计划程序。以 Linux 为例crontab -e里加一行0 2 * * * cd /home/user/crawler /usr/bin/python3 main.py logs/crawl.log 21这一行的意思是每天凌晨两点执行采集任务标准输出和错误输出都追加到同一个日志文件。相比在代码里写time.sleep(86400)做定时系统级的调度更可靠进程崩了也不会影响下次启动。调试阶段可以用python main.py手动运行确认无误后再交给 cron。可视化部分不需要一开始就上复杂的 Web 框架。用sqlite3执行聚合查询再把结果输出成 JSON前端用任意图表库都能画。关键是要在爬虫运行过程中把每批次的抓取数量、耗时、失败数写进单独的统计表这是后续做 python 爬虫可视化界面的数据基础。5. 验收实验与四个进阶技巧开题报告的技术验证不能以“跑通了”结束要把结果和参数挂钩。我会做一个小实验分别用 0 秒、0.5 秒、1 秒三种延迟请求同一个列表页记录耗时、失败率和数据量用数据说明延迟与成功率之间的关系。这比单纯展示抓取效果更有说服力。sqlite3 catalog.db select count(*), count(distinct url) from books; sqlite3 catalog.db select date(crawled_at), count(*) from books group by date(crawled_at);这两条命令分别回答“库里有多少条记录”和“每天抓了多少”都是评委大概率会追问的细节。把它们放进日志输出验收时直接展示省去临时写查询的尴尬。四个进阶技巧值得在报告里留出位置。第一并发改造时优先选ThreadPoolExecutor通过with语句控制最大并发数不要一上来就追求异步线程池的方式在中小规模下调试成本更低。第二抓取页面时保存一份原始 HTML 快照压缩后按日期归档出问题时可以离线重放不再依赖网络。第三把请求头、延迟、重试次数统一做成配置文件或者在类初始化时传入方便不同站点之间复用同一套代码。第四每次任务结束前打印一条“任务完成成功XX条失败XX条耗时XX秒”的日志这是所有后续优化最基础的参照指标。最后提醒一点所有验证都应以日志输出的 URL 列表为准不要只数终端上的打印行。把queue里的每一个待抓取 URL 记录到文件任务结束即可拿它与数据库里的 URL 做差集差集里的条目就是“抓了但没入库”的脏数据来源。用这个方法检查一遍通常能发现一半以上的隐藏 bug。本文还有配套的精品资源点击获取
返回列表