数据采集第一次作业报告

数据采集第一次作业报告

学号:102302113 姓名:王泽炜 班级:数据科学与大数据技术


作业① 上海软科大学排名定向爬虫

1)爬虫实验

实验说明

  • 目标网址:http://www.shanghairanking.cn/rankings/bcur/2020
  • 技术栈:requests + BeautifulSoup
  • 功能描述:爬取上海软科2020中国大学排名页面,屏幕打印排名、学校名称、省市、学校类型、总分五项信息。

完整代码

# -*- coding: utf-8 -*-
"""
作业①:上海软科大学排名爬虫
学号:102302113  姓名:王泽炜
目标网址:http://www.shanghairanking.cn/rankings/bcur/2020
技术栈:requests + BeautifulSoup
"""

import requests
from bs4 import BeautifulSoup

# 请求头,模拟浏览器访问
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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}

# 目标URL
URL = "http://www.shanghairanking.cn/rankings/bcur/2020"


def fetch_page(url):
    """发送GET请求获取网页内容"""
    try:
        response = requests.get(url, headers=HEADERS, timeout=15)
        response.raise_for_status()  # 检查请求状态码是否为200
        response.encoding = response.apparent_encoding  # 使用正确的编码
        return response.text
    except requests.RequestException as e:
        print(f"请求失败:{e}")
        return None


def parse_ranking(html_text):
    """使用BeautifulSoup解析HTML,提取排名信息"""
    if not html_text:
        return []

    soup = BeautifulSoup(html_text, "lxml")
    result_list = []

    # 查找排名表格
    table = soup.find("table")
    if not table:
        print("未找到排名表格,尝试其他选择器...")
        # 备用:查找包含数据的行
        rows = soup.find_all("tr")
    else:
        rows = table.find_all("tr")

    # 遍历每一行,跳过表头
    for row in rows[1:]:
        cols = row.find_all("td")
        if len(cols) >= 5:
            rank = cols[0].get_text(strip=True)
            school = cols[1].get_text(strip=True)
            province = cols[2].get_text(strip=True)
            school_type = cols[3].get_text(strip=True)
            score = cols[4].get_text(strip=True)
            result_list.append({
                "排名": rank,
                "学校名称": school,
                "省市": province,
                "学校类型": school_type,
                "总分": score,
            })
    return result_list


def print_ranking(data_list, max_show=30):
    """格式化输出排名信息"""
    if not data_list:
        print("未获取到数据")
        return

    # 打印表头
    print(f"{'排名':<6}{'学校名称':<20}{'省市':<10}{'学校类型':<10}{'总分':<10}")
    print("-" * 56)
    # 打印数据行
    for item in data_list[:max_show]:
        print(f"{item['排名']:<6}{item['学校名称']:<20}{item['省市']:<10}"
              f"{item['学校类型']:<10}{item['总分']:<10}")
    print(f"\n共获取 {len(data_list)} 条数据(展示前 {min(max_show, len(data_list))} 条)")


def main():
    """主函数:获取页面 -> 解析 -> 输出"""
    print("=" * 60)
    print("上海软科2020中国大学排名爬虫")
    print("=" * 60)

    html = fetch_page(URL)
    if html:
        data = parse_ranking(html)
        print_ranking(data)


if __name__ == "__main__":
    main()

运行结果(前10条)

排名 学校名称 省市 学校类型 总分
1 清华大学 北京 综合 852.5
2 北京大学 北京 综合 746.7
3 浙江大学 浙江 综合 649.2
4 上海交通大学 上海 综合 625.9
5 南京大学 江苏 综合 566.1
6 复旦大学 上海 综合 556.7
7 中国科学技术大学 安徽 理工 526.4
8 华中科技大学 湖北 综合 497.7
9 武汉大学 湖北 综合 488.0
10 中山大学 广东 综合 457.2

任务1


2)心得体会

