数据采集第一次作业

数据采集第一次作业

一、作业①:软科大学排名定向爬取

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 运行结果

image
image


2. 作业心得

本次作业使用 requests和 BeautifulSoup 对软科中国大学排名页面进行定向爬取。通过实践,我掌握了以下要点:

  1. 请求头的设置:直接请求目标网站会被拒绝,必须添加 User-Agent 伪装成浏览器访问。
  2. 表格定位:使用 soup.find("table") 定位排名表格,再通过 find_all("tr") 逐行遍历,跳过表头后逐列提取数据。
  3. 文本清洗:学校名称单元格同时包含中文名和英文名,通过正则 ^([^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 运行结果

image
image
image


2. 作业心得

本次作业我选择当当网作为目标商城,以"书包"为关键词爬取商品名称和价格。主要收获如下:

  1. 编码问题:当当网页面使用 gb18030 编码,而非普遍使用的 utf-8。如果不指定编码,中文会显示为乱码。关键词也需要先按GBK编码再进行URL编码,否则搜索不到结果。
  2. 正则提取价格:价格文本中可能包含 ¥、元 等符号,使用 re.search(r"\d+(?:\.\d+)?", text) 可准确提取数字部分。
  3. 表格对齐:中文在终端中的显示宽度是英文字符的两倍,直接用 ljust 会导致对齐错乱。通过 unicodedata.east_asian_width 判断字符宽度,实现了中英文混排的整齐输出。
  4. 数据持久化:将结果同时打印到屏幕并保存为文本文件,方便后续数据分析。

通过本次实践,我熟悉了 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 运行结果

image

1.3 保存的图片

image


2. 作业心得

本次作业爬取了福州大学新闻网"院系风采"页面的所有 JPG/JPEG/PNG 图片。主要收获如下:

  1. 多种图片来源:页面中图片不仅存在于 <img> 标签的 src 属性,还可能藏在 data-src、data-original、srcset 等属性以及 CSS 的 background-image 中。因此代码中收集了多种属性,并额外解析了 <style> 和行内 style 中的 url()。
  2. 文件类型校验:仅凭扩展名判断图片类型并不完全可靠,有些链接没有扩展名。代码通过读取文件头魔数(JPEG 的 \xff\xd8\xff、PNG 的 \x89PNG\r\n\x1a\n)来判断真实格式,避免下载到非图片内容。
  3. 去重处理:使用MD5哈希对图片内容去重,避免同一张图片因不同链接被重复保存。
  4. 文件名安全:URL中可能包含中文、空格、特殊字符,通过 unquote 解码+正则替换非法字符+截断长度,确保文件名合法。

通过本次作业,我深入理解了网页中图片资源的各种加载方式,也掌握了二进制文件下载、文件头识别等实用技巧。

posted @ 2026-10-13 16:41  oqst  阅读(3)  评论(0)    收藏  举报