資料採集作業3

淋祁發表於2024-11-11
這個作業屬於哪個課程 2024資料採集與融合技術實踐
這個作業要求在哪裡 作業3
這個作業的目標 1.爬蟲設計 2.掌握scrapy爬蟲框架,並實際應用 3.掌握scrapy爬取動態頁面(實現scrapy與selenium的對接)
學號 102202123

Gitee🔗:作業3

作業整體思路:

  • 建立一個scrapy專案(scrapy startproject scrapy_name)
  • 新建一個爬蟲(scrapy genspider spider_name spider_domain)
  • items.py(需要爬取的資料項)
  • 爬蟲專案修改(頁面url變化、解析響應邏輯、爬取資料)
  • piplines.py(下載、儲存)
  • settings.py(以下是一些配置說明👇)
    • 最大併發請求下載數量CONCURRENT_REQUESTS
    • 資料管道ITEM_PIPELINES
    • 圖片下載地址IMAGES_STORE 實驗3.1
      • 下載路徑DEFAULT_IMAGES_URLS_FIELD
      • 本地儲存路徑DEFAULT_IMAGES_RESULT_FIELD
      • 注:這些都在items有定義
    • 自動延遲AUTOTHROTTLE 溫和模擬模擬爬取網站
    • 是否遵循爬蟲協議ROBOTSTXT_OBEY 本次作業通常為False,不然沒有許可權爬下來x
    • 其他預設建立專案即配置(專案名稱、爬蟲模組定義等)

注:由於此處標明瞭具體過程,部落格撰寫重心為關鍵程式碼實現思路

作業①

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

詳細程式碼見gitee🔗作業3-實驗3.1

實踐過程

1.items.py

最終目標需要下載圖片,所以這裡定義了兩個資料專案類:

  • 爬取網站圖片url
  • 最後下載圖片本地路徑
class WeatherImageItem(scrapy.Item):
    image_urls = scrapy.Field()  # 用於儲存圖片的 URL 列表
    images = scrapy.Field()       # 用於儲存下載後的圖片資訊

2.修改爬蟲檔案

①檢測爬取網站
若直接爬取題目中所指的網站,檢測ELEMENT可知:

  • 元素節點混亂,不統一
  • 不能滿足題目的翻頁
  • 圖片數不可能達到學號後三位

自己摸索找到了天氣網-圖片這個網站

url檢查:

  • index會隨翻頁改變
  • 中間有欄位是你選擇模組的拼音縮寫
        for page in range(start, end + 1):
            if page == 1:
                url = f'https://p.weather.com.cn/{block}/index.shtml'
                yield scrapy.Request(url=url, callback=self.parse)
            else:
                url = f'https://p.weather.com.cn/{block}/index_{page}.shtml'
                yield scrapy.Request(url=url, callback=self.parse)

網頁檢查:

pictures = response.xpath('//div[@class="oi"]')
item = WeatherImageItem()
url = picture.xpath('.//div[@class="tu"]/a/img/@src').extract()[0]  # 提取圖片連結
item["image_urls"] = [url]

②設定最大爬取數

    def __init__(self, max_items=None, *args, **kwargs):
        super(WeatherSpider, self).__init__(*args, **kwargs)
        self.max_items = int(max_items) if max_items else None
        self.item_count = 0  # 引入計數器,每提取一個圖片路徑就+1,直到達到max——items

同時在迴圈獲取圖片下載路徑時,要判別item_count的大小,達到最大就截斷:

        for picture in pictures:
            if self.max_items and self.item_count >= self.max_items:
                self.log(f"Reached maximum items limit: {self.max_items}", level=scrapy.log.DEBUG)
                print(f"Reached maximum items limit: {self.max_items}")
                return

3.piplines.py

管道類本身帶有下載圖片的功能,這裡使用了內建管道ImagesPipeline來處理和下載圖片

  • 圖片 URL 的提取: 它能夠從 item 中提取圖片 URL,併為每個 URL 傳送下載請求。
  • 圖片儲存: 下載的圖片會按照配置的儲存路徑儲存到本地。
  • 下載結果的處理: 下載完成後,可以在 item_completed 方法中處理下載結果。
  • 下載結果預設存在{指定路徑}/full/下