通过本次作业①的练习,我系统地掌握了用 requests 库发送 HTTP 请求和用 BeautifulSoup 解析 HTML 的基本流程,主要收获如下:

(1)请求头和编码问题:最开始直接请求目标网页时,遇到了乱码和被拒绝访问的问题。后来通过添加 User-Agent、Accept-Language 等请求头伪装成浏览器,并用 response.apparent_encoding 自动识别页面编码后,页面内容才正常显示。这让我深刻体会到爬虫不只是发个请求就完事,还需要考虑目标网站的反爬策略和页面编码。

(2)BeautifulSoup 的使用方式:我学会了先用 soup.find("table") 定位到整个排名表格,再用 find_all("tr") 遍历每一行,每行里用 find_all("td") 取到各个单元格的数据,最后用 get_text(strip=True) 去除多余空白。这种"从大到小、逐层深入"的解析思路让我对 DOM 树结构有了更清晰的理解。

(3)结构化数据提取:在解析过程中,我用一个字典列表来存储每条排名数据(排名、学校、省市、类型、总分),而不是简单地拼字符串。这种结构化存储不仅让最终的格式化输出更方便,也便于后续如果要做数据分析可以直接使用。

(4)异常处理的重要性:我在 fetch_page 函数里用了 try...except 捕获 requests.RequestException,并在 raise_for_status() 后自动识别编码。这样即使网络抖动或目标网站临时不可访问,程序也不会直接崩溃,而是给出友好提示。这培养了我写代码时要考虑边界情况和异常路径的习惯。


作业② 当当网书包比价爬虫

1)爬虫实验

实验说明

  • 目标网址:当当网搜索"书包"(http://search.dangdang.com/?key=书包&page_index=1)
  • 技术栈:requests + re(正则表达式)
  • 功能描述:向当当网搜索页面发送请求,用正则匹配提取商品名称和价格,最后按价格排序输出比价结果。

完整代码

# -*- coding: utf-8 -*-
"""
作业②:商城书包比价爬虫
学号:102302113  姓名:王泽炜
目标网址:当当网搜索"书包"
技术栈:requests + re(正则表达式)
功能:以"书包"为关键词,抓取搜索页面的商品名称和价格,进行比价
"""

import requests
import re

# 请求头,模拟浏览器访问(防止被反爬)
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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Referer": "https://www.dangdang.com/",
}


def fetch_search_page(keyword, page=1):
    """
    发送GET请求,获取当当网搜索页面HTML
    :param keyword: 搜索关键词
    :param page: 页码
    :return: 页面HTML字符串,失败返回None
    """
    url = f"http://search.dangdang.com/?key={keyword}&page_index={page}"
    try:
        response = requests.get(url, headers=HEADERS, timeout=15)
        response.raise_for_status()         # 状态码检查
        response.encoding = response.apparent_encoding  # 自动识别编码
        return response.text
    except requests.RequestException as e:
        print(f"[错误] 请求失败:{e}")
        return None


def extract_titles(html_text):
    """
    用正则提取商品名称列表
    当当网商品标题格式:<a title="商品名" name="itemlist-title" ...>
    """
    pattern = re.compile(
        r'<a\s[^>]*title="\s*([^"]+?)\s*"[^>]*name="itemlist-title"[^>]*>',
        re.IGNORECASE
    )
    return pattern.findall(html_text)


def extract_prices(html_text):
    """
    用正则提取商品价格列表
    当当网价格格式:<span class="price_n">&yen;38.00</span>
    也可能是:<span class="search_now_price">&yen;49.80</span>
    """
    # 优先匹配 price_n
    prices = re.findall(r'<span\s+class="price_n">&yen;(\d+\.?\d*)</span>', html_text)
    # 如果不够,补充 search_now_price
    prices2 = re.findall(r'<span\s+class="search_now_price">&yen;(\d+\.?\d*)</span>', html_text)
    if not prices:
        prices = prices2
    return prices


