数据采集实践第一次作业
数据采集实践第一次作业博客
本次作业使用 Python 语言,基于
requests与BeautifulSoup(部分配合re正则表达式)完成三个网络数据采集任务:
① 爬取软科2020中国大学排名并规整输出;② 爬取当当网"书包"关键词搜索结果的商品价格信息;③ 爬取指定网页中的所有 JPG/JPEG/PNG 图片并保存到文件夹。
作业①:2020 中国大学排名爬虫
1. 大学排名实验
实验目标:用 requests 和 BeautifulSoup 方法爬取软科排名网址(https://www.shanghairanking.cn/rankings/bcur/2020)的数据,屏幕打印爬取的大学排名信息,输出格式为:排名、学校名称、省市、学校类型、总分。
网页勘探:先用浏览器打开排名页,确认数据以规整表格呈现;再按 F12 查看元素结构,定位到每一行 <tr>,展开后能看到 <td> 单元格(排名、校徽、校名、省市、类型、总分),我们要的信息就在这几个单元格里。

实现思路:
- 使用
requests.get()携带 User-Agent 请求网页,raise_for_status()校验请求状态,apparent_encoding处理编码; - 使用 BeautifulSoup 的
select("tr")遍历表格行,get_text(strip=True)提取单元格文本; - 对爬取到的原始文本进行数据清洗:过滤掉混在学校名中的英文校名和"双一流/985/211"标签,识别省市字段,从整行中兜底定位总分;
- 用固定列宽格式化输出表格。
完整代码(作业1_大学排名.py):
# -*- coding: utf-8 -*-
"""
作业①:2020 年中国大学排名爬虫
使用 requests + BeautifulSoup 获取并以规整表格形式输出排名。
"""
import requests
from bs4 import BeautifulSoup
URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
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"
)
}
def fetch_html(url):
"""请求网页并返回 HTML。"""
response = requests.get(url, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
return response.text
def parse_ranking(html):
"""从网页表格行中提取排名数据,并清理混入学校名中的英文名/标签文字。"""
soup = BeautifulSoup(html, "html.parser")
result = []
for row in soup.select("tr"):
cells = [cell.get_text(" ", strip=True) for cell in row.select("th, td")]
if len(cells) < 4:
continue
# 只处理第一列为数字排名的行
rank_text = cells[0].strip().rstrip(".")
if not rank_text.isdigit():
continue
# 不同网页版本列结构不同:先按当前列位置取值
name = cells[1] if len(cells) > 1 else ""
province = cells[2] if len(cells) > 2 else ""
school_type = cells[3] if len(cells) > 3 else ""
score = cells[4] if len(cells) > 4 else ""
# 清除常见排名标签、英文校名和其他附加信息
for marker in ("双一流", "985", "211"):
name = name.replace(marker, " ")
name = " ".join(name.split())
# 省市字段常被当前网站放在学校名同一个单元格中,尝试从文本末尾识别
known_regions = [
"北京", "上海", "天津", "重庆", "河北", "山西", "辽宁", "吉林",
"黑龙江", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
"湖北", "湖南", "广东", "海南", "四川", "贵州", "云南", "陕西",
"甘肃", "青海", "台湾", "内蒙古", "广西", "西藏", "宁夏", "新疆"
]
combined = " ".join(cells[1:])
if province not in known_regions:
found = next((region for region in known_regions if combined.endswith(region)), "")
if found:
province = found
# 网页结构错位时,分数一般是纯数字或小数,尝试从整行中寻找
if not score or not any(ch.isdigit() for ch in score):
candidates = [x for x in cells if x.replace(".", "", 1).isdigit()]
if len(candidates) >= 2:
score = candidates[-1]
if name:
result.append((rank_text, name, province, school_type, score))
return result
def print_table(rows):
"""以固定列宽输出表格。"""
headers = ("排名", "学校名称", "省市", "学校类型", "总分")
widths = (6, 30, 12, 12, 10)
line = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
print(line)
print("|" + "|".join(f" {h:<{w}} " for h, w in zip(headers, widths)) + "|")
print(line)
for row in rows:
values = list(row)
# 终端表格以字符长度粗略对齐;长名称截断,避免破坏表格版式
values[1] = values[1][:28]
values[3] = values[3][:10]
print("|" + "|".join(f" {str(v):<{w}} " for v, w in zip(values, widths)) + "|")
print(line)
print(f"共提取 {len(rows)} 条大学排名信息。")
def main():
try:
html = fetch_html(URL)
rows = parse_ranking(html)
if not rows:
print("请求成功,但未能解析出排名表格。网页结构可能已变化。")
print("请检查网页是否能正常打开;如果数据由 JavaScript 动态加载,需要进一步适配。")
return
print_table(rows)
except requests.RequestException as exc:
print(f"网页请求失败:{exc}")
if __name__ == "__main__":
main()
运行结果(成功提取 30 条大学排名信息,前 30 名规整输出):
可以看到,程序输出的表格包含排名、学校名称、省市、学校类型、总分五列,其中清华大学以 852.5 分位居第一,与网页上的真实排名完全一致,共提取 30 条大学排名信息。
2. 作业①心得体会
这次实验让我第一次完整走通了"请求 → 解析 → 清洗 → 输出"的爬虫流程。最大的体会是:爬取只是开始,数据清洗才是重头戏。软科网页的学校名称单元格里混着英文校名和"双一流"等标签,省市有时和校名挤在同一单元格,如果不做清洗,输出的表格会非常杂乱。我通过维护省份列表、替换标签关键词、按行兜底找分数等方式逐步把数据"洗"干净了。此外我还认识到模拟浏览器 User-Agent 的重要性——不加请求头很容易被网站拒绝;raise_for_status() 和 try/except 则让程序在网页不可访问时能给出友好提示而不是直接崩溃。
作业②:当当网"书包"商品价格爬虫
1. 当当网爬虫实验
实验目标:用 requests 和 bs4 方法设计某个商城(自己选择)商品的价格信息爬虫,爬取该商城以关键词"书包"搜索页面的数据,爬取商品名称和价格,输出格式为:序号、价格、商品名称。
网页勘探:直接用浏览器打开当当网"书包"搜索结果页,确认商品以列表形式呈现;按 F12 选中任意一个商品,可见商品名称在 <a> 标签的 title 属性中,价格则在 search_now_price / price 这类 class 里。
实现思路:
- 选用当当网作为目标商城,直接请求关键词"书包"的搜索结果页 URL;
- 请求头除 User-Agent 外增加
Referer,降低被反爬拦截的概率; - 用正则表达式按
<li>商品块切分页面,在块内分别匹配价格(优先匹配search_now_price/price类名,兜底匹配¥符号)和商品名称(优先取<a>的title属性,兜底取name/title类链接文本); - 对商品去重,并按"序号 + 价格 + 名称"格式规整输出;
- 程序同时把原始 HTML 保存为
dangdang_search_debug.html,方便网页结构变化时排查; - 为便于存档与比价,程序额外把结果导出为
schoolbags.csv(UTF-8 BOM,Excel 可直接打开)。
完整代码(作业2_当当网书包爬虫.py):
# -*- coding: utf-8 -*-
"""
作业②:当当网"书包"商品定向爬虫
使用 requests 请求页面,使用 re 正则表达式提取商品名称和价格。
注意:网站页面结构和反爬策略可能变化;程序会保存原始 HTML 便于检查。
"""
import re
import os
import csv
import requests
from html import unescape
URL = "https://search.dangdang.com/?key=%CA%E9%B0%FC&act=input"
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"
),
"Referer": "https://www.dangdang.com/"
}
def clean_html_text(text):
"""去除 HTML 标签并解码实体字符。"""
text = re.sub(r"<[^>]+>", " ", text)
text = unescape(text)
return re.sub(r"\s+", " ", text).strip()
def parse_products(html):
"""按商品列表块解析名称和价格,兼容常见当当网 class 命名。"""
products = []
# 当当网搜索结果常使用 li 作为商品容器
blocks = re.findall(r"<li\b[^>]*>(.*?)</li>", html, flags=re.I | re.S)
for block in blocks:
# 先找商品价格区域
price_match = re.search(
r'<[^>]*class=["\'][^"\']*(?:search_now_price|price)[^"\']*["\'][^>]*>'
r'.{0,200}?([0-9]+(?:\.[0-9]{1,2})?)',
block,
flags=re.I | re.S
)
if not price_match:
price_match = re.search(r"¥\s*([0-9]+(?:\.[0-9]{1,2})?)", block)
# 商品名称优先取 title 属性,再取 name/title 类链接文本
name_match = re.search(r'<a\b[^>]*title=["\']([^"\']+)["\']', block, flags=re.I | re.S)
if not name_match:
name_match = re.search(
r'<a\b[^>]*class=["\'][^"\']*(?:name|title)[^"\']*["\'][^>]*>(.*?)</a>',
block, flags=re.I | re.S
)
if price_match and name_match:
price = price_match.group(1)
name = clean_html_text(name_match.group(1))
# 过滤空名称、非商品文本
if name and len(name) > 2:
products.append((price, name))
# 去重
unique = []
seen = set()
for item in products:
if item not in seen:
seen.add(item)
unique.append(item)
return unique
def main():
try:
response = requests.get(URL, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
html = response.text
# 保存网页原文,方便定位网站结构变化
with open("dangdang_search_debug.html", "w", encoding="utf-8") as file:
file.write(html)
products = parse_products(html)
if products:
print(f"{'序号':<6}{'价格(元)':<12}商品名称")
print("-" * 80)
for index, (price, name) in enumerate(products, 1):
print(f"{index:<6}{price:<12}{name}")
print(f"共提取 {len(products)} 件商品。")
# 将结果导出为 CSV,便于后续比价与存档
save_path = "schoolbags.csv"
with open(save_path, "w", encoding="utf-8-sig", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["序号", "价格(元)", "商品名称"])
for index, (price, name) in enumerate(products, 1):
writer.writerow([index, price, name])
print(f"结果已导出到:{os.path.abspath(save_path)}")
return
print("当前页面未能解析到商品名称和价格。原始网页已保存为 dangdang_search_debug.html。")
print("如果文件内容是验证码/访问受限页面,正则表达式无法提取商品。")
print("可打开该 HTML 搜索 'search_now_price'、'price' 或商品名称,确认当前网页结构。")
except requests.RequestException as exc:
print(f"请求当当网失败:{exc}")
if __name__ == "__main__":
main()
运行结果(成功提取 60 件"书包"商品的价格和名称):
程序按序号输出每件商品的价格与名称,共提取 60 件商品,价格从几十元到数百元不等,与当当网搜索页真实展示的商品信息一致。
2. 作业②心得体会
这个实验和作业①最大的不同是:电商网页结构复杂、反爬更严格,BeautifulSoup 的常规解析经常拿不到数据,我改用正则表达式配合多级兜底策略(先按 class 匹配、再按 ¥ 符号匹配)才稳定提取出商品信息。调试过程中我学会了"保存原始 HTML 再分析"的排查方法——把响应原文存成 dangdang_search_debug.html,在文件里搜索目标字段,就能准确判断是请求被拦截了还是解析规则不对。另外我也体会到爬虫代码要写得"防御性强":网页结构随时可能变化,多一层兜底匹配、多一些异常提示,程序的生命力就更强。把结果额外导出成 CSV,也让我第一次有了"数据落盘、可复用"的意识。
作业③:网页图片下载爬虫
1. 图片下载实验
实验目标:爬取一个指定网页(< https://news.fzu.edu.cn/yxfd.htm )的所有> JPG、JPEG 和 PNG 格式图片文件,将图片保存在一个文件夹中。
网页勘探:先用浏览器打开"影像福大"栏目页面,确认图片以 <img> 标签嵌入;重点检查页面底部的分页区,发现该栏目共 7 个分页,链接规律为 yxfd/{n}.htm。题目要求爬取"所有"图片,因此必须遍历全部 7 个分页,只抓首页会漏掉约 6/7 的图片。
实现思路:
- 先读取分页区,生成"首页 + 全部后续分页"的地址列表(
get_page_urls()); - 逐页用 BeautifulSoup 遍历所有
<img>标签,同时兼容src、data-src、data-original等懒加载属性; - 用
urljoin把相对地址补全为绝对地址,按 URL 后缀筛选.jpeg/.jpg/.png(无后缀的链接先保留,下载后按响应的 Content-Type 判断); - 下载时用
requests.Session()复用连接提高效率;文件名做 Windows 非法字符清洗,并加序号前缀防止重名; - 跳过空响应和非图片内容,逐张统计成功/失败情况,图片统一保存到
downloaded_images文件夹。
完整代码(作业3_网页图片下载.py):
# -*- coding: utf-8 -*-
"""
作业③:下载福州大学新闻网页中的 JPEG、JPG、PNG 图片
使用 requests 获取网页和图片;BeautifulSoup 解析 img 标签。
目标网页(影像福大)存在多个分页,本程序会先读取分页总数,
再逐页收集图片链接,最终把全站符合条件的图片统一保存到 downloaded_images 文件夹。
"""
import os
import re
from urllib.parse import urljoin, urlparse, unquote
import requests
from bs4 import BeautifulSoup
PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"
SAVE_DIR = "downloaded_images"
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"
)
}
ALLOWED_EXTENSIONS = {".jpeg", ".jpg", ".png"}
def get_page_urls(home_url):
"""读取分页区,生成首页 + 全部后续分页的地址列表。"""
response = requests.get(home_url, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, "html.parser")
page_urls = [home_url] # 首页固定纳入
pagination = soup.select_one(".n_right .pb_sys_common .p_pages")
if pagination:
# 从分页区读取最大页码,按 "yxfd/{n}.htm" 规律补全所有分页
numbers = [
int(tag.get_text(strip=True))
for tag in pagination.select(".p_no a, .p_no_d")
if tag.get_text(strip=True).isdigit()
]
total_pages = max(numbers) if numbers else 1
for n in range(total_pages - 1, 0, -1):
page_urls.append(urljoin(home_url, f"yxfd/{n}.htm"))
# 去重并保持顺序
seen, unique = set(), []
for url in page_urls:
if url not in seen:
seen.add(url)
unique.append(url)
return unique
def get_image_urls(page_url):
"""从单个网页中提取并规范化图片链接(兼容懒加载属性)。"""
response = requests.get(page_url, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, "html.parser")
image_urls = set()
for img in soup.find_all("img"):
# 常见图片地址属性,兼容懒加载网页
src = (
img.get("src")
or img.get("data-src")
or img.get("data-original")
or img.get("data-lazy-src")
)
if not src:
continue
image_url = urljoin(page_url, src.strip())
path = urlparse(image_url).path.lower()
# 按 URL 后缀筛选指定格式;部分图片无扩展名时,下载后再检查响应类型
if os.path.splitext(path)[1] in ALLOWED_EXTENSIONS or not os.path.splitext(path)[1]:
image_urls.add(image_url)
return image_urls
def safe_filename(image_url, index, content_type):
"""根据图片 URL 生成安全文件名,并确保使用允许的图片扩展名。"""
path_name = os.path.basename(unquote(urlparse(image_url).path))
name, ext = os.path.splitext(path_name)
ext = ext.lower()
if ext not in ALLOWED_EXTENSIONS:
content_type = (content_type or "").lower()
if "png" in content_type:
ext = ".png"
elif "jpeg" in content_type or "jpg" in content_type:
ext = ".jpg"
else:
return None
# 清理 Windows 文件名不允许的字符
name = re.sub(r'[\\/:*?"<>|\s]+', "_", name).strip("._")
if not name:
name = f"image_{index:03d}"
return f"{index:03d}_{name[:80]}{ext}"
def download_images(image_urls, save_dir):
"""逐个下载图片,跳过非图片响应,并统计下载结果。"""
os.makedirs(save_dir, exist_ok=True)
success_count = 0
with requests.Session() as session:
session.headers.update(HEADERS)
for index, image_url in enumerate(image_urls, start=1):
try:
response = session.get(image_url, timeout=20)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "")
filename = safe_filename(image_url, index, content_type)
if filename is None:
print(f"[跳过] 不是 JPEG/JPG/PNG 图片:{image_url}")
continue
# 避免把网页错误页误保存成图片
if not response.content:
print(f"[跳过] 文件内容为空:{image_url}")
continue
file_path = os.path.join(save_dir, filename)
with open(file_path, "wb") as file:
file.write(response.content)
success_count += 1
print(f"[成功] {filename}")
except requests.RequestException as exc:
print(f"[失败] {image_url},原因:{exc}")
return success_count
def main():
try:
page_urls = get_page_urls(PAGE_URL)
print(f"共发现 {len(page_urls)} 个分页:")
for url in page_urls:
print(" -", url)
print()
all_image_urls = set()
for page_url in page_urls:
all_image_urls |= get_image_urls(page_url)
all_image_urls = sorted(all_image_urls)
print(f"全部分页中共发现 {len(all_image_urls)} 个候选图片链接。\n")
if not all_image_urls:
print("没有发现符合条件的图片链接,请检查网页是否能正常访问或图片是否由 JavaScript 加载。")
return
count = download_images(all_image_urls, SAVE_DIR)
print(f"\n下载完成:成功保存 {count} 张图片。")
print(f"保存目录:{os.path.abspath(SAVE_DIR)}")
except requests.RequestException as exc:
print("网页请求失败:", exc)
print("请检查网络连接、网页是否可访问,或稍后重试。")
if __name__ == "__main__":
main()
运行结果(控制台先列出 7 个分页地址,再逐张下载,最终成功保存 65 张图片):
打开 downloaded_images 文件夹,可以看到 65 张 JPG/PNG 图片已按序号命名整齐保存:
随机双击打开其中一张图片,确认能正常显示,证明下载内容均为有效图片:
2. 作业③心得体会
图片下载看似简单,实际要处理的细节远比想象多:相对地址要补全、懒加载属性要兼容、文件名要清洗、非图片响应要过滤。这个实验让我养成了"把每一步可能出错的地方都想一遍"的编程习惯——比如用 Session 复用连接、用序号前缀避免文件重名、用 Content-Type 二次校验防止把错误页存成图片。最关键的收获来自对"所有图片"四个字的理解:只有点开分页区、发现共 7 页并逐页爬取,才真正拿到了全站 65 张图,而不是首页那 1/7。运行时看到一张张 [成功] 打印出来、文件夹里图片整齐排列,成就感十足,也真正理解了"爬虫是细节的工程"这句话。
总结
三个作业覆盖了网络数据采集的三种典型场景:结构化表格数据(大学排名)、电商商品信息(当当网书包)和静态资源文件(网页图片)。通过本次作业,我掌握了:
requests的请求头设置、编码处理、超时与异常控制;BeautifulSoup的find_all/select两种解析方式及懒加载属性的兼容处理;re正则表达式在复杂网页结构下的兜底提取能力;- 数据清洗(去标签、去重、字段识别)与规整化输出(对齐表格、安全文件名)的工程化技巧;
- 面对"全站数据"时先勘察分页结构、再定量爬取的工作方法。
爬虫不仅是"把数据拿下来",更是把数据拿对、拿全、拿干净。这次实践为后续学习动态网页爬取(Selenium/Playwright)和数据分析打下了扎实的基础。
浙公网安备 33010602011771号