爬虫

LeetCode 1236/1242 设计一个(多线程)爬虫解法

单线程题目 LeetCode-1236

具体题目就不说了,直接去 LeetCode 上看就好了。1236 要求使用单线程即可,考察的主要是图的遍历。只要注意到对于新发现的节点需要考虑是否已经访问过就好了。在实际生产中,肯定也是要用广度优先,深度优先基本就会陷进一个网站出不来了。

from urllib.parse import urlsplit

class Solution:
    def crawl(self, startUrl: str, htmlParser: 'HtmlParser') -> List[str]:
        domain = urlsplit(startUrl).netloc
        q = [startUrl]
        visited = set([startUrl])
        while q:
            newUrls = []
            for url in q:
                urls = htmlParser.getUrls(url)
                for newUrl in urls:
                    u = urlsplit(newUrl)
                    if u.netloc != domain:
                        continue
                    if newUrl in visited:
                        continue
                    visited.add(newUrl)
                    newUrls.append(newUrl)
            q = newUrls
        return list(visited)

多线程题目 LeetCode-1242

1242 题要求使用多线程来实现。在现实生活中,爬虫作为一个 IO 密集型的任务,使用多线程是一项必须的优化。

在上述的单线程版本中,我们使用了 visited 这个数组来存放已经访问过的节点,如果我们采用多线程的话,并且在每个线程中并发判断某个 URL 是否被访问过,那么势必需要给这个变量加一个锁。而我们知道,在多线程程序中,加锁往往造成性能损失最大,最容易引起潜在的 bug。那么有没有一种办法可以不用显式加锁呢?

其实也很简单,我们只要把需要把并发访问的部分放到一个线程里就好了。这个想法是最近阅读 The Go Programming Language 得到的启发。全部代码如下:

import threading
import queue
from urllib.parse import urlsplit

class Solution:
    def crawl(self, startUrl: str, htmlParser: 'HtmlParser') -> List[str]:
        domain = urlsplit(startUrl).netloc
        requestQueue = queue.Queue()
        resultQueue = queue.Queue()
        requestQueue.put(startUrl)
        for _ in range(5):
            t = threading.Thread(target=self._crawl, 
                args=(domain, htmlParser, requestQueue, resultQueue))
            t.daemon = True
            t.start()
        running = 1
        visited = set([startUrl])
        while running > 0:
            urls = resultQueue.get()
            for url in urls:
                if url in visited:
                    continue
                visited.add(url)
                requestQueue.put(url)
                running += 1
            running -= 1
        return list(visited)

    def _crawl(self, domain, htmlParser, requestQueue, resultQueue):
        while True:
            url = requestQueue.get()
            urls = htmlParser.getUrls(url)
            newUrls = []
            for url in urls:
                u = urlsplit(url)
                if u.netloc == domain:
                    newUrls.append(url)
            resultQueue.put(newUrls)

在上面的代码中,我们开启了 5 个线程并发请求,每个 worker 线程都做同样的事情:

  1. 从 requestQueue 中读取一个待访问的 url;
  2. 执行一个很耗时的网络请求:htmlParser.getUrls
  3. 然后把获取到的新的 url 处理后放到 resultQueue 中。

而在主线程中:

  1. 从 resultQueue 中读取一个访问的结果
  2. 判断每个 URL 是否已经被访问过
  3. 并分发到 requestQueue 中。

我们可以看到在上述的过程中并没有显式使用锁(当然 queue 本身是带锁的)。原因就在于,我们把对于需要并发访问的结构限制在了一个线程中。

手机如何使用 Charles 抓 https 包

问题描述

安装 Charles 之后,使用手机自带浏览器访问 http://chls.pro/ssl ,下载到了 getssl.crt 文件,点击安装之后提示“未找到可安装的证书”

解决方法

首先下载 Chrome 浏览器,然后再访问 http://chls.pro/ssl ,安装下载到的证书就好了。

通过浏览器访问 https://www.baidu.com ,Charles 中能够抓到包的内容,说明安装成功了

参考

  1. https://segmentfault.com/a/1190000011573699
  2. https://testerhome.com/topics/9445

使用 Puppeteer

在服务器上部署 puppeteer 现在有两个问题:

  1. 如何打包 data-dir 上去
  2. 部署使用 Docker 还是直接手工跑