def pair_products(titles, prices):
    """
    将商品标题和价格按顺序配对,返回字典列表
    当当网页面标题和价格出现顺序一一对应
    """
    products = []
    count = min(len(titles), len(prices))

    for i in range(count):
        try:
            price_val = float(prices[i])
        except ValueError:
            continue

        products.append({
            "序号": len(products) + 1,
            "价格": prices[i],
            "价格数": price_val,
            "商品名": titles[i].strip(),
        })
    return products


def print_comparison(data_list, max_show=30):
    """格式化输出比价结果(按价格排序)"""
    if not data_list:
        print("未获取到数据")
        return

    # 按价格从低到高排序
    sorted_list = sorted(data_list, key=lambda x: x["价格数"])

    print(f"\n{'序号':<6}{'价格':<15}{'商品名'}")
    print("-" * 75)
    for item in sorted_list[:max_show]:
        display_idx = sorted_list.index(item) + 1
        print(f"{display_idx:<6}{'¥' + item['价格']:<14}{item['商品名']}")

    print(f"\n共获取 {len(sorted_list)} 条书包商品数据")
    print(f"价格范围:¥{sorted_list[0]['价格']} ~ ¥{sorted_list[-1]['价格']}")
    print(f"平均价格:¥{sum(p['价格数'] for p in sorted_list) / len(sorted_list):.2f}")


def main():
    """主函数:搜索书包 -> 正则提取 -> 配对比价 -> 输出"""
    print("=" * 75)
    print("当当网书包比价爬虫")
    print("技术栈:requests + re 正则表达式")
    print("=" * 75)

    keyword = "书包"
    html = fetch_search_page(keyword, page=1)

    if not html:
        print("✗ 页面获取失败")
        return

    print(f"✓ 搜索页面获取成功,HTML长度:{len(html)} 字符")

    titles = extract_titles(html)
    prices = extract_prices(html)
    print(f"✓ 用正则提取到商品标题 {len(titles)} 个,价格 {len(prices)} 个")

    products = pair_products(titles, prices)
    print(f"✓ 成功配对 {len(products)} 件书包商品")

    print_comparison(products)


if __name__ == "__main__":
    main()

运行结果(价格排序前10条)

序号 价格 商品名
1 ¥26.50 仙逆1 网络大神耳根代表作...(含"书包"关键词的图书)
2 ¥39.99 书包幼儿园女孩 潮小兔子书包3-6岁大中小班...
3 ¥52.80 书包小学生男女生1-2-3-6年级儿童书包男孩防水轻便...
4 ¥55.00 ins超火学生书包女韩版原宿ulzzang嘻哈双肩包...
5 ¥59.00 小学生书包男1-3-4-6年级儿童书包男孩6-12周岁...
6 ¥62.00 双肩包女新款牛津布小背包韩版潮女士尼龙百搭时尚帆布书包包
7 ¥69.99 新款初中生书包女撞色百搭可手提学生补习包男大容量...
8 ¥75.00 新款书包小学生男孩女生一二三到六年级男轻便护脊...
9 ¥88.00 新款库洛米女孩三到六减负超轻便一年级儿童小学生...
10 ¥89.99 书包女新款初中生高中学生大容量轻便减负护脊...

共获取60条书包商品数据,价格范围 ¥26.50 ~ ¥678.30,平均价格 ¥123.27

任务2


2)心得体会

作业②要求用 requests + re 正则表达式来爬取商城数据,这和作业①的 BeautifulSoup 方案截然不同,让我对两种解析方式的适用场景有了深入体会。

(1)正则表达式的"硬核"本质:用正则提取数据不像 BeautifulSoup 那样有标签导航感,而是直接在原始字符串里"捞"数据。当当网的商品标题在 <a title="xxx" name="itemlist-title"> 里,价格在 <span class="price_n">&yen;38.00</span> 里。我分别写了两条正则来匹配这两种模式:

  • 标题:r'<a\s[^>]*title="\s*([^"]+?)\s*"[^>]*name="itemlist-title"[^>]*>'
  • 价格:r'<span\s+class="price_n">&yen;(\d+\.?\d*)</span>'

