資料採集與融合技術第三次作業

李迦勒發表於2024-11-12

資料採集與融合技術第三次作業

學號姓名 Gitee倉庫地址
102202116 李迦勒 李迦勒/黑馬樓直面爬蟲 - 碼雲 - 開源中國

作業①:

要求:指定一個網站,爬取這個網站中的所有的所有圖片,例如:中國氣象網(http://www.weather.com.cn)。使用scrapy框架分別實現單執行緒和多執行緒的方式爬取。–務必控制總頁數(學號尾數2位)、總下載的圖片數量(尾數後3位)等限制爬取的措施。輸出資訊: 將下載的Url資訊在控制檯輸出,並將下載的圖片儲存在images子檔案中,並給出截圖。

程式碼如下:

item.py

點選檢視程式碼
import scrapy

class DangdangImagesItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
import scrapy

class DangdangImagesItem(scrapy.Item):
image_urls = scrapy.Field()
images = scrapy.Field()
#spider.py

點選檢視程式碼
import os
import scrapy
from dangdang_images.items import DangdangImagesItem

class DangdangSearchSpider(scrapy.Spider):
name = 'dangdang_search'
allowed_domains = ['search.dangdang.com','ddimg.cn']
start_urls = ['https://search.dangdang.com/?key=����&act=input'] # 替換為實際的搜尋URL
max_images = 128 # 最大圖片下載數量
max_pages = 28 # 最大頁數
image_count = 0 # 已下載圖片數量計數
page_count = 0 # 已訪問頁面計數

點選檢視程式碼
def parse(self, response):
    # 檢查是否達到爬取的頁數限制
    if self.page_count >= self.max_pages or self.image_count >= self.max_images:
        return

    # 獲取所有書籍封面圖片的 URL
    image_urls = self.extract_image_urls(response)

    for url in image_urls:
        if self.image_count < self.max_images:
            self.image_count += 1
            item = DangdangImagesItem()
            # 使用 response.urljoin 補全 URL
            item['image_urls'] = [response.urljoin(url)]
            print("Image URL:", item['image_urls'])
            yield item
        else:
            return  # 如果圖片數量達到限制,停止爬取

    # 控制頁面數量並爬取下一頁
    self.page_count += 1
    next_page = response.css("li.next a::attr(href)").get()
    if next_page and self.page_count < self.max_pages:
        yield response.follow(next_page, self.parse)

def extract_image_urls(self, response):
    # 獲取所有書籍封面圖片的 URL
    image_urls = response.css("img::attr(data-original)").getall()
    if not image_urls:
        # 有些圖片URL屬性可能是 `src`,嘗試備用選擇器
        image_urls = response.css("img::attr(src)").getall()
    return image_urls

import os
import scrapy
from dangdang_images.items import DangdangImagesItem
import concurrent.futures

class DangdangSearchSpider(scrapy.Spider):
    name = 'dangdang_search'
    allowed_domains = ['search.dangdang.com', 'ddimg.cn']
    start_urls = ['https://search.dangdang.com/?key=%CA%E9%B0%FC&act=input']  # 替換為實際的搜尋URL
    max_images = 135  # 最大圖片下載數量
    max_pages = 35  # 最大頁數
    image_count = 0  # 已下載圖片數量計數
    page_count = 0  # 已訪問頁面計數

    def parse(self, response):
        # 檢查是否達到爬取的頁數限制
        if self.page_count >= self.max_pages or self.image_count >= self.max_images:
            return

        # 獲取所有書籍封面圖片的 URL
        image_urls = response.css("img::attr(data-original)").getall()
        if not image_urls:
            # 有些圖片URL屬性可能是 `src`,嘗試備用選擇器
            image_urls = response.css("img::attr(src)").getall()

        # 使用 ThreadPoolExecutor 下載圖片
        with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
            futures = []
            for url in image_urls:
                if self.image_count < self.max_images:
                    self.image_count += 1
                    item = DangdangImagesItem()
                    # 使用 response.urljoin 補全 URL
                    item['image_urls'] = [response.urljoin(url)]
                    print("Image URL:", item['image_urls'])
                    futures.append(executor.submit(self.download_image, item))
                else:
                    break  # 如果圖片數量達到限制,停止爬取

        # 控制頁面數量並爬取下一頁
        self.page_count += 1
        next_page = response.css("li.next a::attr(href)").get()
        if next_page and self.page_count < self.max_pages:
            yield response.follow(next_page, self.parse)

    def download_image(self, item):
        # 獲取圖片 URL
        image_url = item['image_urls'][0]

        # 確定儲存路徑
        image_name = image_url.split("/")[-1]  # 從 URL 中提取圖片檔名
        save_path = os.path.join('./images2', image_name)

        try:
            # 傳送 GET 請求下載圖片
            response = requests.get(image_url, stream=True)
            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(chunk_size=8192):
                    f.write(chunk)

            self.logger.info(f"Image downloaded and saved to {save_path}")

        except requests.exceptions.RequestException as e:
            self.logger.error(f"Failed to download image from {image_url}: {e}")

pipelines.py

點選檢視程式碼
class DangdangImagesPipeline:
def process_item(self, item, spider):
return item

settings.py

點選檢視程式碼
BOT_NAME = "dangdang_images"
SPIDER_MODULES = ["dangdang_images.1"]
NEWSPIDER_MODULE = "dangdang_images.1"
DEFAULT_REQUEST_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
}

