数据采集实践第三次作业—102302131陈宇新

数据采集实践第三次作业—102302131陈宇新

代码路径:https://gitee.com/chenyuxin0328/data-collection/tree/master/作业3

作业1

要求:指定一个网站,爬取这个网站中的所有的所有图片,例如中国气象网(http://www.weather.com.cn)。实现单线程和多线程的方式爬取。

完整代码

import requests
from bs4 import BeautifulSoup
import os
import time
from urllib.parse import urljoin, urlparse
from concurrent.futures import ThreadPoolExecutor

# --- 配置信息:这里是目标和参数 ---
TARGET_URL = 'http://www.weather.com.cn' # 我们的目标网站!
SAVE_DIR = 'my_crawler_images'         # 图片要保存在这里
MAX_WORKERS = 10                       # 设定最大并发线程数,看看能多快!
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'
}
# -----------------

def create_directory():
    """开始前,先创建图片保存目录,防止找不到家!"""
    if not os.path.exists(SAVE_DIR):
        os.makedirs(SAVE_DIR)
        print(f"【准备工作】创建了图片存放目录: {SAVE_DIR}")


def get_image_urls(target_url):
    """
    第一步:访问目标网页,找到所有图片的地址!
    """
    print(f"\n--- 1. 抓取网页内容,解析图片链接 ({target_url}) ---")
    urls = set()
    try:
        # 请求网页内容
        response = requests.get(target_url, headers=HEADERS, timeout=10)
        response.raise_for_status()
        response.encoding = response.apparent_encoding # 尝试解决中文乱码问题

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

        # 遍历所有 <img> 标签,寻找 src 或 data-src
        for img in soup.find_all('img'):
            img_src = img.get('src') or img.get('data-src')

            if img_src:
                # 相对路径转成完整的绝对路径,Scrapy里是urljoin
                absolute_url = urljoin(target_url, img_src)

                # 简单检查,确保它是图片格式
                if absolute_url.startswith('http') and any(
                        ext in absolute_url.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif']):
                    urls.add(absolute_url)

    except requests.exceptions.RequestException as e:
        print(f"【错误】访问 {target_url} 失败了: {e}")

    print(f"【结果】找到了 {len(urls)} 个独特的图片链接,不错!")
    return list(urls)


def download_image(img_url):
    """
    单个下载任务:把图片从网上取下来,存到本地。
    """
    # 告诉大家我们正在下载哪个文件
    print(f"→ 正在下载: {img_url}")

    try:
        response = requests.get(img_url, headers=HEADERS, stream=True, timeout=10)
        response.raise_for_status()

        # 聪明地从 URL 里提取文件名
        url_path = urlparse(img_url).path
        filename = os.path.basename(url_path)

        # 万一URL里没有文件名,就自己生成一个独一无二的名字!
        if not filename or '.' not in filename:
            filename = f"image_{time.time()}_{os.urandom(4).hex()}.png"

        save_path = os.path.join(SAVE_DIR, filename)

        # 写入文件,使用 stream 模式边下载边写入,节省内存
        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

    except requests.exceptions.RequestException:
        # 如果下载失败,就跳过这个,继续下一个
        # print(f"【跳过】下载 {img_url} 失败...")
        pass


# --- 3. 单线程实现 (串行):一步一个脚印,慢慢爬 ---
def single_thread_crawler(urls):
    print("\n--- 2. [单线程模式] 开始串行下载任务 ---")
    start_time = time.time()

    for url in urls:
        download_image(url)

    end_time = time.time()
    # 重点:记录耗时,方便跟多线程做比较!
    print(f"\n【单线程总结】爬取耗时: {end_time - start_time:.2f} 秒 (好慢呀...)")


# --- 4. 多线程实现 (并行):大家一起上,效率翻倍! ---
def multi_thread_crawler(urls):
    print(f"\n--- 3. [多线程模式] 开始并行下载任务 (并发数: {MAX_WORKERS}) ---")
    start_time = time.time()

    # ThreadPoolExecutor:这就是实现并发的关键!
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        # 使用 map 函数,让线程池里的工人同时去下载图片
        executor.map(download_image, urls)

    end_time = time.time()
    # 重点:记录耗时,证明多线程的优越性!
    print(f"\n【多线程总结】爬取耗时: {end_time - start_time:.2f} 秒 (哇!快多了!)")


# --- 主程序执行:开始我们的实验! ---
if __name__ == '__main__':
    create_directory()

    # 步骤 1: 获取所有图片 URL
    all_urls = get_image_urls(TARGET_URL)

    if not all_urls:
        print("【失败】没有找到任何链接,退出程序。")
    else:
        # 步骤 2: 单线程性能测试 (只测试前20个,节约时间)
        print("\n--- [性能对比实验开始] ---")
        single_thread_crawler(all_urls[:20])

        # 步骤 3: 多线程性能测试 (这次爬全部,展示最终效率)
        multi_thread_crawler(all_urls)
        print("\n--- [性能对比实验结束] ---")

关键代码解释

1)from concurrent.futures import ThreadPoolExecutor:导入 ThreadPoolExecutor,这是实现“多线程并发”的关键,让我们能同时下载多个图片,大大加快速度!
2)TARGET_URL = 'http://www.weather.com.cn':定义了我们的“爬取目标”,程序会先访问这个地址去寻找图片链接。
3)HEADERS:模拟浏览器的身份信息,这是“反爬虫的第一道防线”。加上它,网站就不容易发现我们是爬虫,避免被拒绝访问。
4)create_directory():在下载图片前,先运行这个函数“创建本地文件夹”(images 目录),确保下载的图片有地方存放。
5)requests.get(target_url, headers=HEADERS, ...):发送主要的 GET 请求去获取网页的 HTML 内容。
6)soup = BeautifulSoup(response.text, 'html.parser'):用内置解析器把获取到的 HTML 文本整理成 BeautifulSoup 对象,方便我们用 Python 语句来“定位标签”
7)for img in soup.find_all('img'):这是寻找图片链接的语句,它会“找出网页上所有的 标签”。
8)img_src = img.get('src') or img.get('data-src'):从 标签中提取图片的 URL。因为有些网站会把图片地址放在 src,有些放在 data-src,所以我们都得检查一下。
9)absolute_url = urljoin(target_url, img_src):将图片链接从相对路径(比如 /logo.png)“转换成完整的绝对 URL”,确保我们可以直接访问。
10)with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:启动线程池,设定“最大工人数量(线程数)”,开始准备并行工作。
11)executor.map(download_image, all_urls):这是加速的核心!将 download_image 这个下载任务,“同时分配给所有线程”去执行,实现多线程下载,效率远高于排队(单线程)。
12)requests.get(img_url, ..., stream=True):下载单个图片时开启 stream=True,让程序可以“边下载边写入文件”,避免将大文件一次性加载到内存中。
13)for chunk in response.iter_content(...):实现流式写入,将下载的数据“分成小块(Chunk)”,然后逐块写入本地文件,既省内存又高效。
14)os.path.basename(url_path):从复杂的图片 URL 中“提取出简洁的文件名”,方便我们保存。