写完之后我发现一个规律——正则的核心是"先锁定包含目标片段的长模式,再用括号捕获需要的那一小段"。[^>]* 这种"匹配任意非大于号字符"的写法特别有用,可以灵活跳过标签里的其它属性。

(2)反爬策略的实战体验:我一开始想爬京东和苏宁,结果要么返回空页面、要么只有几千字节的验证页。最后发现当当网的搜索页虽然返回24万字符的HTML,但商品数据全部嵌入在静态HTML里(非Ajax动态加载),所以正则可以直接捞到60个标题+60个价格。这让我认识到选对目标网站比写对代码更重要——作业里写的"自己选择某个商城"其实很有深意。

(3)标题和价格的配对技巧:当当网页面里标题和价格是按页面渲染顺序一一对应的。我用 min(len(titles), len(prices)) 取较小值来配对,避免一边多一边少时出现下标越界。然后又写了一个 print_comparison 函数按价格升序排序输出,这样比价的目的才真正达到了。这个"提取→配对→清洗→排序→展示"的完整数据处理流水线,让我对"爬虫不只是抓数据,更是处理数据"有了更深认识。

(4)正则 vs BeautifulSoup:两个作业做下来我觉得,当HTML结构规整、标签嵌套清晰时 BeautifulSoup 更直观;但当目标片段分散、或页面是HTML+JS混杂的单文件时,正则反而更高效。真正的项目里两者通常是配合使用的。


作业③ 福州大学新闻网图片爬虫

1)爬虫实验

实验说明

  • 目标网址:https://news.fzu.edu.cn/yxfd.htm (福州大学新闻网 - 院系动态栏目)
  • 技术栈:requests + BeautifulSoup + re
  • 功能描述:爬取网页中所有 JPEG/JPG/PNG 格式的图片文件,保存到本地 downloaded_images 文件夹。

完整代码

# -*- coding: utf-8 -*-
"""
作业③:网页图片爬虫
学号:102302113  姓名:王泽炜
目标网址:https://news.fzu.edu.cn/yxfd.htm  (福州大学新闻网 - 院系动态)
技术栈:requests + BeautifulSoup + re
功能:爬取网页中所有 JPEG / JPG / PNG 格式图片并保存到本地文件夹
"""

import os
import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse

# 请求头
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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "zh-CN,zh;q=0.9",
}

# 目标URL
TARGET_URL = "https://news.fzu.edu.cn/yxfd.htm"

# 保存图片的文件夹
OUTPUT_DIR = "downloaded_images"


def ensure_dir(dir_path):
    """确保保存目录存在,不存在则创建"""
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
        print(f"✓ 创建图片保存目录:{dir_path}")


def fetch_html(url):
    """获取目标网页HTML内容"""
    try:
        response = requests.get(url, headers=HEADERS, timeout=15)
        response.raise_for_status()
        response.encoding = response.apparent_encoding
        return response.text
    except requests.RequestException as e:
        print(f"[错误] 请求页面失败:{e}")
        return None


