惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
H
Hacker News: Front Page
Stack Overflow Blog
Stack Overflow Blog
B
Blog
I
InfoQ
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
F
Full Disclosure
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recent Announcements
Recent Announcements
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Jina AI
Jina AI
T
Tailwind CSS Blog
S
Secure Thoughts
P
Privacy International News Feed
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
W
WeLiveSecurity
Google Online Security Blog
Google Online Security Blog
P
Privacy & Cybersecurity Law Blog
D
DataBreaches.Net
Engineering at Meta
Engineering at Meta
Know Your Adversary
Know Your Adversary
P
Palo Alto Networks Blog
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Project Zero
Project Zero
V2EX - 技术
V2EX - 技术
H
Heimdal Security Blog
博客园 - Franky
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
H
Help Net Security
Martin Fowler
Martin Fowler
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
G
GRAHAM CLULEY
博客园 - 【当耐特】

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 continuation 教程: 理解 CPS 如何开发一个比特币符文(Runes)协议 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Rust 语言容易让新手困惑的一个“过度优化” Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计 鼓吹 Cursor 的人技术能力都差 关于 Code Review 的礼节 假如启动一个新的以太坊 PoS 网络 发行加密货币的最好方式 所有 BFT 共识的区块链都是中心化的 对 2025 年区块链行业的预测 Restaking 项目的经济难题 如何看懂任意区块链项目的技术架构 为什么不要做以太坊的二次开发 为什么不要做智能合约开发和 DeFi 开发 我的加密货币定投策略(一) PoS 类型的区块链如何处理分叉 Ethereum Casper 为什么需要 EIP-7251 区块链中的 PBFT 不需要第二次投票 开发者的思维方式 发币的核心要点 炒币投资的小 tips 为什么说 PoW 比 PoS 更加去中心化 牛市对普通人来说意味着什么 如何健康地远程工作 为什么比特币不用概率加密函数 程序员的 “服从权威” 心理 区块链技术面试题 如何区分公有链和联盟链 对 Layer 2 项目创业想法的回复 对区块链共识机制的理解 Pebbling Game 鹅卵石游戏 PDP 文件证明的局限性 不要小瞧 ChatGPT 为什么炒币不是一个好主意 一种在区块链上生成随机数的机制 为什么以太坊的私钥计算不可逆 关于以太坊的私钥碰撞 “猜均值的2/3” dApp 游戏设计 Proof of Storage/Space/Replication 的区别 Proofs of Retrievability 文件证明的含义 对 S-PDP 文件证明的示例和解释 我的加密货币交易机器人 对区块链行业的见闻 随机确认块的共识机制 VRF + BFT 共识引起交易失败的问题 为什么要重视编程思想 对 Web 3.0 的理解 GitBook 好用吗? 一种区块链节点存储扩容的方式 一种基于“自我中心主义”的共识机制 链表常见算法题及解析 基于 Multi-Linked List 的区块链设想 理解哈希函数与序列化 联盟链比公有链差在哪儿 在 Dijkstra 算法中保存路径 为什么数字货币使用区块链是政治问题 区块链:下一代数字身份认证体系的基石 网页技术能实现 3D 建模吗? 给区块链一个定义 从 Erlang 开始了解 Actor 模型 一种侧边导航栏的交互方式 Rust 的 ownership 是什么? Haskell 中的 Monad 是什么? 浅析 Libra 背后的区块链技术 对区块链的理性认识 Rust 基础语法概述 Kotlin:简化版的 Scala JavaScript 有关联数组吗? 主流编程语言的异常处理机制 Go 语言基本语法 Scala 语法基础 用 Scala 改写 Java 浅度实践 Python 获取海贼王更新信息 HTML5 音乐可视化
基于 Java 的爬虫框架 WebCollector
2019-08-10 · via smallyu的博客

2019-08-10

Long, Long Ago,网络上出现大量Python爬虫教程,各种培训班借势宣扬Python,近几年又将噱头转向人工智能。爬虫是一个可以简单也可以复杂的概念,就好比建造狗屋和建筑高楼大厦都是在搞工程。

由于工作的缘故,我需要使用WebCollector爬取一些网页上的数据。其实宏观上,爬虫无非就是访问页面文件,把需要的数据提取出来,然后把数据储存到数据库里。难点往往在于,一是目标网站的反爬策略,这是让人比较无奈的斗智斗勇的过程;二是目标网页数量大、类型多,如何制定有效的数据爬取和数据分析方案。

概述

这是一张简略的概念图,受屏幕宽度限制,可能无法看清内容,请在新标签页打开图片,或者直接点击 这里。这张图片并不是完美的,甚至还包含不完全正确的实现方式,具体内容会在后面阐述。