from scrapy.pipelines.images import ImagesPipeline

class WeatherImagesPipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        for ok, result in results:
            if ok:
                image_url = result['url']
                print(f"Downloaded image: {image_url}")
        return item

4.settings.py

主要解釋標題下的配置說明

IMAGES_STORE = 'images'  # 儲存圖片的目錄
DEFAULT_IMAGES_URLS_FIELD = 'image_urls'  # urls存放路徑
DEFAULT_IMAGES_RESULT_FIELD = 'images'  # 下載後圖片所處本地路徑

CONCURRENT_REQUESTS = 16  # 設定併發請求數

題目要求單執行緒和多執行緒分別編寫
在scrapy中體現為併發請求數的設定,即CONCURRENT_REQUESTS

  • 單執行緒爬取:將 CONCURRENT_REQUESTS 設定為 1,這意味著 Scrapy 只會在任何時刻處理一個請求,儘管它仍然是非同步的。
  • 多執行緒爬取:將 CONCURRENT_REQUESTS 設定為更高的值(例如 16),這樣 Scrapy 將同時處理多個請求,儘管它仍然是在非同步環境中。

實踐結果

本次實踐爬取的是天氣現場2-5頁

實踐心得

這次實驗是我自己在天氣網裡面摸索找到這個模組進行作業的,好像更符合題意,但美中不足的是,我檢查了所有模組,這個網站圖片數量比較少,都沒法爬下我學號後三位那麼多圖片,這次沒有直接截停圖片下載的體驗。
我是做完所有作業再來寫部落格,我考慮了一下,可能用selenium,模擬使用者點選網站,就能進一步爬取更多圖片了。在天氣網-圖片中,我選取的模組頁面只將一張圖片作為封面,其實點進去,還有多張圖片,如果能動態模擬點選並進行下一頁翻頁,可以獲得更多圖片。
從這題我也瞭解到了scrapy管道類的內建方法ImagesPipeline,用於高效下載圖片,它有很完善的框架,不用像先前還需要自己寫下載圖片的程式碼。

作業②

要求:熟練掌握 scrapy 中 Item、Pipeline 資料的序列化輸出方法;使用scrapy框架+Xpath+MySQL資料庫儲存技術路線爬取股票相關資訊。
候選網站:東方財富網:https://www.eastmoney.com/
輸出資訊:MySQL資料庫儲存和輸出:序號、股票程式碼、股票名稱、最新報價、漲跌幅、漲跌額、成交量、振幅、最高、最低、今開、昨收,表頭英文命名

詳細程式碼見gitee🔗作業3-實驗3.2

實踐過程

1.實現scrapy與selenium對接

由於東方財富網的行情模組是動態渲染的,所以需要完成以上對接才能成功爬取資料

    def __init__(self):
        self.driver = webdriver.Chrome()  # 使用 Chrome 瀏覽器
        self.max_stocks = 123  # 最大爬取數量
        self.current_count = 0  # 當前已爬取數量
        super().__init__()

    def parse(self, response):
        self.driver.get(response.url)  # 開啟網站
        time.sleep(2)  # 等待頁面載入

包括後面Selector型別也是透過selenium驅動獲得網頁原始碼

sel = Selector(text=self.driver.page_source)
# ...(資料爬取在第二點)

爬取完畢也要退出瀏覽器

    def closed(self, reason):
        self.driver.quit()  # 確保退出瀏覽器

2.網頁檢查與資料爬取




