07 使用 Java 进行爬虫

Wu Jun 2020-03-10 00:28:21
08 系统设计 > 1 通用设计

一、数据采集

1 Jsoup

Jsoup 可以请求和解析 HTML 页面。

Maven 引包

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.11.3</version>
</dependency>

1.1 Jsoup 请求页面

Jsoup.connect(url).get(); 可以直接请求一个 HTML 页面,也可以设置请求参数

// 获取连接 可 header 方法设置头信息
Connection con = Jsoup.connect(myurl)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36)")
        .header("X-FORWARDED-FOR",fakeIP)
        .header("X-Real-IP", fakeIP)
        .header("Upgrade-Insecure-Requests", "1")
        .cookies(map)
        .followRedirects(false);

// 获取响应
Connection.Response rs = con.execute();
// 获取 cookie
Map<String, String> cookies = rs.cookies();
Document document = rs.get();

1.2 Jsoup 解析页面

Jsoup.parse() 可以解析一个 HTML 页面,得到一个 Document

String result = null;
Document document = null;
try {
    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
    result = response.getBody();
    if (null != result && !result.contains("user not exist")) {
        document = Jsoup.parse(result);
    }
} catch (Exception e) {
    e.printStackTrace();
}

Jsoup 中 Document 继承了 Element,Element 有很多方法可以操作 html 节点

例如 select 方法

Elements elements =doc.select(".tab02 .fn-right");

2 Selenium

Selenium 其实是个自动化测试工具,也可以用来操作浏览器进行爬虫。

Maven 引包

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>3.141.59</version>
</dependency>

2.1 PhantomJS

PhantomJS 是一个无头浏览器,现在已暂停开发了,也被很多网站的反爬虫封禁了。

新版本的 Selenium 也不再支持 PhantomJS 了,推荐使用 Chrome 或 Firefox 的无头版本来替代。

驱动支持,windows 下面是 phantomjs.exe, linux 下面是 phantomjs

1)初始化
// 必要参数
DesiredCapabilities dcaps = new DesiredCapabilities();
//设置参数
String header= "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.22 Safari/537.36 SE 2.X MetaSr 1.0";
dcaps.setCapability("phantomjs.page.settings.userAgent", header);
dcaps.setCapability("phantomjs.page.customHeaders.User-Agent", header);
//不载入图片
dcaps.setCapability("phantomjs.page.settings.loadImages", false);
//不截屏
dcaps.setCapability("takesScreenshot", false);
//ssl证书支持
dcaps.setCapability("acceptSslCerts", true);
//js支持
dcaps.setJavascriptEnabled(true);
//驱动支持,windows 下面是 phantomjs.exe, linux 下面是 phantomjs
dcaps.setCapability("phantomjs.binary.path", ".../phantomjs.exe");
//cli 参数
ArrayList<String> cliArgsCap = new ArrayList<String>();
cliArgsCap.add("--web-security=false");
cliArgsCap.add("--ssl-protocol=any");
cliArgsCap.add("--ignore-ssl-errors=true");
cliArgsCap.add("--proxy=...");
cliArgsCap.add("--proxy-type=http");
cliArgsCap.add("--load-images=false");
cliArgsCap.add("--webdriver-loglevel=NONE");
dcaps.setCapability("phantomjs.cli.args", cliArgsCap);
//初始化
PhantomJSDriver webDriver =  new PhantomJSDriver(dcaps);
2)请求页面
String html = "";
try{
    //设置超时时间
    webDriver.manage().timeouts().pageLoadTimeout(30, TimeUnit.SECONDS);
    webDriver.get(url);
    Thread.sleep(500);
    html = webDriver.getPageSource();
}catch(Exception e){
    e.printStackTrace();
}finally{
    if (webDriver != null) {
        webDriver.quit();
    }
}

2.2 Chromium

驱动支持,windows 下面是 chromedriver_77_win32.exe, linux 下面需要安装 chromium 和 chromium-chromedriver