puppeteer 的相关资料:

  1. API 文档。https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md
  2. Browserless 的 Docker 镜像。https://docs.browserless.io/docs/docker-quickstart.html
  3. PP Cluster。https://github.com/thomasdondorf/puppeteer-cluster
  4. Awesome Puppeteer。https://github.com/transitive-bullshit/awesome-puppeteer
  5. GitHub Topic。https://github.com/topics/puppeteer

无头浏览器和 Puppeteer 的一些最佳实践

在做爬虫的时候,总会遇到一些动态网页,他们的内容是 Ajax 加载甚至是加密的。虽然说对于一些大站来说,分析接口是值得的,但是对于众多的小网站来说,一个一个分析接口太繁琐了,这时候直接使用浏览器渲染就简单得多了。

以往比较流行的是 selenium + phantomjs 的组合,不过在自从 Google 官方推出了谷歌浏览器的无头模式和 puppeteer 这个库以后,稳定性和易用度都大幅得到了提升,本文也主要探讨谷歌浏览器和 puppeteer。另外 puppeteer 也有第三方的 Python 移植,叫做 pyppeteer,不过这个库目前来看不太稳定(个人使用体验)。另外 pyppeteer 这个库使用了 asyncio,如果你的爬虫使用的是普通的同步语法,那么也还是不方便调用 pyppeteer 这个库,个人建议还是使用官方的 node 版 puppeteer,如果需要在 Python 中调用,直接调用 node 然后渲染就可以了。

browserless 是一家在提供云端浏览器渲染服务的公司,本文翻译了他们关于如何提升无头浏览器稳定性和性能的两篇文章并添加了本人在使用过程中遇到的一些问题和经验总结。browserless 的两篇原文链接在最后。

不要使用无头浏览器

Headless Chrome 占用大量的资源。无论如何,只要可以的话,不要运行无头浏览器。特别是千万别在你跑其他应用的服务器上跑。无头浏览器的行为难以预测,对资源占用非常多,就像是 Rick and Morty 里面的 Meseeks(美国动画片《瑞克和莫蒂》中,召唤出了过多的 Meseeks 导致出了大问题)。几乎所有你想通过浏览器用的事情(比如说运行 JavaScript)都可以使用简单的 Linux 工具来实现。Cheerio 和其他的库提供了优雅的 Node API 来实现 HTTP 请求和采集等需求。

比如,你可以像这样获取一个页面并抽取内容:

import cheerio from 'cheerio';
import fetch from 'node-fetch';
 
async function getPrice(url) {
    const res = await fetch(url);
    const html = await res.test();
    const $ = cheerio.load(html);
    return $('buy-now.price').text();
}
 
getPrice('https://my-cool-website.com/');

显然这肯定不能覆盖所有的方面,如果你正在读这篇文章的话,你可能需要一个无头浏览器,所以接着看吧。

使用 docker 来管理 Chrome

在 Linux 上跑 Chrome 的话,很可能连字体渲染都没有,还要安装好多的依赖。Chrome 除了浏览之外,还会有好多的莫名其妙的线程,所以最好使用 docker 来管理。建议使用 browserless/chrome 这个镜像,这个镜像是 browserless 这家专门做 Chrome 渲染的公司在生产环境中使用的镜像。关于这个镜像的文档在这里:https://docs.browserless.io/docs/docker.html (英文)

docker run -p 8080:3000 --restart always -d --name browserless browserless/chrome
const puppeteer = require('puppeteer');
 
    // 从 puppeteer.launch() 改成如下
    const browser = await puppeteer.connect({ browserWSEndpoint: 'ws://localhost:3000' });
    const page = await browser.newPage();
 
    await page.goto('http://www.example.com/');
    const screenshot = await page.screenshot();
 
    await browser.disconnect();

保持 Chrome 在运行状态

当负载很高的情况下,Chrome 启动可能会花上好几秒钟。对大多数情况来说,我们还是希望避免这个启动时间。所以,最好的办法就是预先启动好 Chrome,然后让他在后台等着我们调用。

