Маленькие партнеры канала «Птичий язык»
один,Puppeteer
Введение и установка
Puppeteer
ЯвляетсяNode
Библиотека, предоставляющая высокоуровневый API для управления по протоколу DevTools.Chromium
. запусти это в гуглеheadless
После браузераSelenium
был прямо оставлен мной, потому чтоPuppeteer
заNodejs
Разработчики слишком дружелюбны, (при нормальных обстоятельствах) нужно толькоnpm i puppeteer
, вы можете завершить установку без установки других зависимых библиотек (Я был слишком молод в начале о(╥﹏╥)о, это непросто).
Для системной среды на работе используется MacOS, а на сервер развернута Centos 7.
существуетMacOS
Это очень просто, просто нужноnpm i puppeteer
На линии. Не могут быть установлены следующие несколько решений:
# 1. 设置环境变量跳过下载 Chromium(2018-09-03已失效)
set PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
# 2. 只下载模块而不build,但chromium需要自行下载(2018-09-03有效)
npm i --save puppeteer --ignore-scripts
# 3. Puppeteer从v1.7.0开始额外提供一个puppeteer-core的库,它只包含Puppeteer的核心库,默认不下载chromium
npm i puppeteer-core
# 如果连puppeteer都安装不了,建议使用淘宝镜像
npm config set registry="https://registry.npm.taobao.org"
еслиChromium
скачается сама, затем запуститеheadless
Следующие элементы конфигурации необходимо добавить в браузер
this.browser = await puppeteer.launch({
// MacOS应该在"xxx/Chromium.app/Contents/MacOS/Chromium",Linux应该"/usr/bin/chromium-browser"
executablePath: "Chromium的安装路径",
// 去沙盒
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
Загрузка Chromium, другие зависимости должны быть установлены под Linux
Нажмите, чтобы узнать о вариантах использования Puppeteer
2. Навыки
Ленивая загрузка скриншотов
Когда скриншоты или рептилии некоторые страницы часто встречаются в ленивом способе отображения данных. Первый экран не отображает всю информацию нам. В ответ на ленивую нагрузку используйте прокрутку до конца для трещины. Какие? Ленивая загрузка недоступна, попробуйте напрямую настроить свой интерфейс, или есть другие высококачественные способы указать
page.evaluate(pageFunction, ...args): эта функция позволяет нам использовать встроенные селекторы DOM.
Обратите особое внимание здесьpageFunction
Способ передачи параметров:
const result = await page.evaluate(param1, param2, param3 => {
return Promise.resolve(8 + param1 + param2 + param3);
}, param1, param2, param3);
// 也可以传一个字符串:
console.log(await page.evaluate('1 + 2')); // 输出 "3"
const x = 10;
console.log(await page.evaluate(`1 + ${x}`)); // 输出 "11"
Код: в качестве примера возьмем ленивую загрузку Jianshu.
/**
* 懒加载页面自动滚动
*/
const path = require('path');
const puppeteer = require('puppeteer-core');
const log = console.log;
(async () => {
const browser = await puppeteer.launch({
// executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'),
// 关闭headless模式, 会打开浏览器
headless: false,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
const page = await browser.newPage();
await page.goto('https://www.jianshu.com/u/40909ea33e50');
await autoScroll(page);
// fullPage截图
await page.screenshot({
path: 'auto_scroll.png',
type: 'png',
fullPage: true,
});
await browser.close();
})();
async function autoScroll(page) {
log('[AutoScroll begin]');
await page.evaluate(async () => {
await new Promise((resolve, reject) => {
// 页面的当前高度
let totalHeight = 0;
// 每次向下滚动的距离
let distance = 100;
// 通过setInterval循环执行
let timer = setInterval(() => {
let scrollHeight = document.body.scrollHeight;
// 执行滚动操作
window.scrollBy(0, distance);
// 如果滚动的距离大于当前元素高度则停止执行
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
log('[AutoScroll done]');
// 完成懒加载后可以完整截图或者爬取数据等操作
// do what you like ...
}
Элемент Точный Скриншот
Точные скриншоты, как следует из названия, представляют собой область, занимаемую элементами на странице.
抠
вниз. затем замените его наPuppeteer
Способ борьбы с этим заключается в использованииscreenshot
изclip
параметр, по координатам элемента относительно области просмотра (x、y
), а также ширину и высоту элемента (width、height
) скриншот позиционирования. Разумеется, селектор элементов должен быть точным, иначе он не сможет делать точные скриншоты.
- Клип параметра page.screenshot
-
element.getBoundingClientRect()
: с помощью этого метода можно получить относительное положение элемента в окне просмотра (возвращаемый объект включает в себяleft、top、width、height
), соответствующие точки знаний можно найти в Google -
$eval
: этот метод выполняется внутри страницыdocument.querySelector
, а затем передайте соответствующий элемент в качестве первого параметра вpageFunction
const path = require('path');
const puppeteer = require('puppeteer-core');
const log = console.log;
(async () => {
const browser = await puppeteer.launch({
// executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'),
// 关闭headless模式, 会打开浏览器
headless: false,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
const page = await browser.newPage();
await page.goto('https://www.jianshu.com/');
const pos = await getElementBounding(page, '.board');
// clip截图
await page.screenshot({
path: 'element_bounding.png',
type: 'png',
clip: {
x: pos.left,
y: pos.top,
width: pos.width,
height: pos.height
}
});
await browser.close();
})();
async function getElementBounding(page, element) {
log('[GetElementBounding]: ', element);
const pos = await page.$eval(element, e => {
// 相当于在evaluate的pageFunction内执行
// document.querySelector(element).getBoundingClientRect()
const {left, top, width, height} = e.getBoundingClientRect();
return {left, top, width, height};
});
log('[Element position]: ', JSON.stringify(pos, undefined, 2));
return pos;
}
Хорошо, пока мы можем сделать скриншоты большинства элементов, остальные элементы, которые прокручиваются внутри
Скриншот внутреннего элемента прокрутки
Внутренняя прокрутка: по сравнению с традиционной прокруткой оконной формы, ее основная полоса прокрутки находится внутри страницы (или элемента), а не в форме браузера. Чаще всего в интерфейсе управления фоном полоса прокрутки в левой колонке и область содержимого справа разделены.
Представьте, что, когда открывается музыкальная облачная музыка, появится два внутренних полоса прокрутки на первом экране. Если мы хотим увидеть больше плейлистов, нам нужно сдвинуть полосы прокрутки вниз. То же самое относится и к внутренним скриншонам прокрутки. В сочетании с прокруткой страницы целевой элемент подвергается воздействию в видимом диапазоне, а затем точные скриншоты достигаются через координаты окна.
шаг:
- Получите координаты целевого элемента и определите, находится ли он в пределах текущего видимого диапазона.Если он находится в области просмотра, нет необходимости прокручивать
- Поскольку это внутренняя прокрутка, родительский элемент с полосой прокрутки должен быть установлен за пределами целевого элемента, а целевой элемент отображается косвенно путем прокрутки родительского элемента. Итак, на этом шаге необходимо определить селектор родительского элемента.
- Прокрутите родительский элемент, имитируя страницу (настройка
window.scrollBy
илиscrollLeft scrollTop
), достаточно, чтобы целевой объект появился в окне - Поскольку катится, необходимо реагировать координаты целевого элемента (
getBoundingClientRect
) - Сделайте скриншот с новыми координатами
Вот небольшая подробность о том, как узнать, есть ли у элемента полосы прокрутки. если элемент не имеетX轴
полосу прокрутки, затем установите егоscrollLeft
Эффекта нет, в это время можно делать только глобальную прокрутку.
// 如果scrollWidth值大于clientWidth值,则可以说明其出现了横向滚动条
element.scrollHeight > element.clientHeight
// 如果scrollHeight值大于clientHeight值,则可以说明其出现了竖向滚动条
element.scrollHeight > element.clientHeight
Пример кода: начните сNODEJS Официальная документацияНапример, сделайте снимок экрана телетайпа в левом столбце.
/**
* 截取左侧栏中TTY所在的li节点
*/
const path = require('path');
const puppeteer = require('puppeteer-core');
const log = console.log;
(async () => {
const browser = await puppeteer.launch({
executablePath: path.join(__dirname, './chromium/Chromium.app/Contents/MacOS/Chromium'),
// 关闭headless模式, 会打开浏览器
headless: false,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
const page = await browser.newPage();
await page.setViewport({width: 1920, height: 600});
const viewport = page.viewport();
// Nodejs官方Api文档站
await page.goto('https://nodejs.org/dist/latest-v10.x/docs/api/');
// await page.waitFor(1000);
// 这里强烈建议使用 waitForNavigation,1000这中魔鬼数字会让代码变得不放心
await page.waitForNavigation({
// 20秒超时时间
timeout: 20000,
// 不再有网络连接时判定页面跳转完成
waitUntil: [
'domcontentloaded',
'networkidle0',
],
});
// step1: 确定内滚动的父元素选择器
const containerEle = '#column2';
// step1: 确定目标元素选择器
const targetEle = '#column2 ul:nth-of-type(2) li:nth-of-type(40)';
// step1: 获取目标元素在当前视窗内的坐标
let pos = await getElementBounding(page, targetEle);
// 使用内置的DOM选择器
const ret = await page.evaluate(async (viewport, pos, element) => {
// step1: 判断目标元素是否在当前可视范围内
const sumX = pos.width + pos.left;
const sumY = pos.height + pos.top;
// X轴和Y轴各需要移动的距离
const x = sumX <= viewport.width ? 0 : sumX - viewport.width;
const y = sumY <= viewport.height ? 0 : sumY - viewport.height;
const el = document.querySelector(element);
// strp3: 将元素滚动进视窗可视范围内
// 此处需要判断目标元素的x、y是否可滚动,如果元素不能滚动则滚动window
// 如果scrollWidth值大于clientWidth值,则可以说明其出现了横向滚动条
if (el.scrollWidth > el.clientWidth) {
el.scrollLeft += x;
} else {
window.scrollBy(x, 0);
}
// 如果scrollHeight值大于clientHeight值,则可以说明其出现了竖向滚动条
if (el.scrollHeight > el.clientHeight) {
el.scrollTop += y;
} else {
window.scrollBy(0, y);
}
return [el.scrollHeight, el.clientHeight];
}, viewport, pos, containerEle);
// step4: 由于目标元素在视窗外,且处于内滚动父元素内,所以需要重新获取坐标
pos = await getElementBounding(page, targetEle);
// await page.waitFor(1000);
// 这里强烈建议使用 waitForNavigation,1000这中魔鬼数字会让代码变得不放心
await page.waitForNavigation({
// 20秒超时时间
timeout: 20000,
// 不再有网络连接时判定页面跳转完成
waitUntil: [
'domcontentloaded',
'networkidle0',
],
});
// 5. 截图
await page.screenshot({
path: 'scroll_and_bounding.png',
type: 'png',
clip: {
x: pos.left,
y: pos.top,
width: pos.width,
height: pos.height
}
});
await browser.close();
})();
3. Яма, на которую наступили: вLinux
установить наChromium
Оказывается: опыт установки Chromium в среде Linux может быть незабываемым. Установить
puppeteer
, Хром скачивается автоматически, что часто заканчивается сбоем по известным причинам. Chromium может быть успешно загружен после смены источника зеркала, но после запуска Различные ошибки вызваны отсутствием частичных зависимостей от Linux. После установки необходимых зависимостей код работает без сбоев. Однако на скриншоте обнаружил, что китайские шрифты в браузере все рамочные. ОК, установите библиотеку шрифтов, китайские иероглифы отображаются нормально!
Лучшие практики после того, как вы наступили на яму
- использовать
Chromium
иnpm包
Отдельный способ, только установкаpuppeteer-core
,пройти черезexecutablePath
Внедрить самостоятельную загрузкуChromium
, сильно ускоряетnpm install
скорость. - Переключите зеркальный источник Linux на зеркальный источник Ali, который можно быстро загрузить.
Chromium
- Измените проект для использования
Docker
Разверните, чтобы избежать ситуации, когда локальная разработка идет нормально, но после перехода в онлайн возникают различные проблемы. - старайтесь избегать использования
page.waifFor(1000)
, 1000 миллисекунд это всего лишь грубая оценка времени, лучше пусть программа сама решает
Связанные решения:
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y
# 设置阿里镜像源
echo "https://mirrors.aliyun.com/alpine/edge/main" > /etc/apk/repositories
echo "https://mirrors.aliyun.com/alpine/edge/community" >> /etc/apk/repositories
echo "https://mirrors.aliyun.com/alpine/edge/testing" >> /etc/apk/repositories
# 安装Chromium及依赖,包括中文字体支持
apk -U --no-cache update
apk -U --no-cache --allow-untrusted add zlib-dev xorg-server dbus ttf-freefont chromium wqy-zenhei@edge -f
После установки нужно зайти в песочницу для запуска, хотя официально это не рекомендуется.
Песочница Linux: в области компьютерной безопасности песочница — это изолированный рабочий механизм для программ, целью которого является ограничение разрешений ненадежных процессов. Методы песочницы часто используются для выполнения непроверенных или ненадежных клиентских программ. Во избежание недоверенные программы могут нарушить работу других программ.
-
--no-sandbox
: перейти в песочницу, чтобы запустить -
--disable-dev-shm-usage
: по умолчанию,Docker
управлять/dev/shm
Контейнер с общей памятью 64 МБ. Обычно это слишком мало для Chrome и приводит к сбою Chrome при отображении больших страниц. Чтобы исправить, контейнер должен быть запущенdocker run --shm-size=1gb
увеличить/dev/shm
емкость. Начиная с Chrome 65, используйте--disable-dev-shm-usage
флаг для запуска браузера, который будет писать в файл разделяемой памяти/tmp
вместо/dev/shm
.
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
В-четвертых, черезDocker容器
Развернуть проект
В конце проекта я обнаружил, что Chromium нужно устанавливать каждый раз, и каждый раз могут возникать неожиданные проблемы. Чтобы сэкономить время и деньги и сделать более значимые вещи,
shell脚本
иDocker容器化
Оптимизировать приведенный выше процесс развертывания.
Процесс разработки докера
- Определите базовое изображение
- На основании подготовки базового изображения
Dockerfile
- в соответствии с
Dockerfile
Создайте образ проекта - Переместите созданный образ в
Docker仓库
, если развертывание приватизации экспортирует образ напрямую, перейдите в среду клиента, чтобы импортировать его. - Извлеките образ проекта, чтобы создать его и запустить на тестовом/производственном компьютере.
Docker容器
- Убедитесь, что проект работает правильно
Здесь, чтобы развернутьPuppeteer
сервис как пример
Определить базовое изображение
# 在Docker Hub或私有仓库上搜索需要的镜像
docker search node
перейти кDocker HubМожно посмотреть более подробное описание и версию
# 在这选择 `node:10-alpine` 为基础镜像
docker pull node:10-alpine
написатьDockerfile
(Руководство неполное, рекомендуется найти более подробную информацию в Интернете)
FROM
: указывает базовое изображение, которое должно бытьDockerfile
Первая директива без комментариев в
FROM <image name>
FROM node:10-alpine
MAINTAINER
: установить автора зеркала
MAINTAINER <author name> (不推荐使用,推荐使用LABEL来指定镜像作者)
LABEL MAINTAINER="zhangqiling" (推荐)
RUN
: Команда, которая будет выполняться в оболочке или среде exec. Инструкция RUN добавляет новый слой во вновь созданный образ, а результат следующего коммита используется в следующей инструкции в Dockerfile.
RUN <command>
# RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交
RUN echo "https://mirrors.aliyun.com/alpine/edge/main" > /etc/apk/repositories
# 执行多条命令时,可以通过 \ 换行
RUN apk -U add \
zlib-dev \
xorg-server
RUN
Промежуточные образы, созданные директивой, кэшируются и используются в следующей сборке. Если вы не хотите использовать эти изображения кеша, вы можете указать во время сборки--no-cache
параметры, такие как:docker build --no-cache
.
CMD
: Предоставляет команду выполнения по умолчанию для контейнера.Dockerfile
Только один раз разрешеноCMD
директива, если их несколькоCMD
, и только последний вступит в силу
# 有三种形式
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
COPY
: для копирования файлов или каталогов из среды сборки на зеркало
COPY <src>... <dest>
COPY ["<src>",... "<dest>"]
# 将项目复制到my_app目录下
COPY . /workspase/my_app
ADD
: также копирует файлы или каталоги в среде сборки на зеркало
ADD <src>... <dest>
ADD ["<src>",... "<dest>"]
по сравнению сCOPY
, ADD
из<src>
может бытьURL
. В то же время, если это сжатый файл,Docker
будет автоматически распаковываться.
WORKDIR
: уточнитьRUN
,CMD
иENTRYPOINT
рабочий каталог команды
WORKDIR /workspase/my_app
ENV
: установить переменную среды
# 两种方式
ENV <key> <value>
ENV <key>=<value>
VOLUME
: разрешить доступ к каталогам из контейнера на хост
VOLUME ["/data"]
EXPOSE
: указывает порт, который контейнер прослушивает во время выполнения.
EXPOSE <port>;
Прикрепил тест пройденDockerfile
Образец
Несколько заметок
- Используйте отечественную зеркальную станцию Alibaba Cloud для ускорения установки зависимостей
- По умолчанию отображение на китайском языке не поддерживается. Необходимо использовать бесплатные китайские шрифты Wenquanyi. Эта библиотека доступна только в
https://mirrors.aliyun.com/alpine/edge/testing/
могу найти - Городской район по умолчанию в контейнере не является районом Дунба, что повлияет на печать журнала и потребует сброса часового пояса.
- Внутри док-контейнера на машине Centos
npm install
сообщит об ошибке, установитеnpm config set unsafe-perm true
После гладкой установки, в чем причина? (Докер не проблема на MacOS)
# 拉取node镜像
FROM node:10-alpine
# 设置镜像作者
LABEL MAINTAINER="qiyang.hqy@dtwave-inc.com"
# 设置国内阿里云镜像站、安装chromium 68、文泉驿免费中文字体等依赖库
RUN echo "https://mirrors.aliyun.com/alpine/v3.8/main/" > /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/v3.8/community/" >> /etc/apk/repositories \
&& echo "https://mirrors.aliyun.com/alpine/edge/testing/" >> /etc/apk/repositories \
&& apk -U --no-cache update && apk -U --no-cache --allow-untrusted add \
zlib-dev \
xorg-server \
dbus \
ttf-freefont \
chromium \
wqy-zenhei@edge \
bash \
bash-doc \
bash-completion -f
# 设置时区
RUN rm -rf /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 设置环境变量
ENV NODE_ENV production
# 创建项目代码的目录
RUN mkdir -p /workspace
# 指定RUN、CMD与ENTRYPOINT命令的工作目录
WORKDIR /workspace
# 复制宿主机当前路径下所有文件到docker的工作目录
COPY . /workspace
# 清除npm缓存文件
RUN npm cache clean --force && npm cache verify
# 如果设置为true,则当运行package scripts时禁止UID/GID互相切换
# RUN npm config set unsafe-perm true
# 安装pm2
RUN npm i pm2 -g
# 安装依赖
RUN npm install
# 暴露端口
EXPOSE 3000
# 运行命令
ENTRYPOINT pm2-runtime start docker_pm2.json