FROM .../openjdk:8-jdk-alpine
RUN echo -e "http://mirrors.aliyun.com/alpine/v3.8/main/\nhttp://mirrors.aliyun.com/alpine/v3.8/community/" > /etc/apk/repositories
RUN apk update
RUN apk add chromium=68.0.3440.75-r0 libexif udev chromium-chromedriver=68.0.3440.75-r0
1)配置驱动路径
@Component
public class DriverConfig {
    @Value("${webdriver.chrome.driver}")
    private String webDriver;
    @Bean
    public void setWebDriver(){
        System.setProperty("webdriver.chrome.driver",webDriver);
    }
}
2)工具化复用
/**
 * 单线程浏览器,不关可复用
 */
public class WebDriverUtils {

    private static ChromeDriver webDriver = null;

    public static Document getDocument(String url) {
        WebDriver webDriver = WebDriverUtils.getInstance();
        webDriver.get(url);
        String pageSource = webDriver.getPageSource();
        return Jsoup.parse(pageSource);
    }

    public static WebDriver getInstance() {
        if (needToInitialize()) {
            initWebDriver();
        }
        return webDriver;
    }

    public static void shutdown() {
        if(null != webDriver){
            webDriver.close();
        }
    }

    private static boolean needToInitialize() {
        boolean flag = false;
        try {
            webDriver.getWindowHandle();
        } catch (Throwable a) {
            flag = true;
        }
        return flag;
    }

    private static void initWebDriver() {
        if (null != webDriver) {
            webDriver.quit();
            webDriver = null;
        }
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        options.addArguments("--no-sandbox");
        options.addArguments("--disable-gpu");
        options.addArguments("blink-settings=imagesEnabled=false");
        options.addArguments("--proxy-server=...");
        options.addArguments("--proxy-type=http");
        webDriver = new ChromeDriver(options);
    }
}

二、应对反爬虫

1 应对限制 header

最简单的反爬策略就是检测 http 请求的 header,例如 User-Agent、Referer 字段,可能限制同一个 User-Agent 的请求频率,或者检测 Referer 是否是来自自己允许的域名。

应对这种情况,可以准备一组常见的 User-Agent,在请求的时候随机选用。Referer 字段也别忘记换成目标的真实 Referer。

2 应对限制用户

进一步的反爬可以是检测用户行为,限制 ip 或者账号。

2.1 限制 ip

同一个 ip 的请求频率过高,资源方可能会对 ip 进行限制,这时候就需要代理 ip 池了。

逻辑简单的限制 ip 可以通过自己生成一些假 ip 来进行欺骗。逻辑严谨的就需要一个公网 ip 池了,这个有商业化 ip 池可以使用,也可以自己去网上收集。

一般情况不会有封 ip 的情况发生,因为太容易误伤了,除非你爬得太狠了。

2.2 限制未登陆用户

有的网站允许未登录用户访问一定量的数据,超量之后就进行限制。可以利用这个特性,反复模拟新的未登录用户,来获取数据。

例如今日头条,你每次建立一个新的 session,网站会给你分配一个 session_id 放在 cookie 里,之后的请求就带上这个 session_id。当使用这个 session_id 的请求被网站限制了之后,就另起一个 session 获取新的 session_id 进行数据抓取。

2.3 限制登陆用户

最严格的限制是只允许登陆后的用户请求数据,并且对用户的请求频率有所限制。这种情况要不就放弃了,直接去买会员或其它的网站的充值业务;要不就去尝试批量新建账号(一般也有限制);或者去尝试登出再登陆,看看网站有没有逻辑漏洞。

3 应对二次请求

上面说的方法适用于直接请求接口就返回数据的情况,但现在很多网站是先通过请求返回 html 页面,再通过页面的加载,然后执行 ajax 二次请求数据,或者执行 js 代码生成的数据。

