node基金爬蟲,自導自演瞭解一下?

youngdro發表於2018-05-07

那是一個風和日麗的下午,我入手了人生第一把基金,從此以後,這隻雞?就跌入了萬劫不復的深淵,之後我居然還傻傻地追加了幾筆,到現在為止此坑都還沒填平...

“是時候動用一些封印的力量了”,我捂緊又皺又癟的荷包,扛起node大寶劍,從新手村起步,屠龍...哦不,殺雞之旅徐徐展開。

node基金爬蟲,自導自演瞭解一下?

問道

我詢遍了村中姓“網”的長老,終於拿到了3條至關重要的資訊卷軸,有了它們,我便可以有機會一窺雞精國的全貌了。

  • 卷軸1——戶口卷軸fund.eastmoney.com/allfund.htm…

    node基金爬蟲,自導自演瞭解一下?
    這裡列出了每隻雞的程式碼號以及它們聽完令我虎軀一震的名字,不愧是雞精國,點了點居然有7000多號雞口。

  • 卷軸2——檔案卷軸http://fund.eastmoney.com/f10/000001.html

    node基金爬蟲,自導自演瞭解一下?
    此卷軸神奇了,變換地址末尾的程式碼號,就能看到對應的那隻雞的基本檔案,知雞知彼,百戰不殆。

  • 卷軸3——M卷軸fund.eastmoney.com/f10/F10Data…

    node基金爬蟲,自導自演瞭解一下?
    沒想到這卷軸的力量實為霸道,居然還是個動態卷軸,改變地址咒語中的程式碼code、開始日期sdate、截止日期edate和分頁數量per,它就能呈現出這隻雞的生活作息表,是肥了還是瘦了,是開心了還是不開心了...

至此,雞精國江山圖譜我已盡收心中。


修煉

古老的卷軸已經給了我足夠多的線索,而我深知自己在這篇傳說中的主角光環,所以無需結印便召喚出了棲身V8莽林中的小神獸——爬蟲,擁有node血統的它身行動迅猛,嗅覺靈敏,給它一根雞毛,它就能幫我找到雞窩,但要想縱橫整個雞精國,還需對它加以訓練。

首先我得獲取以下裝備,這樣爬蟲和雞和人就都能正常交流了。

const express = require('express'); //搭建服務
const events = require('events'); //事件監聽
const request = require('request'); //傳送請求
const iconv = require('iconv-lite'); //網頁解碼
const cheerio = require('cheerio'); //網頁解析
const MongoClient = require('mongodb').MongoClient; //資料庫
const app = express(); //服務端例項
const Event = new events.EventEmitter(); //事件監聽例項
const dbUrl = "mongodb://localhost:27017/"; //資料庫連線地址
複製程式碼

我給這可愛的小神獸取了個庸俗的名字:FundSpider,給了它一個封裝後的嗅覺增強器fetch

// 基金爬蟲
class FundSpider {
    // 資料庫名,表名,併發片段數量
    constructor(dbName='fund', collectionName='fundData', fragmentSize=1000) {
        this.dbUrl = "mongodb://localhost:27017/";
        this.dbName = dbName;
        this.collectionName = collectionName;
        this.fragmentSize = fragmentSize;
    }
    // 獲取url對應網址內容,除utf-8外,需指定網頁編碼
    fetch(url, coding, callback) {
        request({url: url, encoding : null}, (error, response, body) => {
            let _body = coding==="utf-8" ? body : iconv.decode(body, coding);
            if (!error && response.statusCode === 200){
                // 將請求到的網頁裝載到jquery選擇器中
                callback(null, cheerio.load('<body>'+_body+'</body>'));
            }else{
                callback(error, cheerio.load('<body></body>'));
            }
        });
    }
}
複製程式碼

現在,把每隻雞的程式碼號篩出來:

// 批量獲取所有的基金程式碼
fetchFundCodes(callback) {
    let url = "http://fund.eastmoney.com/allfund.html";
    // 原網頁編碼是gb2312,需對應解碼
    this.fetch(url, 'gb2312', (err, $) => {
        let fundCodesArray = [];
        if(!err){
            $("body").find('.num_right').find("li").each((i, item)=>{
                let codeItem = $(item);
                let codeAndName = $(codeItem.find("a")[0]).text();
                let codeAndNameArr = codeAndName.split(")");
                let code = codeAndNameArr[0].substr(1);
                let fundName = codeAndNameArr[1];
                if(code){
                    fundCodesArray.push(code);
                }
            });
        }
        callback(err, fundCodesArray);
    });
}
複製程式碼

接著,給爬蟲打造件定位追蹤的裝備,根據雞的程式碼就能查到它的檔案:

// 根據基金程式碼獲取對應基本資訊
fetchFundInfo(code, callback){
    let fundUrl = "http://fund.eastmoney.com/f10/" + code + ".html";
    let fundData = {fundCode: code};
    this.fetch(fundUrl,"utf-8", (err, $) => {
        if(!err){
            let dataRow = $("body").find(".detail .box").find("tr");
            fundData.fundName = $($(dataRow[0]).find("td")[0]).text();//基金全稱
            fundData.fundNameShort = $($(dataRow[0]).find("td")[1]).text();//基金簡稱
            fundData.fundType = $($(dataRow[1]).find("td")[1]).text();//基金型別
            fundData.releaseDate = $($(dataRow[2]).find("td")[0]).text();//發行日期
            fundData.buildDate = $($(dataRow[2]).find("td")[1]).text();//成立日期/規模
            fundData.assetScale = $($(dataRow[3]).find("td")[0]).text();//資產規模
            fundData.shareScale = $($(dataRow[3]).find("td")[1]).text();//份額規模
            fundData.administrator = $($(dataRow[4]).find("td")[0]).text();//基金管理人
            fundData.custodian = $($(dataRow[4]).find("td")[1]).text();//基金託管人
            fundData.manager = $($(dataRow[5]).find("td")[0]).text();//基金經理人
            fundData.bonus = $($(dataRow[5]).find("td")[1]).text();//分紅
            fundData.managementRate = $($(dataRow[6]).find("td")[0]).text();//管理費率
            fundData. trusteeshipRate = $($(dataRow[6]).find("td")[1]).text();//託管費率
            fundData.saleServiceRate = $($(dataRow[7]).find("td")[0]).text();//銷售服務費率
            fundData.subscriptionRate = $($(dataRow[7]).find("td")[1]).text();//最高認購費率
        }
        callback(err, fundData);
    });
}
複製程式碼

以上拿到的資訊在雞精國建國之日起就幾乎未曾變動,即使它們建國後都成了精。要是後面我每次想翻看檔案都得召喚爬蟲,讓它重複勞動,伙食費都怕不夠。還好在新手成長禮包中領取到了一份MongoDB寶箱,有自如存取的能力,那便將這些檔案統統儲存起來,後日翻閱便可無患。

在訓練的過程中,爬蟲一出手便是併發地追蹤,我發現一次性把7000多隻雞查個底朝天,總會有大概三分之一的雞下落不明,看來是有些吃不消了,為了控制爬蟲的追蹤節奏,是時候得有新夥伴加入了。

// 併發控制器,控制單次併發呼叫的數量
class ConcurrentCtrl {
    // 呼叫者上下文環境,併發分段數量(建議不要超過1000),呼叫函式,總引數陣列,資料庫表名
    constructor(parent, splitNum, fn, dataArray=[], collection){
        this.parent = parent;
        this.splitNum = splitNum;
        this.fn = fn;
        this.dataArray = dataArray;
        this.length = dataArray.length; // 總次數
        this.itemNum = Math.ceil(this.length/splitNum); // 分段段數
        this.restNum = (this.length%splitNum)===0 ? splitNum : (this.length%splitNum); // 最後一次分段的餘下次數
        this.collection = collection;
    }
    // go(0)啟動呼叫,迴圈計數中達到分段數量便進行下一次片段併發
    go(index) {
        if((index%this.splitNum) === 0){
            if(index/this.splitNum !== (this.itemNum-1)){
                this.fn.call(this.parent, this.collection, this.dataArray.slice(index,index+this.splitNum));
            }else{
                this.fn.call(this.parent, this.collection, this.dataArray.slice(index,index+this.restNum));
            }
        }
    }
}
複製程式碼

