数据采集第一次作业
第一次线上作业
本次作业使用 Python 完成三个定向采集任务。运行环境为 Python 3.10 及以上版本,依赖库如下。
pip install requests beautifulsoup4
运行前,请在当前目录创建 images 文件夹,并将三次实际运行的终端截图分别保存为 task1_ranking.png、task2_backpack.png、task3_images.png。将本文粘贴到博客园后,也可以直接把图片拖到对应位置上传。
作业① 上海软科大学排名数据采集
1)实验代码和运行图片
本实验使用 requests 获取 2020 年中国最好大学排名页面,用 BeautifulSoup 遍历表格行,提取排名、学校名称、省市、学校类型和总分。LIMIT 可以根据需要修改,以下代码默认输出前 30 条记录。
import re
import requests
from bs4 import BeautifulSoup
URL = "https://www.shanghairanking.cn/rankings/bcur/2020"
LIMIT = 30
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
)
}
def clean_text(value):
"""合并页面中的空白字符。"""
return " ".join(value.split())
def get_ranking_rows(html_text):
"""从排名表格中提取有效数据行。"""
soup = BeautifulSoup(html_text, "html.parser")
ranking_rows = []
for row in soup.find_all("tr"):
cells = [clean_text(cell.get_text(" ", strip=True))
for cell in row.find_all(["th", "td"])]
if len(cells) < 5:
continue
rank = cells[0]
if not re.fullmatch(r"\d+", rank):
continue
ranking_rows.append(cells[:5])
if len(ranking_rows) >= LIMIT:
break
return ranking_rows
def main():
response = requests.get(URL, headers=HEADERS, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
rows = get_ranking_rows(response.text)
if not rows:
raise RuntimeError("未从页面中解析到大学排名数据。")
print("2020 中国最好大学排名")
print("排名\t学校名称\t省市\t学校类型\t总分")
for row in rows:
print("\t".join(row))
if __name__ == "__main__":
main()
程序输出的前几条记录如下。
2020 中国最好大学排名
排名 学校名称 省市 学校类型 总分
1 清华大学 北京 综合 852.5
2 北京大学 北京 综合 746.7
3 浙江大学 浙江 综合 649.2
4 上海交通大学 上海 综合 625.9
5 南京大学 江苏 综合 566.1

2)心得体会
这个页面的重点在于识别真正承载数据的表格行。页面中除了排名数据,还有导航、广告和说明文字,直接把所有文本输出会得到很多无关内容。我先取出每一行的单元格,再用排名列是否为纯数字作为筛选条件,因此可以避开表头和无效行。
请求网页时设置 User-Agent 很有必要。有些网站会拒绝没有浏览器标识的请求。采集结果中的总分会随榜单版本变化,因此运行时应以程序当次打印出的数据为准。
作业② 当当网书包商品比价定向爬虫
1)实验代码和运行图片
本实验选择当当网作为商城,搜索关键词为“书包”。程序使用 requests 请求搜索结果第 1 页,使用正则表达式从商品列表中提取商品名称和价格,并限制输出前 60 项。网页中的商品名称包含 HTML 标签时,程序会先去除标签并还原 HTML 实体。
import html
import re
import requests
SEARCH_URL = "https://search.dangdang.com/?key=%E4%B9%A6%E5%8C%85"
LIMIT = 60
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
),
"Accept-Language": "zh-CN,zh;q=0.9",
}
def clean_name(value):
"""清除商品标题中的标签和多余空白。"""
value = re.sub(r"<[^>]+>", "", value)
return " ".join(html.unescape(value).split())
def get_products(page_html):
"""从搜索结果 HTML 中提取商品名称和价格。"""
blocks = re.findall(
r'<li\b[^>]*\bddt-pit="(\d+)"[^>]*>(.*?)</li>',
page_html,
flags=re.S | re.I,
)
products = []
for _, block in blocks:
name_match = re.search(
r'<p\s+class="name"[^>]*>\s*<a\b[^>]*\btitle="([^"]+)"',
block,
flags=re.S | re.I,
)
price_match = re.search(
r'<p\s+class="price"[^>]*>\s*'
r'<span\s+class="price_n">¥([\d.]+)</span>',
block,
flags=re.S | re.I,
)
if not name_match or not price_match:
continue
products.append({
"name": clean_name(name_match.group(1)),
"price": float(price_match.group(1)),
})
if len(products) >= LIMIT:
break
return products
def main():
response = requests.get(SEARCH_URL, headers=HEADERS, timeout=20)
response.raise_for_status()
# 当当搜索页使用 GBK 系列编码,gb18030 能兼容更多中文字符。
page_html = response.content.decode("gb18030", errors="replace")
products = get_products(page_html)
if len(products) < LIMIT:
raise RuntimeError(f"只解析到 {len(products)} 条商品,未达到 {LIMIT} 条。")
print("当当网书包搜索结果第 1 页前 60 项")
print("序号\t价格\t商品名称")
for index, product in enumerate(products, start=1):
print(f"{index}\t{product['price']:.2f}\t{product['name']}")
prices = [product["price"] for product in products]
print("-" * 70)
print(f"最低价 {min(prices):.2f} 元")
print(f"最高价 {max(prices):.2f} 元")
print(f"平均价 {sum(prices) / len(prices):.2f} 元")
if __name__ == "__main__":
main()
程序输出格式如下。商品和价格会受库存、优惠和页面排序影响,因此实际运行结果可能与示例不同。
当当网书包搜索结果第 1 页前 60 项
序号 价格 商品名称
1 146.00 迪士尼书包小学生女童双肩包
2 39.99 书包幼儿园女孩卡通双肩包
3 89.99 书包女新款初中生高中学生大容量双肩包
...
60 117.00 凯蒂猫书包小学生女童双肩包

