Java爬蟲爬取網站電影下載連結
之前有看過一段時間爬蟲,瞭解了爬蟲的原理,以及一些實現的方法,本專案完成於2017年初,一直放在那裡,現在和大家分享出來。
網路爬蟲簡單的原理就是把程式想象成為一個小蟲子,一旦進去了一個大門,這個小蟲子就像進入了新世界一樣,只要符合他的口味的東西就會放在自己的袋子裡,但是他還不滿足,只要見到可以開啟的門,他都要進去看看,裡面有沒有他想要的東西有就裝起來,直到每個門裡都看了一遍,確定沒有了之後,他才肯放棄,這樣下來,他的袋子已經裝滿了想要的東西。
上述內容表述起來就是:網路爬蟲就是一個自動提取網頁內容的程式,這個程式的行為像一個蟲子似的,爬來爬去。一般的網路爬蟲都有一個或者多個網頁的URL作為開始,從開始的網頁上獲取URL,開啟連結並把符合條件的內容儲存下來,這樣一直進行下去,直到條件不符合的時候,程式執行結束。
以下只是簡單的一個爬蟲,爬取一個電影下載網站上的迅雷下載連結,用到了兩個輔助佇列,一個存提取過的連結作為用來判斷當前連結是否已經開啟過,另一個是存需要進行操作的連結佇列,存進去的連結都會進行操作(開啟連結抓取有用的內容)。最後獲取到的下載連結存在一個set集合中,用來保證抓到的連結不會重複。
---以下是一個分析原始碼的過程,後期發現這個自定義編碼類並沒有用上---由於在網頁上顯示的只是一串文字,當用戶點選後,網站會將下載連結會經過URL編碼,編碼成迅雷能夠識別的下載連結,網站上使用的是Javascript的URL編碼,Java提供了一個URL編碼類,在java.net包中有一個URLEncoder類,但是和網站上的編碼規則有點不一致,我們需要Java的URL編碼的原始碼,通過分析原始碼其實很難簡單地發現對什麼字元編碼,對什麼不進行編碼,和Javascript的URL編碼比較之後,對其進行改造,經過比對,發現Java的編碼,對'@','[',']',':','/'
主要原始碼:
static { //dontNeedEncoding方法:不需要編碼的字元 dontNeedEncoding = new BitSet(256); int i; //字元a-z不需要編碼 for (i = 'a'; i <= 'z'; i++) { dontNeedEncoding.set(i); } //字元A-Z不需要編碼 for (i = 'A'; i <= 'Z'; i++) { dontNeedEncoding.set(i); } //字元0-9不需要編碼 for (i = '0'; i <= '9'; i++) { dontNeedEncoding.set(i); } //不需要編碼:空格、中劃線、下劃線、點、星號 dontNeedEncoding.set(' '); /* encoding a space to a + is done * in the encode() method */ dontNeedEncoding.set('-'); dontNeedEncoding.set('_'); dontNeedEncoding.set('.'); dontNeedEncoding.set('*'); dfltEncName = AccessController.doPrivileged( new GetPropertyAction("file.encoding") ); }
改造後的url編碼類,主要增加對'@','[',']',':','/'特殊字元的編碼
static {
dontNeedEncoding = new BitSet(256);
int i;
for (i = 'a'; i <= 'z'; i++) {
dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
dontNeedEncoding.set(i);
}
dontNeedEncoding.set(' '); /*
* encoding a space to a + is done in the
* encode() method
*/
dontNeedEncoding.set('-');
dontNeedEncoding.set('_');
dontNeedEncoding.set('.');
dontNeedEncoding.set('*');
//增加下面的字元,不對他們編碼
dontNeedEncoding.set('@');
dontNeedEncoding.set('[');
dontNeedEncoding.set(']');
dontNeedEncoding.set(':');
dontNeedEncoding.set('/');
}
其他的程式碼片段沒有影響,由於我們將原始碼複製過來自己定義了規則,建立一個URLEncoders類,有些引用會報錯,刪除之後並沒有對編碼功能影響
完整自定義編碼類URLEncoders:
package url;
import java.io.UnsupportedEncodingException;
import java.io.CharArrayWriter;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.BitSet;
public class URLEncoders {
static BitSet dontNeedEncoding;
static final int caseDiff = ('a' - 'A');
static String dfltEncName = null;
static {
dontNeedEncoding = new BitSet(256);
int i;
for (i = 'a'; i <= 'z'; i++) {
dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
dontNeedEncoding.set(i);
}
dontNeedEncoding.set(' '); /*
* encoding a space to a + is done in the
* encode() method
*/
dontNeedEncoding.set('-');
dontNeedEncoding.set('_');
dontNeedEncoding.set('.');
dontNeedEncoding.set('*');
dontNeedEncoding.set('@');
dontNeedEncoding.set('[');
dontNeedEncoding.set(']');
dontNeedEncoding.set(':');
dontNeedEncoding.set('/');
}
@Deprecated
public static String encode(String s) {
String str = null;
try {
str = encode(s, dfltEncName);
} catch (UnsupportedEncodingException e) {
}
return str;
}
public static String encode(String s, String enc)
throws UnsupportedEncodingException {
boolean needToChange = false;
StringBuffer out = new StringBuffer(s.length());
Charset charset;
CharArrayWriter charArrayWriter = new CharArrayWriter();
if (enc == null)
throw new NullPointerException("charsetName");
try {
charset = Charset.forName(enc);
} catch (IllegalCharsetNameException e) {
throw new UnsupportedEncodingException(enc);
} catch (UnsupportedCharsetException e) {
throw new UnsupportedEncodingException(enc);
}
for (int i = 0; i < s.length();) {
int c = (int) s.charAt(i);
if (dontNeedEncoding.get(c)) {
if (c == ' ') {
c = '+';
needToChange = true;
}
out.append((char) c);
i++;
} else {
do {
charArrayWriter.write(c);
if (c >= 0xD800 && c <= 0xDBFF) {
if ((i + 1) < s.length()) {
int d = (int) s.charAt(i + 1);
if (d >= 0xDC00 && d <= 0xDFFF) {
charArrayWriter.write(d);
i++;
}
}
}
i++;
} while (i < s.length()
&& !dontNeedEncoding.get((c = (int) s.charAt(i))));
charArrayWriter.flush();
String str = new String(charArrayWriter.toCharArray());
byte[] ba = str.getBytes(charset);
for (int j = 0; j < ba.length; j++) {
out.append('%');
char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
ch = Character.forDigit(ba[j] & 0xF, 16);
if (Character.isLetter(ch)) {
ch -= caseDiff;
}
out.append(ch);
}
charArrayWriter.reset();
needToChange = true;
}
}
return (needToChange ? out.toString() : s);
}
}
所以當我研究好了以上的編碼規則之後才發現,迅雷會識別未經過編碼的連結,三種連結迅雷都可以下載,如下圖:
所以以上就當是提高了一下分析原始碼的能力……(無奈)
程式缺點:
- 爬蟲程式碼並沒有做任何的優化,可以說效率非常低,程式用到了遞迴,並且在程式執行的過程中開啟的連結可能會形成一個環形,也就是開啟一個連結,之後程式執行的過程中會再次找到這個連結,所以這條路線就斷了。
- 程式在執行過程中,如果當前訪問的網址時間過長,會丟擲異常,也會影響效率
- 在程式執行時候發現程式受網路影響非常大
最後執行的效果列印最後結果(每次結果都會不一樣,發現網路穩定時候爬出來的連結能多些):
從執行時間和有效連結可以看出效率很低,其實爬蟲的程式碼很簡單,從網頁獲取程式碼,對其進行解析這麼一個過程
主程式:並沒有呼叫其他的方法,那個編碼類也沒有用上
package function;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import queue.Queue;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
public class Crawler {
public static Queue q1 = new Queue();
public static Queue q2 = new Queue();
public static Set<String> set = new TreeSet<String>();
public static int i = 0;
public static void main(String[] args) {
Document doc = null;
try {
long begin = System.currentTimeMillis();
doc = Jsoup.connect("http://www.dytt8.net/index.htm").get();
Elements links = doc.select("a[href]");
for (Element link : links) {
String linkHref = link.attr("href");
Pattern pattern = Pattern.compile("^/html/+(.)+.html");
Pattern pattern0 = Pattern
.compile("http://www.dytt8.net/html/+(.)+.html");
Pattern pattern1 = Pattern.compile("^ftp://+((.)+)+");
if (pattern.matcher(linkHref).matches() == true
|| pattern0.matcher(linkHref).matches() == true) {
q1.insertQueue(linkHref);
q2.insertQueue(linkHref);
open("http://www.dytt8.net" + q1.outQueue());
}
}
Iterator<String> it = set.iterator();
// while(it.hasNext()){
// String url=(String)it.next();
// int last=url.lastIndexOf(".");
// int last1=url.lastIndexOf("]");
// // System.out.print(url.substring(last1+1, last)+" ");
// System.out.println(URLEncoders.encode(url,"utf-8"));
// }
System.out.println("一共爬取" + q2.size() + "條連結");
long end = System.currentTimeMillis();
System.out.println("用時" + (end - begin) + "ms");
System.out.println("一共" + set.size() + "條下載連結");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void open(String url) {
Document doc = null;
try {
doc = Jsoup.connect(url).get();
Elements links = doc.select("a[href]");
for (Element link : links) {
String linkHref = link.attr("href");
Pattern pattern = Pattern.compile("^/html/+(.)+.html");
Pattern pattern0 = Pattern
.compile("http://www.dytt8.net/html/+(.)+.html");
Pattern pattern1 = Pattern.compile("^ftp://+((.)+)+");
if (pattern.matcher(linkHref).matches() == true
|| pattern0.matcher(linkHref).matches() == true) {
q1.insertQueue(linkHref);
q2.insertQueue(linkHref);
if (q2.contains(linkHref) == false) {
open("http://www.dytt8.net" + q1.outQueue());
}
} else if (pattern1.matcher(linkHref).matches() == true) {
System.out.println(linkHref);
set.add(linkHref);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
專案地址:
(文章修改於2018年4月6日)