有了它的幫助,將爬蟲每次行動的併發量控制在1000左右,會是一個比較理想的節奏;接著,教會爬蟲自動把每次獵取到的雞精檔案放入MongoDB寶箱中,由小至大,先具體告訴爬蟲,每次併發追蹤後應該做什麼。

// 併發獲取的基金資訊片段儲存到資料庫指定的表
fundFragmentSave(collection, codesArray){
    for (let i = 0; i < codesArray.length; i++) {
        this.fetchFundInfo(codesArray[i], (error, fundData) => {
            if(error){
                Event.emit("error_fundItem", codesArray[i]);
                Event.emit("fundItem", codesArray[i]);
            }else{
                // 指定每條資料的唯一標誌是基金程式碼,便於查詢與排序
                fundData["_id"] = fundData.fundCode;
                collection.save(fundData, (err, res) => {
                    Event.emit("correct_fundItem", codesArray[i]);
                    Event.emit("fundItem", codesArray[i]);
                    if (err) throw err;
                });
            }
        });
    }
}
複製程式碼

如此,爬蟲便學會了在追蹤過程中隨時報告情況,每條路線的追蹤結束後都會發出名為fundItem的訊號,出錯或者成功時分別會發出error_fundItemcorrect_fundItem的訊號。

接下來,配合新夥伴ConcurrentCtrl,只要告訴爬蟲要追蹤的程式碼號集合codesArray,捉雞千里之外也不過瞬息之事:

// 併發獲取給定基金程式碼陣列中對應的基金基本資訊,並儲存到資料庫
fundToSave(error, codesArray=[]){
    if(!error){
        let codesLength = codesArray.length;
        let itemNum = 0; // 已爬過的數量
        let errorItems = []; // 爬取失敗的基金程式碼陣列
        let errorItemNum = 0; // 爬取失敗的基金程式碼數量
        let correctItems = []; // 爬取成功的基金程式碼陣列
        let correctItemNum = 0; // 爬取成功的基金程式碼數量
        console.log(`基金程式碼共計 ${codesLength} 個`);
        // 資料庫連線
        MongoClient.connect(this.dbUrl,  (err, db) => {
            if (err) throw err;
            // 資料庫例項
            let fundDB = db.db(this.dbName);
            // 資料表例項
            let dbCollection = fundDB.collection(this.collectionName);
            // 併發控制器例項
            let concurrentCtrl = new ConcurrentCtrl(this, this.fragmentSize, this.fundFragmentSave, codesArray, dbCollection);
            // 事件監聽
            Event.on("fundItem", (_code) => {
                // 計數
                itemNum++;
                console.log(`index: ${itemNum} --- code: ${_code}`);
                // 併發控制
                concurrentCtrl.go(itemNum);
                // 所有基金資訊爬取完畢
                if (itemNum >= codesLength) {
                    console.log("save finished");
                    if(errorItems.length > 0){
                        console.log("---error code----");
                        console.log(errorItems);
                    }
                    // 關閉資料庫
                    db.close();
                }
            });
            Event.on("error_fundItem", (_code) => {
                errorItems.push(_code);
                errorItemNum++;
                console.log(`error index: ${errorItemNum} --- error code: ${_code}`);
            });
            Event.on("correct_fundItem", (_code) => {
                correctItemNum++;
            });
            // 片段式併發啟動
            concurrentCtrl.go(0);
        });
    }else{
        console.log("fundToSave error");
    }
}
複製程式碼