Obey robots.txt rules

ROBOTSTXT_OBEY = False

圖片儲存路徑

IMAGES_STORE = './images'

爬取的圖片


作業心得

首先,透過使用Scrapy框架,我瞭解到其高效的工作流程,尤其在請求排程、資料處理和資料輸出等方面都十分靈活。在這次實踐中,我設計了兩個爬蟲,一個是單執行緒的,一個是多執行緒的,以此體驗兩者的差異。多執行緒雖然速度更快,但也更容易遇到連線被阻斷的情況,因此我在程式中引入了對請求頻率的控制。

在爬取時,我還設定了頁數和圖片數量的限制,以確保程式符合作業要求,並避免給伺服器帶來過大的壓力。這種控制措施也讓我更深入理解了爬蟲道德和資料獲取的邊界。

另外,將圖片的 URL 資訊輸出到控制檯以及儲存圖片時,我學會了如何使用 Scrapy 的 Item 管理爬取的資料,這種結構化的資料儲存方式很便於後續處理。

作業②

要求:熟練掌握 scrapy 中 Item、Pipeline 資料的序列化輸出方法;使用scrapy框架+Xpath+MySQL資料庫儲存技術路線爬取股票相關資訊。
候選網站:東方財富網:https://www.eastmoney.com/

輸出資訊:MySQL資料庫儲存和輸出格式如下:
表頭英文命名例如:序號id,股票程式碼:bStockNo……,由同學們自行定義設計

程式碼如下:

item.py

點選檢視程式碼
import scrapy

class StockScraperItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
import scrapy

import scrapy

class StocksScraperItem(scrapy.Item):
bStockNo = scrapy.Field()
bStockName = scrapy.Field()
fLatestPrice = scrapy.Field()
fChangeRate = scrapy.Field()
fChangeAmount = scrapy.Field()
fVolume = scrapy.Field()
fTurnover = scrapy.Field()
fAmplitude = scrapy.Field()
fHighest = scrapy.Field()
fLowest = scrapy.Field()
fOpeningPrice = scrapy.Field()
fPreviousClose = scrapy.Field()
spider.py
import scrapy
import json
import re

class StocksSpider(scrapy.Spider):
name = 'stocks'
點選檢視程式碼
# 股票分類及介面引數
cmd = {
    "滬深京A股": "f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048",
    "上證A股": "f3&fs=m:1+t:2,m:1+t:23",
    "深證A股": "f3&fs=m:0+t:6,m:0+t:80",
    "北證A股": "f3&fs=m:0+t:81+s:2048",
}

