数据采集第一次作业
数据采集第一次作业——requests 定向爬虫实践
本次作业使用 Python 语言,基于 requests 网络请求库,配合 BeautifulSoup 解析库与 re 正则表达式,完成三个定向数据采集任务。
一、作业①:上海软科 2020 中国大学排名爬取
1)实验代码与运行结果
实验目标:用 requests 和 BeautifulSoup 库方法定向爬取 http://www.shanghairanking.cn/rankings/bcur/2020 的数据,屏幕打印爬取的大学排名信息,输出格式为:排名、学校名称、省市、学校类型、总分。
实现思路:
- 用
requests.get()携带 User-Agent 请求头获取网页源码,并用apparent_encoding处理编码; - 用
BeautifulSoup的find_all("tr")遍历表格行,用get_text(strip=True)提取单元格文本; - 过滤掉表头和无效行(排名列不是纯数字的行),只保留有效数据行;
- 用固定列宽格式化输出表格。
完整代码:
# -*- coding: utf-8 -*-
"""
作业①:用 requests + BeautifulSoup 定向爬取软科 2020 中国大学排名
输出:排名、学校名称、省市、学校类型、总分
"""
import re
import requests
from bs4 import BeautifulSoup
URL = "http://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/120.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")
rows = []
for tr in soup.find_all("tr"):
cells = [
cell.get_text(" ", strip=True)
for cell in tr.find_all(["td", "th"])
]
if len(cells) < 5:
continue
rank = cells[0].strip()
if not re.fullmatch(r"\d+", rank):
continue
rows.append(cells[:5])
return rows
def print_table(rows):
"""按固定列宽格式化打印排名表格"""
print(
f"{'排名':<6}{'学校名称':<18}{'省市':<10}"
f"{'学校类型':<10}{'总分':<8}"
)
print("-" * 60)
for row in rows:
print(
f"{row[0]:<6}{row[1]:<18}{row[2]:<10}"
f"{row[3]:<10}{row[4]:<8}"
)
if __name__ == "__main__":
html = fetch_html(URL)
data = parse_ranking(html)
if not data:
print("未获取到排名数据,请检查页面结构是否变化。")
else:
print_table(data)
运行结果:

2)心得体会
这道题的重点在于识别真正承载数据的表格行。页面中除了排名数据,还有导航、广告和说明文字,直接用 find_all("tr") 会取到很多无关行。我先取出每一行的单元格,再用“排名列是否为纯数字”作为筛选条件,因此可以避开表头和无效行。
请求网页时设置 User-Agent 是必要的,有些网站会拒绝没有浏览器标识的请求。编码方面,requests 拿到响应后默认按响应头的 charset 解码,国内部分网站不返回 charset 或返回的与实际不一致,因此用 apparent_encoding 更稳妥。
此外,软科页面不同年份的表格 class 名称可能变化,所以我没有依赖具体的 class,而是直接遍历所有 <tr> 并用数据特征筛选,这样即使页面小改也不会失效。
二、作业②:当当网“书包”商品比价定向爬虫
1)实验代码与运行结果
实验目标:用 requests 和 re 库方法设计商品比价定向爬虫,爬取当当网以关键词“书包”搜索页面的商品名称和价格。
实现思路:
- 用
requests.get()请求当当网搜索页,用gbk解码; - 先用正则定位商品列表容器
<ul class="bigimg">,再从中提取每个<li>商品块; - 在商品块内用正则提取商品名称(
<p class="name">下<a>的title属性)和价格(<span class="price_n">中的数字); - 对提取结果去重后格式化输出。
完整代码:
# -*- coding: utf-8 -*-
"""
作业②:用 requests + re 爬取当当网“书包”搜索结果的商品名称和价格
"""
import os
import re
import time
import random
from urllib.parse import quote
import requests
SAVE_DIR = "html_pages"
os.makedirs(SAVE_DIR, exist_ok=True)
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",
}
def download_page(keyword="书包", page=1):
"""下载当当网搜索页并保存为本地 HTML 文件"""
url = f"http://search.dangdang.com/?key={quote(keyword)}&page_index={page}"
print(f"正在下载第 {page} 页:{url}")
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
html = resp.content.decode("gbk", errors="ignore")
except requests.RequestException as e:
print("请求失败:", e)
return None
filename = os.path.join(SAVE_DIR, f"{keyword}_page{page}.html")
with open(filename, "w", encoding="utf-8") as f:
f.write(html)
print(f" 已保存:{filename}({len(html)} 字符)")
time.sleep(random.uniform(1.5, 3.0))
return html
def parse_html(html, max_items=60):
"""
用正则从 HTML 中解析商品名称和价格。
返回 [(名称, 价格), ...]
"""
items = []
ul_match = re.search(
r'<ul[^>]*class="bigimg"[^>]*>(.*?)</ul>', html, re.S
)
scope = ul_match.group(1) if ul_match else html
li_list = re.findall(r"<li[^>]*>.*?</li>", scope, re.S)
print(f" 共找到 {len(li_list)} 个 <li> 块")
for li in li_list:
if len(items) >= max_items:
break
name_match = re.search(
r'<p[^>]*class="name"[^>]*>.*?<a[^>]*title="([\s\S]+?)"',
li, re.S
)
if not name_match:
name_match = re.search(
r'<a[^>]*title="([\s\S]+?)"', li, re.S
)
price_match = re.search(
r'<span[^>]*class="price_n"[^>]*>\s*(?:¥|¥)?\s*([\d.]+)',
li, re.S
)
if not price_match:
price_match = re.search(
r'<span[^>]*class="search_now_price"[^>]*>'
r'\s*(?:¥|¥)?\s*([\d.]+)',
li, re.S
)
if name_match and price_match:
name = name_match.group(1).strip()
price = price_match.group(1).strip()
items.append((name, price))
return items
if __name__ == "__main__":
all_items = []
for page in range(1, 4):
html = download_page("书包", page)
if html is None:
continue
print(f"\n===== 分析第 {page} 页 =====")
items = parse_html(html)
all_items.extend(items)
print(f" 本页提取 {len(items)} 项")
seen = set()
unique_items = []
for name, price in all_items:
key = (name, price)
if key not in seen:
seen.add(key)
unique_items.append((name, price))
print(f"\n共提取 {len(all_items)} 项,去重后 {len(unique_items)} 项")
print(f"{'序号':<6}{'价格':<12}{'商品名'}")
print("-" * 70)
for i, (name, price) in enumerate(unique_items[:60], 1):
print(f"{i:<6}{price:<12}{name}")
运行结果:


