102302149赖翊煊数据采集与融合技术第三次作业

作业1

部分代码及其结果展示

点击查看代码
import os
import time
import urllib.request
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
from bs4.dammit import UnicodeDammit

class ImageDownloader:
    def __init__(self):
        # 基础配置
        self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}
        self.image_dir = "images"
        self.max_pages = 4  # 最大页数
        self.max_images = 49  # 最大图片数
        self.downloaded_count = 0  # 计数变量
        os.makedirs(self.image_dir, exist_ok=True)  # 自动创建目录

    def _format_url(self, img_url):
        """统一图片URL格式"""
        if img_url.startswith('//'):
            return f'http:{img_url}'
        elif img_url.startswith('/'):
            return f'http://www.weather.com.cn{img_url}'
        elif not img_url.startswith('http'):
            return f'http://www.weather.com.cn/{img_url}'
        return img_url

    def download_image(self, img_url, page_url):
        """下载单张图片(带计数控制)"""
        if self.downloaded_count >= self.max_images:
            return False

        try:
            img_url = self._format_url(img_url)
            req = urllib.request.Request(img_url, headers=self.headers)
            with urllib.request.urlopen(req, timeout=10) as res:
                img_data = res.read()

            # 生成文件名
            filename = os.path.basename(img_url.split('?')[0]) or f"image_{self.downloaded_count + 1}.jpg"
            if '.' not in filename:
                filename += '.jpg'
            filepath = os.path.join(self.image_dir, filename)

            with open(filepath, 'wb') as f:
                f.write(img_data)

            self.downloaded_count += 1
            print(f"[{self.downloaded_count}/{self.max_images}] 保存: {filename}")
            return True
        except Exception as e:
            print(f"下载失败 {img_url}: {str(e)[:30]}")
            return False

    def get_images_from_page(self, url):
        """从页面提取图片URL列表"""
        try:
            req = urllib.request.Request(url, headers=self.headers)
            with urllib.request.urlopen(req, timeout=10) as res:
                data = UnicodeDammit(res.read(), ["utf-8", "gbk"]).unicode_markup
            return [img.get('src') for img in BeautifulSoup(data, "lxml").find_all('img') if img.get('src')]
        except Exception as e:
            print(f"页面解析失败 {url}: {str(e)}")
            return []

    def get_target_pages(self):
        """获取目标页面列表(限制页数)"""
        base = "http://www.weather.com.cn"
        pages = [
            base,
            f"{base}/weather/101010100.shtml",  # 北京
            f"{base}/weather/101020100.shtml",  # 上海
            f"{base}/weather/101280101.shtml",  # 广州
            f"{base}/weather/101280601.shtml"   # 福州
        ]
        return pages[:self.max_pages]

    def single_thread(self):
        """单线程下载"""
        start = time.time()
        self.downloaded_count = 0
        pages = self.get_target_pages()
        print(f"单线程模式:{len(pages)}页,最多{self.max_images}张图")

        for page_url in pages:
            if self.downloaded_count >= self.max_images:
                break
            print(f"\n处理页面: {page_url.split('/')[-1]}")
            for img_url in self.get_images_from_page(page_url):
                if not self.download_image(img_url, page_url) or self.downloaded_count >= self.max_images:
                    break

        print(f"\n单线程完成:{self.downloaded_count}张,耗时{time.time()-start:.2f}秒")

    def multi_thread(self, threads=5):
        """多线程下载"""
        start = time.time()
        self.downloaded_count = 0
        pages = self.get_target_pages()
        print(f"多线程模式({threads}线程):{len(pages)}页,最多{self.max_images}张图")

        with ThreadPoolExecutor(threads) as executor:
            for page_url in pages:
                if self.downloaded_count >= self.max_images:
                    break
                print(f"\n处理页面: {page_url.split('/')[-1]}")
                for img_url in self.get_images_from_page(page_url):
                    if self.downloaded_count >= self.max_images:
                        break
                    executor.submit(self.download_image, img_url, page_url)

        print(f"\n多线程完成:{self.downloaded_count}张,耗时{time.time()-start:.2f}秒")

if __name__ == "__main__":
    downloader = ImageDownloader()
    # 先运行单线程再运行多线程
    downloader.single_thread()
    downloader.multi_thread()

图片3

图片3

心得体会

通过本次作业,我巩固了网络请求、HTML 解析、多线程编程等技能,提升了问题排查与需求落地能力。同时也发现不足:缺乏图片去重机制、多线程未完全规避共享变量冲突、应对反爬能力有限。
此次实践让我深刻体会到,编程学习需理论结合实际,只有在解决具体问题的过程中,才能真正掌握技术的核心逻辑与应用场景。

作业2

部分代码及其结果展示

点击查看代码
import scrapy
from eastmoney_stock.items import EastmoneyStockItem

