如何使用Lightrun在生產(chǎn)環(huán)境中調(diào)試jsoup Java代碼
譯文譯者 | 李睿
審校 | 孫淑娟
網(wǎng)站抓取(Scraping)是一門安全性比較薄弱的學(xué)科。人們經(jīng)常使用服務(wù)器來解決,而調(diào)試和解決這些問題非常困難,至少現(xiàn)在是這樣。
抓取采用現(xiàn)代瀏覽器構(gòu)建的網(wǎng)站比十年前更具挑戰(zhàn)性。jsoup是一個(gè)方便的API,它通過DOM遍歷、CSS選擇器、類似JQuery的方法等使抓取網(wǎng)站變得簡單。但這并非沒有挑戰(zhàn),因?yàn)槊總€(gè)抓取的API都可能是一顆定時(shí)炸彈。
現(xiàn)實(shí)世界的HTML是脆弱的。因?yàn)樗皇且粋€(gè)文檔化的API,所以會(huì)在沒有通知的情況下進(jìn)行更改。當(dāng)Java程序在抓取方面失敗時(shí),可能就面臨更多的麻煩。在某些情況下,這是一個(gè)簡單的問題,可以在本地復(fù)制并部署。但在本地測(cè)試用例中,DOM樹中的一些細(xì)微變化可能更難觀察到。在這些情況下,需要在推動(dòng)更新之前了解解析樹中的問題。否則,開發(fā)的軟件產(chǎn)品可能會(huì)損壞。
什么是jsoup?JavaHTML解析器
在深入了解調(diào)試jsoup的具體細(xì)節(jié)之前,先回答上面的問題,并討論jsoup背后的核心概念。
jsoup網(wǎng)站將其定義為:jsoup是一個(gè)用于處理真實(shí)世界HTML的Java庫。它使用HTML5 DOM方法和CSS選擇器提供了一個(gè)非常方便的API,用于獲取URL以及提取和操作數(shù)據(jù)。
jsoup實(shí)現(xiàn)了WHATWG HTML5規(guī)范,并將HTML解析為與現(xiàn)代瀏覽器相同的DOM。
考慮到這一點(diǎn),可以直接從同一個(gè)網(wǎng)站上獲取一個(gè)簡單的示例:
Java
1 Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
2 log(doc.title());
3 Elements newsHeadlines = doc.select("#mp-itn b a");
4 for (Element headline : newsHeadlines) {
5 log("%s\n\t%s",
6 headline.attr("title"), headline.absUrl("href"));
7 }
這段代碼片段摘自維基百科的標(biāo)題。在上面的代碼中,可以看到幾個(gè)有趣的特性:
- 與URL的連接實(shí)際上是無縫的——只需將字符串URL傳遞給connect方法。
- 某些子元素有特殊情況。例如。Title被公開為一個(gè)簡單的方法,它返回一個(gè)字符串而不從DOM樹中選擇。
- 可以使用非常復(fù)雜的選擇器語法來選擇條目。
簡單的jsoup測(cè)試
為了演示調(diào)試,創(chuàng)建了一個(gè)簡單的演示。
XML
1 <dependency>
2 <groupId>org.jsoup</groupId>
3 <artifactId>jsoup</artifactId>
4 <version>1.14.3</version>
5 </dependency>
可以使用以下Maven依賴項(xiàng)將jsoup安裝到任何Java程序中。Maven將無縫下載jsoupjar:
Java
1 public Set<String> listLinks(String url, boolean includeMedia) throws IOException {
2 Document doc = Jsoup.connect(url).get();
3 Elements links = doc.select("a[href]");
4 Elements imports = doc.select("link[href]");
5
6 Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
7 if(includeMedia) {
8 Elements media = doc.select("[src]");
9 for (Element src : media) {
10 result.add(src.absUrl("src"));
11 //result.add(src.attr("abs:src"));
12 }
13 }
14
15 for (Element link : imports) {
16 result.add(link.absUrl("abs:href"));
17 }
18
19 for (Element link : links) {
20 result.add(link.absUrl("abs:href"));
21 }
22
23 return result;
24 }
本段代碼可以獲取輸入的字符串URL。也可以使用輸入流,但這在解析相對(duì)URL時(shí)會(huì)稍微復(fù)雜一些(無論如何都需要一個(gè)基本URL)。然后搜索具有src屬性的鏈接和對(duì)象。最后代碼將它們?nèi)刻砑拥揭粋€(gè)集合中,以保持條目的排序和唯一性。
我們使用以下代碼將其公開為Web服務(wù):
1
2 public class ParseLinksWS {
3 private final ParseLinks parseLinks;
4
5 public ParseLinksWS(ParseLinks parseLinks) {
6 this.parseLinks = parseLinks;
7 }
8
9 ("/parseLinks")
10 public Set<String> listLinks( String url, (required = false) Boolean includeMedia) throws IOException {
11 return parseLinks.listLinks(url, includeMedia == null ? true : includeMedia);
12 }
13 }
一旦運(yùn)行應(yīng)用程序,就可以通過一個(gè)簡單的curl命令使用它:
Java
1 curl -H "Content-Type: application/json" "http://localhost:8080/parseLinks?url=https%3A%2F%2Flightrun.com"
這將打印出Lightrun主頁中引用的URL列表。
調(diào)試內(nèi)容失敗
當(dāng)元素對(duì)象更改時(shí),會(huì)出現(xiàn)典型的字符串抓取問題。例如,維基百科可以更改其頁面的結(jié)構(gòu),而上面的選擇方法可能會(huì)失敗。這通常是一個(gè)微妙的失敗,是在處理嵌套節(jié)點(diǎn)元素和文檔間依賴關(guān)系時(shí)。例如Java對(duì)象層次結(jié)構(gòu)中缺少DOM元素,這可能會(huì)觸發(fā)選擇方法的失敗。大多數(shù)開發(fā)人員通過記錄大量數(shù)據(jù)來解決這個(gè)問題。產(chǎn)生這個(gè)問題的原因有三個(gè):
- 日志數(shù)據(jù)量大——它們既難以閱讀,又非常昂貴。
- 隱私/GDPR違規(guī)——被抓取的網(wǎng)站可能包含特定用戶的私人信息。
- 在最初實(shí)施抓取之后,抓取的站點(diǎn)可能會(huì)更改為包含私人信息。記錄這些私人信息可能會(huì)違反各種隱私法規(guī)。
如果沒有足夠的日志并且無法在本地重現(xiàn)問題,就會(huì)陷入到添加日志、構(gòu)建、測(cè)試、部署、重現(xiàn)這樣的重復(fù)循環(huán)中。
Lightrun提供了一種更好的方法。只需直接在生產(chǎn)中跟蹤特定故障、驗(yàn)證問題,并創(chuàng)建適用于一個(gè)部署的修復(fù)程序。
注:本文假設(shè)安裝了Lightrun并了解其背后的基本概念。如果沒有,可以查看文檔。
在瀏覽器DOM中找到自己的方式
假設(shè)不知道從何開始,那么jsoup API是一個(gè)很好的起點(diǎn)。它可以帶回用戶代碼。很酷的是,無論代碼如何都會(huì)有效。通過深入研究API調(diào)用,可以找到快照的正確行/文件。
在此處按ctrl鍵(在Mac上使用Meta-click)選擇方法調(diào)用:
Java
1 Elements links = doc.select("a[href]");
它帶到了Element類。在其中,按ctrl鍵單擊選擇器“select”方法,可以放置一個(gè)條件快照來查看每個(gè)執(zhí)行“a[href]”查詢的情況:
這可以顯示執(zhí)行該查詢的方法/行:
這對(duì)縮小文檔對(duì)象層次結(jié)構(gòu)中的一般問題區(qū)域有很大幫助。
有時(shí)采用快照可能還不夠,可能需要使用日志。日志記錄的優(yōu)點(diǎn)是可以生成大量信息,但僅針對(duì)特定情況和按需生成。
日志的價(jià)值在于,它們能夠以非常類似于單步執(zhí)行代碼的方式跟蹤問題。放置快照的位置對(duì)于日志來說是有問題的。我們知道發(fā)送的查詢,但還沒有返回的值。可以用日志輕松解決這個(gè)問題。首先,添加一個(gè)包含以下文本的日志:
"Executing query {query}"
然后,要找出返回了多少條目,只需轉(zhuǎn)到調(diào)用者(我們知道這要?dú)w功于快照中的堆棧)并在那里添加以下日志:
Links query returned {links.size()}
這會(huì)產(chǎn)生以下日志,讓我們看到有147個(gè)a[href]鏈接。這樣做的好處是額外的日志與場(chǎng)景中預(yù)先存在的日志交錯(cuò):
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Executing query a[href]
Feb 02, 2022 11:25:27 AM com.lightrun.demo.jsoupdemo.service.ParseLinks listLinks
INFO: LOGPOINT: Links query returned 147
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Executing query link[href]
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Executing query [src]
避免安全和GDPR問題
GDPR和安全問題可能是將用戶信息泄漏到日志中的問題。這可能是一個(gè)主要問題,Lightrun可以幫助顯著降低這種風(fēng)險(xiǎn)。
Lightrun提供了兩種可能的解決方案,可以在適用時(shí)串聯(lián)使用。
(1)日志管道
GDPR的最大問題是日志攝取。如果記錄私人用戶數(shù)據(jù),然后將其發(fā)送到云端,它會(huì)在那里保存很長時(shí)間,并且事后很難找到,也很難修復(fù)。
Lightrun提供了將Lightrun的所有注入日志直接通過管道傳輸?shù)絀DE的能力。這樣做的好處是可以消除可能使用日志的其他開發(fā)人員的干擾。它還可以跳過攝取(可選)。
如果僅將日志發(fā)送到插件,需要將管道模式選擇為“插件”。
(2)PII減少/阻止列表
個(gè)人身份信息(PII)是GDPR法規(guī)的核心,也是一個(gè)主要的安全風(fēng)險(xiǎn)。而企業(yè)中的惡意開發(fā)人員可能希望使用Lightrun來竊取用戶信息。阻止列表阻止開發(fā)人員在特定文件中放置操作。
驗(yàn)證個(gè)人身份信息(PII) 可以減少從日志中隱藏匹配特定模式的信息(例如信用卡格式等)。這可以由管理員角色在Lightrun Web界面中定義。
結(jié)語
對(duì)于Java內(nèi)容抓取,jsoup顯然是領(lǐng)導(dǎo)者。使用jsoup進(jìn)行開發(fā)遠(yuǎn)遠(yuǎn)超過字符串操作,甚至在處理連接方面。除了獲取文檔對(duì)象外,它還處理DOM元素和腳本所需的復(fù)雜方面。
抓取是一項(xiàng)有風(fēng)險(xiǎn)的業(yè)務(wù)。當(dāng)網(wǎng)站發(fā)生輕微變化時(shí),它可能會(huì)在眨眼間崩潰。更糟糕的是,它可能會(huì)以奇怪的方式影響某些用戶,而這些方式不可能在本地復(fù)制。
而有了Lightrun,可以直接在生產(chǎn)環(huán)境中調(diào)試此類故障,并快速發(fā)布工作版本。
原文標(biāo)題:??Debugging jsoup Java Code in Production Using Lightrun??,作者:Shai Almog