Python爬虫新手避坑指南:用BeautifulSoup爬取学校公告信息(含反爬应对)

📅 发布时间:2026/7/13 23:05:09 👁️ 浏览次数:
Python爬虫新手避坑指南:用BeautifulSoup爬取学校公告信息(含反爬应对)
Python爬虫新手避坑指南用BeautifulSoup爬取学校公告信息含反爬应对最近几年身边想学点技术、自己动手解决信息获取问题的朋友越来越多了。Python爬虫无疑是其中最受欢迎的技能之一。它听起来很“黑客”但入门门槛其实不高尤其当你只是想自动化地收集一些公开信息比如学校官网的通知公告时。然而很多新手朋友兴冲冲地打开教程照着代码敲一遍却常常在真正面对一个具体网站时卡壳——要么数据死活抓不下来要么程序跑一半就崩溃要么存下来的数据乱七八糟。这感觉就像拿到了藏宝图却总在第一个岔路口迷路。这篇文章就是为你准备的“岔路口路标”。我们不谈那些高深莫测的分布式爬虫框架也不去触碰法律和道德的灰色地带就聚焦一个非常具体且高频的需求如何用Python的BeautifulSoup库稳定、完整地爬取一个典型的高校信息网站公告。我会把重点放在那些教程里往往一笔带过但实际开发中一定会遇到的“坑”上比如分页逻辑的识别、动态加载内容的应对、请求被拒绝的常见原因以及如何优雅地保存数据。目标是让你看完后不仅能跑通一个案例更能获得一套可复用的、具备基础“反反爬”能力的代码模板和解决问题的思路。1. 环境准备与目标网站分析在动手写任何一行爬虫代码之前有两件事比编码本身更重要搭建好合适的工具环境以及像侦探一样仔细审视你的目标网站。很多爬取失败根源都在于准备工作没做好。1.1 核心工具库安装我们将使用最经典、对新手最友好的Python爬虫组合requestsBeautifulSoup。此外为了数据持久化会用到csv或pandas。请确保你的Python环境建议3.7以上版本中已安装这些库。# 使用pip进行安装 pip install requests beautifulsoup4 pandas lxml这里简单解释一下requests用于向网站服务器发送HTTP请求并获取响应内容。它是我们与网络交互的“手”。BeautifulSoup一个HTML/XML解析库。它能把服务器返回的复杂HTML文档转换成一棵结构清晰的树让我们能像查字典一样轻松找到想要的数据标签。我们选择lxml作为它的解析器因为它的速度通常比内置的html.parser更快、更健壮。pandas一个强大的数据分析库。我们主要利用它来将爬取的数据规整地保存为Excel或CSV文件比直接用Python内置的csv模块更方便。注意在安装lxml时如果遇到编译错误尤其在Windows上可以尝试从这个网站下载对应你Python版本和系统位数的预编译.whl文件进行离线安装。1.2 目标网站结构侦察假设我们的目标是爬取“某大学新闻通知网”的所有历史公告。以常见的结构为例网址可能是http://news.university.edu.cn/notice/。第一步手动浏览理解网站逻辑。打开网站看看公告是如何呈现的是列表页详情页还是所有内容都在一个长列表中分页机制这是第一个关键点。翻到页面底部看看是传统的“第1页2页3页...”链接还是“加载更多”按钮或是滚动到底部自动加载无限滚动。对于传统分页观察URL的变化规律例如page1,page2或者index_1.html,index_2.html。数据位置在列表页每条公告通常包含标题、发布日期、可能还有摘要。点击一条公告进入详情页查看完整的正文内容。第二步使用开发者工具DevTools进行“解剖”。这是爬虫工程师最重要的工具没有之一。按F12打开。Elements面板查看网页的HTML源代码。在这里你可以找到数据对应的HTML标签。例如你可能发现所有公告条目都包裹在一个div classnews-list下的多个li标签中。每条公告的标题在一个a classtitle里日期在一个span classdate里。Network面板网络面板这是应对动态内容的关键。刷新页面观察加载过程中浏览器发送了哪些请求。重点关注XHR或Fetch类型的请求这些往往是网站通过JavaScript动态加载数据时发出的API请求。如果你在Elements面板里找不到数据但页面上却显示了那么数据几乎肯定是通过这类请求异步加载的。你需要找到这个请求的URL、参数和响应格式通常是JSON。侦察项目操作方法目的与意义页面结构查看Elements面板右键点击元素“检查”定位目标数据所在的HTML标签及属性如class、id分页逻辑点击不同页码观察浏览器地址栏URL变化确定构造多页请求的URL规律动态加载打开Network面板筛选XHR/Fetch请求滚动页面或点击加载更多识别数据是否通过JavaScript异步请求获取并找到真实数据接口请求头在Network面板点击任一请求查看Headers选项卡下的Request Headers获取模拟浏览器访问所需的关键信息如User-Agent通过以上步骤你应该能回答我需要请求哪些URL数据在响应HTML里还是JSON里用什么CSS选择器或标签路径来提取数据2. 基础爬取从单页到多页掌握了网站结构我们就可以开始编写爬虫的核心逻辑了。这一节我们从最简单的单页爬取开始逐步扩展到处理多页数据。2.1 发送请求与解析单页首先我们尝试爬取第一页的数据。核心步骤是发送请求 - 获取响应 - 解析HTML - 提取数据。import requests from bs4 import BeautifulSoup import pandas as pd # 目标URL以第一页为例 url http://news.university.edu.cn/notice/ # 设置请求头这是绕过基础反爬的第一步 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 } try: # 发送GET请求 response requests.get(url, headersheaders) # 检查请求是否成功状态码200表示成功 response.raise_for_status() # 设置正确的编码避免中文乱码 response.encoding response.apparent_encoding # 或直接指定 utf-8 # 使用BeautifulSoup解析HTML内容指定lxml解析器 soup BeautifulSoup(response.text, lxml) # 假设我们通过侦察发现每条公告在一个class为news-item的div里 news_items soup.find_all(div, class_news-item) data_list [] for item in news_items: # 提取标题假设标题在a标签内class为title title_tag item.find(a, class_title) title title_tag.get_text(stripTrue) if title_tag else N/A # 提取链接获取a标签的href属性注意可能是相对路径 link title_tag[href] if title_tag and title_tag.has_attr(href) else N/A # 将相对URL补全为绝对URL if link and not link.startswith(http): link requests.compat.urljoin(url, link) # 提取日期假设日期在span classdate里 date_tag item.find(span, class_date) date date_tag.get_text(stripTrue) if date_tag else N/A # 将数据存入字典 data_list.append({ 标题: title, 链接: link, 发布日期: date }) # 使用pandas将数据转换为DataFrame便于查看和保存 df pd.DataFrame(data_list) print(f成功爬取到 {len(df)} 条公告) print(df.head()) # 预览前几条数据 except requests.exceptions.RequestException as e: print(f请求过程中发生错误: {e}) except Exception as e: print(f解析过程中发生未知错误: {e})这段代码已经包含了几个新手必须掌握的关键点异常处理网络请求可能失败服务器错误、超时等用try...except包裹起来能让程序更健壮。健壮的数据提取使用if...else或三元表达式处理可能找不到的标签避免程序因某条数据格式异常而崩溃。URL补全urljoin函数能智能地将相对路径如/notice/123.html与基础URL拼接成完整地址。2.2 实现自动分页爬取单页成功之后处理多页就是循环和URL构造的问题。我们需要根据之前侦察到的分页规律来设计循环。情况一URL包含页码参数这是最简单的情况比如page1。base_url http://news.university.edu.cn/notice/?page{} all_data [] for page_num in range(1, 11): # 假设爬取前10页 current_url base_url.format(page_num) print(f正在爬取第 {page_num} 页: {current_url}) # 这里可以加入延时避免请求过快 # time.sleep(1) try: response requests.get(current_url, headersheaders) response.raise_for_status() soup BeautifulSoup(response.text, lxml) # ... 数据提取逻辑与2.1节相同 ... # 将提取的data_list添加到all_data中 all_data.extend(data_list) except requests.exceptions.RequestException as e: print(f第 {page_num} 页请求失败: {e}) # 可以选择跳过或中断 # 最终保存所有数据 final_df pd.DataFrame(all_data) print(f总共爬取到 {len(final_df)} 条公告)情况二总页数未知我们可能需要先解析第一页从分页器中获取总页数。# 首先解析第一页找到分页控件 soup BeautifulSoup(requests.get(first_page_url, headersheaders).text, lxml) # 假设分页器最后一个页码按钮的文本是总页数 page_links soup.find(div, class_pagination).find_all(a) # 一种常见的策略找到所有页码数字取最大值 page_numbers [int(link.text) for link in page_links if link.text.isdigit()] total_pages max(page_numbers) if page_numbers else 1 print(f检测到总页数: {total_pages})提示在循环中务必在每次请求间添加适当的延时例如time.sleep(2)这是对目标网站服务器的基本礼貌也能显著降低因请求频率过高而被封IP的风险。3. 常见反爬机制与应对策略当你按照教程顺利写好了爬虫却返回一堆错误代码或空白内容时很可能你遇到了网站的反爬虫机制。对于高校网站这类非商业敏感站点反爬措施通常比较基础但足以拦住没有准备的新手。3.1 请求头Headers伪装这是最基本也是最有效的防线。服务器会检查请求头中的User-Agent字段来判断请求是否来自真实的浏览器。使用Pythonrequests库的默认User-Agent很容易被识别。解决方案在请求中模拟一个主流浏览器的完整请求头。headers { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, Upgrade-Insecure-Requests: 1, }你可以从浏览器的Network面板中复制任意一个请求的Request Headers直接作为字典使用。通常只设置User-Agent就足够了但在一些严格的网站上Accept和Accept-Language也可能被检查。3.2 频率限制与IP封禁如果短时间内发送大量请求服务器可能会暂时或永久封禁你的IP地址。应对策略组合拳增加延时在循环请求间使用time.sleep(random.uniform(1, 3))加入随机性更模拟人工。使用会话Sessionrequests.Session()对象可以复用TCP连接并自动管理cookies比单次请求更高效、更接近真实浏览器行为。处理Cookies有些网站通过cookies跟踪会话。使用Session对象会自动处理。如果需要手动设置可以从浏览器复制Cookie字符串。import time import random from requests import Session session Session() session.headers.update(headers) # 为整个会话设置请求头 for page in range(total_pages): # ... 构造URL ... response session.get(current_url) # 使用session发起请求 # ... 解析数据 ... time.sleep(random.uniform(1.5, 4)) # 随机延时3.3 动态内容加载JavaScript渲染这是新手最大的“拦路虎”。你查看网页源代码右键 - 查看网页源代码发现里面没有数据但浏览器里却能正常显示。这是因为数据是通过JavaScript在页面加载后再发起Ajax请求获取并渲染到页面上的。识别方法在Network面板中筛选XHR或Fetch请求在页面加载或滚动时观察新出现的请求查看其Preview或Response看是否包含你需要的数据通常是JSON格式。解决方案直接请求API接口如果找到了数据接口API那么恭喜你这往往比解析HTML更简单、更稳定。你只需要模拟这个请求即可。# 假设发现数据来自一个JSON接口 api_url http://news.university.edu.cn/api/notices params {page: page_num, size: 20} response session.get(api_url, paramsparams) data response.json() # 直接解析JSON # 从data字典中提取所需字段使用Selenium或Playwright如果网站接口复杂或数据被加密直接请求API困难可以考虑使用浏览器自动化工具。它们能控制一个真实的浏览器如Chrome加载页面、执行JS等数据渲染完成后再获取完整的HTML。但这会大大增加资源消耗和复杂度应作为最后手段。3.4 其他常见问题与处理SSL证书验证错误在请求时设置verifyFalse仅用于测试生产环境有安全风险。连接超时设置timeout参数如requests.get(url, timeout10)。重定向处理requests默认自动处理重定向。可以通过allow_redirectsFalse禁用或检查response.history查看重定向链。状态码处理除了200还应处理404页面不存在、403禁止访问、429请求过多等状态码做出相应逻辑判断。4. 数据清洗、存储与代码健壮性爬取到数据只是成功了一半如何把数据干净、有序地保存下来并确保爬虫能长时间稳定运行是另一半同样重要的课题。4.1 数据清洗与规整从HTML中提取的文本常常包含多余的空格、换行符或不可见字符。def clean_text(text): 清洗文本的辅助函数 if not text: return # 去除首尾空白字符 text text.strip() # 将多个连续空白字符包括空格、换行、制表符替换为单个空格 import re text re.sub(r\s, , text) return text # 在提取数据时使用 title clean_text(title_tag.get_text()) if title_tag else N/A对于日期字段最好能统一转换为Python的datetime对象便于后续分析和排序。from datetime import datetime date_str 2023-10-27 try: # 根据网站日期格式调整解析格式 date_obj datetime.strptime(date_str, %Y-%m-%d) formatted_date date_obj.strftime(%Y/%m/%d) # 统一格式 except ValueError: formatted_date date_str # 解析失败则保留原字符串4.2 多种数据存储方式根据数据量和使用场景选择适合的存储方式。1. CSV文件轻量、通用适合中小规模数据。import csv with open(university_notices.csv, w, newline, encodingutf-8-sig) as f: # utf-8-sig支持Excel中文 writer csv.DictWriter(f, fieldnames[标题, 链接, 发布日期]) writer.writeheader() writer.writerows(all_data) # all_data是字典列表2. Excel文件利用pandas非常简单。final_df.to_excel(公告信息.xlsx, indexFalse) # indexFalse不保存行索引3. JSON文件适合嵌套结构复杂的数据。import json with open(data.json, w, encodingutf-8) as f: json.dump(all_data, f, ensure_asciiFalse, indent2) # indent美化格式4. 数据库SQLite适合数据量大、需要复杂查询的场景。import sqlite3 conn sqlite3.connect(notices.db) cursor conn.cursor() cursor.execute(CREATE TABLE IF NOT EXISTS notices (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, link TEXT UNIQUE, -- 链接设为唯一防止重复存储 publish_date DATE)) # 插入数据 for item in all_data: cursor.execute(INSERT OR IGNORE INTO notices (title, link, publish_date) VALUES (?, ?, ?), (item[标题], item[链接], item[发布日期])) conn.commit() conn.close()4.3 提升代码健壮性与可维护性一个只能跑一次的脚本和一个可以长期运行的爬虫工具差别在于健壮性。日志记录使用logging模块替代print可以方便地记录信息、警告和错误并输出到文件。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(crawler.log), logging.StreamHandler()]) logging.info(f开始爬取第{page_num}页)配置化将URL、请求头、选择器、数据库连接信息等写入配置文件如config.py或config.yaml使核心代码更清晰修改配置更方便。函数化与模块化将发送请求、解析页面、保存数据等逻辑封装成独立的函数或类。例如可以创建一个SchoolNoticeCrawler类。断点续爬对于多页爬取可以将已成功爬取的页码记录到一个文件中。当程序因网络或错误中断后重启时可以跳过已完成的页码从断点处继续。最后也是最重要的务必遵守网站的robots.txt协议尊重网站所有者的意愿。在爬取前访问http://news.university.edu.cn/robots.txt查看哪些路径是允许或禁止爬取的。对于个人学习和小规模数据采集保持礼貌的访问频率通常不会造成问题。但切记爬虫的价值在于提升效率而非滥用资源。将学到的技术用在正道上才是长久之道。