数据采集第一次作业
数据采集第一次作业
一、作业①:软科大学排名定向爬取
1. 作业代码与运行结果
1.1 代码实现
from urllib.request import urlopen, Request
from bs4 import BeautifulSoup
import re
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/120.0.0.0 Safari/537.36"
}
# 发送请求,获取网页源码
req = Request(url, headers=headers)
resp = urlopen(req)
html = resp.read().decode("utf-8")
soup = BeautifulSoup(html, "lxml")
N = 30 # 爬取前 30条数据
# 定位排名表格
table = soup.find("table")
rows = table.find_all("tr")[1:]
print("排名 学校名称 省市 学校类型 总分")
count = 0
for row in rows:
if count >= N:
break
tds = row.find_all("td")
rank = tds[0].get_text(strip=True)
full_text = tds[1].get_text(strip=True)
match = re.search(r'^([^A-Z]+)', full_text)
school_name = match.group(1).strip() if match else full_text
province = tds[2].get_text(strip=True)
school_type = tds[3].get_text(strip=True)
score = tds[4].get_text(strip=True)
print(f"{rank:<4}{school_name:<18}{province:<10}{school_type:<10}{score}")
count += 1
1.2 运行结果


2. 作业心得
本次作业使用 requests和 BeautifulSoup 对软科中国大学排名页面进行定向爬取。通过实践,我掌握了以下要点:
- 请求头的设置:直接请求目标网站会被拒绝,必须添加
User-Agent伪装成浏览器访问。 - 表格定位:使用
soup.find("table")定位排名表格,再通过find_all("tr")逐行遍历,跳过表头后逐列提取数据。 - 文本清洗:学校名称单元格同时包含中文名和英文名,通过正则
^([^A-Z]+)提取中文部分,保证输出整洁。
通过这次作业,我理解了 HTML 表格结构解析的一般思路,也体会到网页结构分析在爬虫中的重要性。
二、作业②:商城商品比价定向爬虫
1. 作业代码与运行结果
1.1 代码实现
from urllib.request import urlopen, Request
from urllib.parse import quote
from bs4 import BeautifulSoup
import os
import re
import unicodedata
KEYWORD = "书包" # 搜索关键词
MAX_ITEMS = 60 # 最多爬取并打印多少件商品
SEARCH_URL = "https://search.dangdang.com/" # 当当网商品搜索页
PAGE_ENCODING = "gb18030" # 当当网页面编码是GBK
RESULT_FILE = "书包商品价格.txt"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/128.0.0.0 Safari/537.36",
"Referer": "https://www.dangdang.com/"}
def build_search_url(keyword):
"""构造搜索 URL,关键词按 GBK 编码后再进行 URL 编码。"""
encoded_keyword = quote(keyword.encode("gbk"))
return f"{SEARCH_URL}?key={encoded_keyword}&act=input"
def fetch_html(url, encoding=PAGE_ENCODING, timeout=20):
"""请求网页并返回解码后的 HTML 源码。"""
request = Request(url, headers=HEADERS)
with urlopen(request, timeout=timeout) as response:
raw_bytes = response.read()
return raw_bytes.decode(encoding, errors="replace")
def extract_price(text):
"""从文本中提取第一个数字作为价格。"""
match = re.search(r"\d+(?:\.\d+)?", text)
return float(match.group()) if match else None
def parse_products(html, max_items=MAX_ITEMS):
"""解析商品列表,返回包含序号、价格、商品名、链接的字典列表。"""
soup = BeautifulSoup(html, "lxml")
product_list = soup.find("ul", class_="bigimg")
if product_list is None:
raise RuntimeError("没有找到商品列表,页面结构可能已改变或被服务器拦截")
products = []
for li in product_list.find_all("li", recursive=False):
name_tag = li.select_one("p.name a")
price_tag = li.find("span", class_="price_n")
if name_tag is None or price_tag is None:
continue
name = (name_tag.get("title") or name_tag.get_text(strip=True)).strip()
price = extract_price(price_tag.get_text(strip=True))
if not name or price is None:
continue
link_tag = li.find("a", href=True)
link = link_tag["href"] if link_tag else ""
if link.startswith("//"):
link = "https:" + link
products.append({
"序号": len(products) + 1,
"价格": price,
"商品名": name,
"链接": link,
})
if len(products) >= max_items:
break
return products
def display_width(text):
"""计算文字在终端里的显示宽度(汉字等全角字符按 2 个字符宽计算)。"""
return sum(2 if unicodedata.east_asian_width(char) in "WF" else 1 for char in text)
def pad(text, width):
text = str(text)
return text + " " * max(0, width - display_width(text))
def print_table(products):
"""在终端打印商品表格及价格统计信息。"""
print(f"【{KEYWORD}】搜索结果(数据来源:当当网)\n")
print(pad("序号", 6) + pad("价格", 10) + "商品名")
print("-" * 90)
for product in products:
print(pad(product["序号"], 6) + pad(f"{product['价格']:.2f}", 10) + product["商品名"])
if products:
prices = [product["价格"] for product in products]
print("-" * 90)
print(f"共 {len(products)} 件商品:最低价 {min(prices):.2f} 元,"
f"最高价 {max(prices):.2f} 元,平均价 {sum(prices) / len(prices):.2f} 元")
def save_result(products, file_name=RESULT_FILE):
"""将爬取结果保存到文本文件。"""
with open(file_name, "w", encoding="utf-8") as file:
file.write(pad("序号", 6) + pad("价格", 10) + "商品名\t链接\n")
for product in products:
file.write(pad(product["序号"], 6) + pad(f"{product['价格']:.2f}", 10)
+ f"{product['商品名']}\t{product['链接']}\n")
print(f"\n结果已保存到:{os.path.abspath(file_name)}")
def main():
url = build_search_url(KEYWORD)
print(f"正在爬取:{url}\n")
html = fetch_html(url)
products = parse_products(html, MAX_ITEMS)
print_table(products)
save_result(products)
if __name__ == "__main__":
main()
1.2 运行结果



