第一次作业

作业①

1.大学排名动态网页爬取实验

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

排名 学校名称 省市 学校类型 总分
1 清华大学 北京 综合 852.5
2

核心代码
由于网站翻页url不会改变。且只能使用requests和BeautifulSoup(如果用模拟鼠标就需要用 Selenium),因此我最终发现https://www.shanghairanking.cn/_nuxt/static/1760667299/rankings/bcur/202011/payload.js这里含有我想要的全部信息。但是这份 JS 是 Nuxt 的 JSONP/IIFE,里面大量字段不是字面量,而是变量代号(如 eq、dj…)。最终思路是把 Nuxt 的 JSONP 拆开:形参–实参映射 → 找到 return{} → 抠出 data[] → 锚点定位每所学校对象 → 抽字段并替换变量。JS 里大量值是 变量代号(比如 eq、ef、nf…),必须先把 形参 ↔ 实参 对齐,再把对象里这些代号替换回真实值。之前“分数变成 ef/nf”的坑,就是忘了给 score 也做映射。

首先,先把 Nuxt 的 NUXT_JSONP(..., (function(a,b,c){...})( 实参... )) 里的形参和实参对上。我没有用re,而是“数括号 + 处理引号”,最后把实参段改成 Python 风格后一次性 literal_eval 成列表,避免被字符串里的逗号坑到。

import ast

def slice_block(s, i, L, R):                 # 小括号/花括号配对工具
    dep=0; ins=False; esc=False; l=i
    while i < len(s):
        ch = s[i]
        if ins:
            if esc: esc=False
            elif ch=="\\": esc=True
            elif ch=='"': ins=False
        else:
            if ch=='"': ins=True
            elif ch==L:
                dep+=1; l = l if dep!=1 else i
            elif ch==R:
                dep-=1
                if dep==0: return l, i
        i += 1
    raise ValueError("括号未配平")

def parse_params_args(js):                    # 形参与实参 → 映射表
    h = js.find("(function(")
    p_end = js.find(")", h+10)
    params = [x.strip() for x in js[h+10:p_end].split(",") if x.strip()]

    body_l = js.find("{", p_end)              # 函数体 {...}
    body_l, body_r = slice_block(js, body_l, "{", "}")

    i = body_r + 1                            # 紧跟的 ( 实参 )
    while i < len(js) and js[i].isspace(): i += 1
    if i < len(js) and js[i] == ')': i += 1
    while i < len(js) and js[i].isspace(): i += 1
    a_l, a_r = slice_block(js, i, "(", ")")
    args_str = js[a_l+1:a_r]

    safe = (args_str.replace("true","True").replace("false","False")
                     .replace("null","None").replace("void 0","None"))
    values = ast.literal_eval("[" + safe + "]")

    n = min(len(params), len(values))         # 个别版本会差 1
    return {params[i]: values[i] for i in range(n)}

我用 univNameCn 当锚点,向左回溯到 { 再匹配 },拿到这所学校的完整对象。
对象内部不用正则,自己读 key: value:字符串去引号,数字/变量名截到逗号/右花括号

def object_around(pos, text):                 # 锚点处向左回溯到“{”,取完整对象
    i = pos
    while i>0 and text[i] != '{': i -= 1
    l, r = slice_block(text, i, "{", "}")
    return text[l:r+1]

def read_after_colon(obj, key):               # 读 key: value 的 value(字符串/数字/变量)
    p = obj.find(key)
    if p < 0: return None
    c = obj.find(":", p); i = c + 1
    while i < len(obj) and obj[i].isspace(): i += 1
    if i >= len(obj): return None
    if obj[i] in "\"'":                       # "字符串"
        q = obj[i]; j = i+1; esc=False
        while j < len(obj):
            ch = obj[j]
            if esc: esc=False
            elif ch=="\\": esc=True
            elif ch==q: return obj[i+1:j]
            j += 1
        return None
    j = i                                     # 数字/变量名
    while j < len(obj) and obj[j] not in ",}\r\n\t ":
        j += 1
    return obj[i:j]

def resolve(token, mapping):                  # 变量名 → 真值;其它原样
    if token is None: return ""
    return str(mapping.get(token, token))

def is_number(s):                             # 不是数就别往下用了
    try: float(s); return True
    except: return False

其余只剩:requests.get() 把 payload.js 取回/落地;用 BeautifulSoup 初始化一下(满足“使用”要求);最后把 rows 按格式打印即可。
实验结果
1

1

2.心得体会

这题一开始卡得挺久:看到是 JS,我下意识想“正则一把梭”或者“整段转 JSON”。结果 Nuxt 的 JSONP 里全是变量代号,既 split 不开,也 parse 不成。后来换了思路:不和整段较劲,只把三段关键内容抠出来(形参与实参、return{}、data[]),其它都当成普通字符串处理。
两个坑印象最深:

字符串里的逗号会把实参切坏。解决是把整段实参改成 Python 风格后,一次性 literal_eval,就不用担心逗号在哪了。

一开始只给 ranking/省份/类型 做了映射,忘了给 score 也做,于是分数就变成了 ef/nf 这种变量名。后来把 score 一起走 resolve(),并且用 is_number() 把假分数过滤掉,输出就干净了。

作业②

1.商城商品比价定向爬虫实验

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

序号 价格 商品名
1 65.00 xxx
2