那麼,捉雞大法便算是修煉成了,巨集可縱覽雞精全國戶口檔案,微可輕取數只殺之於無形:

// 未傳參則獲取所有基金基本資訊,給定基金程式碼陣列則獲取對應資訊,均更新到資料庫
fundSave(_codesArray){
    if(!_codesArray){
        // 所有基金資訊爬取儲存
        this.fetchFundCodes((err, codesArray) => {
            this.fundToSave(err, codesArray);
        })
    }else{
        // 過濾可能的非陣列入參的情況
        _codesArray = Object.prototype.toString.call(_codesArray)==='[object Array]' ? _codesArray : [];
        if(_codesArray.length > 0){
            // 部分基金資訊爬取儲存
            this.fundToSave(null, _codesArray);
        }else{
            console.log("not enough codes to fetch");
        }
    }
}
複製程式碼

那怎麼發動呢?咒語如下,不過別忘了把MongoDB寶箱的蓋子開啟。

let fundSpider = new FundSpider("fund","fundData",1000);
// 更新儲存全部基金基本資訊
fundSpider.fundSave();
// 更新儲存程式碼為000001和040008的基金的基本資訊
// fundSpider.fundSave(['000001','040008']);
複製程式碼

去吧,皮卡蟲!我看著爬蟲分出1000個幻影,然後嗖一聲同時消失。當我默唸10秒後,開啟MongoDB寶箱,便見到了如下光景:

node基金爬蟲,自導自演瞭解一下?

我仰天大笑,終於讓我知道了你們這些雞所有的底細!啊哈哈哈!

誒等等,就算我知道了每隻雞的一家老小、背景如何、房產幾套,可天下的雞是殺不完了,雞精更是如此,我要這鐵棒有何用?我要這檔案又如何?( ˙-˙ ) 還是不安,還是氐惆...

我需要的是:定向殺雞

差點忘了還有第三個動態卷軸:M卷軸,藉助它的力量,便能知道任何一隻雞吃沒吃飽、胖了還是瘦了,好不好逮。看來爬蟲需要再加點技能了。

// 日期轉字串
getDateStr(dd){
    let y = dd.getFullYear();
    let m = (dd.getMonth()+1)<10 ? "0"+(dd.getMonth()+1) : (dd.getMonth()+1);
    let d = dd.getDate()<10 ? "0"+dd.getDate() : dd.getDate();
    return y + "-" + m + "-" + d;
}
// 爬取並解析基金的單位淨值,增長率等資訊
fetchFundUrl(url, callback){
    this.fetch(url, 'gb2312', (err, $)=>{
        let fundData = [];
        if(!err){
            let table = $('body').find("table");
            let tbody = table.find("tbody");
            try{
                tbody.find("tr").each((i,trItem)=>{
                    let fundItem = {};
                    let tdArray = $(trItem).find("td").map((j, tdItem)=>{
                        return $(tdItem);
                    });
                    fundItem.date = tdArray[0].text(); // 淨值日期
                    fundItem.unitNet = tdArray[1].text(); // 單位淨值
                    fundItem.accumulatedNet = tdArray[2].text(); // 累計淨值
                    fundItem.changePercent  = tdArray[3].text(); // 日增長率
                    fundData.push(fundItem);
                });
                callback(err, fundData);
            }catch(e){
                console.log(e);
                callback(e, []);
            }
        }
    });
}
// 根據基金程式碼獲取其選定日期範圍內的基金變動資料
// 基金程式碼,開始日期,截止日期,資料個數,回撥函式
fetchFundData(code, sdate, edate, per=9999, callback){
    let fundUrl = "http://fund.eastmoney.com/f10/F10DataApi.aspx?type=lsjz";
    let date = new Date();
    let dateNow = new Date();
    // 預設開始時間為當前日期的3年前
    sdate = sdate?sdate:this.getDateStr(new Date(date.setFullYear(date.getFullYear()-3)));
    edate = edate?edate:this.getDateStr(dateNow);
    fundUrl += ("&code="+code+"&sdate="+sdate+"&edate="+edate+"&per="+per);
    console.log(fundUrl);
    this.fetchFundUrl(fundUrl, callback);
}
複製程式碼