2)心得体会
这道题让我深刻体会到了“先下载、再离线解析”的调试优势。一开始我直接用 requests 请求页面然后用正则匹配,反复失败却不知道原因。后来改为先把 HTML 保存到本地,再打开文件查看真实结构,才发现当当网大图模式下价格用的是 class="price_n",而不是我以为的 search_now_price。离线调试让我能反复查看 HTML 结构、逐步调整正则表达式。
正则表达式中,[\s\S]+? 比 [^"]+ 更适合匹配属性值,因为 title 属性中可能包含换行符,[^"] 不匹配换行会导致匹配失败。另外,当当网使用 GB2312 编码,用 resp.content.decode("gbk") 比 resp.text 更可靠。
请求频率方面,当当网也有一定的反爬机制,所以我在每次请求之间加了 1.5 到 3 秒的随机延时,避免被服务器限制。
三、作业③:网页图片批量下载
1)实验代码与运行结果
实验目标:爬取给定网页(https://news.fzu.edu.cn/yxfd.htm)中所有 JPEG、JPG、PNG 格式的图片文件,并保存到文件夹中。
实现思路:
- 用 requests 请求页面,用 BeautifulSoup 解析所有
<img>标签; - 兼容
src、data-src、data-original等多种懒加载属性; - 用
urljoin补全相对路径,只保留.jpg、.jpeg、.png扩展名; - 通过 URL 关键词过滤、文件大小过滤、图片尺寸过滤三重机制排除图标和占位图。
完整代码:
# -*- coding: utf-8 -*-
"""
作业③:爬取指定网页中所有 JPEG/JPG/PNG 图片并保存到文件夹
"""
import os
import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from PIL import Image
from io import BytesIO
def download_images(page_url, save_dir="downloaded_images"):
"""
爬取指定网页中的 JPEG、JPG、PNG 图片,并保存到本地文件夹。
自动过滤 logo、图标、按钮、占位图等无关小图。
"""
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",
}
allowed_ext = (".jpg", ".jpeg", ".png")
exclude_keywords = [
"logo", "icon", "button", "blank", "loading",
"spacer", "arrow", "bg", "background", "placeholder",
"ad", "banner", "share", "qrcode"
]
MIN_WIDTH = 100
MIN_HEIGHT = 100
MIN_FILE_SIZE = 5 * 1024
try:
response = requests.get(page_url, headers=headers, timeout=15)
response.raise_for_status()
response.encoding = response.apparent_encoding
soup = BeautifulSoup(response.text, "html.parser")
content_area = (
soup.find("div", id="vsb_content")
or soup.find("div", class_="content")
or soup.find("div", class_="article-content")
or soup.find("div", class_="news_content")
)
if content_area:
print("已定位正文区域,只提取正文中的图片。")
img_tags = content_area.find_all("img")
else:
print("未找到正文区域,提取页面全部图片。")
img_tags = soup.find_all("img")
if not img_tags:
print("页面中没有找到任何 <img> 标签。")
return
os.makedirs(save_dir, exist_ok=True)
count = 0
for img in img_tags:
src = (
img.get("src")
or img.get("data-src")
or img.get("data-original")
)
if not src:
continue
img_url = urljoin(page_url, src)
path = urlparse(img_url).path.lower()
if not path.endswith(allowed_ext):
continue
if any(kw in img_url.lower() for kw in exclude_keywords):
print(f"跳过无关图片:{img_url}")
continue
try:
img_resp = requests.get(
img_url, headers=headers, timeout=15
)
img_resp.raise_for_status()
if len(img_resp.content) < MIN_FILE_SIZE:
print(f"跳过过小文件:{img_url}")
continue
try:
im = Image.open(BytesIO(img_resp.content))
w, h = im.size
if w < MIN_WIDTH or h < MIN_HEIGHT:
print(f"跳过小尺寸图片:{img_url} ({w}x{h})")
continue
except Exception:
print(f"无法识别图片格式,跳过:{img_url}")
continue
filename = os.path.basename(path)
if not filename:
filename = f"image_{count}.jpg"
filepath = os.path.join(save_dir, filename)
base, ext = os.path.splitext(filename)
num = 1
while os.path.exists(filepath):
filepath = os.path.join(
save_dir, f"{base}_{num}{ext}"
)
num += 1
with open(filepath, "wb") as f:
f.write(img_resp.content)
count += 1
print(f"已保存:{filepath} ({w}x{h})")
time.sleep(0.5)
except requests.RequestException as e:
print(f"图片下载失败:{img_url},原因:{e}")
print(f"\n共保存 {count} 张图片到文件夹:{save_dir}")
except requests.RequestException as e:
print(f"页面请求失败:{e}")
if __name__ == "__main__":
target_url = "https://news.fzu.edu.cn/yxfd.htm"
download_images(target_url, save_dir="downloaded_images")
运行结果:


2)心得体会
这道题最大的教训是:不要盲目爬取页面中的所有 <img> 标签。第一次运行时,我下载了 50 多张图片,其中大部分是 logo、按钮图标、加载动画。后来我加入了三重过滤机制:① URL 关键词过滤,排除包含 logo、icon、loading 等词的链接;② 文件大小过滤,小于 5KB 的图片直接跳过;③ 图片尺寸过滤,用 Pillow 读取实际宽高,小于 100×100 的跳过。
此外,学校服务器对 HTTPS 请求有时会返回 SSL 错误,后来我在请求中加入了 verify=False 和重试机制来解决。图片链接还需要用 urljoin 把相对路径补全为绝对路径,否则 requests 无法正确下载。

浙公网安备 33010602011771号