基于phantom爬虫探索

基于node、phantom爬任何能打开的页面

Posted by Kai on August 6, 2018

前言

是的,我们要爬medium.com文章。medium网站浏览器能打开,但是用普通爬虫的方式请求会timeout,使用phantom即可解决。medium文章url + ‘?format=json’会自动转成json格式,这为我们带来极大便利。至于为什么做了严谨防爬手段的网站却提供能轻易得到的json,我就不得而知了,估计是方便自己开发调用吧… 预览图

phantom篇

PhantomJS是一个基于webkit的JavaScript API。它使用QtWebKit作为它核心浏览器的功能,使用webkit来编译解释执行JavaScript代码。任何你可以在基于webkit浏览器做的事情,它都能做到。它不仅是个隐形的浏览器,提供了诸如CSS选择器、支持Web标准、DOM操作、JSON、HTML5、Canvas、SVG等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。PhantomJS的用处可谓非常广泛,诸如网络监测、网页截屏、无需浏览器的 Web 测试、页面访问自动化等。

PhantomJS是个独立的库
而phantom是node下的库
这里我们将使用node下的phantom库 https://github.com/amir20/phantomjs-node
yarn add phantom

首先我们爬一个列表页

const url = 'https://medium.com/_/api/topics/22d054b693da/stream?limit=25&to=1504625051983?format=json'

use

this.instance = await phantom.create([], {
    logLevel: 'info'
});
this.page = await this.instance.createPage();
const status = await this.page.open(url);
if (status === "success") {
    const content = await this.page.property('content');
    const tmp = content.replace('<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">])}while(1);&lt;/x&gt;', '');
    indexJson = tmp.replace('</pre></body></html>', '');//这里去掉前后浏览器自动加上的tag
} else {
    console.log("runIndex 网页加载失败");
}

获取到列表页json后,处理分析json拿到详情页的链接

const articleList = [];
const items = indexJson.payload.streamItems[0].section.items;
for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const postId = item.post.postId;
    const post = indexJson.payload.references.Post[postId];
    const User = indexJson.payload.references.User;

    let homeCollection = 'null';
    if (post.homeCollectionId) {
        const collection = indexJson.payload.references.Collection[post.homeCollectionId];
        homeCollection = collection.domain ? collection.domain : collection.slug
    }
    const articleParams = {
        postId: postId,
        title: post.title,
        creator: User[post.creatorId].username,
        homeCollection: homeCollection,
        subtitle: post.content.subtitle,
        previewImage: post.virtuals.previewImage.imageId,
        totalClapCount: post.virtuals.totalClapCount,
        uniqueSlug: post.uniqueSlug,
        detailUrl: this.getDetailUrl({
            homeCollection: homeCollection,
            uniqueSlug: post.uniqueSlug,
            creator: User[post.creatorId].username
        }),
        publishTime: post.latestPublishedAt,
        createdTime: new Date().getTime()
    }
    articleList.push(articleParams);
};
getDetailUrl(params) { // 组装详情页链接方法
    let pre = 'medium.com';
    if (params.homeCollection)
        if (params.homeCollection.includes('.')) {
            pre = params.homeCollection;
        } else if (params.homeCollection == 'null') {
        pre += '/@' + params.creator;
    } else {
        pre += '/' + params.homeCollection;
    }
    return `https://${pre}/${params.uniqueSlug}`
}

再循环使用phantom爬取到详情页的内容json

sequelize篇

我们将爬取到的页面存放mysql,这里我们使用sequelize来操作mysql。

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, SQLite 和 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能.

首先定义表

defineTable() {
    // 连接数据库
    this.sequelize = new Sequelize(config.mysql.database, config.mysql.user, config.mysql.password, {
        host: config.mysql.host,
        dialect: 'mysql',
        pool: { max: 5, min: 0, acquire: 30000, idle: 10000 },
    });

    this.articleTable = this.sequelize.define('articles', {
        postId: { type: Sequelize.STRING },
        title: { type: Sequelize.STRING },
        creator: { type: Sequelize.STRING },
        homeCollection: { type: Sequelize.STRING },
        subtitle: { type: Sequelize.STRING },
        previewImage: { type: Sequelize.STRING },
        totalClapCount: { type: Sequelize.INTEGER},
        uniqueSlug: { type: Sequelize.STRING },
        detailUrl: { type: Sequelize.STRING },
        publishTime: { type: Sequelize.DATE },
        createdTime: { type: Sequelize.DATE }
    }, {
        timestamps: false,
    }); // 定义文章列表 · 表
    this.articleDetailTable = this.sequelize.define('article_detail', {
        postId: { type: Sequelize.STRING },
        original: { type: Sequelize.TEXT },
        translated: { type: Sequelize.TEXT },
        createdTime: { type: Sequelize.DATE }
    }, {
        timestamps: false,
    }); // 定义文章详情 · 表
    this.statisticsTable = this.sequelize.define('statistics', {
        successInsertNum: { type: Sequelize.INTEGER },
        startTime: { type: Sequelize.DATE },
        endTime: { type: Sequelize.DATE },
        note: { type: Sequelize.STRING }
    }, {
        timestamps: false,
    }); // 定义统计表

}

