crawler

Playwright: 比 Puppeteer 更好用的浏览器自动化工具

在 Playwright 之前,我一般会使用 Selenium 或者 Puppeteer 来进行浏览器自动化操作。然而,Selenium 经常会有一些奇怪的 bug, Puppeteer 则是没有官方 Python 版,非官方版本也只有 async 版本,并且也是有一些奇怪的 bug. 另外,众所周知,Python 的 Async API 并不是那么好使。

Playwright 是微软出品的浏览器自动化工具,代码质量应该是有足够保证的。而且它还官方支持同步版的 Python API, 同时支持三大浏览器,所以赶紧切换过来了。

特别注意 Playwright 的拼写,别把中间的 “w” 丢了。

安装

pip install playwright==1.8.0a1  # 很奇怪,必须指定版本,不指定会安装到一个古老的版本
python -m playwright install  # 安装浏览器,此处国内网络可能会有问题(你懂的),请自行解决

基本使用

Playwright 支持 Firefox/Chrome/WebKit(Safari). 其中 webkit 最轻量了,所以没有什么特殊需求最好使用 webkit, 不要使用 chromium.

from playwright.sync_api import sync_playwright as playwright

with playwright() as pw:
    webkit = pw.webkit.launch(headless=False)
    context = webkit.new_context()  # 需要创建一个 context
    page = context.new_page()  # 创建一个新的页面
    page.goto("https://www.apple.com")
    print(page.content())
    webkit.close()

Playwright 官方推荐使用 with 语句来访问,不过如果你不喜欢的话,也可以用 pw.start() 和 pw.stop().

新概念:Context

和 Puppeteer 不同的是,Playwright 新增了 context 的概念,每个 context 就像是一个独立的匿名模式会话,非常轻量,但是又完全隔离。比如说,可以在两个 context 中登录两个不同的账号,也可以在两个 context 中使用不同的代理。

通过 context 还可以设置 viewport, user_agent 等。

context = browser.new_context(
  user_agent='My user agent'
)
context = browser.new_context(
  viewport={ 'width': 1280, 'height': 1024 }
)
context = browser.new_context(
    http_credentials={"username": "bill", "password": "pa55w0rd"}
)

# new_context 其他几个比较有用的选项:
ignore_https_errors=False
proxy={"server": "http://example.com:3128", "bypass": ".example.com", "username": "", "password": ""}
extra_http_headers={"X-Header": ""}

context 中有一个很有用的函数context.add_init_script, 可以让我们设定在调用 context.new_page 的时候在页面中执行的脚本。

# hook 新建页面中的 Math.random 函数,总是返回 42
context.add_init_script(script="Math.random = () => 42;")
# 或者写在一个文件里
context.add_init_script(path="preload.js")

还可以使用 context.expose_bindingcontext.expose_function 来把 Python 函数暴露到页面中,不过个人感觉还是使用 addinitscript 暴露 JS 函数方便一些。

和 Puppeteer 一样,Playwright 的核心概念依然是 page, 核心 API 几乎都是 page 对象的方法。可以通过 context 来创建 page.

页面基本操作

按照官网文档,调用 page.goto(url) 后页面加载过程:

  1. 设定 url
  2. 通过网络加载解析页面
  3. 触发 page.on(“domcontentloaded”) 事件
  4. 执行页面的 js 脚本,加载静态资源
  5. 触发 page.on(“laod”) 事件
  6. 页面执行动态加载的脚本
  7. 当 500ms 都没有新的网络请求的时候,触发 networkidle 事件

page.goto(url) 会跳转到一个新的链接。默认情况下 Playwright 会等待到 load 状态。如果我们不关心加载的 CSS 图片等信息,可以改为等待到 domcontentloaded 状态,如果页面是 ajax 加载,那么我们需要等待到 networkidle 状态。如果 networkidle 也不合适的话,可以采用 page.waitforselector 等待某个元素出现。不过对于 click 等操作会自动等待。

page.goto(url, referer="", timeout=30, wait_until="domcontentloaded|load|networkidle")

Playwright 会自动等待元素处于可操作的稳定状态。当然也可以用 page.wait_for_* 函数来手工等待:

page.wait_for_event("event", event_predict, timeout)
page.wait_for_function(js_function)
page.wait_for_load_state(state="domcontentloaded|load|networkidle", timeout)
page.wait_for_selector(selector, timeout)
page.wait_for_timeout(timeout)  # 不推荐使用

对页面的操作方法主要有:

# selector 指的是 CSS 等表达式
page.click(selector)
page.fill(selector, value)  # 在 input 中填充值

# 例子
page.click("#search")

# 模拟按键
page.keyboard.press("Z")
# 支持的按键名字
# F1 - F12, Digit0- Digit9, KeyA- KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp
# 如果是单个字母,那么是大小写敏感的,比如 a 和 A 发送的是不同的按键
# 组合按键
page.keyboard.press("Shift+A")  # Shift, Ctrl, Meta, Alt, 其中 Meta 应该是相当于 Win/Cmd 键
# 其他的按键参考这里:https://playwright.dev/python/docs/api/class-keyboard

# 模拟输入
page.keyboard.type("hello")

在编写爬虫的过程中,向下滚动触发数据加载是个很常见的操作。但是如果想要向下滚动页面的话,似乎现在没有特别好的方法。本来我以为会有 page.scroll 这种方法的,然而并没有。在没有官方支持之前,暂时可以使用 page.keyboard.press("PageDown") 来向下滚动。

获取页面中的数据的主要方法有:

page.url  # url
page.title()  # title
page.content()  # 获取页面全文
page.inner_text(selector)  # element.inner_text()
page.inner_html(selector)
page.text_content(selector)
page.get_attribute(selector, attr)

# eval_on_selector 用于获取 DOM 中的值
page.eval_on_selector(selector, js_expression)
# 比如:
search_value = page.eval_on_selector("#search", "el => el.value")

# evaluate 用于获取页面中 JS 中的数据,比如说可以读取 window 中的值
result = page.evaluate("([x, y]) => Promise.resolve(x * y)", [7, 8])
print(result) # prints "56"

选择器表达式