class StockSpider(scrapy.Spider):
    name = 'stock_spider'
    allowed_domains = ['eastmoney.com']
    start_urls = ['https://quote.eastmoney.com/center/gridlist.html#hs_a_board']
    def parse(self, response):
        stock_rows = response.xpath('//tbody/tr')
        self.logger.info(f"共提取到 {len(stock_rows)} 行原始数据")
        for row_idx, row in enumerate(stock_rows, 1):
            columns = [col.strip() for col in row.xpath('./td//text()').getall() if col.strip()]
            self.logger.debug(f"第 {row_idx} 行原始列数据:{columns}(共 {len(columns)} 列)")
            if row_idx == 1:
                self.logger.debug(f"第 {row_idx} 行,跳过")
                continue
            if len(columns) != 20:
                self.logger.debug(f"第 {row_idx} 行列数异常({len(columns)}列),跳过")
                continue
            item = EastmoneyStockItem()
            item['serial_id'] = columns[0]                  # 序号
            item['stock_code'] = columns[1]                 # 股票代码
            item['stock_name'] = columns[2]                 # 股票名称
            item['latest_price'] = columns[6]               # 最新价
            item['price_change_rate'] = columns[7]          # 涨跌幅
            item['price_change_amount'] = columns[8]        # 涨跌额
            item['volume'] = columns[9]                     # 成交量(手)
            item['turnover'] = columns[10]                  # 成交额
            item['amplitude'] = columns[11]                 # 振幅
            item['highest_price'] = columns[12]             # 最高
            item['lowest_price'] = columns[13]              # 最低
            item['opening_price'] = columns[14]             # 今开
            item['previous_close'] = columns[15]            # 昨收
            self.logger.info(f"第 {row_idx} 行解析完成:{columns[1]} {columns[2]}")
            yield item
        self.logger.info(f"所有有效股票数据已解析完成")

图片3.2

图片3.2

心得体会

实验中,我用Item定义股票数据字段(如股票代码bStockNo、涨跌幅fChangeRate等),通过 XPath 精准提取页面表格数据;借助 Selenium 解决动态加载问题,确保数据完整获取;在Pipeline中实现 SQL 异步存储,同时处理数据序列化与去重。
过程中也遇到挑战:东方财富网页面动态渲染导致初始 XPath 匹配失效,通过 Selenium 等待页面加载后再解析得以解决;数据库字段类型与爬取数据格式不匹配,调整表结构并在 Pipeline 中添加数据转换逻辑后正常存储。
此次实验不仅巩固了 Scrapy 框架的使用技巧,更理解了动态页面爬取与数据持久化的完整流程,提升了问题排查与方案优化能力,为后续复杂爬虫项目积累了实践经验。

作业3

部分代码及其结果展示

点击查看代码
import scrapy
import os
from boc_exchange.items import BocExchangeItem


class BocSpider(scrapy.Spider):
    name = 'boc'
    allowed_domains = ['boc.cn']
    start_urls = ['https://www.boc.cn/sourcedb/whpj/']

    def parse(self, response):
        # 保存页面HTML到本地(用于调试)
        self.save_page_html(response)

        # 匹配表格中所有数据行(排除表头行)
        rows = response.xpath('//table/tr')[1:]  # [1:] 跳过表头行(第一行是<th>)
        self.logger.info(f"找到{len(rows)}行数据记录")

        for i, row in enumerate(rows):
            try:
                # 提取货币名称(第1列)
                currency = row.xpath('./td[1]/text()').get()
                if not currency or currency.strip() == '':
                    self.logger.warning(f"第{i + 1}行无有效货币名称,跳过")
                    continue

                item = BocExchangeItem()
                # 列对应关系(根据你提供的HTML):
                # 列1:货币名称 → currency
                # 列2:现汇买入价 → tbp
                # 列3:现钞买入价 → cbp
                # 列4:现汇卖出价 → tsp
                # 列5:现钞卖出价 → csp
                # 列6:中行折算价(暂不提取)
                # 列7:发布日期(包含日期时间)
                # 列8:发布时间(暂不提取,用列7的完整时间)
                item['currency'] = currency.strip()
                item['tbp'] = row.xpath('./td[2]/text()').get(default='').strip()
                item['cbp'] = row.xpath('./td[3]/text()').get(default='').strip()
                item['tsp'] = row.xpath('./td[4]/text()').get(default='').strip()
                item['csp'] = row.xpath('./td[5]/text()').get(default='').strip()

                # 提取发布时间(第7列,格式:2025.11.18 20:54:39)
                raw_time = row.xpath('./td[7]/text()').get(default='').strip()
                item['time'] = raw_time.replace('.', '-')

                self.logger.info(f"第{i + 1}行提取成功: {item}")
                yield item
            except Exception as e:
                self.logger.error(f"第{i + 1}行提取失败: {str(e)}")

    def save_page_html(self, response):
        """保存页面HTML到本地用于调试"""
        if not os.path.exists('html_debug'):
            os.makedirs('html_debug')
        with open('html_debug/boc_exchange.html', 'w', encoding='utf-8') as f:
            f.write(response.text)
        self.logger.info("页面HTML已保存到html_debug文件夹")

图片3.3

由于一开始使用body定位,但是一直爬取不到数据,所以我就把爬到到的页面源码保存下来,发现与控制台的有些许区别

图片3.3

图片3.3

后面调整了匹配规则为//table/tr就成功爬取到内容了。

心得体会

本次实验采用 Scrapy+XPath+MySQL 技术路线爬取中国银行网外汇数据,熟练掌握了 Scrapy 中 Item、Pipeline 的数据序列化与存储方法,收获显著。
我通过 Item 明确定义 Currency(货币名称)、TBP(现汇买入价)等核心字段,确保数据结构规范;利用 XPath 精准定位页面表格数据,解决了 HTML 结构解析难题;在 Pipeline 中实现 MySQL 异步存储,同时处理数据序列化与重复数据更新,保障数据持久化的高效与准确。
针对网站表格动态加载与数据格式差异问题,通过优化 XPath 匹配规则、添加数据清洗逻辑得以解决。此次实践不仅巩固了 Scrapy 框架核心组件的使用技巧,更理清了 "数据提取 - 序列化 - 存储" 的完整流程,提升了动态页面爬取与数据库交互的实战能力,为后续数据采集项目奠定了坚实基础。
https://gitee.com/lai-yixuan/2025_crawl_project/tree/master/作业3

posted @ 2025-11-18 22:25  yck666  阅读(13)  评论(0)    收藏  举报