爬虫

构建最小版本的 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

网页更新与重抓策略

我们知道网页总是会更新的。在大规模的网络爬取中,一个很重要的问题是重抓策略,也就是在什么时候去重新访问同一个网页已获得更新。要获得这个问题的解,需要满足如下两个条件:

1. 尽可能地少访问,以减少自身和对方站点的资源占用
2. 尽可能快的更新,以便获得最新结果

这两个条件几乎是对立的,所以我们必须找到一种算法,并获得一个尽可能优的折衷。

可以使用泊松过程:https://stackoverflow.com/questions/10331738/strategy-for-how-to-crawl-index-frequently-updated-webpages

为什么不使用 scrapy?

最近面了几家公司,每当我提到头条的爬虫都是自己写的时候,对方一个下意识的问题就是
“为什么不使用开源的 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 源码解读](http://kaito-kidd.com/2016/11/01/scrapy-code-analyze-architecture/)

知乎移动端接口分析

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

# 抓包

“`
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
“`

如何破解被 JS 加密的数据

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

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

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

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

# 具体步骤

有空了再写。。

参考:

1. [中国天气质量网返回结果加密的破解](https://cuiqingcai.com/5024.html)
2. [破解 Google 翻译的token](https://zhuanlan.zhihu.com/p/32139007)
3. [JavaScript 生成 Cookie](https://github.com/jhao104/spider/tree/master/PyV8%E7%A0%B4%E8%A7%A3JS%E5%8A%A0%E5%AF%86Cookie)
4. [常见加密算法](http://liehu.tass.com.cn/archives/1016)

爬虫利器 Chrome Headless 和 Puppeteer 最佳实践

> 翻译自:https://docs.browserless.io/blog/2018/06/04/puppeteer-best-practices.html

browserless 已经运行了200万次的 chrome headless 请求,下面是他们总结出来的最佳实践:

# 一、不要使用无头浏览器

![](https://ws2.sinaimg.cn/large/006tNc79gy1fs056p3uvaj319w0fcacy.jpg)

无头 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/’);
“`

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

# 二、不要在不需要的时候运行无头浏览器

我们遇到过好多客户尝试在不使用的时候也保持浏览器开着,这样他们就总能够直接连上浏览器。尽管这样能够有效地加快连接速度,但是最终会在几个小时内变糟。很大程度上是因为浏览器总会尝试缓存并且慢慢地吃掉内存。只要你不是在活跃地使用浏览器,就关掉它。

“`
import puppeteer from ‘puppeteer’;

async function run() {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto(‘https://www.example.com/’);

// More stuff …page.click() page.type()

browser.close(); // <- Always do this! } ``` 在 browserless,我们会给每个会话设置一个定时器,而且在WebSocket链接关闭的时候关闭浏览器。但是如果你使用自己独立的浏览器的话,记得一定要关闭浏览器,否则你很可能在半夜还要陷入恶心的调试中。 # 三、 `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 支持),但是总还是要花费时间的。

# 四、并行化浏览器,而不是页面

上面我们已经说过尽量不要使用浏览器,而且只在需要的时候才打开浏览器,下面的这条最佳实践是——在一个浏览器中只使用一个会话。尽管通过页面来并行化可能会给你省下一些时间,如果一个页面崩溃了,可能会把整个浏览器都带翻车。而且,每个页面都不能保证是完全干净的(cookies 和存储可能会互相渗透)。

不要这样:

“`
import puppeteer from ‘puppeteer’;

// Launch one browser and capture the promise
const launch = puppeteer.launch();

const runJob = async (url) {
// Re-use the browser here
const browser = await launch;
const page = await browser.newPage();
await page.goto(url);
const title = await page.title();

browser.close();

return title;
};
“`

要这样:

“`
import puppeteer from ‘puppeteer’;

const runJob = async (url) {
// Launch a clean browser for every “job”
const browser = puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
const title = await page.title();

browser.close();

return title;
};
“`

每一个新的浏览器实例都会得到一个干净的 `–user-data-dir` (除非你手工设定)。也就是说会是一个完全新的会话。如果 Chrome 崩溃了,也不会把其他的会话一起干掉。

# 五、队列和限制并发

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

最好也最简单的方法是使用 browserless 提供的镜像:

“`
# Pull in Puppeteer@1.4.0 support
$ docker pull browserless/chrome:release-puppeteer-1.4.0
$ docker run -e “MAX_CONCURRENT_SESSIONS=10” browserless/chrome:release-puppeteer-1.4.0
“`

上面限制了并发连接数到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);
“`

# 七、使用 docker 来管理 Chrome

Chrome 除了浏览之外,还会有好多的莫名其妙的线程,所以最好使用 docker 来管理

Python爬虫利器——lxml 和 xpath 表达式

最近要做下微信爬虫,之前写个小东西都是直接用正则提取数据就算了,如果需要更稳定的提取数据,还是使用 xpath 定位元素比较可靠。周末没事,从爬虫的角度研究了一下 python xml/html 相关的库。

Python 标准库中自带了 xml 模块,但是性能不够好,而且缺乏一些人性化的 API。相比之下,第三方库 lxml 是用 Cython 实现的,而且增加了很多实用的功能,可谓爬虫处理网页数据的一件利器。

严格来说,html 并不是 xml 的一种,不过 lxml 对于 xml 和 html 都有很好的支持,分别使用 `lxml.etree` 和 `lxml.html`两个模块。

# 解析

网页下载下来以后是个 bytes 的形式,需要构造 DOM 树:

“`

In [1]: html = ”’
…:

helloworld

…: ”’

In [2]: import lxml.html

In [3]: doc = lxml.html.fromstring(html)

In [4]: doc
Out[4]:

“`

# Element 结构

生成的树是一个设计很精妙的结构,可以把它当做一个对象访问当前节点自身的文本节点,可以把他当做一个数组,元素就是他的子节点,可以把它当做一个字典,从而遍历它的属性,下面演示了 lxml 的常见用法:

“`
In [5]: doc.text
Out[5]: ‘hello’

In [6]: doc.tag
Out[6]: ‘p’

In [7]: doc[0].tag
Out[7]: ‘span’

In [11]: for k, v in doc[0].items():
…: print(k, v)
…:
id world

In [12]: doc[0].get(‘id’)
Out[12]: ‘world’

In [13]: doc[0].attrib
Out[13]: {‘id’: ‘world’}
“`

# 遍历树的方法

doc 是一个树形结构,可以通过一些方法访问树中的其他节点:

“`
In [14]: doc.getroottree() # 返回树
Out[14]:

In [19]: doc.getroottree().getroot() # 返回根节点,这里是 lxml 自动生成的 html 节点
Out[19]:

In [20]: doc.getparent() # lxml 自动生成的 body 节点
Out[20]:

In [21]: doc.getprevious()

In [22]: doc.getnext()

In [23]: doc.text_content()
Out[23]: ‘helloworld’

In [25]: lxml.html.tostring(doc, pretty_print=True, encoding=’utf-8′)
Out[25]: b’

helloworld

\n’

“`
注意因为我们给的是一个 html 的片段(`

`),所以 lxml 自动生成了 html 和 body 等节点已构成完整的 html 文档。

如果需要显式地指定生成一个 html 片段文档还是完整文档,可以分别使用:lxml.html.fragment_fromstring 和 lxml.html.document_fromstring 两个方法。

lxml 还有其他一些方法,都列在下面了:

Element.tail

* Element.append(Element) 添加一个子元素
* Element.set(‘attr’, value) 设置属性
* Element.iter(tag_name) 遍历所有后系元素,可以使用 `*`
* ElementTree.getelementpath(Element)
* Element.getroottree() 返回对应的树
* ElementTree.getpath(Element) 返回一个元素的 xpath
* ElementTree.getroot() 返回根节点
* HtmlElement.drop_tree() 删除当前节点下的所有节点,但是保留text
* HtmlElement.drop_tag() 删除当前节点,但是保留它的子节点和text
* HtmlElement.classes 返回类
* HtmlElement.find_class(class_name) 按照 class 查找 tag
* HtmlElement.get_element_by_id(id, *default) 按照 id 查找元素
* HtmlElement.iterlinks() 遍历所有连接
* HtmlElement.make_links_absolute(base_url=None, resolve_base_href=True) 把所有连接变成绝对链接
* HtmlElement.resolve_base_href() 解析 base 标签
* HtmlElement.rewrite_links(link_repl_func) 替换所有的链接

# XPath

XPath 实在太强大了,在定位元素方面绝对是秒杀 CSS 选择器。在 lxml 中,节点和树分别具有xpath 函数。

lxml 中的 xpath 方法,对于 xpath 表达式应该返回元素,总是返回一个数组,即使只有一个元素

“`
In [24]: doc.xpath(‘//span/text()’)
Out[24]: [‘world’]
“`

lxml 中的 xpath 函数支持变量

“`
print(root.xpath(“$text”, text = “Hello World!”))
Hello World!
“`

xpath may return _ElementStringResult, which is not picklable, use xpath(smart_strings=False) to avoid this http://lxml.de/xpathxslt.html#xpath-return-values

lxml 还支持几个函数 `find/findall`,他们使用 ElementPath,是一种类似 xpath 的语言,感觉很是奇怪,lxml 的文档描述他是 xpath 的一个子集,暂时不看了。

# 常见问题

lxml 在遇到小于号的时候会出问题(按照标准,应该编码为 `<`),直接把后面的文档都丢了,但是浏览器兼容性比较好,不会有问题。

by default, the lxml parser is not very error-proof, the html5parser lib is behaves more like your web browser.

lxml.html.html5parser provides same interface with lxml.html

tricks and traps

读《The Anatomy of a large-scale hypertextual Web search engine》

Google 在1997年的论文[1], 到现在(2017)的话, 已经有二十年的历史了, 然而对于编写一个小的搜索引擎, 依然有好多具有指导意义的地方.

The Anatomy of a large-scale hypertextual Web search engine 这篇论文应该是一片总结性质的论文, 而且论文并没有多少的关于数据结构等的实现细节. 只是大体描绘了一下架构.

# Google的算法

首先, Google大量使用了在超文本也就是网页中存在的结构, 也就是锚文本和链接. 还有就是如何有效的处理在网页上, 所有人都可以任意发布任何文字的问题, Google在这片文章里给的解决方案是PageRank.

在20年前, 主要问题是, 网页已经开始快速增长, 然而当时的所有搜索引擎给出的结果只是搜索结果的数量也增长了, 却没能把最相关的结果放在首页. 因为人们并不会因为给出结果多而去多看几页, 所以这样的结果是不可取的. 在设计Google的过程中, Google还考虑了随着web规模的增长, 会对现有的体系造成的影响以及如何应对.

Google 还表达了对当时的搜索引擎都是商业化的, 因而一些诸如用户查询之类的结果无法共学术应用的情况表达了不满. (呵呵, Google这不是打自己的脸么)

对于 PageRank 算法, 提到了简单的公式:

![](https://ws1.sinaimg.cn/large/006tKfTcly1fqazehy4zdj30im02mmxd.jpg)

其中Tx表示的是指向A页面的所有页面, C表示的是一个页面上所有的外链. 对于这个公式的解释是这样的. 假设有一个随机的浏览者, 他不断的点击网页中的链接, 从不点后退, 直到他感到烦了, 然后在随机的拿一个网页开始点击. 其中d就表示了这个人会感到烦了的概率. 这样造成的结果就是如果一个网页有很多的的外链指向他的话, 他就有很大的机会获得比较高的PR, 或者如果一个很权威的站点指向的他的话, 也有很大机会获得比较高的PR.

对于锚文本, 大多数网站都是把他和所在的页联系起来, Google还把锚文本以及PR值和它指向的页面联系起来.

# Google的架构

其实这部分才是我最感兴趣的地方. 之所以今天会抽出时间来阅读这篇论文, 主要就是想写个小爬虫, 然后发现写来写去, 太不优雅了, 才想起翻出Google的论文读一读.

## Google整体架构

![](https://ws2.sinaimg.cn/large/006tKfTcly1fqazes7038j30gn0iitbl.jpg)

Google的架构非常的模块化, 基本上可以看到整个图, 就知道每个模块是负责做什么的. 大概分成了几个部分: 爬虫(下载器), indexer, barrel, sorter, 和(searcher)前端服务.

其中

1. 爬虫负责下载网页, 其中每一个url都会有一个唯一的docID.
2. indexer负责解析网页中的单词, 生成hit记录, 并产生前向索引. 然后抽出所有的链接.
3. URLResolver会把indexer生成的锚文本读取并放到锚文本和链接放到索引中, 然后生成一个docID -> docID 的映射数据库. 这个数据库用来计算PageRank.
4. sorter根据indexer生成的正向索引, 根据wordID建立反向索引. 为了节省内存, 这块是inplace做的. 并且产生了wordID的列表和偏移
5. searcher负责接收用户的请求, 然后使用DumpLexicon产生的lexicon和倒排和PageRank一起做出响应.

## 用到的数据结构

由于一个磁盘寻道就会花费 10ms 的时间(1997), 所以Google几乎所有的数据结构都是存在大文件中的. 他们实现了基于固定宽度ISAM, 按照docID排序的document索引, 索引中包含了当前文件状态, 指向repository的指针, 文件的校验和, 不同的统计信息等. 变长信息, 比如标题和url存在另一个文件中. (YN: SSD对这个问题有什么影响呢)

hitlist指的是某个单词在谋篇文档中出现的位置, 字体, 大小写等信息. Google手写了一个htilist的编码模式, 对于每个hit花费2byte

barrel中存放按照docID排序存放document

## 模块

### 爬虫

爬虫又分为了两个部分, URLServer 负责分发URL给Crawler. Crawler 是分布式的, 有多个实例, 负责下载网页. 每获得一个URL的时候, 都会生成一个docID. Google使用了一个URLServer 和 3个Crawler. 每一个Crawler大概会维持300个连接, 可以达到每秒钟爬取100个网页. 并且使用了异步IO来管理事件.

#### DNS

Google指出爬虫的一个瓶颈在于每个请求都需要去请求DNS. 所以他们在每一个Crawler上都设置了DNS 缓存.

YN: 对于HTTP 1.1来说, 默认连接都是keep-alive的, 对于URLServer分发连接应该应该同一个域名尽量分发到同一个crawler上, 这样可以尽量避免建立连接的开销.

indexer会把下载到的网页分解成hit记录,每一个hit记录了单词, 在文档中的位置, 和大概的字体大小和是否是大写等因素. indexer还会把所有的链接都抽取出来, 并存到一个anchor文件中. 这个文件保存了链接的指向和锚文本等元素.

## rank

Google并没有手工为每一个因素指定多少权重, 而是设计了一套反馈系统来帮助我们调节参数.

# 结果评估

Google认为他们的搜索能够产生最好的结果的原因是因为使用了PageRank. Google在9天内下载了2600万的网页, indexer的处理能力在 54qps, 其中

# 拓展

query cacheing, smart disk allocation, subindices

链接合适应该重新抓取, 何时应该抓取新连接

使用了NFS, 性能有问题

## YN:

如何判定为一个hub也 -> 识别列表
hub页的链接产出率 -> 根据一个列表页是否产生新连接来动态的调整hub页的抓取频率

[1] http://infolab.stanford.edu/~backrub/google.html