我将目标网页分为4种类型:

  1. 静态的网页文档,curl就可以加载到
  2. 需要自定义HTTP请求的页面,比如由POST请求得到的搜索结果页面,或者需要使用Cookie进行鉴权的页面
  3. 页面中包含由JavaScript生成的数据,而我们需要的正是这部分数据。由于js是加载后才执行的,就像CSS加载后由浏览器进行渲染一样,这样的数据无法直接得到
  4. 页面中包含由JavaScript生成的数据,且需要自定义HTTP请求的页面

测试环境

为了便于测试,在本地使用Node.js启动一个简单的服务器,用于接收请求,并返回一个页面作为响应。server.js的内容如下:

var http = require('http')
var fs = require('fs')
var server = http.createServer((req,res) => {
  // 返回页面内容
  fs.readFile('./index.html', 'utf-8', (err,data) => {
    res.end(data);
  });
  // 打印请求中的Cookie信息
  console.log(req.headers.cookie)
})
server.listen(9000) 

index.html的内容更加简单,只包含一个title和一个p标签:

<!DOCTYPE html>
<html>
<head>
    <title>This is a title</title>
</head>
<body>

</body>
</html>

静态页面

这是一个最简版的爬虫程序,在构造方法中调用父类的有参构造方法,同时添加url到待爬取队列中。visit是消费者,每一个url请求都会进入这个方法被处理。

public class StaticDocs extends BreadthCrawler {

    public StaticDocs(String crawlPath, boolean autoParse) {
        super(crawlPath, autoParse);
        this.addSeed("http://127.0.0.1:9000/");
    }

    @Override
    public void visit(Page page, CrawlDatums next) {
        System.out.println(page.doc().title();
        // This is a title
    }

    public static void main(String[] args) throws Exception {
        StaticDocs crawler = new StaticDocs("crawl", true);
        crawler.start(1);
    }
}

Cookie鉴权

需要在header中带cookie请求同样简单,在构造方法中添加相应配置就可以,node.js的命令行会打印出cookie的内容:

public CookieDocs(String crawlPath) {
    super(crawlPath, true);

    // 设置请求插件
    setRequester(new OkHttpRequester() {
        @Override
        public Request.Builder createRequestBuilder(CrawlDatum crawlDatum) {
            return super.createRequestBuilder(crawlDatum)
                    .header("Cookie", "name=smallyu");
        }
    });

    this.addSeed("http://127.0.0.1:9000/");
}

// name=smallyu

JavaScript生成的数据

测试js生成数据的情况需要做一点准备,修改index.html,在body标签中加入这样几行代码:

<div id="content">1</div>
<script>
  document.getElementById('content').innerHTML = '2'
</script>

可以预见,请求中直接返回的div内容是1,然后js经由浏览器执行,改变div的内容为2。访问静态页面的爬虫程序只能进行到第1步,也就是直接获取请求返回的内容。修改StaticDocs.java的visit方法,打印出div的内容看一下,可以确信是1:

System.out.println(page.select("div").text());
// 1

这是一个官方提供的Demo,用于获取js生成的数据。WebCollector依赖于Selenium,使用HtmlUnitDriver运行js:

public class JsDocs {
    public static void main(String[] args) throws Exception {
        Executor executor = (CrawlDatum datum, CrawlDatums next) -> {
            HtmlUnitDriver driver = new HtmlUnitDriver();
            driver.setJavascriptEnabled(true);

            driver.get(datum.url());

            WebElement divEle = driver.findElement(By.id("content"));
            System.out.println(divEle.getText());
            // 2
        };

        //创建一个基于伯克利DB的DBManager
        DBManager manager = new RocksDBManager("crawl");
        //创建一个Crawler需要有DBManager和Executor
        Crawler crawler = new Crawler(manager, executor);
        crawler.addSeed("http://127.0.0.1:9000/");
        crawler.start(1);
    }
}

如果你看过WebCollector的主页,一定可以注意到这个Demo和其他Demo的明显不同。在不需要js生成的数据时,新建的类继承自BreadthCrawler,而BreadthCrawler继承自AutoParseCrawler,AutoParseCrawler又继承自Crawler。现在获取js数据的Demo,直接跳过BreadthCrawler和AutoParseCrawler,实例化了Crawler。

为什么要这样做呢?再次强调,这是官方提供的Demo。

Cookie鉴权后JavaScript生成的数据

根据官方提供的用例,显然是无法设置cookie的,因为Crawler类并没有提供自定义Header的方法。这个自定义Header的方法继承自AutoParseCrawler类。那么如何做到既可以添加Cookie又可以使用HtmlUnitDriver?

其实结果很简单,我在看过WebCollector的代码后发现AutoParseCrawler实现了Executor接口,并且在构造方法中将this赋值给了父类的executor。也就是说,AutoParseCrawler本身就是一个Executor。下面的代码用以表示它们的关系:

public class Crawler {
    protected Executor executor;