处理详情json并插入表

const paragraphs = detailJson.payload.value.content.bodyModel.paragraphs; // detailJson 为文章详情json
const paragraphsParams = [];
for (let i = 0; i < paragraphs.length; i++) {
    const item = paragraphs[i];

    let translatedText = '';
    translatedText = await this.translated(item.text); // 自动翻译 自行找有道、腾讯、百度、搜狗等提供的翻译接口
    // if (item.metadata) await this.getImages(item.metadata); // 发现有图片 上传它
    // 研究发现 图片可以 用阿里云cdn回源 棒
    paragraphsParams.push({
        postId: params.postId,
        original: JSON.stringify(item),
        translated: translatedText,
        createdTime: new Date().getTime()
    })
}
if (test) return;
await this.articleTable.create(params);// 插入列表
await this.articleDetailTable.bulkCreate(paragraphsParams);// 批量插入详情表

服务器之科学访问

小伙伴可能会发现medium.com有时会打不开或者打开慢,如果我们要把爬虫程序放服务器,这就需要让服务器科学访问。这里使用shadowsocks,所以前提是你有shadowsocks账号哦。没有的邮件我kavil@qq.com 体验。

首先安装shadowsocks

yum install python-pip
pip install shadowsocks

新建配置文件shadowsocks.json 这里我们放etc目录

{
    "server": "your_server_ip",      #ss服务器IP
    "server_port": "your_server_port", #端口
    "local_address": "127.0.0.1",   #本地ip
    "local_port": 1080,              #本地端口
    "password": "your_server_passwd",#连接ss密码
    "timeout": 300,                  #等待超时
    "method": "rc4-md5",             #加密方式
    "fast_open": false,             # true  false如果你的服务器 Linux 内核在3.7+可以开启 fast_open 以降低延迟开启方法 echo 3 > /proc/sys/net/ipv4/tcp_fastopen 开启之后 fast_open 的配置设置为 true 即可
    "workers": 1                    # 工作线程数
}

然后后台运行

nohup sslocal -c /etc/shadowsocks.json /dev/null 2>&1 &

如果想增加开机自动运行

echo "nohup sslocal -c /etc/shadowsocks.json /dev/null 2>&1 &" /etc/rc.local

查看是否运行

ps aux |grep sslocal |grep -v "grep"

注意

  1. 使用的是sslocal这个命令,表示shadowsocks以客户端模式工作
  2. 将上述命令里的your_server_ip,your_server_port,your_server_passwd换成自己的,这三个分别代表服务器ip,服务器上shadowsocks的端口以及密码.后面的rc4-md5加密方式也要换成跟server端一致。

服务器之privoxy代理

上述安好了shadowsocks,但它是socks5代理,我们在shell里执行的命令,发起的网络请求现在还不支持socks5代理,只支持http/https代理。为了我门需要安装privoxy代理,它能把电脑上所有http请求转发给shadowsocks。

sudo apt-get install python-pip privoxy

修改或添加下面两行到Privoxy的配置文件底下/usr/local/etc/privoxy/config

listen-address  192.168.1.163:8118 
forward-socks5 / 127.0.0.1:1080 .

第一行的意思是监听来自任意地址的8118访问,第二行表示将请求转发给本地1080端口,也就是你本地的SS客户端所开放的端口。

开启服务

service privoxy start

到这服务器环境就配置好了,回到项目代码,在phantom.create()方法的实例下面调用代理方法

this.instance = await phantom.create([], {
    logLevel: 'info'
});
this.page = await this.instance.createPage();

await this.page.setProxy(`http://127.0.0.1:8118`);

服务器之循环派出爬虫探测更新

最后在服务器上写个自动循环执行爬虫的脚本,$1为间隔时间

#!/bin/sh
while [ true ]; do
echo `date`
sleep $1

pro=`ps -aux|grep 'node index.js'| grep -v grep | awk '{print $2}'`
echo $pro
echo '------------'
for id in $pro
do
kill -9 $id
echo "killed $id"
done
echo '------------'
npm run prod

done

预览图

完整代码看情况开源…

原创文章 转载请注明出处 原文链接 https://kavil.github.io/2018/08/06/%E5%9F%BA%E4%BA%8Ephantom%E7%88%AC%E8%99%AB%E6%8E%A2%E7%B4%A2/