start_urls = []

def start_requests(self):
    for market_code in self.cmd.values():
        for page in range(1, 3):  # 爬取前兩頁
            url = f"https://98.push2.eastmoney.com/api/qt/clist/get?cb=jQuery&pn={page}&pz=20&po=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&dect=1&wbp2u=|0|0|0|web&fid={market_code}&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152"
            yield scrapy.Request(url, callback=self.parse)

def parse(self, response):
    # 提取JSON格式資料
    data = response.text
    left_data = re.search(r'^.*?(?=\()', data)

    if left_data:
        left_data = left_data.group()
        data = re.sub(left_data + '\(', '', data)
        data = re.sub('\);', '', data)

        try:
            stock_data = json.loads(data)
        except json.JSONDecodeError as e:
            self.logger.error(f"JSON Decode Error: {e}")
            return  # 返回以避免後續操作

        self.logger.info(f"Parsed JSON Data: {json.dumps(stock_data, indent=4, ensure_ascii=False)}")  # 列印解析後的資料

        if 'data' in stock_data and 'diff' in stock_data['data']:
            for key, stock in stock_data['data']['diff'].items():  # 遍歷 diff 字典
                # 在此新增除錯資訊,檢查每個股票的資料
                self.logger.debug(f"Stock Data: {stock}")
                yield {
                    'bStockNo': stock.get("f12", "N/A"),
                    'bStockName': stock.get("f14", "N/A"),
                    'fLatestPrice': stock.get("f2", "N/A"),
                    'fChangeRate': stock.get("f3", "N/A"),
                    'fChangeAmount': stock.get("f4", "N/A"),
                    'fVolume': stock.get("f5", "N/A"),
                    'fTurnover': stock.get("f6", "N/A"),
                    'fAmplitude': stock.get("f7", "N/A"),
                    'fHighest': stock.get("f15", "N/A"),
                    'fLowest': stock.get("f16", "N/A"),
                    'fOpeningPrice': stock.get("f17", "N/A"),
                    'fPreviousClose': stock.get("f18", "N/A")
                }
        else:
            self.logger.warning("No 'data' or 'diff' found in stock_data.")
    else:
        self.logger.warning("Left data not found in response.")

pipelines.py

點選檢視程式碼
@classmethod
def from_crawler(cls, crawler):
    return cls(
        host=crawler.settings.get('MYSQL_HOST'),
        port=crawler.settings.get('MYSQL_PORT'),
        user=crawler.settings.get('MYSQL_USER'),
        password=crawler.settings.get('MYSQL_PASSWORD'),
        db=crawler.settings.get('MYSQL_DB'),
    )