核心代码
通过分析发现“没得比”商城的搜索结果页URL结构清晰(实则是淘宝,京东的反爬机制过于厉害导致容易黑号),适合直接用requests请求。数据提取则使用正则表达式完成。

def main():
    """
    主函数,执行爬虫逻辑。
    """
    #创建一个 Session 对象
    session = requests.Session()

    #模拟常见浏览器(防止被反爬取)
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
    })

    # 先访问一次首页
    try:
        print("正在初始化会话...")
        session.get(base_url, timeout=10)
        print("会话初始化成功。")
    except requests.RequestException as e:
        print(f"初始化会话失败: {e}")
        return

    all_products = [] 
    current_page = 1  

    # 翻页循环:抓取前 5 页(不然不太友好)
    while current_page <= 5:
        # 构造分页 URL,观察目标站点的分页规则后拼接
        url = f"{base_url}/Search?keyword={search_keyword}&p={current_page}"
        print(f"正在爬取第 {current_page} 页... -> {url}")

        try:
            # 发送 GET 请求
            response = session.get(url, timeout=10)
            response.raise_for_status()  # 若返回码非 2xx,会抛出 HTTPError 异常
            # 编码处理:用 apparent_encoding 猜测编码,再据此解码为文本
            response.encoding = response.apparent_encoding
            html_content = response.text
        except requests.RequestException as e:
            print(f"请求错误: {e}")
            break

为了能够匹配如图的商品信息,我使用以下的正则表达式

2

        # 使用正则解析目标字段(价格与商品名)

        product_regex = re.compile(
            r'<li class="clearfix">.*?'  # 商品块起点:每个商品在 <li class="clearfix"> 中
            r'<span class="red price">\s*¥\s*([\d\.]+)\s*</span>.*?'  # 价格:捕获 ¥ 后的数字(支持小数)
            r'<h6><a href=".*?" target="_blank">(.*?)</a></h6>',  # 商品名:位于 <h6><a> 的文本
            re.S
        )
        products = product_regex.findall(html_content)

        if not products:
            # 页面中没有按照预期匹配到商品块,通常意味着:被反爬!
            print(f"第 {current_page} 页没有找到更多商品,爬取结束。")
            break

        # 将当前页的结果追加到总列表
        all_products.extend(products)

        if 'class="next' not in html_content:
            print("已到达最后一页,爬取结束。")
            break

        # 递增页码延时(绕过简单的反爬)
        current_page += 1
        time.sleep(1)

实验结果

2

2.心得体会

这个实验在编写正则表达式时,我遇到了几个关键点:首先,商品的价格和标题在HTML源码中相隔较远且包含换行,因此必须使用re.S标志,使.能够匹配包括换行符在内的任意字符。其次,为了防止匹配范围扩得太大,需要使用非贪婪匹配.*?。最后,通过括号()对需要提取的价格和商品名进行分组,findall函数就能直接返回一个包含元组的列表,大大简化了后续处理。

作业③

1.网页图片批量下载实验

实验要求
爬取一个给定网页(https://news.fzu.edu.cn/yxfd.htm)的所有JPEG、JPG或PNG格式图片文件
输出信息:将自选网页内的所有JPEG、JPG或PNG格式文件保存在一个文件夹中

核心代码
首先对页面进行解析

3

不难看出,img src内就是我们需要的内容

def main():
    base_url = "[https://news.fzu.edu.cn/yxfd.htm](https://news.fzu.edu.cn/yxfd.htm)"
    output_folder = "downloaded_images"
    os.makedirs(output_folder, exist_ok=True)

    # 设置请求头,模拟浏览器
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Referer": base_url 
    }
    
    print(f"正在爬取: {base_url}")
    try:
        # 1. 获取网页HTML
        req = urllib.request.Request(base_url, headers=headers)
        with urllib.request.urlopen(req) as response:
            html = response.read().decode("utf-8")
        
        # 2. 正则表达式查找所有图片链接
        img_urls_relative = re.findall(r'<img[^>]+src="([^"]+\.(?:jpg|jpeg|png))"', html, re.I)
        
        # 3. 拼接、下载
        for i, relative_url in enumerate(img_urls_relative):
            # 将相对路径转换为绝对路径
            absolute_url = urljoin(base_url, relative_url)
            filename = os.path.join(output_folder, f"image_{i+1}.jpg")
            
            print(f"  -> 正在下载: {absolute_url}")
            img_req = urllib.request.Request(absolute_url, headers=headers)
            with urllib.request.urlopen(img_req) as img_response, open(filename, "wb") as f:
                f.write(img_response.read())
        
        print(f"\n下载完成!共 {len(img_urls_relative)} 张图片已保存至 {output_folder} 文件夹。")

    except Exception as e:
        print(f"发生错误: {e}")

实验结果

3

2.心得体会

在完成图片下载任务时,我遇到了HTTP 403 Forbidden错误.解决办法是在下载图片的请求头里,手动加入"Referer": "来源网页的URL",模拟是从该网页点击图片链接进行访问的。此外,我还学到了使用urllib.parse.urljoin来处理URL。网页上的图片链接可能是相对路径(如../images/a.jpg),urljoin可以安全地将这些相对路径和页面本身的URL拼接成一个完整的绝对URL,代码健壮性更强。此外,有些同学把gif也爬进来了,其实在正则化查找时就可以过滤掉。

代码地址:https://gitee.com/changqianqi/2025_crawl-project/tree/master/1

posted @ 2025-10-15 22:36  changqianqi  阅读(42)  评论(0)    收藏  举报