对于这种动态的二次数据请求,如果从页面里分析出的 ajax 请求接口,是能够直接调用的,那是最简单的。可现实往往是原网站还会在客户端通过 js 代码对请求进行一定的计算、混淆、加密,即使你获得了接口也没办法直接请求。

3.1 运行 js 文件

可以尝试去破解原网站的加密,如果破解不了 js 文件的话,可以直接调用 js 文件里面的方法,无需破解,直接请求。

1)读取 js 文件

读取 js 文件,或者直接将 js 文件里的内容复制给一个 String 变量。

private static String getStringFromJS() {
    InputStream inputStream  = Application.class.getClassLoader().getResourceAsStream("js/xxx.js");
    StringBuffer sb = new StringBuffer();
    try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
        String line;
        while ((line = in.readLine()) != null) {
            sb.append(line + "\n");
        }
    } catch (Exception e) {

    }
    return sb.toString();
}
2)执行 js 脚本

Java 通过 ScriptEngine 执行 js 脚本

public String getVl5x(String vjkl5) {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine vl5xEngine = manager.getEngineByName("nashorn");

    // 运行 js 代码
    try {
        vl5xEngine.eval(getStringFromJS());
    } catch (ScriptException e) {
        e.printStackTrace();
    }

    Invocable vl5xInvocable = (Invocable) vl5xEngine;
    Object result = null;
    // 调用 js 方法
    try {
        result = vl5xInvocable.invokeFunction("getKey", vjkl5);
    } catch (ScriptException | NoSuchMethodException e) {
        e.printStackTrace();
    }

    String vl5x = (String) result;
    return vl5x;
}

3.2 调用浏览器

如果你捋不清二次请求涉及到的 js 文件,或者其中的调用方法,还可以通过调用浏览器,即上面数据采集中的 Selenium 方法,让浏览器自己去执行来源网站的请求逻辑。

缺点是效率比较低,而且还依赖了第三方浏览器,健壮性降低了。

记住不要使用 PhantomJs 了,不止是因为新版本 Selenium 不支持,而且资源方很容易能够分析出 PhantomJs 的特征进行屏蔽,推荐 headless chrome。

4 应对字体替换

有些网站不给你返回正确内容,它将部分文字用自定义的字体文件替换了,使你爬取到的数据是乱码的。

这种情况不用慌,因为能在页面上正确展现的内容,就能被正确爬取到。

动态获取 .ttf 格式的字体文件,一般跟抓取内容在一个 html 页面中,正则得到 ttf 字体的 url,在线读取内容并解析替换。

public String getRightContent(String ttfUrl, String content) {
    // 根据 url 获取 ttf 文件
    URL ttfHttpUrl = new URL("http:"+ ttfUrl);
    InputStream inStream = ttfHttpUrl.openStream();
    ByteArrayOutputStream outStream = new ByteArrayOutputStream();
    byte[] data = new byte[1024];
    int count = -1;
    long totalCount = 0;
    while((count = inStream.read(data, 0, 1024)) != -1) {
    outStream.write(data, 0, count);
    totalCount += count;
    if (totalCount > 1024 * 1024 * 200)
    {
        throw new IOException("content length for current request exceeds max threshold 200M");
    }
    }
    byte[] getData = outStream.toByteArray();
    File ttfFile = new File("xxx.ttf");
    FileOutputStream fos = new FileOutputStream(ttfFile);
    fos.write(getData);
    if(fos!=null){
        fos.close();
    }
    
    // 解析 ttf 文件
    TrueTypeFont ttf1 = new TTFParser().parse(ttfFile);
    
    //转十进制字体 id
    int fontId = (int)content.toCharArray()[0];
    //根据字体 id 转换 ttf 表下标
    int glyphId = ttf1.getCmap().getCmaps()[0].getGlyphId(fontId);
    //根据下标读取字体, TTF_WORDLIST 是解析出来的字体文件数组
    String word = TTF_WORDLIST[glyphId];
    return word;
}

5 应对验证码

三、反爬虫总结

根据爬取数据遇到的反爬策略,总结一下大概有这些