如果使用 browserless/chrome 这个镜像的话,直接指定 PREBOOT_CHROME=true 就好了。下面的命令会直接启动 10 个浏览器,如果你指定 KEEP_ALIVE,那么在你断开链接(pp.disconnect)的时候也不会关闭浏览器,而只是把相关页面关闭掉。

docker run -d -p 3000:3000 \
    -e DEBUG=browserless* \
    -e PREBOOT_CHROME=true -e MAX_CONCURRENT_SESSIONS=10 -e KEEP_ALIVE=true
    --name browserless browserless/chrome:latest

page.evaluate 是你的好朋友

Puppeteer 有一些很酷的语法糖,比如可以保存 DOM 选择器等等东西到 Node 运行时中。尽管这很方便,但是当有脚本在变换 DOM 节点的时候很可能坑你一把。尽管看起来有一些 hacky,但是最好还是在浏览器中运行浏览器这边的工作。也就是说使用 page.evaluate 来操作。

比如,不要使用下面这种方法(使用了三个 async 动作):

const $anchor = await page.$('a.buy-now');
const link = await $anchor.getProperty('href');
await $anchor.click();
 
return link;

这样做,使用了一个 async 动作:

await page.evaluate(() => {
    const $anchor = document.querySelector('a.buy-now');
    const text = $anchor.href;
    $anchor.click();
});

另外的好处是这样做是可移植的:也就是说你可以在浏览器中运行这个代码来测试下是不是需要重写你的 node 代码。当然,能用调试器调试的时候还是用调试器来缩短开发时间。

最重要的规则就是数一下你使用的 await 的数量,如果超过 1 了,那么说明你最好把代码写在 page.evaluate 中。原因在于,所有的 async 函数都必须在 Node 和 浏览器直接传来传去,也就是需要不停地 json 序列化和反序列化。尽管这些解析成本也不是很高(有 WebSocket 支持),但是总还是要花费时间的。

除此之外,还要牢记使用 puppeteer 的时候是由两个 JS 的执行环境的,别把他们搞混了。在执行 page.evaluate 的时候,函数会先被序列化成字符串,传递给浏览器的 JS 运行时,然后再执行。比如说下面这个错误。

const anchor = 'a';
 
await page.goto('https://example.com/');
 
// 这里是错的,因为浏览器中访问不到 anchor 这个变量
const clicked = await page.evaluate(() => document.querySelector(anchor).click());

修改方法也很简单,把这个参数作为变量传递给 page.evaluate 就可以了。

const anchor = 'a';
 
await page.goto('https://example.com/');
 
// Here we add a `selector` arg and pass in the reference in `evaluate`
const clicked = await page.evaluate((selector) => document.querySelector(selector).click(), anchor);

队列和限制并发

browserless 的镜像一个核心功能是无缝限制并行和使用队列。也就是说消费程序可以直接使用 puppeteer.connect 而不需要自己实现一个队列。这避免了大量的问题,大部分是太多的 Chrome 实例杀掉了你的应用的可用资源。

$ docker run -e "MAX_CONCURRENT_SESSIONS=10" browserless/chrome

上面限制了并发连接数到10,还可以使用MAX_QUEUE_LENGTH来配置队列的长度。总体来说,每1GB内存可以并行运行10个请求。CPU 有时候会占用过多,但是总的来说瓶颈还是在内存上。

不要忘记 page.waitForNavigation

如果点击了链接之后,需要使用 page.waitForNavigation 来等待页面加载。

下面这个不行

await page.goto('https://example.com');
await page.click('a');
const title = await page.title();
console.log(title);

这个可以

await page.goto('https://example.com');
page.click('a');
await page.waitForNavigation();
const title = await page.title();
console.log(title);

屏蔽广告内容

browserless 家的镜像还有一个功能就是提供了屏蔽广告的功能。屏蔽广告可以是你的流量降低,同时提升加载速度。

只需要在连接的时候加上 blockAds 参数就可以了。

启动的时候指定 –user-data-dir

Chrome 最好的一点就是它支持你指定一个用户的数据文件夹。通过指定用户数据文件夹,每次打开的时候都可以使用上次的缓存。这样可以大大加快网站的访问速度。

const browser = await pp.launch({
    args: ["--user-data-dir=/var/data/session-xxx"]
})

不过需要注意的是,这样的话会保存上次访问时候的 cookie,这个不一定是你想要的效果。