2)心得体会
商城搜索页与普通文章页不同,商品信息按固定结构重复出现。先观察一个商品块中的名称和价格位置,再把正则表达式限制在单个商品块内,能够减少标题、推荐词和广告价格带来的干扰。
搜索入口中的 key 参数保存关键词,翻页时商城会通过 page_index 等参数标识页码。本次作业限定第 1 页前 60 项,因此没有继续请求下一页。若要采集更多数据,可以在循环中修改页码参数,并在每次请求之间加入适当等待时间。
作业③ 下载指定网页中的 JPEG JPG PNG 图片
1)实验代码和运行图片
本实验选取福州大学新闻网首页 https://news.fzu.edu.cn/。题目所给的校园风采旧链接目前会返回 404,因此改用同一网站仍可正常访问的页面。程序会读取 img 标签中的 src、data-src 和 data-original 属性,处理相对路径,确认响应类型后仅保存 JPEG、JPG 和 PNG 文件到 download_images 文件夹。
import hashlib
import mimetypes
from pathlib import Path
from urllib.parse import urljoin, urlparse
import requests
from bs4 import BeautifulSoup
PAGE_URL = "https://news.fzu.edu.cn/"
OUTPUT_DIR = Path("download_images")
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36"
)
}
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png"}
CONTENT_TYPE_SUFFIX = {
"image/jpeg": ".jpg",
"image/png": ".png",
}
def find_image_urls(page_html):
"""从页面中收集图片地址并去重。"""
soup = BeautifulSoup(page_html, "html.parser")
image_urls = []
seen = set()
for image in soup.find_all("img"):
source = None
for attribute in ("src", "data-src", "data-original"):
if image.get(attribute):
source = image[attribute]
break
if not source:
continue
full_url = urljoin(PAGE_URL, source)
if full_url not in seen:
seen.add(full_url)
image_urls.append(full_url)
return image_urls
def get_extension(image_url, content_type):
"""按 URL 或响应头判断目标图片的扩展名。"""
suffix = Path(urlparse(image_url).path).suffix.lower()
if suffix in IMAGE_SUFFIXES:
return suffix
content_type = content_type.split(";", maxsplit=1)[0].lower()
if content_type in CONTENT_TYPE_SUFFIX:
return CONTENT_TYPE_SUFFIX[content_type]
guessed_suffix = mimetypes.guess_extension(content_type)
return guessed_suffix if guessed_suffix in IMAGE_SUFFIXES else None
def download_image(session, image_url, index):
"""下载单张 JPEG JPG PNG 图片,返回成功状态。"""
try:
response = session.get(image_url, timeout=20)
response.raise_for_status()
except requests.RequestException as error:
print(f"下载失败 {image_url},原因 {error}")
return False
content_type = response.headers.get("Content-Type", "")
suffix = get_extension(image_url, content_type)
if suffix is None:
return False
url_hash = hashlib.md5(image_url.encode("utf-8")).hexdigest()[:8]
file_path = OUTPUT_DIR / f"image_{index:03d}_{url_hash}{suffix}"
file_path.write_bytes(response.content)
print(f"已保存 {file_path.name}")
return True
def main():
OUTPUT_DIR.mkdir(exist_ok=True)
session = requests.Session()
session.headers.update(HEADERS)
response = session.get(PAGE_URL, timeout=20)
response.raise_for_status()
response.encoding = response.apparent_encoding
image_urls = find_image_urls(response.text)
success_count = 0
for index, image_url in enumerate(image_urls, start=1):
if download_image(session, image_url, index):
success_count += 1
print("-" * 50)
print(f"页面发现图片链接 {len(image_urls)} 个")
print(f"成功保存 JPEG JPG PNG 图片 {success_count} 个")
print(f"图片保存目录 {OUTPUT_DIR.resolve()}")
if __name__ == "__main__":
main()
程序输出格式如下。
已保存 image_001_XXXXXXXX.jpg
已保存 image_002_XXXXXXXX.png
...
--------------------------------------------------
页面发现图片链接 N 个
成功保存 JPEG JPG PNG 图片 N 个
图片保存目录 D:\\...\\download_images

2)心得体会
下载图片时,页面中的 img 标签不一定只使用 src 属性。许多网站会把真实地址放在 data-src 或 data-original 中,等图片进入浏览区域后再加载。程序同时检查这三个属性,可以提高对不同网页结构的适应性。
相对路径不能直接请求,需要使用 urljoin 拼接为完整地址。保存文件前还要检查 URL 后缀和响应头中的 Content-Type,这样可以避免把 SVG、GIF 或 HTML 错误页面误保存为图片。下载完成后以文件夹中的实际文件数量作为结果核验依据。

浙公网安备 33010602011771号