使用如下:

let fundSpider = new FundSpider();
fundSpider.fetchFundData('040008', '2018-03-20', '2018-05-04', 30, (err, data) => {
    console.log(data);
});
複製程式碼

node基金爬蟲,自導自演瞭解一下?

修煉之路,厚積而薄發,我將洞察到的我所需要的關於雞精國的一切,濃縮到了3顆永恆寶石上:

// 所有基金程式碼查詢介面
app.get('/fetchFundCodes', (req, res) => {
    let fundSpider = new FundSpider();
    res.header("Access-Control-Allow-Origin", "*");
    fundSpider.fetchFundCodes((err, data)=>{
        res.send(data.toString());
	});
});
// 根據程式碼查詢基金檔案介面
app.get('/fetchFundInfo/:code', (req, res) => {
    let fundSpider = new FundSpider();
    res.header("Access-Control-Allow-Origin", "*");
    fundSpider.fetchFundInfo(req.params.code, (err, data) => {
        res.send(JSON.stringify(data));
    });
});
// 基金淨值變動情況資料介面
app.get('/fetchFundData/:code/:per', (req, res) => {
    let fundSpider = new FundSpider();
    res.header("Access-Control-Allow-Origin", "*");
    fundSpider.fetchFundData(req.params.code, undefined, undefined, req.params.per, (err, data) => {
        res.send(JSON.stringify(data));
    });
});
app.listen(1234,()=>{
	console.log("service start on port 1234");
});

複製程式碼

決鬥

我來到了雞精國的城池下,node大寶劍剛嵌上的寶石在陽光的照射下熠熠生輝。我劍指城門,大聲喝到:

“你們所有,哦不,你們部分雞的死期到了!”

雞精國護城將出現在了城頭,他瞥見我劍上的寶石,卻冷冷的說到:

“哼,你能洞察到的,不過是那些冷冰冰的資料罷了, 就算將100雞放在你面前,即使拔光了毛,給你一個時辰,就憑那些數字,我想你也無法找到你想要的吧!”

沒想到這護城將真說到做到,他開啟了城門,任由100只雞站在我十米開外,面無懼色。

嘈雜的雞鳴聲令我有些慌亂,果真如他所說,我看著這些幾乎一毛一樣的雞,額頭的汗水開始滴滴墜落,可舉到半空的劍卻遲遲不敢落下。

“路過此地,見你有難,贈予你一件寶物,可助一臂之力。”

身邊突然有一股渾厚的聲音響起,原來是一位長者,我半信半疑接過此物,一個表面無比光滑的銀色薄片,什麼?這竟然是一片資料二向箔!可以將混雜的數字打擊到二維圖表上的二向箔!如此神器令我喜出望外。

“請問長者尊姓大名!”

伊查爾斯 ~”

聲未消失,人卻遠去。

我將二向箔小心地丟向城門正中央,瞬間安靜如斯,護城將錯愕的眼神凝固在了原地,而其他雞精們,都如同紙片般,平鋪在了城牆上。

