数据采集第一次

一.作业一

1.1

要求:

用requests和BeautifulSoup库方法定向爬取给定网址(http://www.shanghairanking.cn/rankings/bcur/2020 )的数据,屏幕打印爬取的大学排名信息。

分析:

(1)定位数据文件:

在开发者模式里,Fetch/XHR只有collect,随后发现静态资源地址:/_nuxt/static/1791618681/rankings/bcur/202011/payload.js,并且包univNameCn、province、univCategory、score、ranking 等字段,于是下载payload.js。最初直接请求静态资源时,服务器返回 403 ,因此使用 Requests 的 Session 建立会话。

(2)payload.js结构:

数据被作为第二个参数传给 NUXT_JSONP 函数,普通 json.loads() 无法直接解析,因此先用 Node.js 执行 JS,再回传 JSON 给 Python。然后从 univData 中每条记录提取univNameCn、province等信息。

1

代码:

# payload.js获取
import requests

page_url = "https://www.shanghairanking.cn/rankings/bcur/2020"

payload_url = (
    "https://www.shanghairanking.cn/_nuxt/static/"
    "1791618681/rankings/bcur/202011/payload.js"
)

session = requests.Session()

headers = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/131.0.0.0 Safari/537.36"
    ),
    "Accept": "*/*",
    "Referer": page_url,
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
}

# 第一步:先访问原网页
page_response = session.get(
    page_url,
    headers=headers,
    timeout=20
)

print("网页状态码:", page_response.status_code)

# 第二步:在同一个会话中请求静态资源
payload_response = session.get(
    payload_url,
    headers=headers,
    timeout=20
)

print("静态资源状态码:", payload_response.status_code)
print("响应内容:", payload_response.text[:300])

if payload_response.status_code == 200:
    with open("payload.js", "w", encoding="utf-8") as f:
        f.write(payload_response.text)

    print("下载成功,已保存 payload.js")
else:
    print("请求仍然失败,请使用浏览器检查该资源的真实请求信息。")
import requests
import subprocess
import json
import os

PAGE_URL = "https://www.shanghairanking.cn/rankings/bcur/2020"

PAYLOAD_URL = (
    "https://www.shanghairanking.cn/_nuxt/static/"
    "1791618681/rankings/bcur/202011/payload.js"
)

HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Referer": PAGE_URL
}

# 1. 下载静态数据
session = requests.Session()

# 先访问网页,建立会话
session.get(PAGE_URL, headers=HEADERS, timeout=20).raise_for_status()

response = session.get(
    PAYLOAD_URL,
    headers=HEADERS,
    timeout=20
)
response.raise_for_status()

with open("payload.js", "w", encoding="utf-8") as f:
    f.write(response.text)

print("静态数据下载成功!")

# 2. 创建 JavaScript 解析脚本
# Nuxt JSONP 文件会把数据作为第二个参数传入
js_code = r"""
const fs = require('fs');

let result = null;

global.__NUXT_JSONP__ = function(path, data) {
    result = data;
};

const source = fs.readFileSync('payload.js', 'utf8');

eval(source);

if (!result) {
    throw new Error('没有成功解析 Nuxt 页面数据');
}

console.log(JSON.stringify(result));
"""

with open("parse_payload.js", "w", encoding="utf-8") as f:
    f.write(js_code)

# 3. 执行 JavaScript,获取解析后的数据
process = subprocess.run(
    ["node", "parse_payload.js"],
    capture_output=True,
    text=True,
    encoding="utf-8"
)

if process.returncode != 0:
    print("解析失败:")
    print(process.stderr)
    raise SystemExit(1)

try:
    page_data = json.loads(process.stdout)
except json.JSONDecodeError:
    print("数据转换 JSON 失败,请检查 JavaScript 输出。")
    raise SystemExit(1)

# 4. 查找大学数据
def find_univ_data(obj):
    if isinstance(obj, dict):
        if isinstance(obj.get("univData"), list):
            return obj["univData"]

        for value in obj.values():
            found = find_univ_data(value)
            if found is not None:
                return found

    elif isinstance(obj, list):
        for value in obj:
            found = find_univ_data(value)
            if found is not None:
                return found

    return None


universities = find_univ_data(page_data)

if universities is None:
    print("当前页面数据中没有找到 univData 数组。")
    raise SystemExit(0)

# 5. 输出排名信息
print(f"\n共解析到 {len(universities)} 条大学记录\n")
print("排名\t学校名称\t省市\t学校类型\t总分")

