前端与爬虫
搜索爬虫, 我们会搜到一大堆 Python 相关的结果
问题: 爬虫和前端有关系吗?
爬虫是什么
爬虫程序是一种计算机程序,旨在通过执行自动化或重复性任务来模仿或替代人类的操作。
爬虫程序执行任务的速度和准确性比真实用户高得多。爬虫程序类型众多,可执行各种任务,并且爬虫程序在互联网流量中的比重也越来越大。
今天我们主要讨论的是 网络爬虫
什么是网络爬虫
网络爬虫(英语:web crawler),也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人。其目的一般为编纂网络索引。--维基百科
如果只是做搜索引擎,那么感兴趣的信息就是互联网中尽可能多的高质量网页;如果要获取某一垂直领域的数据或者有明确的检索需求,那么感兴趣的信息就是根据我们的检索和需求所定位的这些信息,此时,需要过滤掉一些无用信息。前者我们称为通用网络爬虫
,后者我们称为聚焦网络爬虫
由此观之, 爬虫的主要作用就是获取&处理信息
原则上,只要是浏览器(客户端)能做的事情,爬虫都能够做
一个简单的爬虫
- 获取 h1 标签
- 打印 h1 标签的内容
// 获取h1标签
const h1 = document.querySelector('h1');
// 打印h1标签的内容
console.log(h1.textContent);
是不是感觉很熟悉, 这不就是咱们前端日常的 DOM 操作吗?
是的, 通过 dom 操作获取页面信息, 这就是一个爬虫
保存页面爬取的信息
既然咱们已经获取到了数据, 应该如何保存呢?
function downloadTxtFile() {
const h1 = document.querySelector('h1');
const text = h1.textContent; // 要保存的文本内容
const element = document.createElement('a');
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(text)
);
element.setAttribute('download', 'file.txt');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
如果我想保存 Excel 表格呢?
咱们可以使用第三方库如 xlsx 或 exceljs 来生成 Excel 文件, 然后提供下载链接给用户。(不在讨论范围)
爬取服务端渲染的网页
- 环境: Node.js
- 依赖:
- axios (或者其他能发送 http 请求的库),
- cheerio (解析 HTML 字符串, 使用 jQuery 语法操作)
- 目标: https://www.sumingxs.com/xiaoshuo/1/1/
步骤
-
打开页面, 找到需要获取的信息 (标题, 内容, 下一页链接) 的
选择器
-
使用
axios
获取页面的 HTML 字符串const { data } = await axios.get('https://www.sumingxs.com/xiaoshuo/1/1/');
-
使用
cheerio
解析 HTML 字符串const $ = cheerio.load(data);
-
使用
jQuery
语法获取信息const title = $('h1').text(); // const content = $('.con').text() const arr = []; $('.con p').each((i, item) => { arr.push($(item).text()); }); const content = arr.join('\n'); const a = $('.prenext a'); const nextUrl = a.eq(a.length - 1).attr('href');
完整代码
import axios from 'axios';
import * as cheerio from 'cheerio';
import fs from 'fs';
async function getPage(
url = 'https://www.sumingxs.com/xiaoshuo/1/1/',
count = 20
) {
if (count === 0) {
return;
}
const { data } = await axios.get(url);
const $ = cheerio.load(data);
const title = $('h1').text();
console.log(title);
const content = [];
$('.con p').each((i, item) => {
content.push($(item).text());
});
const a = $('.prenext a');
const next = a.eq(a.length - 1).attr('href');
console.log(next);
const { host, protocol } = new URL(url);
const nextUrl = `${protocol}//${host}${next}`;
console.log(nextUrl);
fs.writeFileSync(`res/${title}.txt`, content.join('\n'));
getPage(nextUrl, count - 1);
}
getPage();
编码问题
目标: http://58xs8.com/html/196/196757/26829864.html
这是一个 gbk
编码的页面
老规矩, 咱一上来就
const { data } = await axios.get(
'http://58xs8.com/html/196/196757/26829864.html'
);
const $ = cheerid.load(data);
const title = $('h1').text();
console.log(title);
这是什么情况
处理编码问题
这里不过多讨论编码的原理, 简单说明一下解决方法吧
- 采用 buffer 的数据格式获取页面
- 将 buffer 转换为 utf8 编码字符串
commonJS 模块 iconv-lite
安装
npm i iconv-lite
使用
const { decode } = require('iconv-lite');
const { data } = await axios.get(
'http://58xs8.com/html/196/196757/26829864.html',
{ responseType: 'arraybuffer' }
);
const newData = decode(data, 'gbk').toString('utf8');
const $ = cheerio.load(newData);
const title = $('h1').text();
console.log(title);
EMS 模块 encoding
安装
npm i encoding
使用
import { convert } from 'encoding';
const { data } = await axios.get(
'http://58xs8.com/html/196/196757/26829864.html',
{ responseType: 'arraybuffer' }
);
const newData = convert(data, 'utf8', 'gbk');
const $ = cheerio.load(newData);
const title = $('h1').text();
console.log(title);
爬取客户端渲染的网页
我一上来就
const { data } = await axios.get('https://appid.surge.sh/');
不出意外的话, 我们拿到了一个
...
<div id="root"></div>
...
是的, 我们可以直接去抓接口
但是, 当我们不了解接口字段的时候, 从页面获取也是一种选择
puppeteer
前面我们已经在浏览器手动获取到数据了, 现在咱们用代码来操作浏览器获取页面数据
- 环境: Node.js
- 依赖:
- puppeteer (或者其他能发送 http 请求的库),
- 目标: https://appid.surge.sh/
- 任务 获取页面的账号密码
步骤
打开浏览器
const browser = await pupteer.launch({
// 直接用本地的chrome, window填入chrome快捷方式所指向的路径
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
});
打开一个新页面, 跳转到 https://appid.surge.sh/
const page = await browser.newPage();
await page.goto('https://appid.surge.sh/');
等待页面加载完成
// 数据是异步加载的, 等到数据加载, 渲染页面后的dom出现
await page.waitForSelector('.apple-id-status');
获取页面上的数据
const res = await page.$$eval(
'#app > div > div.ant-flex.css-1qb1s0s.ant-flex-wrap-wrap.ant-flex-align-center.ant-flex-justify-center > div',
(el) => {
const arr = [];
el.forEach((item) => {
const email = item.querySelector(
'.ant-input.css-1qb1s0s[type=text]'
).value;
const password = item.querySelector(
'.ant-input.css-1qb1s0s[type=password]'
).value;
arr.push({ email, password });
});
return arr;
}
);
完整代码
const pupteer = require('puppeteer');
+(async () => {
const browser = await pupteer.launch({
// headless: false,
executablePath:
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
});
const page = await browser.newPage();
await page.goto('https://appid.surge.sh/');
await page.waitForSelector('.apple-id-status');
const res = await page.$$eval(
'#app > div > div.ant-flex.css-1qb1s0s.ant-flex-wrap-wrap.ant-flex-align-center.ant-flex-justify-center > div',
(el) => {
const res = [];
el.forEach((item) => {
const email = item.querySelector(
'.ant-input.css-1qb1s0s[type=text]'
).value;
const password = item.querySelector(
'.ant-input.css-1qb1s0s[type=password]'
).value;
res.push({ email, password });
});
return res;
}
);
console.log(res);
await browser.close();
})();
反爬与解决
道高一尺, 魔高一丈
- 封 IP - 挂代理
- 封 User-Agent 修改 User-Agent
- 封 Cookie 无痕模式
- 动态渲染 浏览器爬虫
- 异步操作 刷接口, 浏览器爬虫
- 图片伪装 ocr
- CSS 偏移 截屏 + ocr
- SVG 映射 截屏 + ocr
- 验证用户操作 模拟操作
当爬虫的成本和所得的利益不符时, 自然反爬