构建最小版本的 Chrome

构建最小版本的 Chromium

为什么需要 Chrome 浏览器渲染

  1. 动态 ajax 页面
  2. 页面编码异常或者结构过甚,lxml 无法解析

dirty page examples:

  1. 页面的 style 在 html 外面,并且有黏贴的 Word 文档。http://gzg2b.gzfinance.gov.cn/gzgpimp/portalsys/portal.do?method=pubinfoView&&info_id=-2316ce5816ab90783eb-720f&&porid=gsgg&t_k=null
  2. 有好多个 html 标签,并且编码不一致。http://www.be-bidding.com/gjdq/jingneng/show_zbdetail.jsp?projectcode=1180903010&flag=3&moreinfo=true

优化方案

  1. 不加载图片和视频,但是保留占位
  2. 使用 proxy api 更改代理
  3. 禁用 H5 相关 API
  4. 删除 ICU 相关

参考文献

  1. https://peter.sh/experiments/chromium-command-line-switches/
  2. https://joydig.com/port-chromium-to-embedded-linux/
  3. Android 上的 Chrome 裁剪,值得借鉴。https://blog.csdn.net/mogoweb/article/details/76653627
  4. 架构图 https://blog.csdn.net/mogoweb/article/details/76653627
  5. webkit 架构图 https://blog.csdn.net/a957666743/article/details/79702895
  6. Chrome proxy API https://developer.chrome.com/extensions/proxy
  7. Chrome 嵌入式裁剪,直击底层 https://joydig.com/category/chromium/
  8. 官方构建教程 https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md
  9. 编译选项https://blog.csdn.net/wanwuguicang/article/details/79751503

awesome crawlers

无头浏览器的使用

  1. 神器:Puppeteer Recorder,可以录制浏览器操作:https://github.com/checkly/puppeteer-recorder

爬虫方案

其他列表

https://github.com/facert/awesome-spider

电商爬虫

拼多多

  1. https://github.com/onetwo1/pinduoduo

大众点评反爬:

  1. https://www.v2ex.com/t/558529#reply18
  2. https://github.com/Northxw/Dianping

电商爬虫

  1. 电商爬虫系统:京东,当当,一号店,国美爬虫,论坛、新闻、豆瓣爬虫 https://github.com/wanghuafeng/e-business

IT 桔子

  1. https://www.makcyun.top/web_scraping_withpython7.html
  2. https://blog.csdn.net/Michael_Cool/article/details/80098990
  3. https://github.com/shulisiyuan/ITjuziSpider/blob/master/itjuziCompanySpider.py

头条视频

  1. https://github.com/fourbrother/python_toutiaovideo

微博爬虫

  1. https://github.com/jinfagang/weibo_terminater

PornHub 爬虫

  1. https://github.com/xiyouMc/WebHubBot
招聘网站

拉钩爬虫:https://mp.weixin.qq.com/s/uQ_KO84ydPU9qj8nm93gnQ

房产网站:https://github.com/lihansunbai/Fang_Scrapy 爬取58同城、赶集网、链家、安居客、我爱我家网站的房价交易数据。

破解CloudFlare 的反爬措施

https://github.com/Anorov/cloudflare-scrape

反爬技术方案

模拟登录:

https://github.com/xchaoinfo/fuck-login

https://github.com/SpiderClub/smart_login

爬虫框架

https://github.com/yijingping/unicrawler

代理抓取

https://github.com/fate0/getproxy

字体反爬整体方案

https://zhuanlan.zhihu.com/p/37838586

反爬教程:https://github.com/FantasticLBP/Anti-WebSpider

浏览器指纹技术——利用 Header 顺序:

  1. https://cnodejs.org/topic/5060722e01d0b80148172f55
  2. https://gwillem.gitlab.io/2017/05/02/http-header-order-is-important/