def open_spider(self, spider):
    self.connection = pymysql.connect(
        host=self.host,
        port=self.port,
        user=self.user,
        password=self.password,
        db=self.db,
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor
    )
    self.cursor = self.connection.cursor()

    import pymysql
    from itemadapter import ItemAdapter

    class StocksScraperPipeline:
        def open_spider(self, spider):
            self.connection = pymysql.connect(
                host='127.0.0.1',
                port=33068,
                user='root',  # 替換為你的MySQL使用者名稱
                password='160127ss',  # 替換為你的MySQL密碼
                database='spydercourse',  # 資料庫名
                charset='utf8mb4',
                use_unicode=True,
            )
            self.cursor = self.connection.cursor()

            # 建立表格
            create_table_sql = """
            CREATE TABLE IF NOT EXISTS stocks (
                id INT AUTO_INCREMENT PRIMARY KEY,
                bStockNo VARCHAR(10),
                bStockName VARCHAR(50),
                fLatestPrice DECIMAL(10, 2),
                fChangeRate DECIMAL(5, 2),
                fChangeAmount DECIMAL(10, 2),
                fVolume BIGINT,
                fTurnover DECIMAL(10, 2),
                fAmplitude DECIMAL(5, 2),
                fHighest DECIMAL(10, 2),
                fLowest DECIMAL(10, 2),
                fOpeningPrice DECIMAL(10, 2),
                fPreviousClose DECIMAL(10, 2)
            );
            """
            self.cursor.execute(create_table_sql)

        def close_spider(self, spider):
            self.connection.close()

        def process_item(self, item, spider):
            print(f"Storing item: {item}")  # 列印每個儲存的項
            try:
                insert_sql = """
                INSERT INTO stocks (bStockNo, bStockName, fLatestPrice, fChangeRate, fChangeAmount, fVolume, fTurnover, fAmplitude, fHighest, fLowest, fOpeningPrice, fPreviousClose) 
                VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                """
                self.cursor.execute(insert_sql, (
                    item['bStockNo'],
                    item['bStockName'],
                    float(item['fLatestPrice']),
                    float(item['fChangeRate']),
                    float(item['fChangeAmount']),
                    int(item['fVolume']),
                    float(item['fTurnover']),
                    float(item['fAmplitude']),
                    float(item['fHighest']),
                    float(item['fLowest']),
                    float(item['fOpeningPrice']),
                    float(item['fPreviousClose']),
                ))
                self.connection.commit()
            except Exception as e:
                print(f"Error storing item: {e}")
                print(f"SQL: {insert_sql} | Values: {item}")
            return item

爬取結果:


實驗心得

我進一步掌握了 Scrapy 框架中資料處理和儲存的流程,尤其是 Item、Pipeline 的使用和資料的序列化輸出。

首先,透過定義 Scrapy 的 Item,我設計了適合的欄位來儲存股票資訊,例如股票程式碼、股票名稱等,按照英文命名規範來組織欄位,確保資料結構清晰。然後在 Pipeline 中實現資料的儲存過程,把資料序列化並寫入 MySQL 資料庫。Pipeline 是 Scrapy 中資料處理的關鍵步驟,這次實踐讓我加深了對 Pipeline 中各處理階段的理解。

為了精確獲取資訊,我使用了 XPath 定位技術,在解析網頁資料時更高效。XPath 表示式使得資料提取更加準確和簡潔,尤其適合處理結構化的網頁內容。

在實現整個爬取流程後,我將資料儲存在 MySQL 資料庫中。這讓我學會了如何在 Scrapy 中配置資料庫連線,並掌握了在 Pipeline 中進行資料庫操作的方法。將資料直接寫入 MySQL,為後續的資料分析和處理奠定了基礎。

作業③:

要求:熟練掌握 scrapy 中 Item、Pipeline 資料的序列化輸出方法;使用scrapy框架+Xpath+MySQL資料庫儲存技術路線爬取外匯網站資料。
候選網站:中國銀行網:https://www.boc.cn/sourcedb/whpj/

程式碼如下:

item.py

點選檢視程式碼
import scrapy

class BocExchangeItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
import scrapy

class BankItem(scrapy.Item):
Currency = scrapy.Field()
TBP = scrapy.Field() # 現匯買入價
CBP = scrapy.Field() # 現鈔買入價
TSP = scrapy.Field() # 現匯賣出價
CSP = scrapy.Field() # 現鈔賣出價
Time = scrapy.Field() # 時間

pipelines.py

點選檢視程式碼
class BocExchangePipeline:
def process_item(self, item, spider):
return item
import pymysql
from scrapy.exceptions import DropItem

class BankPipeline:
def init(self, mysql_host, mysql_user, mysql_password, mysql_db, mysql_port):
self.mysql_host = mysql_host
self.mysql_user = mysql_user
self.mysql_password = mysql_password
self.mysql_db = mysql_db
self.mysql_port = mysql_port