def extract_image_urls(html_text, base_url):
    """
    使用 BeautifulSoup + re 提取所有图片URL
    支持 <img> 标签的 src 和 data-src 属性
    只保留 JPEG/JPG/PNG 格式
    """
    soup = BeautifulSoup(html_text, "lxml")
    url_set = set()  # 用集合去重

    # 扩展名过滤
    ext_pattern = re.compile(r'\.(jpe?g|png)(?:$|\?|#)', re.IGNORECASE)

    # 方法1:BeautifulSoup 查找所有 <img> 标签
    for img_tag in soup.find_all("img"):
        # 常见图片属性:src, data-src, data-original
        for attr in ["src", "data-src", "data-original", "data-lazy-src"]:
            img_url = img_tag.get(attr)
            if img_url:
                img_url = img_url.strip()
                # 跳过空的、base64内联图
                if not img_url or img_url.startswith("data:"):
                    continue
                # 过滤扩展名
                if ext_pattern.search(img_url):
                    # 转成绝对URL
                    abs_url = urljoin(base_url, img_url)
                    url_set.add(abs_url)
                break  # 找到第一个有效属性就好

    # 方法2:用正则补充查找可能漏掉的背景图等
    bg_pattern = re.compile(
        r'(?:background|src)\s*[:=]\s*["\']?([^"\']*?\.(?:jpe?g|png)[^"\']*)["\']?',
        re.IGNORECASE
    )
    for match in bg_pattern.finditer(html_text):
        img_url = match.group(1).strip()
        if img_url and not img_url.startswith("data:"):
            abs_url = urljoin(base_url, img_url)
            url_set.add(abs_url)

    return list(url_set)


def sanitize_filename(url_str):
    """
    从URL中提取安全的文件名
    去除特殊字符,防止Windows下文件创建失败
    """
    parsed = urlparse(url_str)
    filename = os.path.basename(parsed.path)

    # 如果文件名太长或没有扩展名,生成一个
    if len(filename) < 3 or "." not in filename:
        import hashlib
        ext_match = re.search(r'\.(jpe?g|png)', url_str, re.IGNORECASE)
        ext = ext_match.group(1).lower() if ext_match else "jpg"
        filename = hashlib.md5(url_str.encode()).hexdigest()[:12] + "." + ext

    # 清理Windows下不合法的文件名字符
    filename = re.sub(r'[\\/:*?"<>|]', "_", filename)
    return filename


def download_image(img_url, save_dir, index):
    """
    下载单张图片并保存到指定目录
    返回 (成功标志, 文件路径或错误信息)
    """
    try:
        response = requests.get(img_url, headers=HEADERS, timeout=15)
        response.raise_for_status()

        # 检查Content-Type是否真的是图片
        content_type = response.headers.get("Content-Type", "")
        if not content_type.startswith("image/"):
            if not re.search(r'\.(jpe?g|png)', img_url, re.IGNORECASE):
                return False, f"跳过非图片: {img_url[:60]}"

        filename = sanitize_filename(img_url)
        # 防止重名
        filepath = os.path.join(save_dir, filename)
        if os.path.exists(filepath):
            name, ext = os.path.splitext(filename)
            filename = f"{name}_{index}{ext}"
            filepath = os.path.join(save_dir, filename)

        with open(filepath, "wb") as f:
            f.write(response.content)

        file_size_kb = len(response.content) / 1024
        return True, f"✓ {filename} ({file_size_kb:.1f} KB)"

    except Exception as e:
        return False, f"✗ 下载失败 {img_url[:50]}: {e}"


def main():
    """主函数"""
    print("=" * 70)
    print("网页图片爬虫 - 福州大学新闻网")
    print("技术栈:requests + BeautifulSoup + re")
    print("=" * 70)

    # 创建保存目录
    ensure_dir(OUTPUT_DIR)

    # 1. 获取页面HTML
    print(f"\n[1/3] 正在获取目标页面:{TARGET_URL}")
    html = fetch_html(TARGET_URL)
    if not html:
        print("✗ 页面获取失败")
        return
    print(f"✓ 页面获取成功,HTML长度:{len(html)} 字符")

    # 2. 提取图片URL列表
    print(f"\n[2/3] 正在提取页面中的图片URL...")
    img_urls = extract_image_urls(html, TARGET_URL)
    print(f"✓ 共发现 {len(img_urls)} 张图片 (JPEG/JPG/PNG)")

    if not img_urls:
        print("未找到符合条件的图片")
        return

    # 3. 逐张下载
    print(f"\n[3/3] 正在下载图片到 [{OUTPUT_DIR}] 文件夹...")
    success_count = 0
    fail_count = 0

    for idx, img_url in enumerate(img_urls, 1):
        ok, msg = download_image(img_url, OUTPUT_DIR, idx)
        print(f"  [{idx}/{len(img_urls)}] {msg}")
        if ok:
            success_count += 1
        else:
            fail_count += 1

    # 汇总
    print(f"\n{'='*70}")
    print(f"下载完成!成功:{success_count} 张,失败:{fail_count} 张")
    print(f"图片保存在:{os.path.abspath(OUTPUT_DIR)}")
    print(f"{'='*70}")