运行结果

【准备工作】创建了图片存放目录: my_crawler_images

--- 1. 抓取网页内容,解析图片链接 (http://www.weather.com.cn) ---
【结果】找到了 43 个独特的图片链接,不错!

--- [性能对比实验开始] ---

--- 2. [单线程模式] 开始串行下载任务 ---
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/10/24/535D7D0CD6C8B8E511C272C2EA656387.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/A3BCF14CFDAD5F2E97D403F458416BFE.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/20/8D6BE3579BEFFAAEA187B1068D28978F.jpg
→ 正在下载: https://i.i8tq.com/index/search.png
→ 正在下载: http://i.weather.com.cn/images/cn/index/2025/11/21/78445BF216F9C4AB622AB9074BE4FF37.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/26/20210326150416454FB344B92EC8BD897FA50DF6AD15E8.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2019/qxfw_01.png
→ 正在下载: https://i.i8tq.com/mysky/sksd.png
→ 正在下载: https://i.i8tq.com/video/index_spring.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/06/1FAD9AA63152B70E568FC36914F8966E.jpg
→ 正在下载: http://www.weather.com.cn/2025/11/09/CA392075C79C24AE996ED79B15B2B729.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2017/selectCityBtnCur.png
→ 正在下载: https://i.i8tq.com/adImg/zgtqsc.png
→ 正在下载: https://i.i8tq.com/mysky/skwind.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/l/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/19/20210319155915527B7F1E763A8D9C65581C8A4873D2B0.png
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/6E0C1D56F571B91B55F43C95AC250E98_m.png
→ 正在下载: https://i.i8tq.com/jieri/fbh_2021.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nmc_stfc_sfer_er24_achn_l88_p9_20251125010002400.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/D66A4D956BC744F4AC95C0AC199C0F20.jpg

【单线程总结】爬取耗时: 47.60 秒 (好慢呀...)

--- 3. [多线程模式] 开始并行下载任务 (并发数: 10) ---
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/10/24/535D7D0CD6C8B8E511C272C2EA656387.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/A3BCF14CFDAD5F2E97D403F458416BFE.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/20/8D6BE3579BEFFAAEA187B1068D28978F.jpg→ 正在下载: https://i.i8tq.com/index/search.png

→ 正在下载: http://i.weather.com.cn/images/cn/index/2025/11/21/78445BF216F9C4AB622AB9074BE4FF37.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/26/20210326150416454FB344B92EC8BD897FA50DF6AD15E8.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2019/qxfw_01.png
→ 正在下载: https://i.i8tq.com/mysky/sksd.png
→ 正在下载: https://i.i8tq.com/video/index_spring.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/06/1FAD9AA63152B70E568FC36914F8966E.jpg
→ 正在下载: http://www.weather.com.cn/2025/11/09/CA392075C79C24AE996ED79B15B2B729.jpg
→ 正在下载: https://i.tq121.com.cn/i/weather2017/selectCityBtnCur.png
→ 正在下载: https://i.i8tq.com/adImg/zgtqsc.png
→ 正在下载: https://i.i8tq.com/mysky/skwind.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/l/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/03/19/20210319155915527B7F1E763A8D9C65581C8A4873D2B0.png
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/6E0C1D56F571B91B55F43C95AC250E98_m.png
→ 正在下载: https://i.i8tq.com/jieri/fbh_2021.png
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nmc_stfc_sfer_er24_achn_l88_p9_20251125010002400.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/D66A4D956BC744F4AC95C0AC199C0F20.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/z_rada_c_babj_20251125092451_p_dor_achn_cref_20251125_091800.png
→ 正在下载: https://i.i8tq.com/adImg/pc_index.png
→ 正在下载: https://i.i8tq.com/shengtai/tzh202501.jpg
→ 正在下载: https://i.i8tq.com/weather2020/search/rbAd.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/sjztj/2020/07/20/20200720142523B5F07D41B4AC4336613DA93425B35B5E_xm.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/19FB810CD191A8478360C8E3F2485BF2.jpg
→ 正在下载: https://i.i8tq.com/index/img-qrcode-3.png
→ 正在下载: https://i.tq121.com.cn/i/weather2015/index/loading.gif
→ 正在下载: http://i.tq121.com.cn/i/weather2017/cx_new.png
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2019/10/28/20191028144048D58023A73C43EC6EEB61610B0AB0AD74_xm.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/24/CC78B7E96DCAA9F7E5AC32105CD1ADD7.jpg
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/25/BF57450309CE9F67263B9E8DC23FCF9E.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/news/2021/05/14/20210514192548638D53A47159C5D97689A921C03B6546.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/index/2024/03/18/202403181336470A7F03D1DA389C5AC74DDB7AC0745065.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/14/C19340667E7B19F42AD27995DE6E1DFB.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_ncc_zki_shlsk_eci_achn_l88_pb_20251125000000000.png
→ 正在下载: http://pic.weather.com.cn/images/cn/photo/2025/11/05/FF1551323C2D0F90FD2D3BB88E7186D0.jpg
→ 正在下载: http://pi.weather.com.cn/i//product/pic/m/sevp_nsmc_wxbl_fy4b_etcc_achn_lno_py_20251125084500000.jpg
→ 正在下载: https://i.weather.com.cn/images/cn/video/lssj/2025/11/25/FE0476C4C7D5E1F14E8D08400DB3B04E_m.png
→ 正在下载: https://i.i8tq.com/index/logo2.png
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/18/602BF174707604395A015608FF740F08.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/life/shrd/2025/11/14/BAF29F277AA2CAA1AF7FF12F9D92128B.jpg
→ 正在下载: http://i.weather.com.cn/images/cn/science/2020/07/28/202007281001285C97A5D6CAD3BC4DD74F98B5EA5187BF.jpg

image

实验心得

本次实验成功对比了图片下载中单线程和多线程的效率。
核心心得是:图片下载是典型的 I/O 密集型任务。通过引入 ThreadPoolExecutor,程序能够高效利用等待网络响应的时间,实现任务并发,总耗时比串行模式显著缩短。
主要挑战在于确保 URL 路径的准确转换,以及在多线程环境下,需要更健壮地处理因网络超时导致的请求失败和异常

作业2

image

完整代码

项目结构

stock_scraper/  <-- Scrapy 项目根目录 (旧项目)
   ├── stock_scraper/
   │   ├── __init__.py
   │   ├── items.py         <-- 包含 StockItem 和 CurrencyItem (如果为作业三新增)
   │   ├── middlewares.py
   │   ├── pipelines.py     <-- 包含 MysqlPipeline 逻辑 (已修改)
   │   ├── settings.py      <-- 包含 MySQL 配置 (已修改)
   │   └── spiders/
   │       ├── __init__.py
   │       ├── boc_currency.py  <-- 外汇爬虫文件
   │       └── sina_stock.py    <-- 股票爬虫文件 (假设原始爬虫名为这个)
   │
   └── scrapy.cfg

代码

items.py

定义了 StockItem 类,明确了爬虫需要抓取的 所有股票字段(如:股票代码、名称、最新价、涨跌幅等),是数据的容器和规范。

# 文件: currency_scraper/currency_scraper/items.py

import scrapy


class CurrencyItem(scrapy.Item):
    # 货币名称,用作唯一标识
    currency = scrapy.Field()  # Currency

    # 现汇买入价 (TBP)
    tbp = scrapy.Field()  # TBP (Telegraphic Transfer Buying Price)

    # 现钞买入价 (CBP)
    cbp = scrapy.Field()  # CBP (Cash Buying Price)

    # 现汇卖出价 (TSP)
    tsp = scrapy.Field()  # TSP (Telegraphic Transfer Selling Price)

    # 现钞卖出价 (CSP)
    csp = scrapy.Field()  # CSP (Cash Selling Price)

    # 发布时间
    time = scrapy.Field()  # Time
pipelines.py

接收 StockItem,进行最终的数据清洗和格式转换(如将 None 转化为 NULL),并执行 MySQL 数据库的写入(包括建表和 INSERT/UPDATE 操作)

# Scrapy settings for currency_scraper project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     https://docs.scrapy.org/en/latest/topics/settings.html
#     https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#     https://docs.scrapy.org/en/latest/topics/spider-middleware.html

BOT_NAME = "currency_scraper"

SPIDER_MODULES = ["currency_scraper.spiders"]
NEWSPIDER_MODULE = "currency_scraper.spiders"

ADDONS = {}


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = "currency_scraper (+http://www.yourdomain.com)"

# Obey robots.txt rules
ROBOTSTXT_OBEY = True

# Concurrency and throttling settings
#CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 1
DOWNLOAD_DELAY = 1

# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False

# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
#    "Accept-Language": "en",
#}

# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    "currency_scraper.middlewares.CurrencyScraperSpiderMiddleware": 543,
#}

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    "currency_scraper.middlewares.CurrencyScraperDownloaderMiddleware": 543,
#}

# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    "scrapy.extensions.telnet.TelnetConsole": None,
#}

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
#ITEM_PIPELINES = {
#    "currency_scraper.pipelines.CurrencyScraperPipeline": 300,
#}

# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = "httpcache"
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = "scrapy.extensions.httpcache.FilesystemCacheStorage"

# Set settings whose default value is deprecated to a future-proof value
FEED_EXPORT_ENCODING = "utf-8"

# 文件: currency_scraper/settings.py

# ... (原始代码不变) ...

# --- 1. MySQL 数据库配置 (!!! 替换占位符 !!!) ---
# 推荐使用 127.0.0.1 避免 Socket 连接问题
MYSQL_HOST = '127.0.0.1'
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'mysql123'  # <<< 替换成你的 MySQL 密码!
MYSQL_DB = 'boc_currency_db' # 建议使用新的数据库名
# --------------------------------------------------

# --- 2. 启用 ITEM PIPELINES ---
ITEM_PIPELINES = {
    # 启用我们自定义的 Pipeline 类
    "currency_scraper.pipelines.MysqlPipeline": 300,
}

# --- 3. 增加请求头 (反爬虫优化) ---
# 取消 USER_AGENT 的注释,并使用一个常见的浏览器User-Agent
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'

# --- 4. 增加下载延迟 (反爬虫优化) ---
DOWNLOAD_DELAY = 1.0
ROBOTSTXT_OBEY = False # 目标网站可能禁止爬虫,暂时关闭 robots.txt 检查
stock_spider.py

负责发起网络请求(通过 API 接口)、使用 JSON 解析来获取原始数据,并根据字段映射规则生成 StockItem。

import scrapy
from stock_scraper.items import StockItem
import re
import json


class SinaStockApiSpider(scrapy.Spider):
    name = 'sina_stock'
    allowed_domains = ['finance.sina.com.cn']

    # 基础 API URL(不包含 biz_type, page 和 callback)
    BASE_API_URL = "https://stock.finance.sina.com.cn/stock/api/openapi.php/LhbService.getList?list_type=1&extra=symbol,plate,tip,symbol_num&pagesize=50&no_chg_type_3=1"

    # 定义要爬取的业务类型和初始页
    # biz_type=1: 机构专用; biz_type=3: 沪股通/深股通; biz_type=4: 游资/其他
    BIZ_TYPES = [1, 3, 4]

    # 定义每页大小和最大页数 (请根据实际情况调整最大页数)
    PAGESIZE = 50
    MAX_PAGES = 5  # 假设爬取前5页数据

    def start_requests(self):
        """生成初始请求"""
        for biz_type in self.BIZ_TYPES:
            # 初始请求从 page=1 开始
            url = f"{self.BASE_API_URL}&biz_type={biz_type}&page=1&callback=hqccall_biz_{biz_type}_page_1"
            yield scrapy.Request(
                url=url,
                callback=self.parse_api,
                # meta 用于传递状态信息,以便在 parse_api 中使用
                meta={'biz_type': biz_type, 'page': 1}
            )

    def parse_api(self, response):
        """解析 JSONP 格式的 API 响应并生成后续页请求"""
        self.logger.info(f"Processing API data from: {response.url}")

        # 1. 清理 JSONP 格式,提取 JSON 字符串
        # JSONP 格式: hqccall_biz_1_page_1({...JSON DATA...})
        jsonp_data = response.text.strip()

        try:
            start_index = jsonp_data.index('{')
            end_index = jsonp_data.rindex('}') + 1
            json_str = jsonp_data[start_index:end_index]
            data_dict = json.loads(json_str)
        except Exception as e:
            self.logger.error(f"Failed to parse JSONP at {response.url}: {e}")
            return  # 无法解析,退出

        # 2. 提取股票数据列表
        stock_list = data_dict.get('result', {}).get('data', {}).get('data', [])
        current_page = response.meta.get('page')
        biz_type = response.meta.get('biz_type')

        # 3. 遍历数据并 yield Item
        for stock_data in stock_list:
            item = StockItem()

            # --- 字段映射 ---
            # 注意:API 中的 symbol_name 是 Unicode 编码 (\u开头),Python 的 json.loads 会自动解码
            item['stock_code'] = stock_data.get('code', '')
            item['stock_name'] = stock_data.get('symbol_name', '')
            item['latest_price'] = stock_data.get('actprice', '')
            item['price_change_rate'] = stock_data.get('changeratio', '')

            # 以下字段在龙虎榜 API 中可能缺失或名称不同,需要根据你的 Item 调整
            item['price_change_amount'] = None
            item['volume'] = stock_data.get('amount', '')  # 交易额
            item['turnover'] = stock_data.get('turnover', '')  # 换手率
            item['opening_price'] = None
            item['prev_close'] = None
            item['high_price'] = None
            item['low_price'] = None
            item['amplitude'] = None

            # 数据清洗 (主要针对百分号和单位)
            for key, value in item.items():
                if value is not None and isinstance(value, str):
                    cleaned_value = re.sub(r'[\s,%]', '', value)
                    item[key] = cleaned_value.replace('万', '').replace('亿', '')

            yield item

        # 4. 生成下一页请求 (分页逻辑)
        if stock_list and current_page < self.MAX_PAGES:
            next_page = current_page + 1
            next_url = f"{self.BASE_API_URL}&biz_type={biz_type}&page={next_page}&callback=hqccall_biz_{biz_type}_page_{next_page}"

            self.logger.info(f"Generating request for page {next_page}, biz_type {biz_type}")

            yield scrapy.Request(
                url=next_url,
                callback=self.parse_api,
                meta={'biz_type': biz_type, 'page': next_page}
            )

关键代码解释

1)class SinaStockApiSpider(scrapy.Spider):定义了继承自 scrapy.Spider 的主爬虫类,包含了爬虫的名称、域限制以及所有数据抓取和流程控制方法。
2)name = 'sina_stock':定义了爬虫的唯一名称,用于在命令行中(scrapy crawl sina_stock)启动它。
3)allowed_domains = [...]:定义了爬虫允许访问的域名列表,限制了爬虫的活动范围,保障了爬虫的聚焦性。
4)BASE_API_URL = "...":定义了新浪股票 API 的基础接口地址,是构造所有分页和多类型请求的起点。
5)def start_requests(self):重写了 Scrapy 的起始请求方法。这是关键,它不再依赖一个静态的 start_urls 列表,而是主动生成了多个带参数的 scrapy.Request 对象,用于并发抓取不同业务类型和页码的数据。
5)url = f"{self.BASE_API_URL}&biz_type={biz_type}&page={page}...":利用 f-string 动态拼接 URL,将业务类型(biz_type)和页码(page)等请求参数嵌入到 API 接口中,实现数据的筛选和分页获取。
6)def parse_api(self, response):定义了专门用于处理 API 响应的回调函数。当一个 API 请求完成后,Scrapy 会将响应结果传递给这个函数进行处理。
7)jsonp_data = response.text.strip():获取 API 返回的原始文本内容。由于 API 采用 JSONP 格式,返回内容带有函数包裹,需要进行预处理。
8)json_str = jsonp_data[start_index:end_index]:通过查找 { 和 } 的索引,手动去除 JSONP 的函数包裹(例如 hqccall123({...})),得到纯净的 JSON 字符串。
9)data_dict = json.loads(json_str):使用 json 库将清洗后的 JSON 字符串解析为 Python 字典,这样我们就可以像操作普通字典一样提取股票数据了。
10)for stock_data in stock_list:遍历解析字典中包含的实际股票数据列表,对每一条记录进行处理。
11)item = StockItem():实例化一个新的 StockItem 对象,准备将数据字段准确地映射和填充进去。
12)item['stock_code'] = stock_data.get('code', ''):执行字段映射。将 API 返回字典中的 'code' 字段的值,赋值给 Item 中的 'stock_code' 字段,确保数据格式和名称统一。
13)cleaned_value = re.sub(r'[\s,%]', '', value):在数据交付给 Pipeline 前,使用正则表达式对价格、涨跌幅等字段进行基础清洗,去除百分号或空格,确保数据是纯净的数字字符串。
14)yield item:将处理完毕的 StockItem 提交给 Scrapy 引擎,进入 Pipeline 流程,最终存储到 MySQL 数据库中。
15)if stock_list and current_page < self.MAX_PAGES:实现分页逻辑。如果当前页有数据且未达到设定的最大页数,则构造并 yield 下一页的请求,实现自动连续爬取。