深度学习破解点击验证码

  1. https://zhuanlan.zhihu.com/p/34186397
  2. https://github.com/RunningGump/gsxt_captcha
  3. https://github.com/cos120/captcha_crack
  4. CNN 端到端验证码 https://www.jianshu.com/p/08e9d2669b42
  5. Pytorch 验证码识别 https://www.cnblogs.com/king-lps/p/8724361.html
  6. 端到端的不定长验证码识别 https://github.com/airaria/CaptchaRecognition?ts=4
  7. CNN 端到端验证码识别https://github.com/dee1024/pytorch-captcha-recognition
  8. 基于 CNN 的验证码识别 https://github.com/junliangliu/captcha
  9. 变长验证码识别 https://www.jianshu.com/p/25655870b458
  10. https://github.com/cos120/captcha_crack
  11. 生成验证码,可用作训练数据 https://github.com/lepture/captcha
  12. https://github.com/lllcho/CAPTCHA-breaking
  13. https://github.com/yeguixin/captcha_solver

JS 解密与登录

https://github.com/CriseLYJ/awesome-python-login-model https://github.com/OFZFZS/JS-Decryption

中关村 逗游 博客园,37游戏,188游戏中心,立德金融,民投金服,同花顺,金融街,4366, 哔哩哔哩,中国移动 shop99, 连载阅读国美WAP端京东,58同城拉钩起点 滴滴打车 网易博客 手机百度 5173 懒人听书 阿里邮箱 虾米 唯品会 汽车之家 爱卡汽车 酷狗 搜狐微信公众号 ,楚楚街

裁判文书网:https://github.com/sml2h3/mmewmd_crack_for_wenshu

安居客反爬破解:https://www.v2ex.com/t/512956#;

上千家企业新闻网站 https://github.com/NolanZhao/news_feed

数据集

金融公开数据集:https://github.com/PKUJohnson/OpenData/wiki

一个很全的词库

https://github.com/fighting41love/funNLP

涉及内容包括:中英文敏感词、语言检测、中外手机/电话归属地/运营商查询、名字推断性别、手机号抽取、身份证抽取、邮箱抽取、中日文人名库、中文缩写库、拆字词典、词汇情感值、停用词、反动词表、暴恐词表、繁简体转换、英文模拟中文发音、汪峰歌词生成器、职业名称词库、同义词库、反义词库、否定词库、汽车品牌词库、汽车零件词库、连续英文切割、各种中文词向量、公司名字大全、古诗词库、IT词库、财经词库、成语词库、地名词库、历史名人词库、诗词词库、医学词库、饮食词库、法律词库、汽车词库、动物词库、中文聊天语料、中文谣言数据、百度中文问答数据集、句子相似度匹配算法集合、bert资源、文本生成&摘要相关工具、cocoNLP信息抽取工具、国内电话号码正则匹配、清华大学XLORE:中英文跨语言百科知识图谱、清华大学人工智能技术系列报告、自然语言生成、NLU太难了系列、自动对联数据及机器人、用户名黑名单列表、罪名法务名词及分类模型、微信公众号语料、cs224n深度学习自然语言处理课程、中文手写汉字识别、中文自然语言处理 语料/数据集、变量命名神器、分词语料库+代码、任务型对话英文数据集、ASR 语音数据集 + 基于深度学习的中文语音识别系统、笑声检测器、Microsoft多语言数字/单位/如日期时间识别包、中华新华字典数据库及api(包括常用歇后语、成语、词语和汉字)、文档图谱自动生成、SpaCy 中文模型、Common Voice语音识别数据集新版、神经网络关系抽取、基于bert的命名实体识别、关键词(Keyphrase)抽取包pke、基于医疗领域知识图谱的问答系统、基于依存句法与语义角色标注的事件三元组抽取、依存句法分析4万句高质量标注数据、cnocr:用来做中文OCR的Python3包、中文人物关系知识图谱项目、中文nlp竞赛项目及代码汇总、中文字符数据、speech-aligner: 从“人声语音”及其“语言文本”产生音素级别时间对齐标注的工具、AmpliGraph: 知识图谱表示学习(Python)库:知识图谱概念链接预测、Scattertext 文本可视化(python)、语言/知识表示工具:BERT & ERNIE、中文对比英文自然语言处理NLP的区别综述、Synonyms中文近义词工具包、HarvestText领域自适应文本挖掘工具(新词发现-情感分析-实体链接等)、word2word:(Python)方便易用的多语言词-词对集:62种语言/3,564个多语言对、语音识别语料生成工具:从具有音频/字幕的在线视频创建自动语音识别(ASR)语料库。

https://github.com/platonai/pulsar/blob/master/README.zh.md