if __name__ == "__main__":
    main()

运行结果

任务3

截图节选

EE9C07C6486DEFAA9EAF683C37C_48D55A5D_F84E4


2)心得体会

作业③让我体验了一次完整的图片采集流程:定位→提取→清洗→下载→落盘,中间每个环节都有值得总结的地方。

(1)相对URL转绝对URL:页面里的图片路径很多是相对路径(比如 ../images/logo.png),直接用 requests.get() 会报404。我用了 urllib.parse.urljoin(base_url, img_url) 来把相对路径转成完整URL。这个函数会智能处理各种情况——如果原始URL已经是完整的(以 http:// 开头),它就原样返回;如果是相对路径,它会根据 base_url 补全成绝对地址。这是一个非常实用的小工具,避免了手动拼接容易出错的问题。

(2)去重和过滤:我用 Python 的 set() 来存储图片URL,天然去重。同时过滤掉了 base64 内联图片(以 data: 开头的 <img src="data:image/png;base64,...">),因为那种图片直接嵌在HTML里,没必要再下载。另外用正则 \.(jpe?g|png)(?:$|\?|#) 来只保留 JPG/PNG 格式,这里用了 jpe?g 一个正则同时匹配 jpg 和 jpeg,(?:$|\?|#) 可以兼容带查询参数和锚点的URL。

(3)多属性兼容:现代网页的图片标签不一定只有 src,还有 data-src、data-original、data-lazy-src 等懒加载属性。我在代码里遍历了这四个属性,找到了第一个有效值就停止(break)。这种"多个备选属性 + 找到就停"的模式在处理复杂HTML时很常见,避免了只盯着 src 导致漏掉图片的问题。

(4)文件名安全化:下载时我用 os.path.basename(urlparse(url).path) 从URL里取文件名,然后用正则 [\\/:*?"<>|] 清洗掉 Windows 下不合法的文件名字符。还要处理重名问题——如果本地已有同名文件,就在原名后加一个序号后缀。这些细节如果不处理,爬虫在 Windows 下很容易报"文件名包含非法字符"的错误。

(5)BeautifulSoup + re 的组合:这道题我两种都用了——先用 BeautifulSoup 的 find_all("img") 批量拿到所有 <img> 标签,再用正则过滤扩展名;同时还配了一个正则专门匹配 CSS 背景图。两种方案互补,确保尽可能多的图片被提取出来。这让我再次印证了作业②里的体会——真正的爬虫项目里,BeautifulSoup 和 re 通常是配合使用的。


总结

作业 目标网站 技术栈 提取数据 成果
① 上海软科排名 requests + BeautifulSoup 排名、学校、省市、类型、总分 30条排名数据
② 当当网搜索"书包" requests + re 商品名、价格 60件商品 + 比价统计
③ 福州大学新闻网 requests + BeautifulSoup + re JPG/PNG图片 22张图片成功下载

通过三次爬虫作业的实践,我从零开始经历了"选网站→分析HTML→写代码→调bug→跑通"的完整流程,也意识到真实商业网站普遍有反爬策略,但合理选择目标、配合正确的请求头和解析技术,大部分静态HTML页面都是可以被定向采集的。

posted @ 2026-10-22 11:17  莳萝莙  阅读(4)  评论(0)    收藏  举报