程序员三大美德之一:快速有效检索网页数据的“懒惰”程序员指南


全文共9527字,预计学习时长24分钟

图源:unsplash

根据Perl编程语言的作者拉里·沃尔的说法,程序员具有三大美德:懒惰,不耐烦和傲慢。懒惰可以让你全力以赴,降低总能耗,其他人会发现你编写的省力程序很有用。

 

比尔盖茨的观点是:我选择让懒惰的人完成艰巨的任务,因为他可以找到完成任务的捷径。

 

网页抓取或许是一个相当简单的编程问题:在文档的源代码中搜索唯一标识符,提取相关数据,但我认为存在一个更“懒惰”的解决方案——更简单,更快,可以生成更多数据。

 

雅虎财经是财务数据做得最好的网站之一,这也让它成为金融爱好者进行网页抓取的主要目标。几乎每天都有关于StackOverflow的问题,抓取数据的人参考了雅虎财经的某种数据检索(通常是通过网络抓取)。

 

网页抓取问题1

网页抓取者尝试查找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

使用简单的网页抓取解决方案非常简单,但这还不够“懒惰”,让我们看下一个。

 

网页抓取问题2

 

图源:unsplash

网页抓取者正在尝试从统计标签中查找有关股票的企业价值和空头股票数量的数据。他的问题实际上是检索可能存在或不存在的嵌套字典值,但是在检索数据上,他似乎已经找到了更好的解决方法。

 

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行:网页抓取者能够在javascript的变量内找到他要查找的数据:

 

root.App.main = {.... };

在那里,只需访问字典中适当的嵌套键,即可轻松检索数据。但是,确实还有更“懒惰”的办法。

 

“懒惰”的解决方案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数据的API端点。这个方案更简单,可以导出更多数据 ,那么它的速度呢?代码如下:

 

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.5678426799999997lazy #1 min time is 0.11238783999999953web scraping #2 min time is 0.3731000199999997lazy #2 min time is 0.0864451399999993

“懒惰”的替代方案比其网页抓取同类产品快4到5倍!

 

“偷懒”的过程

 

思考一下上面遇到的两个问题:原来的方案里,代码加载到页面后,我们尝试检索数据。“懒惰”的解决方案直接针对数据源,根本不理会前端页面。当你尝试从网站提取数据时,这是一个重要区别和一个很好的方法。

 

步骤1:检查XHR请求

 

XHR(XMLHttpRequest)对象是可用于Web浏览器脚本语言(例如JavaScript)的API,它将HTTP或HTTPS请求发送到Web服务器,并将服务器响应数据加载回脚本中。基本上,XHR允许客户端从URL检索数据,不必刷新整个网页。

 

笔者将使用Chrome进行以下演示,但是其他浏览器也具有类似的功能。

 

·        打开Chrome的开发者控制台。要在Google Chrome中打开开发者控制台,请打开浏览器窗口右上角的Chrome菜单,然后选择更多工具>开发者工具。也可以使用快捷键Option + ⌘+ J(适用于ios系统),或Shift + CTRL + J(适用于Windows / Linux)。

 

·        选择“网络”选项卡。

 

·        然后通过“ XHR”筛选结果

 

 

·        应注意,尽管有些请求包含“ AAPL”,得到的结果将相似但不相同。从调查这些开始,单击最左侧列中包含字符“ AAPL”的链接之一。

 

·        选择其中一个链接后会看到一个附加窗口,其中提供了所选请求的详细信息。第一个选项卡Headers,提供有关浏览器请求和服务器响应的详细信息。你应该立即注意到“Headers”选项卡中的“URL请求”与上面的惰性解决方案中提供的URL请求非常相似。

 

·        如果选择“预览”选项卡,将看到从服务器返回的数据。

 

 

·        好极了!看来我们找到了获取Apple OHLC数据的URL!

 

步骤2:搜寻

 

现在我们已经发现了一些通过浏览器发出的XHR请求。搜索javascript文件,查看是否可以找到更多信息。笔者发现与XHR请求相关的URL共同点是“ query1”和“ query2”。在开发者控制台的右上角,选择三个垂直点,然后在下拉框中选择“搜索”。

 

 

在搜索栏中搜索“ query2”:

 

选择第一个选项。将会弹出一个附加选项卡,其中包含找到“ query2”的位置。应该在这里注意到类似的内容:

 

 

网页抓取解决方案2提取的数据变量与该变量相同。控制台应提供“优质打印”变量的选项。你可以选择该选项,也可以将整行(上面的第11行)复制并粘贴到https://beautifier.io/。或者如果你使用vscode,下载美化扩展,它会做同样的事情。

 

正确格式化后,将整个代码粘贴到文本编辑器或类似的编辑器中,然后再次搜索“ query2”。搜索结果应该在 “ Service Plugin” 中。该部分包含雅虎财经用于在其页面中填充数据的URL。以下是该部分的内容:

 

"tachyon.quoteSummary": {"path": "/v10/finance/quoteSummary/{symbol}","timeout":6000,"query": ["lang", "region","corsDomain", "crumb", "modules",    "formatted"],"responseField":"quoteSummary","get": {"formatted": true}},

以上是“懒惰”的解决方案中使用的URL。

 

图源:unsplash

“懒惰”人类发展的阶梯,适当偷懒,你会进入新世界。


推荐阅读专题

留言点赞发个朋友圈

我们一起分享AI学习与发展的干货

编译组:顾家彤、段筱玥

相关链接:

https://towardsdatascience.com/the-alternative-to-web-scraping-8d530ae705ca

如转载,请后台留言,遵守转载规范

推荐文章阅读

ACL2018论文集50篇解读

EMNLP2017论文集28篇论文解读

2018年AI三大顶会中国学术成果全链接

ACL2017论文集:34篇解读干货全在这里

10篇AAAI2017经典论文回顾

长按识别二维码可添加关注

读芯君爱你

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值