运行结果

image

作业3

image
由于整体的项目结构和框架代码与作业2类似,这里注重将核心的爬虫代码

完整代码

# 文件: currency_scraper/spiders/boc_currency.py

import scrapy
from currency_scraper.items import CurrencyItem


class BocCurrencySpider(scrapy.Spider):
    name = 'boc_currency'
    # 确保 allowed_domains 是正确的
    allowed_domains = ['boc.cn', 'www.boc.cn']
    start_urls = ['https://www.boc.cn/sourcedb/whpj/']

    def parse(self, response):
        self.logger.info(f"Start parsing URL: {response.url}")

        # XPath 定位:数据表格通常在中行页面位于 div[@class="publish"]/table/tbody/tr
        # 我们寻找类名为 publish 的 div 下面的 table 中的所有行 (<tr>)
        # 注意:中行网站的表格结构比较特殊,经常在 <body> 下直接是 <table> 或嵌套在多个 <div> 中

        # 使用更具鲁棒性的 XPath 定位到包含数据的表格行
        rows = response.xpath('//div[@class="publish"]//table//tr')

        # 遍历所有行,跳过表头 (rows[0] 是表头)
        for row in rows[1:]:
            # 提取单元格文本内容 (每个 td 的直接文本内容)
            cells = row.xpath('./td/text()').getall()

            # 确保获取到的列数足够 (外汇牌价表通常有 8 列,索引从 0 开始)
            # 0: 货币名称, 1: 现汇买入价, 2: 现钞买入价, 3: 现汇卖出价, 4: 现钞卖出价, 6: 发布时间
            if len(cells) < 7:
                continue

            item = CurrencyItem()

            # 字段映射
            item['currency'] = cells[0].strip()
            item['tbp'] = cells[1].strip()
            item['cbp'] = cells[2].strip()
            item['tsp'] = cells[3].strip()
            item['csp'] = cells[4].strip()
            item['time'] = cells[6].strip()

            # 传递 Item 给 Pipeline
            yield item