所有資料都在元素對裡面,
再觀察網頁可以注意到,部分帶連結的資料項會巢狀在a元素中,帶顏色(紅表示漲,綠表示跌了)在span裡面

            stocks = sel.xpath('//table/tbody//tr')  # 選擇股票資料的容器
            self.logger.info(f"Found {len(stocks)} stocks.")  # 輸出找到的股票數量

            for stock in stocks:
                if self.current_count >= self.max_stocks:
                    break  # 達到最大爬取數量,停止爬取

                item = StockItem()
                try:
                    item['stock_id'] = stock.xpath('./td[1]/text()').get()  # 序號
                    item['stock_code'] = stock.xpath('./td[2]/a/text()').get()  # 股票程式碼
                    item['stock_name'] = stock.xpath('./td[3]/a/text()').get()  # 股票名稱
                    item['latest_price'] = stock.xpath('./td[5]/span/text()').get()  # 最新報價
                    item['change_percent'] = stock.xpath('./td[6]/span/text()').get()  # 漲跌幅
                    item['change_amount'] = stock.xpath('./td[7]/span/text()').get()  # 漲跌額
                    item['volume'] = stock.xpath('./td[8]/text()').get()  # 成交量
                    item['amplitude'] = stock.xpath('./td[10]/text()').get()  # 振幅
                    item['high'] = stock.xpath('./td[11]/span/text()').get()  # 最高
                    item['low'] = stock.xpath('./td[12]/span/text()').get()  # 最低
                    item['open'] = stock.xpath('./td[13]/span/text()').get()  # 今開
                    item['prev_close'] = stock.xpath('./td[14]/text()').get()  # 昨收

3.翻頁爬取

翻頁觀察發現,url並不會改變
所以這裡用selenium模擬使用者使用瀏覽器點選下一頁翻頁,達成多頁股票資料爬取

            # 查詢並點選“下一頁”按鈕
            try:
                next_button = self.driver.find_element("link text", "下一頁")
                if "disabled" in next_button.get_attribute("class"):
                    break  # 如果“下一頁”按鈕被禁用,停止翻頁
                next_button.click()
                time.sleep(2)  # 等待頁面載入
            except Exception as e:
                self.logger.error(f"Error clicking next page: {e}")
                break  # 如果出現錯誤,停止翻頁

4.連線並將資料儲存在MySQL

這裡已經在編輯資料管道類
思路

  • 連線資料庫(資料庫使用者登入、要連線的提前建立好的資料庫)
  • 建立表(create)
  • 插入資料(insert into)
  • 關閉資料庫
建立表程式碼
    def create_table(self):
        # 建立股票資訊表
        create_table_sql = """
        CREATE TABLE IF NOT EXISTS stocks (
            stock_id VARCHAR(10) PRIMARY KEY,
            stock_code VARCHAR(10) NOT NULL,
            stock_name VARCHAR(50) NOT NULL,
            latest_price REAL,
            change_percent REAL,
            change_amount REAL,
            volume REAL,
            amplitude REAL,
            high REAL,
            low REAL,
            open REAL,
            prev_close REAL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        """
        try:
            self.cursor.execute(create_table_sql)
            self.connection.commit()
            print("Table 'stocks' created successfully.")
        except pymysql.MySQLError as e:
            print(f"Error creating table: {e}")
            self.connection.rollback()  # 出現錯誤時回滾
插入資料程式碼 ```python sql = """ INSERT INTO stocks (stock_id, stock_code, stock_name, latest_price, change_percent, change_amount, volume, amplitude, high, low, open, prev_close) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ try: spider.logger.info(f"Inserting data: {item}") self.cursor.execute(sql, ( item['stock_id'], item['stock_code'], item['stock_name'], item['latest_price'], item['change_percent'], item['change_amount'], item['volume'], item['amplitude'], item['high'], item['low'], item['open'], item['prev_close'] )) self.connection.commit() spider.logger.info("Data inserted successfully.") except pymysql.MySQLError as e: spider.logger.error(f"Error inserting data: {e}") self.connection.rollback() # 出現錯誤時回滾 except Exception as e: spider.logger.error(f"Unexpected error: {e}") ```

5.資料清洗