for university in universities:
    rank = university.get("ranking", "")
    name = university.get("univNameCn", "")
    province = university.get("province", "")
    school_type = university.get("univCategory", "")
    score = university.get("score", "")

    print(f"{rank}\t{name}\t{province}\t{school_type}\t{score}")

结果:

2

1.2心得体会

最开始我期望找到类似 /api/xxx 的接口请求。然而逐一查看所有 XHR 请求也没发现与院校排名相关的接口。然后在AI的提示下查看payload.js,在Response 中看到了完整的排名数据。然后输入某大学的名称,命中payload,证明了这一点。

二.作业2

2.1

要求:

用requests和re库方法设计某个商城(自已选择)商品比价定向爬虫,爬取该商城,以关键词“书包”搜索页面的数据,爬取商品名称和价格。

分析:

(1)URL:

搜索“书包”后,观察地址栏发现基础URL为 http://search.dangdang.com/,搜索参数为 key=书包,并带有 act=input 和 page_index=1。关键词“书包”在URL中会被编码为 %CA%E9%B0%FC,因此代码中需要使用 quote(keyword.encode('gbk')) 进行编码。

(2)网页结构:

使用元素选择器定位商品信息。发现商品名称位于 <p class="name"> 标签下的 <a> 标签的 title 中;商品价格位于 <span class="price_n"> 中。

3

4

(3)翻页:

通过url的page_index参数决定。
5

代码:

import urllib3
import re
import time
import csv
import os
from urllib.parse import quote

# 基础URL和必须的请求头
BASE_URL = "http://search.dangdang.com/"
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Referer': 'http://search.dangdang.com/',
}

http = urllib3.PoolManager()

def get_html(url):
    """发送 GET 请求获取网页 HTML 文本"""
    try:
        response = http.request('GET', url, headers=HEADERS, timeout=10.0)
        if response.status == 200:
            try:
                # 当当网使用GBK编码
                html = response.data.decode('gbk')
            except (UnicodeDecodeError, LookupError):
                html = response.data.decode('utf-8', errors='ignore')
            return html
        else:
            print(f"[错误] 请求失败,状态码: {response.status}")
            return None
    except Exception as e:
        print(f"[异常] 网络请求发生错误: {e}")
        return None

def parse_html(html):
    """从 HTML 文本中解析商品名称和价格"""
    products = []
    # 商品名称在 title 属性中
    name_pattern = re.compile(r'<p class="name"[^>]*>\s*<a[^>]*title="(.*?)"', re.S)
    names = name_pattern.findall(html)
    # 价格在 span class="price_n" 中
    price_pattern = re.compile(r'<span class="price_n">(.*?)</span>', re.S)
    prices = price_pattern.findall(html)

    names = [re.sub(r'\s+', ' ', name).strip() for name in names]        
    prices = [re.sub(r'[^\d.]', '', price.strip()) for price in prices]  

    for name, price in zip(names, prices):
        products.append({'name': name, 'price': price})
    return products

def main():
    keyword = "书包"
    encoded_keyword = quote(keyword.encode('gbk')) # GBK编码
    max_items = 70          
    total_pages = 2         

    all_products = []
    print(f"开始爬取关键词:【{keyword}】的商品数据...\n")
    for page in range(1, total_pages + 1):
        print(f"--- 正在抓取第 {page} 页 ---")
        url = f"{BASE_URL}?key={encoded_keyword}&act=input&page_index={page}"
        html = get_html(url)
        if not html:
            continue
        page_products = parse_html(html)
        if not page_products:
            break
        all_products.extend(page_products)
        if len(all_products) >= max_items:
            break
        time.sleep(1.5) 
    all_products = all_products[:max_items]

    print(f"\n========= 抓取完成,共获取 {len(all_products)} 项商品 =========")
    for idx, prod in enumerate(all_products, 1):
        print(f"{idx:02d}. {prod['name']} | ¥{prod['price']}")

if __name__ == "__main__":
    main()

结果:

4

2.2 心得体会

1.

当当网页面采用 GBK 编码,需要通过 response.data.decode('gbk') 进行解码,否则会出现中文乱码。在解析数据时,使用正则表达式提取 title 属性和价格标签,能够高效地获取结构化数据。

2.

实现翻页功能时,遇到了代码有翻页功能但实际只抓一页的问题。分析发现是 total_pages = 1 和 max_items = 60 导致的。之前的代码里total_pages = 1 导致 range(1, 2) 只循环一次;同时 max_items = 60,当当网每页正好返回约60个商品,导致第一页抓完就触发了 break 跳出循环。因此修改total_pages = 2,max_items = 70,成功抓取到70条数据。