@classmethod
def from_crawler(cls, crawler):
    return cls(
        mysql_host=crawler.settings.get('MYSQL_HOST', 'localhost'),
        mysql_user=crawler.settings.get('MYSQL_USER', 'root'),
        mysql_password=crawler.settings.get('MYSQL_PASSWORD', ''),
        mysql_db=crawler.settings.get('MYSQL_DB', 'boc_exchange'),
        mysql_port=crawler.settings.get('MYSQL_PORT', 3306),
    )

def open_spider(self, spider):
    self.connection = pymysql.connect(
        host=self.mysql_host,
        user=self.mysql_user,
        password=self.mysql_password,
        database=self.mysql_db,
        port=self.mysql_port,
        charset='utf8mb4',
        use_unicode=True
    )
    self.cursor = self.connection.cursor()
    create_table_query = """
    CREATE TABLE IF NOT EXISTS exchange_rates (
        id INT AUTO_INCREMENT PRIMARY KEY,
        Currency VARCHAR(100),
        TBP DECIMAL(10,2),
        CBP DECIMAL(10,2),
        TSP DECIMAL(10,2),
        CSP DECIMAL(10,2),
        Time VARCHAR(20)
    )
    """
    self.cursor.execute(create_table_query)
    self.connection.commit()

def process_item(self, item, spider):
    insert_query = """
    INSERT INTO exchange_rates (Currency, TBP, CBP, TSP, CSP, Time)
    VALUES (%s, %s, %s, %s, %s, %s)
    """
    self.cursor.execute(insert_query, (
        item['Currency'],
        item['TBP'],
        item['CBP'],
        item['TSP'],
        item['CSP'],
        item['Time']
    ))
    self.connection.commit()
    return item

def close_spider(self, spider):
    self.cursor.close()
    self.connection.close()

spider.py

點選檢視程式碼
import scrapy
from boc_exchange.items import BankItem

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

def parse(self, response):
    # 解析表格資料
    table = response.xpath('//table[@class="table-data"]')[0]
    rows = table.xpath('.//tr')

    for row in rows[1:]:  # 跳過表頭
        item = BankItem()
        item['Currency'] = row.xpath('.//td[1]/text()').get().strip()
        item['TBP'] = row.xpath('.//td[2]/text()').get().strip()
        item['CBP'] = row.xpath('.//td[3]/text()').get().strip()
        item['TSP'] = row.xpath('.//td[4]/text()').get().strip()
        item['CSP'] = row.xpath('.//td[5]/text()').get().strip()
        item['Time'] = row.xpath('.//td[6]/text()').get().strip()  # 根據實際情況調整

        yield item

執行結果

實驗心得

在這項外匯資料爬取的作業中,我進一步加深了對 Scrapy 框架的掌握,尤其是透過 Item、Pipeline 以及 MySQL 資料儲存的綜合應用,使得資料的抓取和儲存更加高效和規範。

首先,我定義了 Item 來儲存外匯資料,設定了符合需求的欄位,如貨幣名稱、買入價、賣出價等。在抓取資料時,我使用了 XPath 定位方法,透過清晰的路徑表示式精準提取所需資料。這種方式能夠高效地從網頁中抓取結構化資料,使資料處理更加簡潔和準確。

接著,我在 Pipeline 中實現資料的序列化和儲存。Pipeline 使得我可以逐條處理資料,將其清洗並寫入 MySQL 資料庫。我配置了資料庫連線引數,並在 Pipeline 中對每個 Item 進行處理,將資料直接儲存到資料庫表中,為資料的持久化和後續分析提供了基礎。

透過這次作業,我不僅熟練掌握了 Scrapy 中的 Item 和 Pipeline 用法,還在實踐中體驗瞭如何結合 XPath 提高爬蟲的精準度。同時,將資料存入 MySQL 的過程,讓我熟悉了 Scrapy 與資料庫的整合方法,為之後的資料處理提供了寶貴經驗。

相關文章