前言
是的,我们要爬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);</x>', '');
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"
注意
- 使用的是sslocal这个命令,表示shadowsocks以客户端模式工作
- 将上述命令里的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/
转载请注明出处 原文链接