    public Crawler(DBManager dbManager, Executor executor) {
        // ...
    }
}

public class AutoParseCrawler extends Crawler implements Executor {
    public AutoParseCrawler(boolean autoParse) {
        // 这里的executor指向父类
        this.executor = this;
    }
}

new Crawler时传入一个executor,相当于直接new一个AutoParseCrawler。BreadthCrawler继承自AutoParseCrawler,所以BreadthCrawler本身也是个Executor。再看官方关于自定义Cookie的Demo,如何在其中使用HtmlUnitDriver呢?重写Executor的execute方法。

所以,在定义cookie后获取js生成的数据,使用继承BreadthCrawler的类,然后重写execute就可以。这是一个完整的Demo:

/**
 * @author smallyu
 * @date 2019.08.11 12:18
 */
public class JsWithCookieDocs extends BreadthCrawler {

    public JsWithCookieDocs(String crawlPath) {
        super(crawlPath, true);

        // 设置请求插件
        setRequester(new OkHttpRequester() {
            @Override
            public Request.Builder createRequestBuilder(CrawlDatum crawlDatum) {
                return super.createRequestBuilder(crawlDatum)
                        .header("Cookie", "name=smallyu");
            }
        });

        this.addSeed("http://127.0.0.1:9000/");
    }

    // 直接重写execute即可
    @Override
    public void execute(CrawlDatum datum, CrawlDatums next) throws Exception {
        super.execute(datum, next);

        HtmlUnitDriver driver = new HtmlUnitDriver();
        driver.setJavascriptEnabled(true);

        driver.get(datum.url());

        WebElement divEle = driver.findElement(By.id("content"));
        System.out.println(divEle.getText());
        // 2
        // 同时,node.js的命令行中打印出cookie内容
    }

    // 重写execute就不需要visit了
    public void visit(Page page, CrawlDatums crawlDatums) {}

    public static void main(String[] args) throws Exception {
        JsWithCookieDocs crawler = new JsWithCookieDocs("crawl");
        crawler.start(1);
    }
}

外部代理

也许还没有结束。在一开始概述的图片上,同时定义cookie以及获取js生成的数据,实现方式是内部Selenium + 外部browsermob-proxy。假设没有上述重写execute的方法(官方也确实没有提供类似的Demo),该如何实现想要的效果?一种实践是本地启动一个代理,给代理设置好cookie,然后让Selenium的WebDriver通过代理访问目标页面,就可以在带header的情况下拿到js生成的数据。这是在JsDocs.java的基础上,使用代理的完整实现:

public class JsWithProxyDocs {
    public static void main(String[] args) throws Exception {
        Executor executor = (CrawlDatum datum, CrawlDatums next) -> {

            // 启动一个代理
            BrowserMobProxy proxy = new BrowserMobProxyServer();
            proxy.start(0);
            // 添加header
            proxy.addHeader("Cookie" , "name=smallyu");

            // 实例化代理对象
            Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
            // 由代理对象生成capabilities
            DesiredCapabilities capabilities = new DesiredCapabilities();
            capabilities.setCapability(CapabilityType.PROXY, seleniumProxy);
            // 内置,必须设置
            capabilities.setBrowserName("htmlunit");

            // 使用capabilities实例化HtmlUnitDriver
            HtmlUnitDriver driver = new HtmlUnitDriver(capabilities);
            driver.setJavascriptEnabled(true);

            driver.get(datum.url());

            WebElement divEle = driver.findElement(By.id("content"));
            System.out.println(divEle.getText());   // 2
        };

        //创建一个Crawler需要有DBManager和Executor
        Crawler crawler = new Crawler(new RocksDBManager("crawl"), executor);
        crawler.addSeed("http://127.0.0.1:9000/");
        crawler.start(1);
    }
}

其他

对于WebCollector我已经没有兴趣了解更多,倒是在注意到框架的包名cn.edu.hfut后有种豁然开朗的感觉。凌乱的代码风格,随处可见不知所以的注释,毫无设计美感的代码架构,倒也符合国内不知名大学的开源软件水平,距离工业级的框架,可能还需要N个指数倍东的时间。至于使用过程中遇到depth含义不明、线程非法结束、next.add失效等问题,就这样吧,也在情理之中,整个框架都像是赶工的结果,或者说是学生们拿来练手的项目。我在WebCollector的Github上RP了关于重写execute的问题,从开发者回复的只言片语中,我怀疑开源者自己都没有把里面的东西搞清楚 :P