Python爬蟲實戰之叩富網

SilenceHL發表於2021-04-04

宣告:以下內容均為我個人的理解,如果發現錯誤或者疑問可以聯絡我共同探討

爬蟲介紹

爬蟲是一種按照一定規則自動抓取網路上的資訊資料的程式。我們身處一個大資料的時代,可以通過爬蟲獲取到我們所需要的資料(遵從robots協議中的規則)。

網站介紹

叩富網是江西博辰網路科技公司旗下的一個專業網站。模擬炒股網站初建於2000年,2006年被博辰網路收購,並開始規範系統的運作。目前使用者100多萬,日均IP數10萬左右。是國內唯一一家專業致力於模擬炒股開發和運營的網站。公司旗下有有獎大賽站、免費大賽站以及和其他證券公司合作的網站。

編寫爬蟲的原因和用途

本人閒暇時間會學習投資理財相關內容,並通過叩富網進行模擬炒股,本次想通過編寫爬蟲來程式化自己的模擬交易

robots協議

什麼是robots協議

robots協議以robots.txt檔案形式呈現,是網站中給各類爬蟲規定爬取範圍的檔案,robots.txt存放在網站的根目錄下。我們準備爬取某個網站時,首先應該檢視我們需要的資料該網站是否允許我們爬取,當我們爬取規則之外的資料時,該網站有權利起訴我們非法獲取資料。

robots協議的構成

由User-agent、Allow、Disallow構成。User-agent後面的內容是具體的爬蟲名,如百度爬蟲為Baiduspider,則代表所有爬蟲。Allow後面的內容是允許爬取的URL路徑,如/.jpg$表示可以爬取該網站下的所有.jpg圖片,/表示所有路徑均允許爬取。Disallow後面的內容是不允許爬取的URL路徑,同Allow相反。

常見的規則有哪些

允許爬蟲獲取所有內容:

User-agent: *
Disallow:
# 或者
User-agent: *
Allow: /

禁止爬取所有內容:

User-agent: *
Disallow: /

禁止訪問網站中所有動態頁面

