程序員三大美德之一:快速有效檢索網(wǎng)頁數(shù)據(jù)的“懶惰”程序員指南
根據(jù)Perl編程語言的作者拉里·沃爾的說法,程序員具有三大美德:懶惰,不耐煩和傲慢。懶惰可以讓你全力以赴,降低總能耗,其他人會發(fā)現(xiàn)你編寫的省力程序很有用。
比爾蓋茨的觀點是:我選擇讓懶惰的人完成艱巨的任務,因為他可以找到完成任務的捷徑。
網(wǎng)頁抓取或許是一個相當簡單的編程問題:在文檔的源代碼中搜索唯一標識符,提取相關(guān)數(shù)據(jù),但我認為存在一個更“懶惰”的解決方案——更簡單,更快,可以生成更多數(shù)據(jù)。
雅虎財經(jīng)是財務數(shù)據(jù)做得最好的網(wǎng)站之一,這也讓它成為金融愛好者進行網(wǎng)頁抓取的主要目標。幾乎每天都有關(guān)于StackOverflow的問題,抓取數(shù)據(jù)的人參考了雅虎財經(jīng)的某種數(shù)據(jù)檢索(通常是通過網(wǎng)絡抓取)。
網(wǎng)頁抓取問題1
網(wǎng)頁抓取者嘗試查找Facebook當前的股票價格。代碼如下:
- import requests
- from bs4 importBeautifulSoup
- defparsePrice():
- r = requests.get("https://finance.yahoo.com/quote/FB?p=FB")
- soup =BeautifulSoup(r.text, "lxml")
- price = soup.find( div , { class : My(6px) Pos(r)smartphone_Mt(6px) }).find( span ).text
- print(f the current price: {price} )
該代碼輸出如下:
- the current price: 216.08
使用簡單的網(wǎng)頁抓取解決方案非常簡單,但這還不夠“懶惰”,讓我們看下一個。
網(wǎng)頁抓取問題2
網(wǎng)頁抓取者正在嘗試從統(tǒng)計標簽中查找有關(guān)股票的企業(yè)價值和空頭股票數(shù)量的數(shù)據(jù)。他的問題實際上是檢索可能存在或不存在的嵌套字典值,但是在檢索數(shù)據(jù)上,他似乎已經(jīng)找到了更好的解決方法。
- import requests, re, json, pprint
- p = re.compile(r root.App.main =(.*); )
- tickers = [ AGL.AX ]
- results = {}
- with requests.Session() as s:
- for ticker in tickers:
- r = s.get( https://finance.yahoo.com/quote/{}/key-statistics?p={} .format(ticker,ticker))
- data = json.loads(p.findall(r.text)[0])
- key_stats = data[ context ][ dispatcher ][ stores ][ QuoteSummaryStore ]
- print(key_stats)
- res = {
- Enterprise Value : key_stats[ defaultKeyStatistics ][ enterpriseValue ][ fmt ]
- , Shares_Short : key_stats[ defaultKeyStatistics ][ sharesShort ].get( longFmt , N/A )
- }
- results[ticker] = res
- print(results)
看第3行:網(wǎng)頁抓取者能夠在javascript的變量內(nèi)找到他要查找的數(shù)據(jù):
- root.App.main = {.... };
在那里,只需訪問字典中適當?shù)那短祖I,即可輕松檢索數(shù)據(jù)。但是,確實還有更“懶惰”的辦法。
“懶惰”的解決方案1
- import requests
- r = requests.get("https://query2.finance.yahoo.com/v10/finance/quoteSummary/FB?modules=price")
- data = r.json()
- print(data)
- print(f"the currentprice: {data[ quoteSummary ][ result ][0][ price ][ regularMarketPrice ][ raw ]}")
看看第三行的URL,輸出如下:
- {
- quoteSummary : {
- error : None,
- result : [{
- price : {
- averageDailyVolume10Day : {},
- averageDailyVolume3Month : {},
- circulatingSupply : {},
- currency : USD ,
- currencySymbol : $ ,
- exchange : NMS ,
- exchangeDataDelayedBy :0,
- exchangeName : NasdaqGS ,
- fromCurrency : None,
- lastMarket : None,
- longName : Facebook,Inc. ,
- marketCap : {
- fmt : 698.42B ,
- longFmt : 698,423,836,672.00 ,
- raw : 698423836672
- },
- marketState : REGULAR ,
- maxAge : 1,
- openInterest : {},
- postMarketChange : {},
- postMarketPrice : {},
- preMarketChange : {
- fmt : -0.90 ,
- raw : -0.899994
- },
- preMarketChangePercent :{
- fmt : -0.37% ,
- raw : -0.00368096
- },
- preMarketPrice : {
- fmt : 243.60 ,
- raw : 243.6
- },
- preMarketSource : FREE_REALTIME ,
- preMarketTime :1594387780,
- priceHint : {
- fmt : 2 ,
- longFmt : 2 ,
- raw : 2
- },
- quoteSourceName : Nasdaq Real Time
- Price ,
- quoteType : EQUITY ,
- regularMarketChange : {
- fmt : 0.30 ,
- raw : 0.30160522
- },
- regularMarketChangePercent : {
- fmt : 0.12% ,
- raw : 0.0012335592
- },
- regularMarketDayHigh : {
- fmt : 245.49 ,
- raw : 245.49
- },
- regularMarketDayLow : {
- fmt : 239.32 ,
- raw : 239.32
- },
- regularMarketOpen : {
- fmt : 243.68 ,
- raw : 243.685
- },
- regularMarketPreviousClose : {
- fmt : 244.50 ,
- raw : 244.5
- },
- regularMarketPrice : {
- fmt : 244.80 ,
- raw : 244.8016
- },
- regularMarketSource : FREE_REALTIME ,
- regularMarketTime :1594410026,
- regularMarketVolume : {
- fmt : 19.46M ,
- longFmt : 19,456,621.00 ,
- raw : 19456621
- },
- shortName : Facebook,Inc. ,
- strikePrice : {},
- symbol : FB ,
- toCurrency : None,
- underlyingSymbol : None,
- volume24Hr : {},
- volumeAllCurrencies : {}
- }
- }]
- }
- }the current price: 241.63
“懶惰”的解決方案2
- import requests
- r = requests.get("https://query2.finance.yahoo.com/v10/finance/quoteSummary/AGL.AX?modules=defaultKeyStatistics")
- data = r.json()
- print(data)
- print({
- AGL.AX : {
- Enterprise Value : data[ quoteSummary ][ result ][0][ defaultKeyStatistics ][ enterpriseValue ][ fmt ],
- Shares Short : data[ quoteSummary ][ result ][0][ defaultKeyStatistics ][ sharesShort ].get( longFmt , N/A )
- }
- })
再次看一下第三行的URL,輸出如下:
- {
- quoteSummary : {
- result : [{
- defaultKeyStatistics : {
- maxAge : 1,
- priceHint : {
- raw : 2,
- fmt : 2 ,
- longFmt : 2
- },
- enterpriseValue : {
- raw : 13677747200,
- fmt : 13.68B ,
- longFmt : 13,677,747,200
- },
- forwardPE : {},
- profitMargins : {
- raw : 0.07095,
- fmt : 7.10%
- },
- floatShares : {
- raw : 637754149,
- fmt : 637.75M ,
- longFmt : 637,754,149
- },
- sharesOutstanding : {
- raw : 639003008,
- fmt : 639M ,
- longFmt : 639,003,008
- },
- sharesShort : {},
- sharesShortPriorMonth :{},
- sharesShortPreviousMonthDate :{},
- dateShortInterest : {},
- sharesPercentSharesOut : {},
- heldPercentInsiders : {
- raw : 0.0025499999,
- fmt : 0.25%
- },
- heldPercentInstitutions : {
- raw : 0.31033,
- fmt : 31.03%
- },
- shortRatio : {},
- shortPercentOfFloat :{},
- beta : {
- raw : 0.365116,
- fmt : 0.37
- },
- morningStarOverallRating :{},
- morningStarRiskRating :{},
- category : None,
- bookValue : {
- raw : 12.551,
- fmt : 12.55
- },
- priceToBook : {
- raw : 1.3457094,
- fmt : 1.35
- },
- annualReportExpenseRatio : {},
- ytdReturn : {},
- beta3Year : {},
- totalAssets : {},
- yield : {},
- fundFamily : None,
- fundInceptionDate : {},
- legalType : None,
- threeYearAverageReturn :{},
- fiveYearAverageReturn :{},
- priceToSalesTrailing12Months :{},
- lastFiscalYearEnd : {
- raw : 1561852800,
- fmt : 2019-06-30
- },
- nextFiscalYearEnd : {
- raw : 1625011200,
- fmt : 2021-06-30
- },
- mostRecentQuarter : {
- raw : 1577750400,
- fmt : 2019-12-31
- },
- earningsQuarterlyGrowth : {
- raw : 0.114,
- fmt : 11.40%
- },
- revenueQuarterlyGrowth :{},
- netIncomeToCommon : {
- raw : 938000000,
- fmt : 938M ,
- longFmt : 938,000,000
- },
- trailingEps : {
- raw : 1.434,
- fmt : 1.43
- },
- forwardEps : {},
- pegRatio : {},
- lastSplitFactor : None,
- lastSplitDate : {},
- enterpriseToRevenue : {
- raw : 1.035,
- fmt : 1.03
- },
- enterpriseToEbitda : {
- raw : 6.701,
- fmt : 6.70
- },
- 52WeekChange : {
- raw : -0.17621362,
- fmt : -17.62%
- },
- SandP52WeekChange : {
- raw : 0.045882702,
- fmt : 4.59%
- },
- lastDividendValue : {},
- lastCapGain : {},
- annualHoldingsTurnover :{}
- }
- }],
- error : None
- }
- }{ AGL.AX : { Enterprise Value : 13.73B , Shares Short : N/A }}
“懶惰”的解決方案只是簡單地將請求從使用前端URL更改為某種非官方的返回JSON數(shù)據(jù)的API端點。這個方案更簡單,可以導出更多數(shù)據(jù) ,那么它的速度呢?代碼如下:
- import timeit
- import requests
- from bs4 importBeautifulSoup
- import json
- import re
- repeat =5
- number =5
- defweb_scrape_1():
- r = requests.get(f https://finance.yahoo.com/quote/FB?p=FB )
- soup =BeautifulSoup(r.text, "lxml")
- price = soup.find( div , { class : My(6px) Pos(r)smartphone_Mt(6px) }).find( span ).text
- returnf the current price: {price}
- deflazy_1():
- r = requests.get( https://query2.finance.yahoo.com/v10/finance/quoteSummary/FB?modules=price )
- data = r.json()
- returnf"the currentprice: {data[ quoteSummary ][ result ][0][ price ][ regularMarketPrice ][ raw ]}"
- defweb_scrape_2():
- p = re.compile(r root.App.main = (.*); )
- ticker = AGL.AX
- results = {}
- with requests.Session() as s:
- r = s.get( https://finance.yahoo.com/quote/{}/key-statistics?p={} .format(ticker,ticker))
- data = json.loads(p.findall(r.text)[0])
- key_stats = data[ context ][ dispatcher ][ stores ][ QuoteSummaryStore ]
- res = {
- Enterprise Value : key_stats[ defaultKeyStatistics ][ enterpriseValue ][ fmt ],
- Shares Short : key_stats[ defaultKeyStatistics ][ sharesShort ].get( longFmt , N/A )
- }
- results[ticker] = res
- return results
- deflazy_2():
- r = requests.get( https://query2.finance.yahoo.com/v10/finance/quoteSummary/AGL.AX?modules=defaultKeyStatistics )
- data = r.json()
- return {
- AGL.AX : {
- Enterprise Value : data[ quoteSummary ][ result ][0][ defaultKeyStatistics ][ enterpriseValue ][ fmt ],
- Shares Short : data[ quoteSummary ][ result ][0][ defaultKeyStatistics ][ sharesShort ].get( longFmt , N/A )
- }
- }
- web_scraping_1_times = timeit.repeat(
- web_scrape_1() ,
- setup= import requests; from bs4 import BeautifulSoup ,
- globals=globals(),
- repeat=repeat,
- number=number)
- print(f web scraping #1min time is {min(web_scraping_1_times) / number} )
- lazy_1_times = timeit.repeat(
- lazy_1() ,
- setup= import requests ,
- globals=globals(),
- repeat=repeat,
- number=number
- )
- print(f lazy #1 min timeis {min(lazy_1_times) / number} )
- web_scraping_2_times = timeit.repeat(
- web_scrape_2() ,
- setup= import requests, re, json ,
- globals=globals(),
- repeat=repeat,
- number=number)
- print(f web scraping #2min time is {min(web_scraping_2_times) / number} )
- lazy_2_times = timeit.repeat(
- lazy_2() ,
- setup= import requests ,
- globals=globals(),
- repeat=repeat,
- number=number
- )
- print(f lazy #2 min timeis {min(lazy_2_times) / number} )
- web scraping #1 min time is 0.5678426799999997
- lazy #1 min time is 0.11238783999999953
- web scraping #2 min time is 0.3731000199999997
- lazy #2 min time is 0.0864451399999993
“懶惰”的替代方案比其網(wǎng)頁抓取同類產(chǎn)品快4到5倍!
“偷懶”的過程
思考一下上面遇到的兩個問題:原來的方案里,代碼加載到頁面后,我們嘗試檢索數(shù)據(jù)。“懶惰”的解決方案直接針對數(shù)據(jù)源,根本不理會前端頁面。當你嘗試從網(wǎng)站提取數(shù)據(jù)時,這是一個重要區(qū)別和一個很好的方法。
步驟1:檢查XHR請求
XHR(XMLHttpRequest)對象是可用于Web瀏覽器腳本語言(例如JavaScript)的API,它將HTTP或HTTPS請求發(fā)送到Web服務器,并將服務器響應數(shù)據(jù)加載回腳本中?;旧希琗HR允許客戶端從URL檢索數(shù)據(jù),不必刷新整個網(wǎng)頁。
筆者將使用Chrome進行以下演示,但是其他瀏覽器也具有類似的功能。
· 打開Chrome的開發(fā)者控制臺。要在Google Chrome中打開開發(fā)者控制臺,請打開瀏覽器窗口右上角的Chrome菜單,然后選擇更多工具>開發(fā)者工具。也可以使用快捷鍵Option + ?+ J(適用于ios系統(tǒng)),或Shift + CTRL + J(適用于Windows / Linux)。
- 選擇“網(wǎng)絡”選項卡。
應注意,盡管有些請求包含“ AAPL”,得到的結(jié)果將相似但不相同。從調(diào)查這些開始,單擊最左側(cè)列中包含字符“ AAPL”的鏈接之一。
- 選擇其中一個鏈接后會看到一個附加窗口,其中提供了所選請求的詳細信息。第一個選項卡Headers,提供有關(guān)瀏覽器請求和服務器響應的詳細信息。你應該立即注意到“Headers”選項卡中的“URL請求”與上面的惰性解決方案中提供的URL請求非常相似。
如果選擇“預覽”選項卡,將看到從服務器返回的數(shù)據(jù)。
好極了!看來我們找到了獲取Apple OHLC數(shù)據(jù)的URL!
步驟2:搜尋
現(xiàn)在我們已經(jīng)發(fā)現(xiàn)了一些通過瀏覽器發(fā)出的XHR請求。搜索javascript文件,查看是否可以找到更多信息。筆者發(fā)現(xiàn)與XHR請求相關(guān)的URL共同點是“ query1”和“ query2”。在開發(fā)者控制臺的右上角,選擇三個垂直點,然后在下拉框中選擇“搜索”。
在搜索欄中搜索“ query2”:
選擇第一個選項。將會彈出一個附加選項卡,其中包含找到“ query2”的位置。應該在這里注意到類似的內(nèi)容:
網(wǎng)頁抓取解決方案2提取的數(shù)據(jù)變量與該變量相同??刂婆_應提供“優(yōu)質(zhì)打印”變量的選項。你可以選擇該選項,也可以將整行(上面的第11行)復制并粘貼到https://beautifier.io/。或者如果你使用vscode,下載美化擴展,它會做同樣的事情。
正確格式化后,將整個代碼粘貼到文本編輯器或類似的編輯器中,然后再次搜索“ query2”。搜索結(jié)果應該在 “ Service Plugin” 中。該部分包含雅虎財經(jīng)用于在其頁面中填充數(shù)據(jù)的URL。以下是該部分的內(nèi)容:
- "tachyon.quoteSummary": {
- "path": "/v10/finance/quoteSummary/{symbol}",
- "timeout":6000,
- "query": ["lang", "region","corsDomain", "crumb", "modules", "formatted"],
- "responseField":"quoteSummary",
- "get": {"formatted": true}
- },
以上是“懶惰”的解決方案中使用的URL。
“懶惰”人類發(fā)展的階梯,適當偷懶,你會進入新世界。
本文轉(zhuǎn)載自微信公眾號「讀芯術(shù)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系讀芯術(shù)公眾號。