因為MySQL資料庫資料型別限制,例如漲跌幅的百分號和成交量的,都需要進行展開轉換

        try:
            item['latest_price'] = float(item['latest_price'])
            item['change_percent'] = float(item['change_percent'].replace('%', '').strip()) / 100.0  # 轉換為小數
            item['change_amount'] = float(item['change_amount'])

            # 處理 volume,將萬轉換為實際數值
            item['volume'] = float(item['volume'].replace('萬', '').strip()) * 10000

            # 處理 amplitude,假設其值是百分比形式,去掉百分號並轉換
            item['amplitude'] = float(item['amplitude'].replace('%', '').strip()) / 100.0 if item['amplitude'] else 0.0

            # 處理其他欄位
            item['high'] = float(item['high']) if item['high'] else 0.0
            item['low'] = float(item['low']) if item['low'] else 0.0
            item['open'] = float(item['open']) if item['open'] else 0.0
            item['prev_close'] = float(item['prev_close']) if item['prev_close'] else 0.0

        except ValueError as ve:
            spider.logger.error(f"Value conversion error: {ve}")
            return item  # 如果出現錯誤,可以選擇跳過該條目

6.settings設定

這裡爬取必須進行以下設定,不然沒法爬

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

實踐結果


實踐心得

這題是在老師的提示下才能完成的。在此之前我也沒想到要動態爬取網頁資訊,要讓scrapy和selenium對接。
針對此題url的特殊性,我提前應用selenium的方式實現了翻頁爬取資料操作,成功爬下了123項股票資料,收穫頗豐。
因為是第一次連線MySQL資料庫,很不熟練,也進行了一定的學習,但發現和sqlite3其實比較相似,只是在連線上,MySQL更強調c-s模式互動。

作業③

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

詳細程式碼見gitee🔗作業3-實驗3.3

實踐過程

1.實現與selenium的對接

中國銀行網的貨幣資料也是動態渲染的,所以需要應用標題方法爬取資料
雖為核心方法,但方法同上,不多展示

2.網頁檢查

翻頁時url會變化:

        for page in range(start, end + 1):
            if page == 1:
                url = f'https://www.boc.cn/sourcedb/whpj/index.html'  # 根據實際網站結構生成 URL
                yield scrapy.Request(url=url, callback=self.parse)
            else:
                url = f'https://www.boc.cn/sourcedb/whpj/index_{page - 1}.html'  # 根據實際網站結構生成 URL
                yield scrapy.Request(url=url, callback=self.parse)



由此可見在爬取的時候要跳過第一個tr(裝的是標題,但是屬性也為odd)
並且要注意每兩個tr為一組,一個沒有class屬性,一個class屬性值為odd
結合起來,爬取沒有屬性的tr就對了

    def parse(self, response):
        self.driver.get(response.url)
        time.sleep(2)  # 等待頁面載入
        sel = Selector(text=self.driver.page_source)
        currencies = sel.xpath('//table[@width="100%"]/tbody//tr[not(@class)]')  # 選擇貨幣資料的容器

        for index, currency in enumerate(currencies):
            item = CurrencyItem()
            try:
                item['currency'] = currency.xpath('./td[1]/text()').get()
                item['tbp'] = currency.xpath('./td[2]/text()').get()
                item['cbp'] = currency.xpath('./td[3]/text()').get()
                item['tsp'] = currency.xpath('./td[4]/text()').get()
                item['csp'] = currency.xpath('./td[5]/text()').get()
                item['time'] = currency.xpath('./td[8]/text()').get()

                yield item

            except IndexError as e:
                self.logger.error(f"Error processing stock: {e} - Stock data may be incomplete.")

進行翻頁觀察,每五個小時更新一次,並且只需要爬取兩頁就能爬出一個時間上全部貨幣情況
所以我選取爬取前兩頁

3.piplines和settings配置

piplines.py:MySQL資料庫連線與存入應用
settings.py:不遵循爬蟲協議
都同第二題

實踐結果

實踐心得

透過後面兩道題我也直接接觸了scrapy和selenium對接的方法。在做的過程中除了方法掌握以外,更讓人糾結的應該就是資料爬取的xpath路徑編寫了,經常觀察錯了或者寫非法了,就會爬取失敗,這兩題也應用了很多除錯日誌,來觀察自己在編碼上的錯誤。
上課做得不好就只能課後總結了。做到這裡,基本理順了這次實踐的任務和方法,可能報告上還有不足,一些口語化表達不夠規範等等,也歡迎批評指正。

相關文章