第三次作业

作业①

1.气象网页爬取实验

实验要求
指定一个网站,爬取这个网站中的所有的所有图片,中国气象网(http://www.weather.com.cn)。实现单线程和多线程的方式爬取。
输出信息:将下载的图片保存在images子文件夹中

核心代码
为了从中国天气网的生活频道获取图片资源,我编写了get_images_from_page函数。利用BeautifulSoup解析网页后,通过分析DOM结构发现图片主要集中在class='zhengw'的div容器中。为了保证链接的可用性,我使用了urljoin将相对路径转换为绝对URL,并返回一个包含该页面所有图片链接的列表。
image

# 核心代码: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时,我特别注意了首页(index.shtml)与其他页码(index_num.shtml)的命名差异。程序按顺序解析每一页的HTML,提取图片链接后立即调用download_image函数进行下载,只有当前图片下载完成后才会处理下一张。

def download_image(image_url, save_path):
    try:
        response = requests.get(image_url, headers=HEADERS, stream=True, timeout=15)
        response.raise_for_status()
        os.makedirs(os.path.dirname(save_path), exist_ok=True)

        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(8192):
                f.write(chunk)
        
        print(f"下载成功: {image_url}")
        return True
    except Exception as e:
        print(f"下载失败: {image_url}, 原因: {e}")
        return False

为了高效地完成140张图片的下载任务,我利用Python的concurrent.futures模块实现了多线程并发爬虫。与单线程不同,我将任务拆分为“URL收集”和“并发下载”两个阶段。首先循环翻页直至收集满140个目标URL,然后初始化一个包含10个工作线程的ThreadPoolExecutor线程池。

from concurrent.futures import ThreadPoolExecutor, as_completed

def multi_thread_scraper_140_images(max_workers=10):
    # ... (省略URL收集部分,假设urls_to_download已准备好) ...

    # 核心:使用线程池并发下载
    total_downloaded = 0
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_url = {}
        for i, img_url in enumerate(urls_to_download):
            filename = f"image_{i+1:03d}.jpg"
            save_path = os.path.join(save_dir, filename)
            # 异步提交任务到线程池
            future = executor.submit(download_image, img_url, save_path)
            future_to_url[future] = img_url

        # 接收完成的任务结果
        for future in as_completed(future_to_url):
            if future.result():
                total_downloaded += 1

实验结果
image

2.心得体会

与文本不同,图片属于二进制数据,写入文件时必须指定'wb'模式,并使用iter_content分块写入以保证内存安全;网页源码获取的图片链接是相对路径,必须使用urljoin将其转换为绝对URL才能有效访问。

作业②

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

实验要求
熟练掌握 Scrapy 框架中 Item、Pipeline 数据的序列化与持久化输出方法。
掌握 Scrapy + 动态API分析 + SQLite 数据库存储的技术路线,爬取股票相关信息。
爬取东方财富网的股票列表信息,存入本地 stocks.db 数据库。
输出信息:数据库存储格式符合实验要求,表头使用英文命名(如 bStockNo)。
核心代码
在stock_spider.py中,start_requests方法负责循环构造5页的API请求URL。parse方法负责处理返回的JSON数据。由于返回数据并非标准JSON,需要先进行字符串切片处理,然后进行数据清洗(如将"-"替换为0)和单位换算(如价格除以100,成交额除以1亿),最后封装成StockScraperItem对象并yield到Pipeline。

# 核心代码:stock_spider.py 
import json
from stock_scraper.items import StockScraperItem

class StockSpider(scrapy.Spider):
    name = "stock"
    # ... (省略 start_requests)

    def parse(self, response):
        try:
            # 1. 响应非标准JSON,需切片处理
            json_str = response.text[response.text.find('{'):response.text.rfind('}')+1]
            data = json.loads(json_str)
        except:
            self.logger.error(f"无法解析JSON: {response.url}")
            return

        stock_list = data.get('data', {}).get('diff')
        if not stock_list:
            return

        for item_data in stock_list:
            # 2. 数据清洗:处理空值
            for k in item_data:
                if item_data[k] == "-":
                    item_data[k] = 0
            
            item = StockScraperItem()
            # 3. 数据转换:填充Item并进行单位换算
            item['bStockNo'] = item_data.get('f12')
            item['bStockName'] = item_data.get('f14')
            item['latestPrice'] = float(item_data.get('f2', 0)) / 100
            item['volume'] = float(item_data.get('f5', 0)) / 10000
            item['turnover'] = float(item_data.get('f6', 0)) / 100000000
            # ... (省略其他字段)
            yield item

在pipelines.py中,定义了SqlitePipeline类,用于处理Spider提交的Item。在爬虫启动时,它会自动连接(或创建)stocks.db数据库,执行CREATE TABLE IF NOT EXISTS来确保表结构存在,并使用DELETE FROM清空旧数据。对于接收到的每一个Item(process_item),它会执行参数化的INSERT语句,将数据安全地存入数据库。

# 核心代码:pipelines.py (Pipeline的数据存储)
    def open_spider(self, spider):
        self.db = sqlite3.connect('stocks.db') # 从 settings.py 获取亦可
        self.cursor = self.db.cursor()
        
        # 启动时建表并清空
        self.cursor.execute("""
        CREATE TABLE IF NOT EXISTS stocks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            bStockNo TEXT UNIQUE,
            bStockName TEXT,
            latestPrice REAL,
            ... 
        );
        """)
        self.cursor.execute("DELETE FROM stocks;")
        self.db.commit()

    def process_item(self, item, spider):
        # 准备插入数据的SQL
        insert_sql = """
        INSERT INTO stocks (bStockNo, bStockName, latestPrice, changePercent, 
                            changeAmount, volume, turnover, amplitude, high, 
                            low, openPrice, prevClose)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """
        data = (
            item['bStockNo'], item['bStockName'], item['latestPrice'], 
            item['changePercent'], item['changeAmount'], item['volume'], 
            item['turnover'], item['amplitude'], item['high'], 
            item['low'], item['openPrice'], item['prevClose']
        )
        # 执行插入
        try:
            self.cursor.execute(insert_sql, data)
            self.db.commit()
        except sqlite3.Error as e:
            self.db.rollback()
        return item

实验结果
执行scrapy crawl stock命令后,爬虫成功抓取了东方财富网的前5页股票数据。
image

2.心得体会

本实验让我了解到如何create 一个 scrapy project项目以及Scrapy 框架的解耦思想,即 Spider 负责“爬取”,Item 负责“定义”,Pipeline 负责“处理”。

作业③

1.爬取外汇数据网站

实验要求
熟练掌握 Scrapy 中 Item、Pipeline 数据的序列化输出方法。
使用 Scrapy 框架 + Xpath + 数据库存储技术路线(本实验采用 SQLite),爬取中国银行外汇牌价网站数据。
候选网站:中国银行外汇牌价 (https://www.boc.cn/sourcedb/whpj/)
输出信息:将爬取的数据存储在数据库中,表头包含:Currency (货币名称), TBP (现汇买入价), CBP (现钞买入价), TSP (现汇卖出价), CSP (现钞卖出价), Time (发布时间)。
核心代码

  1. Items 定义 (数据模型) 首先定义数据结构,包含货币名称、四种价格及发布时间,与数据库表结构对应。
# 核心代码:items.py
class BocScraperItem(scrapy.Item):
    currency = scrapy.Field()  # 货币名称
    tbp = scrapy.Field()       # 现汇买入价
    cbp = scrapy.Field()       # 现钞买入价
    tsp = scrapy.Field()       # 现汇卖出价
    csp = scrapy.Field()       # 现钞卖出价
    time = scrapy.Field()      # 发布时间

在boc_spider.py中,为了应对网页结构可能的变化,使用normalize-space清洗HTML中的空白字符,并实现了自动翻页逻辑。

# 核心代码:boc_spider.py (parse方法)
    def parse(self, response):
        # 定位包含表头“货币名称”的 table
        table = response.xpath("//th[contains(normalize-space(.), '货币名称')]/ancestor::table[1]")
        
        rows = table.xpath(".//tr[position()>1]") # 跳过表头
        for row in rows:
            item = BocScraperItem()
            item['currency'] = row.xpath("normalize-space(.//td[1])").get()
            item['tbp'] = row.xpath("normalize-space(.//td[2])").get()
            # ... (省略中间字段提取)
            item['time'] = row.xpath("normalize-space(.//td[8])").get() # 优先取第8列
            yield item
        next_page_url = response.xpath("//a[contains(normalize-space(.), '下一页')]/@href").get()
        if next_page_url:
            yield scrapy.Request(response.urljoin(next_page_url), callback=self.parse)

在pipelines.py中,我使用sqlite3实现了数据的存储。process_item方法中增加了对空值(None或空字符串)的处理,将其转换为0.0,防止插入数据库时报错或数据格式错误。

# 核心代码:pipelines.py (SqlitePipeline类)
    def process_item(self, item, spider):
        insert_sql = """
        INSERT INTO exchange_rates (currency, tbp, cbp, tsp, csp, time)
        VALUES (?, ?, ?, ?, ?, ?)
        """
        # 数据清洗:若爬取值为None则存为0.0
        data = (
            item.get('currency'),
            float(item.get('tbp') or 0.0),
            float(item.get('cbp') or 0.0),
            float(item.get('tsp') or 0.0),
            float(item.get('csp') or 0.0),
            item.get('time')
        )
        try:
            self.cursor.execute(insert_sql, data)
            self.db.commit()
        except sqlite3.Error as e:
            self.db.rollback()
        return item

实验结果
image

2.心得体会

本次实验最大的收获在于提升了XPath定位的鲁棒性。在分析中国银行网页时,发现表格嵌套较多,单纯使用 /table/tr 容易定位错误,因此我使用 //th[contains(...)]/ancestor::table 这种基于内容的相对定位法。此外,在数据入库环节,我意识到实际网页中经常存在某些币种“现钞买入价”为空的情况,如果在 Pipeline 中不进行 or 0.0 的空值预处理,直接进行 float() 转换会导致程序崩溃。

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

posted @ 2025-11-13 18:32  changqianqi  阅读(6)  评论(0)    收藏  举报