三.作业3

3.1

要求:

爬取一个给定网页( https://news.fzu.edu.cn/yxfd.htm )或者自选网页的所有JPEG、JPG或PNG格式图片文件

分析:

(1)HTML 结构:

发现配图在 标签中,因此使用 BeautifulSoup 的 find_all('img') 。

(2)图片链接:

查看元素时,发现有这样的标签:<img src="/__local/F/4D/B3/EE9C07C6486DEFAA9EAF683C37C_48D55A5D_F84E4.jpg" alt>。如果直接对 /__local/... 发起 requests.get(),程序会报错。因此不能直接提取 src ,而是要用 urljoin(target_url, src) 拼成绝对路径。

5

(3)翻页处理:

注意到第一页是 yxfd.htm,第二页是 yxfd/6.htm,直到第七页是 yxfd/1.htm。因此先构建基础路径 base = "https://news.fzu.edu.cn/yxfd",第一页单独写f"{base}.htm",后续页用f"{base}/{n}.htm" for n in range(6, 0, -1)。

6

代码:

import os
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin


def download_images(target_url, save_dir="downloaded_images", page_index=0):
    """
    下载指定页面的所有 JPEG/JPG/PNG 图片。
    page_index: 用于给文件名加前缀,避免不同页面的同名图片冲突。
    """
    # 创建保存目录
    if not os.path.exists(save_dir):
        os.makedirs(save_dir)
        print(f"已创建文件夹: {save_dir}")

    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
    }

    try:
        response = requests.get(target_url, headers=headers, timeout=10)
        response.raise_for_status()
        response.encoding = response.apparent_encoding
    except Exception as e:
        print(f"获取网页失败 [{target_url}],错误信息: {e}")
        return 0  # 返回 0 表示该页下载失败

    soup = BeautifulSoup(response.text, 'html.parser')
    img_tags = soup.find_all('img')

    if not img_tags:
        print(f"在 {target_url} 中未找到任何图片。")
        return 0

    print(f"[页 {page_index + 1}] {target_url} 共找到 {len(img_tags)} 个 <img> 标签,开始过滤并下载...")

    success_count = 0
    for index, img in enumerate(img_tags):
        src = img.get('src') or img.get('data-src')
        if not src:
            continue

        img_url = urljoin(target_url, src)
        base_url = img_url.split('?')[0]

        # 只下载 JPEG / JPG / PNG
        if base_url.lower().endswith(('.jpg', '.jpeg', '.png')):
            try:
                img_response = requests.get(img_url, headers=headers, timeout=10)
                if img_response.status_code == 200:
                    filename = os.path.basename(base_url)
                    if not filename:
                        filename = f"image_{index}.jpg"

                    # 文件名加上页面序号前缀,避免多页之间的重名覆盖
                    filename = f"p{page_index}_{index}_{filename}"
                    file_path = os.path.join(save_dir, filename)

                    with open(file_path, 'wb') as f:
                        f.write(img_response.content)

                    print(f"  成功下载: {filename}")
                    success_count += 1
            except Exception as e:
                print(f"  下载图片失败 [{img_url}]: {e}")

    return success_count


def main():
    # 构建 7 个页面的 URL 列表
    base = "https://news.fzu.edu.cn/yxfd"
    # 第一页: /yxfd.htm,后续页: /yxfd/6.htm ~ /yxfd/1.htm
    page_urls = [f"{base}.htm"] + [f"{base}/{n}.htm" for n in range(6, 0, -1)]

    save_dir = "fzu_images"
    total_success = 0

    for idx, url in enumerate(page_urls):
        print(f"\n{'='*50}")
        print(f"正在处理第 {idx + 1}/7 页: {url}")
        print(f"{'='*50}")
        count = download_images(url, save_dir=save_dir, page_index=idx)
        total_success += count

    print(f"\n{'='*50}")
    print(f"全部任务完成!共成功下载 {total_success} 张图片,保存在 '{save_dir}' 文件夹中。")


if __name__ == "__main__":
    main()

结果:

2

1

3.2心得体会

1.

最初我以为网页源码中的 src 属性均为可直接请求的完整 URL,导致程序运行时因相对路径而请求失败。随后了解到浏览器会自动为相对路径补全当前域名,但 requests 库不具备这一能力。因此要用 urllib.parse.urljoin 完成绝对路径的拼接。

2.

网站的倒序 URL 结构提醒我在编写循环前必须通过浏览器实际翻页来找到规律,不能想当然。

posted @ 2026-10-18 20:03  fswtkb  阅读(2)  评论(0)    收藏  举报