// 基金資料視覺化(前端程式碼)
const React = require("react");
const Echarts = require("echarts");
const EcStat = require("echarts-stat");
const fetch = require("isomorphic-unfetch");
class FundChart extends React.Component{
    constructor(props) {
        super(props);
        // 按鈕切換標誌
        this.state = {
            switchIndex: 1
        }
    }
    // 獲取基金檔案
    fetchFundInfo(code, callback) {
        return fetch(`http://localhost:1234/fetchFundInfo/${code}`).then((res) => {
            res.json().then((data) => {
                callback(data);
            })
        }).catch((err) => {
            console.log(err);
        });
    }
    // 獲取基金淨值變動資料
    fetchFundData(code, per, callback) {
        return fetch(`http://localhost:1234/fetchFundData/${code}/${per.toString()}`).then((res) => {
            res.text().then((data) => {
                callback(JSON.parse(data));
            })
        }).catch((err) => {
            console.log(err);
        });
    }
    // 獲取ECharts繪製的資料
    getChart(fundData) {
        // 起始點淨值
        let startUnitNet = parseFloat(fundData[0].unitNet);
        // 計算其他時間點淨值與起始點淨值的相對百分比
        // 日期為橫座標,淨值為縱座標
        let data = fundData.map(function(item) {
            return [item.date, parseFloat((100.0 * ((parseFloat(item.unitNet) - startUnitNet) / startUnitNet)).toFixed(2))]
        });
        // 取陣列下標為橫座標,淨值為縱座標,用於散點圖與迴歸分析
        let dataRegression = data.map(function(item, i) {
            return [i, item[1]];
        });
        // 折線圖橫座標陣列
        let dateList = data.map(function(item) {
            return item[0];
        });
        // 折線圖縱座標陣列
        let valueList = data.map(function(item) {
            return item[1];
        });
        // 計算線性迴歸
        let myRegression = EcStat.regression('linear', dataRegression);
        // 線性迴歸的的散點排序
        myRegression.points.sort(function(a, b) {
            return a[0] - b[0];
        });
        // 線性迴歸後的擬合方程y=Kx+B
        let K = myRegression.parameter.gradient;
        let B = myRegression.parameter.intercept;
        let optionFold = {
            title: [{
                left: 'center',
            }],
            tooltip: {
                trigger: 'axis'
            },
            xAxis: [{
                data: dateList
            }],
            yAxis: [{
                splitLine: {
                    show: false
                }
            }],
            series: [{
                type: 'line',
                showSymbol: false,
                data: valueList,
                itemStyle: {
                    color: '#3385ff'
                }
            }]
        };
        let optionRegression = {
            title: {
                subtext: 'linear regression',
                left: 'center'
            },
            tooltip: {
                trigger: 'axis',
                axisPointer: {
                    type: 'cross'
                }
            },
            xAxis: {
                type: 'value',
                splitLine: {
                    lineStyle: {
                        type: 'dashed'
                    }
                },
            },
            yAxis: {
                type: 'value',
                splitLine: {
                    lineStyle: {
                        type: 'dashed'
                    }
                },
            },
            series: [{
                name: 'scatter',
                type: 'scatter',
                itemStyle: {
                    color: '#3385ff'
                },
                label: {
                    emphasis: {
                        show: true,
                        position: 'left'
                    }
                },
                data: dataRegression
            }, {
                name: 'line',
                type: 'line',
                showSymbol: false,
                data: myRegression.points,
                markPoint: {
                    itemStyle: {
                        normal: {
                            color: 'transparent'
                        }
                    },
                    label: {
                        normal: {
                            show: true,
                            position: 'left',
                            formatter: myRegression.expression,
                            textStyle: {
                                color: '#333',
                                fontSize: 14
                            }
                        }
                    },
                    data: [{
                        coord: myRegression.points[myRegression.points.length - 1]
                    }]
                }
            }]
        };
        return {
            optionFold: optionFold,
            optionRegression: optionRegression,
            regression: myRegression,
            K: K,
            B: B
        }
    }
    // 繪製圖表
    drawChart(fundData, fundInfo) {
        if (!this.chartFold) {
            this.chartFold = Echarts.init(document.getElementById('chart_fold'));
        }
        if (!this.chartPoints) {
            this.chartPoints = Echarts.init(document.getElementById('chart_points'));
        }
        if (fundData && (fundData.length > 0)) {
            // 更新圖表繪製
            let chartObj = this.getChart(fundData);
            this.chartFold.setOption(chartObj.optionFold);
            this.chartPoints.setOption(chartObj.optionRegression);
        } else {
            // 更新圖表標題
            this.chartFold.setOption({
                title: {
                    text: fundInfo.fundNameShort
                }
            });
            this.chartPoints.setOption({
                title: {
                    text: fundInfo.fundNameShort
                }
            });
        }
    }
    // 時間範圍按鈕切換
    dateSwitch(index, per) {
        this.setState({
            switchIndex: index
        }, () => {
            this.fetchFundData(this.props.code, per, (data) => {
                this.drawChart(data.reverse());
            });
        });
    }
    // 時間範圍按鈕
    getSwitchBtns() {
        let switchArray = [
            ['最近一週', 7],
            ['最近一月', 30],
            ['最近3月', 90],
            ['最近半年', 180],
            ['最近一年', 365],
            ['最近三年', 1095]
        ];
        let switchIndex = this.state.switchIndex;
        return (
            <div>
                {switchArray.map((item, i)=>{
                    let active = (i==switchIndex ? true : false);
                    let label = item[0];
                    let per = item[1];
                    return (<button className={"switch-btn"+(active?" active":"")} onClick={this.dateSwitch.bind(this,i,per)}>{label}</button>)
                })}
            </div>
        )
    }
    componentDidMount() {
        // 預設載入最近一月的基金資料
        this.fetchFundData(this.props.code, 30, (data) => {
            this.drawChart(data.reverse());
        });
        // 基金標題獲取
        this.fetchFundInfo(this.props.code, (data) => {
            console.log(data);
            this.drawChart([], data);
        });
    }
    render() {
        return (
            <div className="fundChart-container">
                <div id="chartbox" className="chart-box">
                    <div className="chart-fold" id="chart_fold"></div>
                    <div className="chart-points" id="chart_points"></div>
                </div>
                <div className="switch-box">
                    {this.getSwitchBtns()}
                </div>
            </div>
        );
    }
    
}
複製程式碼