2. 作业心得
本次作业我选择当当网作为目标商城,以"书包"为关键词爬取商品名称和价格。主要收获如下:
- 编码问题:当当网页面使用
gb18030编码,而非普遍使用的utf-8。如果不指定编码,中文会显示为乱码。关键词也需要先按GBK编码再进行URL编码,否则搜索不到结果。 - 正则提取价格:价格文本中可能包含
¥、元等符号,使用re.search(r"\d+(?:\.\d+)?", text)可准确提取数字部分。 - 表格对齐:中文在终端中的显示宽度是英文字符的两倍,直接用
ljust会导致对齐错乱。通过unicodedata.east_asian_width判断字符宽度,实现了中英文混排的整齐输出。 - 数据持久化:将结果同时打印到屏幕并保存为文本文件,方便后续数据分析。
通过本次实践,我熟悉了 re 库在文本提取中的应用,也加深了对网页编码、请求头等爬虫基础知识的理解。
三、作业③:网页图片批量爬取
1. 作业代码与运行结果
1.1 代码实现
from urllib.request import urlopen, Request
from urllib.parse import urljoin, urlparse, unquote
from bs4 import BeautifulSoup
import hashlib
import os
import re
import sys
PAGE_URL = "https://news.fzu.edu.cn/yxfd.htm"
SAVE_DIR = "fzu_images"
ALLOWED_EXT = (".jpg", ".jpeg", ".png")
MAX_IMAGES = 100
PAGE_ENCODING = "utf-8"
TIMEOUT = 20
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/128.0.0.0 Safari/537.36",
}
# 通过文件头魔数识别真实图片格式
IMAGE_MAGIC = {
b"\xff\xd8\xff": ".jpg",
b"\x89PNG\r\n\x1a\n": ".png",
}
def fetch_bytes(url, timeout=TIMEOUT):
request = Request(url, headers=HEADERS)
with urlopen(request, timeout=timeout) as response:
return response.read(), (response.headers.get("Content-Type") or "").lower()
def fetch_html(url, encoding=PAGE_ENCODING):
"""请求网页并返回 HTML 源码字符串。"""
data, _ = fetch_bytes(url)
return data.decode(encoding, errors="replace")
def to_absolute(page_url, src):
"""把图片地址补全成绝对链接;过滤掉data:、javascript: 这类无效地址。"""
if not src:
return None
src = src.strip()
if not src or src.startswith(("data:", "javascript:", "#")):
return None
if src.startswith("//"):
src = "https:" + src
return urljoin(page_url, src)
def pick_from_srcset(srcset, page_url):
"""从 srcset 属性中取第一个候选链接。"""
first = srcset.split(",")[0].strip().split(" ")[0]
return to_absolute(page_url, first)
def collect_style_text(soup):
"""收集页面里的 CSS 文本(<style> 内容和标签的 style 属性),用于查找背景图片。"""
texts = [tag.get_text() for tag in soup.find_all("style")]
texts += [tag["style"] for tag in soup.find_all(style=True)]
return texts
def collect_image_urls(soup, page_url):
"""遍历页面所有 <img> 标签收集图片链接,返回去重后的绝对链接列表。"""
src_attributes = ("src", "data-src", "data-original", "data-lazy-src", "data-echo", "orisrc")
urls, seen = [], set()
def add(candidate):
"""把新链接加入结果列表,重复的忽略。"""
if candidate and candidate not in seen:
seen.add(candidate)
urls.append(candidate)
for img in soup.find_all("img"):
for attr in src_attributes:
add(to_absolute(page_url, img.get(attr)))
if img.get("srcset"):
add(pick_from_srcset(img["srcset"], page_url))
for style_text in collect_style_text(soup):
for _, css_url in re.findall(r"url\((['\"]?)(.*?)\1\)", style_text):
add(to_absolute(page_url, css_url))
return urls
def is_candidate(url):
"""判断链接是否可能是图片:扩展名符合或在允许列表中。"""
ext = os.path.splitext(urlparse(url).path)[1].lower()
return ext in ALLOWED_EXT or ext == ""
def detect_image_type(data):
"""根据文件头魔数判断图片真实类型,返回扩展名。"""
for magic, ext in IMAGE_MAGIC.items():
if data.startswith(magic):
return ext
return ""
def build_file_name(url, index, ext, save_dir):
"""根据 URL 和序号构造保存文件名,并清理非法字符。"""
raw_name = unquote(os.path.basename(urlparse(url).path)) or "image"
raw_name = re.sub(r'[\\/:*?"<>|\s]+', "_", raw_name)
raw_name = os.path.splitext(raw_name)[0][:40] or "image"
return os.path.join(save_dir, f"{index:03d}_{raw_name}{ext}")
def download_images(urls, save_dir):
"""依次下载图片并保存,返回 (成功数, 跳过数, 失败数)。"""
os.makedirs(save_dir, exist_ok=True)
success = skipped = failed = 0
saved_hashes = set()
for url in urls:
if success >= MAX_IMAGES:
break
try:
data, content_type = fetch_bytes(url)
except Exception as error:
failed += 1
print(f"[失败] {url}({error})")
continue
digest = hashlib.md5(data).hexdigest()
ext = detect_image_type(data)
if ext == "" or digest in saved_hashes:
skipped += 1
continue
file_path = build_file_name(url, success + 1, ext, save_dir)
with open(file_path, "wb") as file:
file.write(data)
saved_hashes.add(digest)
success += 1
print(f"[{success}] 已保存 {os.path.basename(file_path)}"
f"({len(data) / 1024:.1f} KB,{content_type})")
return success, skipped, failed
def main():
"""主函数:抓取网页 → 收集图片链接 → 下载保存 → 打印统计结果。"""
page_url = sys.argv[1] if len(sys.argv) > 1 else PAGE_URL
save_dir = sys.argv[2] if len(sys.argv) > 2 else SAVE_DIR
print(f"目标网页:{page_url}")
html = fetch_html(page_url)
soup = BeautifulSoup(html, "lxml")
print(f"页面中共找到 <img> 标签 {len(soup.find_all('img'))} 个")
image_urls = [url for url in collect_image_urls(soup, page_url) if is_candidate(url)]
print(f"筛选出 {len(image_urls)} 个 jpg/jpeg/png 候选链接,开始下载……\n")
success, skipped, failed = download_images(image_urls, save_dir)
print(f"\n下载完成:成功 {success} 张,跳过 {skipped} 个(格式不符或内容重复),失败 {failed} 个")
print(f"图片保存目录:{os.path.abspath(save_dir)}")
if __name__ == "__main__":
main()
1.2 运行结果

1.3 保存的图片

2. 作业心得
本次作业爬取了福州大学新闻网"院系风采"页面的所有 JPG/JPEG/PNG 图片。主要收获如下:
- 多种图片来源:页面中图片不仅存在于
<img>标签的src属性,还可能藏在data-src、data-original、srcset等属性以及 CSS 的background-image中。因此代码中收集了多种属性,并额外解析了<style>和行内style中的url()。 - 文件类型校验:仅凭扩展名判断图片类型并不完全可靠,有些链接没有扩展名。代码通过读取文件头魔数(JPEG 的
\xff\xd8\xff、PNG 的\x89PNG\r\n\x1a\n)来判断真实格式,避免下载到非图片内容。 - 去重处理:使用MD5哈希对图片内容去重,避免同一张图片因不同链接被重复保存。
- 文件名安全:URL中可能包含中文、空格、特殊字符,通过
unquote解码+正则替换非法字符+截断长度,确保文件名合法。
通过本次作业,我深入理解了网页中图片资源的各种加载方式,也掌握了二进制文件下载、文件头识别等实用技巧。

浙公网安备 33010602011771号