核心代码解释

1)name = 'boc_currency':定义爬虫的唯一启动名称,用于命令行启动(scrapy crawl boc_currency)。
2)start_urls = ['https://www.boc.cn/sourcedb/whpj/']:定义爬虫的起始目标 URL,即中国银行外汇牌价页面。
3)def parse(self, response):Scrapy 接收到网页响应后,默认执行的解析函数,是数据提取的起点。
4)rows = response.xpath('//div[@class="publish"]//table//tr'):使用 XPath 定位到包含外汇数据的核心表格行()。这是精准提取数据的关键步骤。
5)for row in rows[1:]:遍历所有表格行,从索引 [1] 开始,目的是跳过表格的标题行(表头)。
6)cells = row.xpath('./td/text()').getall():定位到当前行中的所有表格单元格(),并用 getall() 提取所有单元格的文本内容,获取原始数据列表。
7)if len(cells) < 7:进行数据有效性检查,确保当前行的数据完整。
8)item = CurrencyItem():实例化一个新的 CurrencyItem 对象,准备进行字段映射和赋值。
9)item['currency'] = cells[0].strip():执行字段映射,将提取到的原始数据列表中的第一项(cells[0])赋值给 Item 的 'currency' 字段。其他价格字段同理。
10)yield item:将封装完整的外汇数据对象 Item 交付给 Scrapy 引擎,使其流转到下一步的 pipelines.py 进行存储。

运行结果

image

心得体会

实验二和实验三的核心在于 API 和数据流的实践。 我们确认了爬取结构化数据时,调用 API 接口(实验二)比解析复杂 HTML 表格(实验三)更高效可靠。整个项目的成功取决于 Scrapy 完整数据流的打通,特别是 Pipeline 中必须严格处理字段名称匹配和数据类型兼容性,这是确保数据能准确无误写入 MySQL 数据库的关键。

posted @ 2025-11-25 18:22  102302131陈宇新  阅读(5)  评论(0)    收藏  举报