node基金爬蟲,自導自演瞭解一下?

node基金爬蟲,自導自演瞭解一下?

node基金爬蟲,自導自演瞭解一下?

“買低不買高,抄底要抄好!”

我一邊大喊著口訣,一邊揮舞著大寶劍,不少雞已被我削成了碎末,彌散在空氣裡。

我目光如龍,當敵人是空,我戰法無窮,我攻勢如風,用寶劍入宮。

我終究還是被攔下了,對面是雞精國的一員悍將,一身法力渾厚凶猛,竟令我節節敗退。

我捂著胸口,強忍著彷彿要從胃裡噴湧而出的血腥味:

“敢...敢問閣下名號?”

“吾乃雞精國大祭師,古皮襖!”

居然是古皮襖!那個傳說中一直罩著雞精國的大祭師古皮襖!據說雞精國國王名存實亡,是古皮襖壟斷大權,他是掌握著命運之力的天才,是舉國上下的風向標!

“世界上有很多東西,你是參不透的”

古皮襖輕蔑地說道。

“你早已經不是第一個死在我手裡的闖入者了,但因為我的悲憫之心,為了紀念你們,我給你們都起了一個稱謂。”

“什麼稱謂...?”

我已經是很勉強地支撐著身體了,但這股好奇心還是讓我忍不住開口問道。

“韭菜”

他話音剛落,便揮起鐮刀近身過來,在我的世界全部寂靜之前,我只能看到他臉上冷漠的微笑。

node基金爬蟲,自導自演瞭解一下?

初入掘金第二篇文章,寫著寫著發現自己編起了段子...感覺標題應該改為“韭菜傳”?總之,瞎編不易,轉載煩請註明出處,銘謝~
複製程式碼
-----------優柔寡斷的分割線---------
複製程式碼

鑑於有評論裡少俠中意原始碼,雙手奉上我稚嫩的github地址:https://github.com/youngdro/fundSpider,少俠們有空可否順便戳一戳那顆buling buling的星星✨,後續我慢慢把其他庫存貨往這上面挪吧(我怕是一個假程式設計師...)

相關文章