User-agent: *
Disallow: /*?*

禁止搜尋引擎抓取網站上所有圖片

User-agent: *

Disallow: /*.jpg$

Disallow: /*.jpeg$

Disallow: /*.gif$

Disallow: /*.png$

Disallow: /*.bmp$

Feeling

爬蟲可以使我們更加高效的獲取到網際網路中的各類資料,但網路不是法外之地我們也要在合規合法的基礎上進行爬取,尊重每一個網站開發者,敬畏法律。

requests

requests庫是學習爬蟲入門最適合的一個第三方庫,它是將Python內建的urllib進行深度封裝的庫。是一個非常成熟的HTTP客戶端庫,當然他也並非完美,我們後期也可以在其基礎上進行補充形成適合自己的一個庫。

快速上手

中文文件:docs.python-requests.org/zh_CN/lat...

官方示例:docs.python-requests.org/zh_CN/lat...

獲取robots協議,檢視可爬取範圍

使用get請求獲取協議內容,並將其輸出控制檯檢視發現規則允許我們爬取整站內容

登陸叩富網

在Chrome瀏覽器中通過開發者選項中的Network選項卡中發現,登陸的時候有一個login.html

我們可以在裡面看到請求URL、請求方式和Form Data所需要的內容,根據這個編寫一個請求檢視一下獲取到的資料

由於是字串格式,所以出現了我們看不懂的字元,通過json轉成Python中的字典格式再檢視

可以看到顯示登陸成功了,接下來通過獲取到的cookie就可以獲取到我們需要的各種資訊了

接下來我們將程式碼優化一下變成一個方法,讓登陸後的cookie可以在以後任意需要的地方使用

import requests
import json
from lxml import etree


def login(username, password):
    """
    登陸叩富網
    :param username: 使用者名稱
    :param password: 密碼
    :return: 登陸成功返回cookie,失敗丟擲對應異常
    """
    login_url = 'http://www.cofool.com/Passport/login.html'
    data = {
        'username': username,
        'password': password
    }
    try:
        response = requests.post(url=login_url, data=data)
    except Exception as e:
        raise Exception("登陸失敗,原因為:{}".format(e))

    content = json.loads(response.content.decode())
    if content['status'] == 0:
        cookie = response.cookies.get_dict()
        return cookie
    else:
        raise Exception("登陸失敗,原因為:{}".format(content['info']))

在出現登入失敗的時候丟擲異常,並顯示錯誤資訊

Feeling

爬蟲是我們與開發者的博弈,我們需要站在他們的角度去分析他們開發的過程,從而更好的理解並去設計我們的相關爬蟲程式。通常爬蟲程式都不是一次就完成的,需要通過我們不斷根據請求的反饋去修改程式,最終通過多層解析得到結果。

獲取賬戶資訊

練習區資訊

通過對相關請求的分析,練習區資訊在http://www.cofool.com/Trade/Stock/index/gid/2.html這個URL下

對頁面分析我們所需要的資料在class為top_ts的div下

XPath

XPath是一門在XML文件中查詢資訊的語言,可以幫助我們在爬蟲中對獲取資料進行查詢得到我們需要的內容。

推薦從大佬崔慶才的個人部落格學習XPath及爬蟲的相關內容

開始通過XPath獲取對應資料,考慮到多個資料在相同的font標籤下,可以一次性獲取所有標籤然後進行處理

def get_account_info(query_category):
    """
    根據query_category獲取賬戶相關資訊
    :param query_category: 查詢類別資訊,賬戶資訊:ai 持倉狀態:ap 當日委託:ac 當日成交:ad 歷史成交:hd 股票收益明細:sd 日資產增長明細:da
    月資產增長明細:ma 榮耀榜 ho
    :return:
    """
    cookie = login('使用者名稱', '密碼')
    if query_category not in ['ai', 'ap', 'ac', 'ad', 'hd', 'sd', 'da', 'ma', 'ho']:
        raise Exception('查詢類別不存在')
    if query_category == 'ai':
        url = 'http://www.cofool.com/Trade/Stock/index/gid/2.html'
        # 抓取賬戶相關資料
        try:
            response = requests.get(url=url, cookies=cookie).content.decode()
        except Exception as e:
            raise Exception("獲取賬戶資料失敗,原因為:{}".format(e))
        if '總盈利率' not in response:
            raise Exception("獲取賬戶資料失敗,未獲取到正確資訊")
        # 使用xpath對抓取到的資料進行清洗得到我們需要的資料
        html = etree.HTML(response)
        account_info = html.xpath('//div [@class="top_ts"]/div//font/text()')
                if len(account_info) != 4:
            raise Exception("獲取賬戶資料失敗,未獲取足夠的賬戶資料")
        gross_profit_rate = account_info[0]  # 總收益率
        initial_funding = account_info[1]  # 初始資金
        number_of_participants = account_info[2]  # 參賽人數
        average_income = account_info[3]  # 平均收益率
        overall_ranking = html.xpath('//div [@class="top_ts"]/div//p[2]/b/text()')[0]  # 總排名
        try:
            total_assets = float(''.join(re.search('\d,\d*,\d*.\d*', html.xpath(
                '//*[@id="left2"]/table/tbody/tr[1]/td[@class="btom zjsr"]/text()')[0])[0].split(',')))
            available_funds = float(''.join(re.search('\d,\d*,\d*.\d*', html.xpath(
                '//*[@id="left2"]/table/tbody/tr[1]/td[@class="btom zjl"]/text()')[0])[0].split(',')))
        except Exception as e:
            raise Exception("獲取賬戶資料失敗,{}".format(e))
        return {'總收益率': gross_profit_rate, '初始資金': initial_funding, '參賽人數': number_of_participants,
                '平均收益率': average_income, '總排名': overall_ranking, '總資產': total_assets, '可用資金': available_funds}

持倉、交易、業績資訊

通過對相關請求的分析,當前持倉、當日委託、當日成交、歷史成交與業績報告都用同一個URL進行請求,只是請求中FormData部分資料不同,分析各頁面資料發現當前持倉、當日委託、當日成交、歷史成交資料類似,將其作為同一個型別進行爬取做簡單修改即可。分頁相關資訊需要根據頁碼資料進行追加處理

...
url = 'http://www.cofool.com/Trade/Stock/tradeItem.html'
    data = {'gid': 'gid', 'uid': 'uid', 'web_id': 'web_id}
    # 根據查詢條件設定對應的type值
    if query_category == 'ap':
        data['type'] = 'position'
    elif query_category == 'ac':
        data['type'] = 'entrust'
    elif query_category == 'ad':
        data['type'] = 'turnover'
    elif query_category == 'hd':
        data['type'] = 'history'
    elif query_category == 'sd':
        data['type'] = 'earnings'
    elif query_category == 'da':
        data['type'] = 'dayasset'
    elif query_category == 'ma':
        data['type'] = 'monthasset'
    elif query_category == 'ho':
        data['type'] = 'honor'
    # 第一次獲取對應資訊,當資訊不存在時返回'暫無交易的資料!'
    try:
        response = requests.post(url=url, data=data).content.decode()
    except Exception as e:
        raise Exception('獲取賬戶資料失敗,原因為:{}'.format(e))
    if ' 暫無交易的資料!' in response:
        return '暫無交易的資料!'
    html = etree.HTML(response)
    # 當有資訊時獲取頁碼數值,
    number_of_pages = len(html.xpath('//div [@class = "clearfix fr"]/a/text()')) + 1
    # 獲取對應表頭名
    col_name = html.xpath('//tr/th/text()')
    # 設定臨時儲存字典
    temp_dict = {}
    # 根據頁碼資訊進行第二次訪問獲取資訊
    for i in range(1, number_of_pages + 1):
        # 新增頁碼資料
        data['p'] = i
        try:
            response = requests.post(url=url, data=data).content.decode()
        except Exception as e:
            raise Exception('獲取賬戶資料失敗,原因為:{}'.format(e))
        # 由於爬取的資料存在大量\n和空格,先進行一次簡單的清洗
        response = re.sub('\n\s|\s', '', response)
        html = etree.HTML(response)
        # 分別獲取各列資訊
        for j in range(len(col_name)):
            xpath_index = j + 1
            xpath_values = html.xpath('//tr/td[{}]//text()|//tr/td[{}]/font/text()|//tr/td[{}]/text()'.format(xpath_index, xpath_index,xpath_index))
            if i == 1:
                temp_dict[col_name[j]] = xpath_values
            # 當出現第二頁及其以上時,進行資料追加處理
            else:
                temp_dict[col_name[j]].extend(xpath_values)
    return pd.DataFrame(temp_dict)

Feeling

爬蟲中資料清洗的過程通常也非常熬人,網站開發者的水平高低與反爬的難度,會導致許多資料清洗起來非常複雜,可能會花費大量的時間還得不到你想要的結果,這時候我們需要保持一顆平常心,針對出現的問題一個一個去解決,只要堅持下去一定能解決的!

賬戶操作

股票資訊

接下來編寫賬戶操作方面的爬蟲,要交易首先要拿到進行交易股票的相關資訊,通過分析相關請求後發現其URL為http://www.cofool.com/Trade/Stock/stockQuote.html,這個比較簡單很快就可以完成對應程式碼

def get_stock_info(stock_code):
    """
    根據股票程式碼獲取相關價格
    :param code:股票程式碼
    :return: 相關價格
    """
    url = "http://www.cofool.com/Trade/Stock/stockQuote.html"
    data = {
        "code": stock_code,
        "uid": 'uid',
    }
    cookie = login('賬戶名', '密碼')
    try:
        response = json.loads(requests.post(url=url, data=data, cookies=cookie).content.decode("utf-8-sig"))
    except Exception as e:
        raise Exception('獲取賬戶資料失敗,原因為:{}'.format(e))
    stock_info = response["info"]
    info_dict = {
        "high_limit": stock_info["surgedLimit"],  # 漲停價
        "low_limit": stock_info["declineLimit"],  # 跌停價
        "stock_name": stock_info['stockName'],  # 股票名稱
        'currentPrice': stock_info["currentPrice"],  # 當前價
    }
    return info_dict

股票交易

通過觀察發現買入與賣出操作的URL差距非常小,並且請求引數也高度相似,可以將其放在同一個方法中

def tradeing(stock_code, tradeing_type, amount=0):
    """
    股票交易,並返回交易狀態
    :param stock_code:股票程式碼
    :param tradeing_type:交易型別
    :param amount:交易數量,預設為0防止出現沒有填寫的情況
    :return:交易狀態
    """
    cookie = login('賬戶名', '密碼')
    # 獲取股票相關資訊
    info_dict = get_stock_info(stock_code)
    price = info_dict["high_limit"]
    # 配置相關資料
    data = {
        "stockName": info_dict['stock_name'],
        "code": stock_code,
        "uid": 'uid',
        "gid": 2,
        "orderPrice": price,
        "orderAmount": amount,
        "declineLimit": info_dict['low_limit'],
        "surgedLimit": info_dict['high_limit']
    }
    if tradeing_type == 'buy':
        url = "http://www.cofool.com/Trade/Stock/buy.html"
        # 買入操作,檢測買入數量防止超過最大買入數量
        available_funds = get_account_info('ai')['可用資金']
        if amount < available_funds / float(price):
            data["orderAmount"] = amount
        else:
            raise Exception('購買數量超過最大可購買數量')
    elif tradeing_type == 'sell':
        url = "http://www.cofool.com/Trade/Stock/sell.html"
        # 賣出操作,從持倉資訊中獲取可賣出數量
        data["orderAmount"] = get_amount(stock_code)
    try:
        content = \
            requests.post(url=url, data=data, cookies=cookie).content.decode("utf-8-sig")
        status = json.loads(content, encoding="unicode_escape")["status"]
    except Exception as e:
        raise Exception('獲取賬戶資料失敗,原因為:{}'.format(e))
    if status == 1:
        print("買入股票:{},股數:{}".format(stock_code, amount))
        return 0
    else:
        raise Exception("買入失敗,失敗原因為:{}".format(json.loads(content, encoding="unicode_escape")["info"]))

持倉數量

在賣出交易的時候需要獲取賬戶中持倉的數量

def get_amount(code):
    """根據股票程式碼獲取可交易數量
    :param code:股票程式碼
    :return:對應股票可用數量
    """
    url = "http://www.cofool.com/Trade/Stock/sellAmount"
    data = {
        "code": code,
        "uid": 'uid',
        "web_id": 'web_id'
    }
    cookie = login('賬戶名', '密碼')
    try:
        content = json.loads(requests.post(url=url, data=data, cookies=cookie).content.decode('unicode_escape'))
    except Exception as e:
        raise Exception("獲取股數資料失敗,{}".format(e))
    status = content['status']
    if status == 1:
        amount = content['info']['frozen_amount']
        return amount
    else:
        raise Exception(content['info'])

到這裡叩富網的爬蟲基本完成了,最後將轉化為類就可以了,完整程式碼可以在我的GitHub倉庫檢視最新的程式碼

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章