在上面的代码中,我们使用了 CSS 表达式(比如#button)来选取元素。实际上,Playwright 还支持 XPath 和自己定义的两种简单表达式,并且是自动识别的。

自动识别实际上是有一点点坑的,凡是 . 开头的都会被认为是 CSS 表达式,比如说 .close, .pull-right 等等,但是 .//a 这种也会被认为是 CSS 导致选择失败,所以使用 XPath 的时候就不要用 . 开头了。

# 通过文本选择元素,这是 Playwright 自定义的一种表达式
page.click("text=login")

# 直接通过 id 选择
page.click("id=login")

# 通过 CSS 选择元素
page.click("#search")
# 除了常用的 CSS 表达式外,Playwright 还支持了几个新的伪类
# :has 表示包含某个元素的元素
page.click("article:has(div.prome)")
# :is 用来对自身做断言
page.click("button:is(:text('sign in'), :text('log in'))")
# :text 表示包含某个文本的元素
page.click("button:text('Sign in')")  # 包含
page.click("button:text-is('Sign is')")  # 严格匹配
page.click("button:text-matches('\w+')")  # 正则
# 还可以根据方位匹配
page.click("button:right-of(#search)")  # 右边
page.click("button:left-of(#search)")  # 左边
page.click("button:above(#search)")  # 上边
page.click("button:below(#search)")  # 下边
page.click("button:near(#search)")  # 50px 之内的元素

# 通过 XPath 选择
page.click("//button[@id='search'])")
# 所有 // 或者 .. 开头的表达式都会默认为 XPath 表达式

对于 CSS 表达式,还可以添加前缀css=来显式指定,比如说 css=.login 就相当于 .login.

除了上面介绍的四种表达式以外,Playwright 还支持使用 >> 组合表达式,也就是混合使用四种表达式。

page.click('css=nav >> text=Login')

复用 Cookies 等认证信息

在 Puppeteer 中,复用 Cookies 也是一个老大难问题了。这个是 Playwright 特别方便的一点,他可以直接导出 Cookies 和 LocalStorage, 然后在新的 Context 中使用。

# 保存状态
import json
storage = context.storage_state()
with open("state.json", "w") as f:
    f.write(json.dumps(storage))

# 加载状态
with open("state.json") as f:
    storage_state = json.loads(f.read())
context = browser.new_context(storage_state=storage_state)

监听事件

通过 page.on(event, fn) 可以来注册对应事件的处理函数:

def log_request(intercepted_request):
    print("a request was made:", intercepted_request.url)
page.on("request", log_request)
# sometime later...
page.remove_listener("request", log_request)

其中比较重要的就是 request 和 response 两个事件

拦截更改网络请求

可以通过 page.on(“request”) 和 page.on(“response”) 来监听请求和响应事件。

from playwright.sync_api import sync_playwright as playwright

def run(pw):
    browser = pw.webkit.launch()
    page = browser.new_page()
    # Subscribe to "request" and "response" events.
    page.on("request", lambda request: print(">>", request.method, request.url))
    page.on("response", lambda response: print("<<", response.status, response.url))
    page.goto("https://example.com")
    browser.close()

with playwright() as pw:
    run(pw)

其中 request 和 response 的属性和方法,可以查阅文档:https://playwright.dev/python/docs/api/class-request

通过 context.route, 还可以伪造修改拦截请求等。比如说,拦截所有的图片请求以减少带宽占用:

context = browser.new_context()
page = context.new_page()
# route 的参数默认是通配符,也可以传递编译好的正则表达式对象
context.route("**/*.{png,jpg,jpeg}", lambda route: route.abort())
context.route(re.compile(r"(\.png$)|(\.jpg$)"), lambda route: route.abort())
page.goto("https://example.com")
browser.close()

其中 route 对象的相关属性和方法,可以查阅文档:https://playwright.dev/python/docs/api/class-route

灵活设置代理

Playwright 还可以很方便地设置代理。Puppeteer 在打开浏览器之后就无法在更改代理了,对于爬虫类应用非常不友好,而 Playwright 可以通过 Context 设置代理,这样就非常轻量,不用为了切换代理而重启浏览器。

context = browser.new_context(
    proxy={"server": "http://example.com:3128", "bypass": ".example.com", "username": "", "password": ""}
)

杀手级功能:录制操作直接生成代码

Playwright 的命令行还内置了一个有趣的功能:可以通过录制你的点击操作,直接生成 Python 代码。

python -m playwright codegen http://example.com/

Playwright 还有很多命令行功能,比如生成截图等等,可以通过 python -m playwright -h 查看。

其他

除此之外,Playwright 还支持处理页面弹出的窗口,模拟键盘,模拟鼠标拖动(用于滑动验证码),下载文件等等各种功能,请查看官方文档吧,这里不赘述了。对于写爬虫来说,Playwright 的几个特性可以说是秒杀 Puppeteer/Pyppeteer:

  1. 官方同步版本的 API
  2. 方便导入导出 Cookies
  3. 轻量级设置和切换代理
  4. 支持丰富的选择表达式

快点用起来吧!

参考

  1. https://playwright.dev/python/docs/core-concepts

编写一个爬虫的思路,当遇到反爬时如何处理

本站/公众号/专栏不误正业好久了,今天终于写一篇爬虫的文章,然而并没有案例,也没有代码。

写了这么多年爬虫了,经常还是会撞上反爬机制。虽然大多数时候都能解决,但是毕竟反爬机制多种多样,有时候遇到一个许久不见的反爬机制,也会感到手生,一时想不上来应对方法,而浪费不少时间。最近写了不少爬虫,接下来一段时间又不写了,趁着手还比较熟,记录一下备忘,方便大家也方便自己。

之前写过一篇常用的反爬虫封禁手段概览, 但是主要是从反爬的角度来的,这篇主要从写爬虫的角度来说说。

开章明义,当遇到反爬机制时,想要做到把数据爬下来,无非四个方法:

  1. 加代理
  2. 降速度
  3. 破解接口
  4. 多注册几个账户

好多文章为了显示自己高大上,吹些什么高并发呀,分布式,机器学习破解验证码的幺蛾子,都是扯淡。与其扯这些东西,不如老老实实把数据爬下来才是王道,如果非要扯上一些 fancy 的东西,那把监控做好比啥都重要

补充说明一下,本文探讨的是数据收集型的小型爬虫,也就是你要对少数站点在较短时间内收集大量信息。而非搜索引擎型全网爬虫,即对大量站点在较长时间内收集综合信息。(全网当然要上高并发了)

为什么说爬虫不要扯高并发?

我们知道计算机程序按瓶颈不同大概分为两类,CPU 密集型和 IO 密集型。CPU 密集型就是偏重计算的任务,比如说编解码啥的;IO 密集型就是偏重于网络的任务,比如说下载或者 web 服务器。那么爬虫是哪种呢?你估计要回答 IO 密集型,恭喜你答对了。但是这不是我想说的重点,重点是爬虫不光是 IO 密集型的任务,实际上我想把它称作 IP 密集型任务。

什么是 IP 密集型任务呢?按照上面的定义我们知道,也就是说,对爬虫来说,最瓶颈的地方其实是你持有的 IP 的数量!作为一个合格的爬虫编写者,你肯定已经擅长伪造各种 HTTP headers, 破解 JS 的加密参数,但是唯独一个 — 来源 IP — 你是无法伪造的。好多看起来很难搞的事情,如果对方站点的小霸王服务器撑得住,只要加上足够的 IP 就很简单啦,不用绞尽脑汁去想各种策略了。

为什么不要用现成的框架?

上面说了,所谓的”高并发”对爬虫没有任何卵用,那么像是 Scrapy 这种采用了协程以便提高并发的框架我就不是很懂了。以前我专门写过一篇为什么不要用 Scrapy 的文章,所以这里就不再展开细说了。

另外如果你爬虫写多了肯定有自己的一套东西了,这时候你可能会有自己的一个小框架,这是可以的。但是我还是想提两点:

  1. 千万不要做成从模板生成新的爬虫项目的功能。假如你改了模板里的一个 bug 怎么办?以前生成的爬虫还挨个修改吗?
  2. 框架尽量简单,把可以复用的功能提取成单独的 utility 函数或者库。难免有需要改框架或者不适用框架的时候,这时候依然可以复用单独的模块。

拿到抓取任务时的思路

言归正传,我们开始说当拿到一个站点需要爬取时该如何处理。

数据量较小的爬取

首先开始 easy 模式。如果你要抓的网站结构比较简单,而你要的数据也比较少。那么你首先要考虑的是不要编写爬虫. 在浏览器控制台里写个 js 表达式 console.log 一下说不定就把数据导出来了。

如果你要的数据稍微多一点时,这时候点开一个页面然后复制数据出来可能就比较复杂了。这时候可以考虑写个小脚本,别直接 while True 写个死循环就了事儿,每爬一个页面至少 time.sleep(1) 是对对方网站最起码的尊重。当然你的老板可能要数据比较急,但是多少也要悠着点。

浏览器动态加载怎么办?

初学者在这里可能遇到第一个坑:动态网页。这时候可能是个好事儿,也可能是个坏事儿。如果是动态网页,数据自然是 ajax 加载的,如果 ajax 请求没有参数验证的话,那么就简单了,只是从解析 html 变成了解析 json 而已。

另一种情况是接口是需要参数验证的,这时候又分两种处理方式:

  1. 如果只是爬一下数据,直接上浏览器,爬完了事儿。
  2. 如果嫌浏览器资源占用太多,那么往往就会需要破解接口,这种情况下需要一定的 JS 逆向能力。

有的网站对浏览器甚至还做了一些限制,他会检测你的浏览器是正常的用户浏览器还是用程序控制的自动化浏览器。不过这都不是问题,无非就是伪造一下 webdriver 对象和 navigator 对象罢了。这个我也写过一篇具体文章讲如何伪造。

当然这时候也可能遇到情况比较简单的特殊情况,那就是对方的某个更新接口是固定的,而且加密参数里面没有时间戳,那么直接重复请求这个接口就行了。一般来说这种情况算是”瞎猫撞见死耗子”, 多数情况下,接口的签名都校验了搜索的参数和时间戳,也就是你变换关键词,或者想重放请求的话是行不通的,这时候就老老实实破解吧。

一般破解 JS 其实也都不难,常用的信息摘要,或者加密方法也没多少。不过先别接着破解,花上五分钟搜索一下有没有别人已经破解过了,可能就省了你半天到几天的功夫,何乐而不为呢?

实在要开始破解的话,在 JS 的控制台中全局搜索 (Opt+Cmd+F) 一下 AES, MD5 之类的关键词,可能就有收获。另一方面在 ajax 请求上加上断点,逐步找到加密的函数。

找到加密函数之后,如果简单一点的,直接写在一个函数里的,可以抽取出来直接调用 node 执行算出参数,或者你比较勤快用 Python 重写一下都可以。然而比较棘手的是有些函数是和 window 对象或者 DOM 绑定在一起的,这时候也可以考虑把整个 JS 文件保存下来,补全需要的接口。

最常见的 IP 封禁

正如我们前面说的,作为一个爬虫老手,伪造和破解简单的加密都是基本功了,比较蛋疼的还是封禁 IP, 这你下什么苦功夫也没法解决的,是一个资本问题。

当我们爬取的速率比较快的时候,就可能被对方拉黑 IP, 这时候有可能是临时性拉黑,有可能是持续性拉黑,有可能是永久性拉黑。

永久性拉黑比较狠,也没啥办法,直接换 IP 吧。需要区分的是临时性的拉黑和持续性拉黑。如果是临时性拉黑,也就是你的请求超过阈值了就会请求失败,但是过段时间自己又恢复了,那么你程序逻辑也不用改,反正就一直请求呗,总有数据的。如果是持续性拉黑就比较坑了,也就是说如果你被拉黑了还不停止请求,那么就一直出不了小黑屋,必须停下来 sleep 几秒。这时候你的程序的逻辑就需要适应这种机制。如果是单独的脚本还好,对于一些标准化的系统就需要考虑这些机制。

当我们需要换 IP 的时候,肯定不能手工去记得过几分钟换一下子 IP 了,那也太烦人了,一般是需要一个 IP 池子的。

代理 IP 按照质量和来源又分为几类:

  1. 比较垃圾的公用 IP
  2. 比较稳定的机房 IP
  3. 民用网段 IP

网上有一些站点会提供一些免费的代理 IP, 估计他们都是扫来的。这些 IP 可能都有无数的程序在扫描,使用他们,所以可以说是公用的 IP 了。通过收集验证这些 IP, 可以构造一个代理池子。如果实在很穷,或者抓取量不是很大,可以用这种 IP. 虽然这些 IP 特别慢,失败率特别高,总比用自己的一个出口 IP 要好一些。

比较稳定的机房 IP. 这种一般就需要花钱买了,稍微想多抓点数据,一个月 100 那是起步。对付大多数的抓取已经足够了。

对于有一些变态的站点,他们甚至会验证来源 IP 的用途。比如说一看你 IP 来自阿里云机房,啥也不说直接拉黑。这时候就需要所谓的”民用 IP”了。这种有专门的厂商和 App 合作来提供民用网络出口,也可以自己买 ADSL 机器自动拨号搭建,反正成本都是非常非常高了,一个月至少 1000 起步了。

带上账户或者验证码

IP 毕竟算是匿名的。对于一些数据更敏感的网站来说,他们可能要求你登录后才能访问。如果数据不多,那么直接用自己账户跑一下就完了。如果每个账户的访问额度有限,或者要爬的数据特别多,那可能需要注册不少账户,这时候只要不是付费账户,那么其实都好说(除了微信). 你可以选择:

  • 买一些账号,比如说微博账号也就一块半一个而已。
  • 自己注册一些,网上有免费邮箱,也有手机短信接码平台。

这时候不要急着去写个脚本自动化注册啥的,可能你并不需要那么多的账户。

比需要账户稍微弱一点的限制是验证码,图文验证码现在都不是问题了,直接打码平台或者训练个模型都很简单。复杂一点的点按,图片选字等就当饭后甜点吧,弄得出来弄不出来全看运气了。在这里我想说的一点是,请分辨一下你要爬的网站是每次请求必须验证码,还是在封禁 IP 之前出验证码。如果不是每次请求都出验证码,直接加大代理池吧,没必要抠这些东西,真的,时间才是最宝贵的。

不过这里需要特别注意的一点是:一定要考虑清楚其中的法律风险,需要账户访问已经说明这不是公开数据了,可能会触发对方的商业利益或者触犯用户的隐私,一定三思而后爬。

事情没那么简单

如果一个网站只采用一种手段,那么我们分析起问题来就简单了。然而遗憾的是,基本没这种好事儿。比如说一个网站可能即检测了浏览器的 webdriver, 而且还要封 IP, 这时候你就得用浏览器再加上代理,有时候给浏览器设置代理这件事情还挺复杂。还有可能你用账户 Cookies 爬起来飞快,但是竟然还会封 IP, 哼哧哼哧加上了代理池,又发现账户不能换登录 IP, 简直想骂人。

还有一些神仙网站,比如说淘宝或者裁判文书网,可能本文说的都完全没有任何价值,不过好在我大概不会碰这些网站了。

选哪些接口爬

其实我发现一般常见爬虫任务无非几种:

  1. 找到网站的一个列表,把里面数据全都爬下来。
  2. 自己弄些 id 或者关键词,通过查询或者搜索接口把数据全都爬下来。
  3. 刷网站的一些更新或者推荐接口,以期不断抓取。

首选的肯定是无状态的接口,搜索接口在大多说网站还是可以直接就拿来用的。如果有需要登录的,也有不需要登录的接口,那想都不用想,肯定爬不需要登录的接口。哪怕登录了,好多还是会封 IP, 何必呢?有些网站的详情或者翻页可能就需要登录了,实在没办法也只能走这些接口。这时候一定要做好登录出口和普通爬虫的代理池隔离。

要判断清楚爬虫任务是爬全量数据还是增量数据。一般来说爬全量数据的需求都有点扯,一定要和需求方 argue 一下,可能对方根本就没想清楚,觉得先把数据存下来然后慢慢再想怎么用,对于这种傻 X 需求一定要顶回去,别急着跪舔或者炫技。如果一定要爬全量,那也可以慢慢来,不用非着急忙慌的把对方网站都搞挂了,这并不是啥值得炫耀的,而是应该被鄙视。而且一般网站也不会把全量数据让你都能看到,比如可能只能翻 500 页,这时候只能通过细分查询条件来尽可能多地获得一些数据。增量的话一般就好说一点,就像上面说的,定时刷一下更新或者推荐接口就好了。

要爬 App 吗?一般来说现在的网站还是有足够的数据的,除非是一些只有 App 而没有网站的站点,那就没办法了。App 的破解和 JS 其实思路一样,但是可能好多 App 加了壳,或者把加密算法写到了 C 里面,相比 JS 来说,完全不是一个数量级了。对于 App 的破解我基本一窍不通,也就是靠写一些网页爬虫混口饭吃。

你应该庆幸的一点是,你需要写的只是一个爬虫,而不是发帖的机器人,一般网站对于这种制造垃圾数据的防范机制肯定比爬虫要复杂很多。

既然谈到破解 App 了,那么再多说一点。有不少同学觉得从爬虫出发,往深了发展就是逆向,那可就大错特错了,有两点:

  1. 搞逆向工资很低啊!甚至比爬虫还低,不信自己去招聘 App 上看看。你从一个工资高的搞到一个工资低的,这不是给自己找不自在吗?为什么不往钱更多的后端发展呢?哪怕洗洗数据,玩儿玩儿 Spark/Flink 都比搞爬虫强啊,更别说逆向了。好多同学以爬虫工程师自居,实际上这个身份认知就谬之大矣,爬虫就是一个小小的工具而已,用不上作为一种职位。如果基础差,多学点计算机理论知识,后端工程师才称得上是一个职位。
  2. 相比”拿来”来说,”创造”是一件更美好的事情。爬虫当然是拿来别人的东西,逆向就更狠了,别人都明说了不给的东西,还要抢过来,这样其实是不好的,更别说其中的法律风险了。自己去做一个 App 或者一个网站,一个其他产品,让真实的用户来使用,这样的感觉更好一点。而且也好公开吹牛逼啊,你做了一个 App 有 10 万日活可以公开吹逼,但是你把人家网站底裤扒光了,总不好意思大张旗鼓吧?爬虫毕竟是灰色地带的事情,锦衣夜行真的很不爽。

这里再次特别强调一下:破解别人的 App 可能是非法行为,需要负法律责任。

最后,总结一下

所以总结下来,我觉得遇到一个网站的需要考虑的思路是:

  1. 预估下需要爬的数据和时间节点,算出来每秒需要爬多少数据。别上来就设计个啥架构,八成根本用不上。
  2. 如果需要的速率比较小,那么直接 time.sleep(5) 慢慢跑着,也就是尽量不要触发封禁。
  3. 尽量找到一个公开的,不需要登录就能访问的接口或者页面,直接上代理池,别想那么多别的。
  4. 能从一个接口拿到的数据,不要再去多请求其他的接口,尽量减少访问量。
  5. 能很快破解的 JS 也可以破解一下,比较复杂的直接上浏览器,浏览器就直接做好伪装,省得出问题。
  6. 需要登录认证的一定要考虑 Cookie 异地失效的问题,最好使用单独的高质量 IP. 做一套路由机制,保证每个 Cookie 都从同一个 IP 出去。

总之,一次解决一个问题,不要同时触发两个反爬问题,容易按下葫芦起了瓢。

就是这些吧,本文核心观点 — 最简单粗暴的还是加大电量(误加 IP 池,如果一个不够,那就两个。加钱能解决的问题都不是问题。好多同学可能觉得你这叫哪门子爬虫啊,分布式系统也没有,最好玩的逆向你说去网上抄别人的答案,哪还有毛意思啊!然而,很遗憾,这才是现实世界,对于业务来说,爬虫最重要的是你拿到有用的数据,而不是写代码写牛逼了,有这时间回家陪陪家人不好么~

参考文献

本文没有任何参考文献,纯意识流瞎写。文中引用了之前写的几篇文章,懒得贴了,感兴趣自己在网站或者公众号找吧。

PS: 监控很重要,爬虫最怕跑着跑着对面改版了或者加反爬了,有了监控才好及时发现问题。关于监控,强烈推荐 Prometheus, 可以参考我以前的文章。

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 本身是带锁的)。原因就在于,我们把对于需要并发访问的结构限制在了一个线程中。

当然如果可以用锁的话,也可以在每个 worker 线程中计数。而这种情况下,为了使用 running > 0 这个条件,一定要首先在发现新的 url 的时候 running++,在处理完整个页面之后再 running–。

手机如何使用 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

为什么不使用 scrapy?

请参考更新版本: https://yifei.me/note/838

最近面了几家公司,每当我提到头条的爬虫都是自己写的时候,对方一个下意识的问题就是: “为什么不使用开源的 scrapy?”。实际上我在头条的 lead 就是 scrapy 的 contributor,而他自己也不用自己的框架,显然说明 scrapy 不适合大型项目,那么具体问题在哪儿呢?今天终于有时间了,详细写写这个问题。

爬虫并不需要一个框架

Web 服务器是一个爬虫可以抽象出来的是各种组件。而 scrapy 太简陋了,比如说去重,直接用的是内存中的一个集合。如果要依赖 scrapy 写一个大型的爬虫,几乎每个组件都要自己实现,那有何必用 scrapy 呢?

scrapy 不是完整的爬虫框架

一个完整的爬虫至少需要两部分,fetcher 和 frontier。其中 fetcher 用于下载网页,而 frontier 用于调度。scrapy 重点实现的是 fetcher 部分,也就是下载部分。

scrapy 依赖 twisted

这一点导致 scrapy 深入后曲线非常地陡峭,要想了解一些内部的机理,必须对 twisted 比较明了。而 twisted 正如它的名字一样,是非常扭曲的一些概念,虽然性能非常好,但是要理解起来是要花上不少时间的。

scrapy 适合的领域

scrapy 主要适合一次性地从指定的站点爬取一些数据

最重要的并不是你使用不使用 Scrapy,而是你不能为每一站点去单独写一个爬虫的脚本。代码的灵活度实在太大了,对于没有足够经验的工程师来说,写出来的脚本可能很难维护。重点是要把主循环掌握在爬虫平台的手中,而不是让每一个脚本都各行其是。

参考

  1. scrapy 源码解读

为什么不使用 scrapy,而是从头编写爬虫系统?

最近面了几家公司,每当我提到头条的爬虫都是自己写的时候,对方一个下意识的问题就是:“为什么不使用开源的 scrapy?”。实际上我在头条的 lead 就是 scrapy 的 contributor,而他自己也不用自己的框架,显然说明 scrapy 不适合大型项目。那么具体问题在哪儿呢?今天终于有时间了,详细写写这个问题。

个人不喜欢 scrapy 原因一言以蔽之:高不成,低不就,弊大于利。总的来说,需要使用代码来爬一些数据的大概分为两类人:

  1. 非程序员,需要爬一些数据来做毕业设计、市场调研等等,他们可能连 Python 都不是很熟;
  2. 程序员,需要设计大规模、分布式、高稳定性的爬虫系统,对他们来说,语言都无所谓的,更别说用不用框架了。

为什么不适合初学者?

对于初学者来说用不上 scrapy 的原因很简单:

  1. scrapy 太复杂了;
  2. scrapy 采用异步模式带来的高性能和在反爬面前实际上没有任何卵用;
  3. scrapy 项目冗余的代码结构对初学者完全是过度设计。

对于一个任何一个已经入门的程序员来说,Python 都算不上一个很复杂的语言,除了不用大括号可能让一些人感觉有些不适应之外,基本上看看语法上手就能写了。但是恰恰是因为我们都是老司机了,所以不能体会到每一行代码对于外行来说可能『比登天还难』。如果不用 scrapy,可能我只需要这样:

# 以下代码未经测试,可能有些许 bug
import requests

def main():
    for i in range(100):
        rsp = requests.get(f"http://www.example.com/{i}.html")
        with open("example-{i}.html", "w") as f:
            print(f"saving {i}")
            f.write(rsp.text)

if __name__ == "__main__":
    main()

就写好了一个简单的爬虫。而使用 scrapy 呢,大概需要这样吧:

# 以下代码未经测试,可能有些许 bug
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        for i in range(100):
            yield scrapy.Request(url=f"http://www.example.com/{i}.html", callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        with open("example-%s.html" % page, "wb") as f:
            f.write(response.body)
        self.log("Save file %s" % page)

先不说代码行数增长了不少,初学者会问到这些问题:“什么是 class?为什么类还有参数?啊,什么是继承?yield 又是什么鬼,那个 scrapy.Request 又是啥?”这些都是心智负担。那么 scrapy 这些心智负担又给我们带来了什么好处呢?好处是性能和稍微统一的代码结构,但是其实这两个对初学者并没有什么卵用啊……

Scrapy 采用了 Twisted 作为基础,实现了基于协程的高并发。协程看着虽然挺好,但是对于非程序员来说,他们往往就想对一个站点做定向爬取,你说你蹭蹭蹭把并发涨上去了,无非两个后果:

  1. 对方承受不住你爬,挂掉了,你拿不到数据;
  2. 对方把你封禁了,疯狂弹验证码,你拿不到数据。

所以,对于非程序员做的一些定向爬取来说,速度是没有意义的,甚至往往是越慢越好。Scrapy out。

那么统一的代码结构有什么卵用吗?答案依然是没有。我们知道,在 web 开发领域,稍微有点规模的项目还是要使用框架的,哪怕是 flask 这种微框架。这是因为,在 web 开发领域,有经典的 MVC 模式,我们需要路由、模板、ORM 这些固定的组件,所以主循环是由框架和 web server 来控制的。而对于爬虫呢?其实没有什么固定的模式,scrapy 也仅仅是定义了几个钩子函数而已,反倒因为我们没有了主循环,在编写一些特定逻辑的时候非常受到掣肘。

另外,scrapy 提供的一些其他功能,比如说抓取的队列或者去重等等,个人感觉有过度封装的味道,而且也都是在内存里,在反爬导致爬虫挂掉这种故障面前没有什么卵用,不二次开发的话还是得重爬。对于小白来说,也不用想 redis 这些幺蛾子,其实可以用 Google 最开始使用的一个很简单的方法,就把每个新抓到的 url 写到一个 txt 文件就好了,爬虫每次重启的时候首先读取这个 txt 就好了,网上乱七八糟的教程大多是炫技的。

为什么不适合大型爬虫系统?

前面说到,scrapy 基于 twisted。twisted 是 Python 的一个异步框架,最大的问题就是太难懂了,而且现在官方应支持了 asyncio,所以 twisted 的未来堪忧,甚至比起 twisted 来说,我更愿意投入时间到 curio 这样新兴的有潜力的异步框架。第二点就是 scrapy 控制了主循环,所以二次开发相当于只能在他的框架内做一些修修补补,并且还要兼容 twisted。

scrapy 依赖 twisted 这一点导致 scrapy 深入后曲线非常地陡峭,要想了解一些内部的机理,必须对 twisted 比较明了。而 twisted 正如它的名字一样,是非常扭曲的一些概念,虽然性能非常好,但是要理解起来是要花上不少时间的。

Web 服务器是一个爬虫可以抽象出来的是各种组件。而 scrapy 太简陋了,比如说去重,直接用的是内存中的一个集合。如果要依赖 scrapy 写一个大型的爬虫,几乎每个组件都要自己实现,那有何必用 scrapy 呢?

既然要开发大型爬虫系统,那么其中很重要的一部分就是爬虫的调度了。一种比较简单的模式是 scheduler 作为 master,全局调度。另一种模式没有 master,所有的爬虫 worker 都是对等的。在实际生产中显然是第一种用的更多。Scrapy 作为一个框架,实际上只是实现了下载部分而已,很难称得上一个框架。

对于大型爬虫系统,最重要的并不是你使用不使用 Scrapy,而是你不能为每一站点去单独写一个爬虫的脚本。代码的灵活度实在太大了,对于没有足够经验的工程师来说,写出来的脚本可能很难维护。重点是要把主循环掌握在爬虫平台的手中,而不是让每一个脚本都各行其是。

显然 scheduler 这部分是不能再用一个爬虫框架来实现的,连主循环都没有怎么写逻辑呢?我们可能还要实现增量爬取,或者消费业务方发来的爬取请求等各种业务,这块显然是在 scheduler 里面的,那么这个爬虫系统无非是 scheduler 分发任务给各个 worker 来抓取。worker 还可以使用 scrapy 实现,但是呢,这个 worker 其实已经弱化为一层薄薄的 downloader 了,那我要他干嘛呢?scrapy 的核心逻辑也不过是个深度或者广度优先的遍历而已,少一个依赖不好么……

总结一下,爬虫的工作量要么在反爬,要么在调度等业务逻辑,本身只是一个 requests.get 而已,scrapy 提供的种种抽象对于初学者太复杂,大型系统又用不上,所以个人不推荐使用包括但不限于 scrapy 在内的所有爬虫框架

建议所有认为学习框架会使自己变强的人读读:Stop learning frameworks 和评论,中文翻译

以上仅代表个人观点,欢迎讨论,不要人身攻击。

参考

  1. scrapy 源码解读

知乎移动端接口分析

最近想注册一些知乎的机器人玩玩儿,比如给自己点赞之类的,通过抓包分析,获得了完整注册登录流程。

注册和登录抓包

1 POST https://api.zhihu.com/auth/digits
      ← 401 application/json 97b 246ms
2 GET https://api.zhihu.com/captcha
     ← 200 application/json 22b 233ms
3 PUT https://api.zhihu.com/captcha
     ← 202 application/json 5.46k 323ms
4 POST https://api.zhihu.com/captcha
      ← 201 application/json 16b 295ms
5 POST https://api.zhihu.com/sms/digits
      ← 201 application/json 16b 353ms
6 POST https://api.zhihu.com/validate/digits
      ← 201 application/json 16b 409ms
7 POST https://api.zhihu.com/validate/register_form
      ← 200 application/json 16b 279ms
8 POST https://api.zhihu.com/register
      ← 201 application/json 761b 529ms

逐行分析一下每个包:

  1. 这个请求发送了 username: +86155xxxxxxxx 请求,然后返回了 缺少验证码票据,应该是表示缺少验证码。
  2. 应该不是请求验证码,而是请求是否需要验证码,返回了"show_captcha": false,虽然表示的是不需要验证码,但是还是弹出了验证码,奇怪。
  3. 注意这个请求是 PUT,POST 参数height: 60, width: 240。然后返回了验证码:{"img_base64": ...}, base64 解码后就是验证码
  4. 这一步 POST 正确的 captcha 并通过验证,参数是:input_text: nxa8, 返回是:{ "success": true }
  5. 这一步请求发送短信验证码,POST 参数是:phone_no: +86155xxxxxxxx, 发挥是:{ "success": true }
  6. 提交验证码,POST 参数是:phone_no: +86155xxxxxxxx, digits: xxxxxx, 返回是:{ "success": true }
  7. 填写用户信息,POST 参数是:phone_no: +86155xxxxxxxx, gender: 0, fullname: XXX, 返回是:{ "success": true }
  8. 上一步注册了用户,这一步是向知乎请求新的 access token。

请求 POST 参数:

digits:        865405
fullname:      Lucindai
phone_no:      +8615568995304
register_type: phone_digits

返回数据如下:

{
    "access_token": "...",
    "cookie": { },
    "expires_in": 2592000,
    "lock_in": 1800,
    "old_id": 155681178,
    "refresh_token": "...",
    "token_type": "bearer",
    "uid": "...",
    "unlock_ticket": "...",
    "user_id":...
}

其中的 refresh token 和 access token 都是 OAuth2 中的参数,可以用于使用 OAuth2 访问知乎的 API。可以使用 zhihu_oauth 这个库来访问知乎。

知乎的 API 还需要在 header 中设定一些特殊参数,可以参考 zhihu_oauth 中的参数

再注册成功之后还应该设定密码,这样之后就可以使用密码登录了。

PUT https://api.zhihu.com/account/password
new_password=xxxxxx

与桌面 API 的转换

有意思的是,知乎的移动版 API 实际上和桌面版 API 是一致的

如何破解被 JS 加密的数据

由于网页和 JavaScript 都是明文的,导致很多 API 接口都直接暴露在爬虫的眼里,所以好多网站选择使用混淆后的 JavaScript 来加密接口。其中有分为两大类:

  1. 通过 JavaScript 计算一个参数或者 Cookie 作为接口的签名验证
  2. 返回的数据是加密的,需要使用 JavaScript 解密

不过总的来说,这两种加密的破解思路都是一样的。

  1. 找到相关的网络请求。如果找不到,清空缓存,尝试触发
  2. 打断点找到相关代码,可以是 ajax 断点或者 js 断点。或者直接看网络请求的 initiator
  3. 逐层分析,找到加密函数
  4. 使用 node 执行 js 代码获得数据

具体步骤

有空了再写。

参考:

  1. 中国天气质量网返回结果加密的破解
  2. 破解 Google 翻译的 token
  3. JavaScript 生成 Cookie
  4. 常见加密算法
  5. 微博 Cookie 生成
  6. 又一篇微博的
  7. 威锋网 X-Request-ID

爬虫 IP 封禁与反封禁

反爬虫的核心在于区分开正常用户访问和恶意爬虫用户。来源 IP 是访问很重要的一个特征,我们可以从来源 IP 的角度来做出不少反爬虫策略。

  • 是否是代理IP
  • 是否是民用IP
  • IP 地理信息

一般来说,大规模的爬虫我们都会放到服务器上去跑,搭建代理集群也会在服务器上,而正常用户的IP地址则来自家用IP范围内。这就给反爬虫的一方提供了便利,对于来自数据中心的请求可以直接限制访问甚至直接屏蔽掉,而对于家用的IP地址则宽容一些。

下面我们来看几个实例

直接爬取网站

一般正常用户的页面访问量很小,如果发现某个 IP 的访问量特别大,那么肯定是爬虫,直接封禁即可,或者每次都需要输入验证码访问。

IP 被封禁后一般不会被解封,或者需要很长时间,这时候只有两种思路,要么降低频率,更改自己的行为特征,避免被封,要么更换 IP。一般来说,不管怎样更改自己的行为,访问量还是很难降下来的,这时候只能换一个 IP 继续爬。

使用代理网站提供的代理IP

一些黑客会使用端口扫描器扫描互联网上的开放代理,然后免费或者付费提供给其他用户使用,比如下面这些网站:

免费代理

但是这些网站的代理中能直接使用的可能不到10%,而且失效时间很短。所以要使用这些代理 IP,需要首先爬取这些网站,然后随取随用。

利用 ADSL 服务器更换 IP

网上有一些小的厂商代理了各地运营商的服务,搭建了一些小的服务器,一般内存只有 512M,而硬盘只有 8G,但是好处是通过 ADSL 上网,因此可以随时更换 IP。比如笔者搭建的这个动态代理:

ADSL

每三十分钟更换一次 IP,而这些服务器也很便宜,在 100-200 每月,所以大可以搭建一个集群,这样基本上一个 IP 被封之前也基本被换掉了。

要封禁这种用户也很简单,可以看出虽然 IP 在更换,但是基本上还是在一个 B 段之内,一个 B 段也就6w个用户,直接封了就行了

利用数据中心提供的更换 IP 接口来

有些爬虫会利用阿里云或者AWS的弹性 IP 来爬数据,反爬虫的第一步可以把阿里云的 IP 都屏蔽掉,正常用户一般是不会用这些 IP 来访问的。

附录

阿里云的出口 IP 列表:

deny 42.96.128.0/17;
deny 42.120.0.0/16;
deny 42.121.0.0/16;
deny 42.156.128.0/17;
deny 110.75.0.0/16;
deny 110.76.0.0/19;
deny 110.76.32.0/20;
deny 110.76.48.0/20;
deny 110.173.192.0/20;
deny 110.173.208.0/20;
deny 112.74.0.0/16;
deny 112.124.0.0/16;
deny 112.127.0.0/16;
deny 114.215.0.0/16;
deny 115.28.0.0/16;
deny 115.29.0.0/16;
deny 115.124.16.0/22;
deny 115.124.20.0/22;
deny 115.124.24.0/21;
deny 119.38.208.0/21;
deny 119.38.216.0/21;
deny 119.42.224.0/20;
deny 119.42.242.0/23;
deny 119.42.244.0/22;
deny 120.24.0.0/14;
deny 120.24.0.0/16;
deny 120.25.0.0/18;
deny 120.25.64.0/19;
deny 120.25.96.0/21;
deny 120.25.108.0/24;
deny 120.25.110.0/24;
deny 120.25.111.0/24;
deny 121.0.16.0/21;
deny 121.0.24.0/22;
deny 121.0.28.0/22;
deny 121.40.0.0/14;
deny 121.42.0.0/18;
deny 121.42.0.0/24;
deny 121.42.64.0/18;
deny 121.42.128.0/18;
deny 121.42.192.0/19;
deny 121.42.224.0/19;
deny 121.196.0.0/16;
deny 121.197.0.0/16;
deny 121.198.0.0/16;
deny 121.199.0.0/16;
deny 140.205.0.0/16;
deny 203.209.250.0/23;
deny 218.244.128.0/19;
deny 223.4.0.0/16;
deny 223.5.0.0/16;
deny 223.5.5.0/24;
deny 223.6.0.0/16;
deny 223.6.6.0/24;
deny 223.7.0.0/16;
101.200.0.0/15 
101.37.0.0/16 
101.37.0.0/17 
101.37.0.0/24 
101.37.128.0/17 
103.52.196.0/22 
103.52.196.0/23 
103.52.196.0/24 
103.52.198.0/23 
106.11.0.0/16 
106.11.0.0/17 
106.11.0.0/18 
106.11.1.0/24 
106.11.128.0/17 
106.11.32.0/22 
106.11.36.0/22 
106.11.48.0/21 
106.11.56.0/21 
106.11.64.0/19 
110.173.192.0/20 
110.173.196.0/24 
110.173.208.0/20 
110.75.0.0/16 
110.75.236.0/22 
110.75.239.0/24 
110.75.240.0/20 
110.75.242.0/24 
110.75.243.0/24 
110.75.244.0/22 
110.76.0.0/19 
110.76.21.0/24 
110.76.32.0/20 
110.76.48.0/20 
112.124.0.0/16 
112.125.0.0/16 
112.126.0.0/16 
112.127.0.0/16 
112.74.0.0/16 
112.74.0.0/17 
112.74.116.0/22 
112.74.120.0/22 
112.74.128.0/17 
112.74.32.0/19 
112.74.64.0/22 
112.74.68.0/22 
114.215.0.0/16 
114.55.0.0/16 
114.55.0.0/17 
114.55.128.0/17 
115.124.16.0/22 
115.124.20.0/22 
115.124.24.0/21 
115.28.0.0/16 
115.29.0.0/16 
118.190.0.0/16 
118.190.0.0/17 
118.190.0.0/24 
118.190.128.0/17 
118.31.0.0/16 
118.31.0.0/17 
118.31.0.0/24 
118.31.128.0/17 
119.38.208.0/21 
119.38.216.0/21 
119.38.219.0/24 
119.42.224.0/20 
119.42.242.0/23 
119.42.244.0/22 
119.42.248.0/21 
120.24.0.0/14 
120.24.0.0/15 
120.25.0.0/18 
120.25.104.0/22 
120.25.108.0/24 
120.25.110.0/24 
120.25.111.0/24 
120.25.112.0/23 
120.25.115.0/24 
120.25.136.0/22 
120.25.64.0/19 
120.25.96.0/21 
120.27.0.0/17 
120.27.128.0/17 
120.27.128.0/18 
120.27.192.0/18 
120.55.0.0/16 
120.76.0.0/15 
120.76.0.0/16 
120.77.0.0/16 
120.78.0.0/15 
121.0.16.0/21 
121.0.24.0/22 
121.0.28.0/22 
121.196.0.0/16 
121.197.0.0/16 
121.198.0.0/16 
121.199.0.0/16 
121.40.0.0/14 
121.42.0.0/18 
121.42.0.0/24 
121.42.128.0/18 
121.42.17.0/24 
121.42.192.0/19 
121.42.224.0/19 
121.42.64.0/18 
123.56.0.0/15 
123.56.0.0/16 
123.57.0.0/16 
139.129.0.0/16 
139.129.0.0/17 
139.129.128.0/17 
139.196.0.0/16 
139.196.0.0/17 
139.196.128.0/17 
139.224.0.0/16 
139.224.0.0/17 
139.224.128.0/17 
140.205.0.0/16 
140.205.128.0/18 
140.205.192.0/18 
140.205.32.0/19 
140.205.76.0/24 
182.92.0.0/16 
203.107.0.0/24 
203.107.1.0/24 
203.209.224.0/19 
218.244.128.0/19 
223.4.0.0/16 
223.5.0.0/16 
223.5.5.0/24 
223.6.0.0/16 
223.6.6.0/24 
223.7.0.0/16 
39.100.0.0/14 
39.104.0.0/14 
39.104.0.0/15 
39.104.0.0/24 
39.106.0.0/15 
39.108.0.0/16 
39.108.0.0/17 
39.108.0.0/24 
39.108.128.0/17 
39.96.0.0/13 
39.96.0.0/14 
39.96.0.0/24 
42.120.0.0/16 
42.121.0.0/16 
42.156.128.0/17 
42.96.128.0/17 
45.113.40.0/22 
45.113.40.0/23 
45.113.40.0/24 
45.113.42.0/23 
47.92.0.0/14 
47.92.0.0/15 
47.92.0.0/24 
47.94.0.0/15

squid proxy

Install squid

plain old apt-get update && apt-get install squid3 apache2-utils -y

Basic squid conf

/etc/squid3/squid.conf instead of the super bloated default config file

# note that on ubuntu 16.04, use squid instead of squid3
auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid3/passwords
auth_param basic realm proxy
acl authenticated proxy_auth REQUIRED
http_access allow authenticated
forwarded_for delete
http_port 0.0.0.0:3128

Please note the basic_ncsa_auth program instead of the old ncsa_auth

Setting up a user

sudo htpasswd -c /etc/squid3/passwords username_you_like, on 16.04, it’s squid, not squid3
and enter a password twice for the chosen username then
sudo service squid3 restart

see: https://stackoverflow.com/questions/3297196/how-to-set-up-a-squid-proxy-with-basic-username-and-password-authentication

centos

I have to use centos, since adsl providers are not capable of providing ubuntu

check out this wonderful article: https://hostpresto.com/community/tutorials/how-to-install-and-configure-squid-proxy-on-centos-7/

yum install -y epel-release
yum install -y squid
yum install -y httpd-tools
systemctl start squid
systemctl enable squid
touch /etc/squid/passwd && chown squid /etc/squid/passwd
htpasswd -c /etc/squid/passwd root

edit /etc/squid/squid.conf

auth_param basic program /usr/lib64/squid/basic_ncsa_auth /etc/squid/passwd
auth_param basic children 5
auth_param basic realm Squid Basic Authentication
auth_param basic credentialsttl 2 hours
acl auth_users proxy_auth REQUIRED
http_access allow auth_users
http_port 3128

一个小问题

squid 默认只允许代理 443 端口的https流量,而会拒绝对其他端口的connect请求。需要更改配置文件

To fix this, add your port to the line in the config file:
acl SSLports port 443
so it becomes
acl SSL
ports port 443 4444
squid 默认还禁止了除了443之外的所有connect
deny CONNECT !SSL_Ports # 删掉这一句