有时候会面临一段没有网络的时间,尽量利用时间准备把 LeetCode 的题目都下载到电脑上,需要用到一个小爬虫。
JSoup 早就听说过,但一直没有正式使用过(本来 Java 就用的少),但 JSoup 是个非常好用的 DOM 处理工具,因此这次使用一番。
首先看到 LeetCode 页面,关键元素大概如下
<tr>
<td>
<span class="None"> </span>
</td>
<td>173</td>
<td>
<a href="/problems/binary-search-tree-iterator/">Binary Search Tree Iterator</a>
</td>
<td>28.7%</td>
<td value='2'>Medium</td>
</tr>
这个 table-row 节点就是每道题的题目以及链接,而每个链接的 <a> 标签也在 <td> 中,
<td>
<a href="/problems/binary-search-tree-iterator/">Binary Search Tree Iterator</a>
</td>
因此首先直接选取 <td>
Document doc = Jsoup. connect(url).get();
Elements links = doc.select( "td" );
至于为什么不直接选取<a>节点原因有二
- 页面内有很多非题目的<a>链接,而只有在<td>下的才是题目对应链接
- 因为一个特殊的原因必须要把<td>选出来,原因如下:
观察 LeetCode 列表会发现其实有两种情况
170 后面带有一个书的标签,意味着这一条其实不是一个oj题目,点进去后会跳转到其他页面。从后面的结果可以看到这种点进去又跳转其实会让程序阻塞。而观察改节点会发现是这样的
<td>
<a href="/problems/two-sum-ii-input-array-is-sorted/">Two Sum II - Input array is sorted</a>
<i class="fa fa-book"></i>
</td>
注意到这个节点与普通题不同,多出了一个放书籍图标的<i>标签,这种节点不需要。除此之外,子节点中不包含<a>节点的也不能要。排除这两种节点后就可以选取了
for(Element link : links){
if(!link.select("i").isEmpty() ||
link.select("a").isEmpty()) //存在<i>或不存在<a>
continue;
Element aTag = (link.select("a")).first(); //只有一个,直接第一个
String linkHref = aTag.attr("href");
System.out.println("Get "+linkHref);
MainApp.funcList.add(linkHref); //funcList 存的是所有oj题目的地址
}
如果答应出来,获得的地址是这样的(从上面的td节点也可以看出)
Get /problems/factorial-trailing-zeroes/
Get /problems/excel-sheet-column-number/
Get /problems/majority-element/
存的是相对路径。因此进入下载题目时,不要忘记加上前缀。题目下载后就可以存到本地啦。代码如下
FileOutputStream outFile=new FileOutputStream("LeetCode.txt");
int k = 0;
while(!MainApp.funcList.isEmpty()){
url = MainApp.funcList.remove();
doc = Jsoup.connect("https://oj.leetcode.com"+url).get();
Element title = doc.select(".question-title h3").first();
Element content = doc.select(".question-content").first();
outFile.write((k+++".["+title.text()+"]\r\n").getBytes());
outFile.write((content.text()+"\r\n\r\n").getBytes());
}
这样一来一个单线程的下载爬虫就完成了。改成多线程主要需要给 funclist 以及输出流或者目标文件加锁,就不赘述了。