第二次作业

作业①

1.气象网页爬取实验

实验要求
在中国气象网(http://www.weather.com.cn)给定城市集的7日天气预报,并保存在数据库。

image

核心代码
为了持久化存储爬取的天气信息,我使用sqlite3创建了一个WeatherDB类。它在初始化时会创建一个weathers表(如果不存在),并提供一个insert方法用于插入数据。

# 核心代码:1.py (WeatherDB 类)
import sqlite3

class WeatherDB:
    def openDB(self):
        self.con=sqlite3.connect("weathers.db")
        self.cursor=self.con.cursor()
        try:
            # 建立一个以城市和日期为主键的表,防止数据重复
            self.cursor.execute("create table weathers (wCity varchar(16),wDate varchar(16),wWeather varchar(64),wTemp varchar(32), constraint pk_weather primary key (wCity,wDate))")
        except:
            self.cursor.execute("delete from weathers")

    def insert(self,city,date,weather,temp):
            try:
                self.cursor.execute("insert into weathers (wCity,wDate,wWeather,wTemp) values (?,?,?,?)" ,(city,date,weather,temp))
            except Exception as err:
                print(err) # 打印错误,如主键冲突

为了获取北京的天气数据,我构造了包含城市代码的URL。在请求时,必须添加User-Agent请求头来模拟浏览器,否则会请求失败。同时,由于网站编码可能在utf-8和gbk之间切换,我使用了UnicodeDammit来智能判断和转码,确保BeautifulSoup能正确解析。通过F12分析网页源码,我定位到所有7天的数据都在一个

    标签(class='t clearfix')下的
  • 标签中。
    image

    # 核心代码:1.py (forecastCity 方法 - 解析部分)
                    # 1. 定位到7个li标签
                    lis=soup.select("ul[class='t clearfix'] li")
                    for li in lis:
                        try:
                            # 2. 分别提取日期和天气
                            date=li.select('h1')[0].text
                            weather=li.select('p[class="wea"]')[0].text
                            
                            # 3. 处理复杂的温度数据
                            # 温度可能在span/i中,也可能只在p.tem中
                            temp_span = li.select_one('p[class="tem"] span')
                            temp_i = li.select_one('p[class="tem"] i')
                            if temp_span and temp_i:
                                temp = temp_span.text + "/" + temp_i.text
                            else:
                                temp = li.select_one('p[class="tem"]').text.strip()
                            
                            # 4. 插入数据库
                            self.db.insert(city,date,weather,temp)
                        except Exception as err:
                            print(f"Error processing one day: {err}")
    
    

    实验结果
    image

    2.心得体会

    这个任务关键点在于:必须模拟浏览器User-Agent;要处理好gbk/utf-8的混合编码问题;BeautifulSoup解析时,对于结构不统一的标签(如温度)要写好if/else分支,保证程序的鲁棒性。

    作业②

    1.股票信息定向爬虫实验

    实验要求
    用requests和BeautifulSoup库方法定向爬取股票相关信息,并存储在数据库中。
    image

    核心代码
    打开东方财富网https://quote.eastmoney.com/center/gridlist.html#hs_a_board,按F12进入开发者工具,通过搜索某一个股票名找到存信息的js文件
    image
    为了获取数据,我直接使用requests库请求这个API的URL,随后我使用正则表达式从响应文本中提取出括号内的{...}部分。

    def get_stock_data(page):
        # F12抓包获取的API地址,f...&pn={page}是关键参数
        base_url = (
            "[https://push2.eastmoney.com/api/qt/clist/get](https://push2.eastmoney.com/api/qt/clist/get)"
            "?np=1&fltt=1&invt=2"
            "&cb=jQuery371037061826685359867_1761721082041" # JSONP回调函数名
            "&fs=m%3A0%2Bt%3A6%2Bf%3A!2%2Cm%3A0%2Bt%3A80%2Bf%3A!2"
            "&fields=f12%2Cf13%2Cf14%2Cf1%2Cf2%2Cf4%2Cf3%2Cf152%2Cf5%2Cf6%2Cf7%2Cf15%2Cf18%2Cf16%2Cf17%2Cf10%2Cf8%2Cf9%2Cf23"
            f"&pn={page}" # <--- 页码
            "&pz=20&po=1"
            "&_=1761721082043"
        )
        resp = requests.get(base_url)
        text = resp.text
    
        # 1. 使用正则从JSONP中提取JSON
        json_str = re.search(r'\(({.*})\)', text).group(1)
        
        # 2. 把字符串转成 python 字典
        data = json.loads(json_str)
    

    为了得到和网页上一致的、易于阅读的数据 ,我遍历JSON中的data["data"]["diff"]列表。API返回的原始数据需要清洗:
    1.缺失值用"-"表示,必须替换为0,否则float()会报错。
    2.价格单位是“分”,需要/ 100。
    3.成交量是“股”,需要/ 10000(万手)。
    4.成交额是“元”,需要/ 100000000(亿)。

    page_rows = []
        for item in data["data"]["diff"]:
            # 1. 清洗缺失值
            for k in item:
                if item[k] == "-":
                    item[k] = 0
            # 2. 提取数据并进行单位换算
            row = (
                item.get('f12', ''),                        # 股票代码
                item.get('f14', ''),                        # 股票名称
                float(item.get('f2', 0)) / 100,             # 最新价 (分 -> 元)
                float(item.get('f3', 0)),                   # 涨跌幅 (%)
                float(item.get('f4', 0)) / 100,             # 涨跌额
                float(item.get('f5', 0)) / 10000,           # 成交量 (股 -> 万手)
                float(item.get('f6', 0)) / 100000000,       # 成交额 (元 -> 亿)
                float(item.get('f7', 0)),                   # 振幅 (%)
                # ... 省略 high, low, open, prev_close ...
            )
            page_rows.append(row)
        return page_rows
    

    实验结果
    image

    2.心得体会

    这个任务让我深刻体会到F12开发者工具的重要性。现代网页的数据和展示是分离的,爬虫不应该只盯着HTML,更应该去分析Network面板中的XHR请求。爬取API返回的JSON数据,比解析混乱的HTML要简单、高效得多。关键在于找到API、分析参数,并对返回数据进行清洗和单位换算。

    作业③

    1.爬取中国大学2021主榜实验

    实验要求
    爬取中国大学2021主榜(https://www.shanghairanking.cn/rankings/bcur/2021)所有院校信息。

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

    核心代码
    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 也做映射。
    屏幕录制 2025-10-29 194927

    首先,先把 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() 把假分数过滤掉,输出就干净了。

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

posted @ 2025-10-29 20:21  changqianqi  阅读(29)  评论(0)    收藏  举报