跳转至

Welcome

爬虫

这个截图是百度的爬虫爬取网站的记录。 baiduspider.jpeg

如何做一个最简单的爬虫? 一条bash命令爬深圳7天天气

#获取7天的天气
wget -q http://www.weather.com.cn/weather/101280601.shtml -O - \
| grep 'class="wea"\|^<h1>'  | grep -o ">.*<" | sed 's/>//;s/<//' \
| awk 'ORS=NR%2==1?" ":"\n" {print}'  

7日(今天) 晴
8日(明天) 晴
9日(后天) 晴
10日(周五) 晴
11日(周六) 晴
12日(周日) 晴
13日(周一) 晴转小雨

这条命令如何写出来的

  1. 定目标,想好要爬什么,如爬深圳天气
  2. 先在浏览器访问天气网站,网址http://www.weather.com.cn/, 再找到深圳7天天气的网页链接为http://www.weather.com.cn/weather/101280601.shtml
  3. 右键菜单里选择查看网页源码,搜索天气的内容所在位置
  4. 找规律: 日期和天气的行有特定起始内容,日期这行以<h1>开头,天气这行包含class="wea"
  5. 格式化: 清除不要的内容,使用sed和awk

拓展

只有这样没有意义, 不如自己在浏览器上看? 但如果一次性查找武汉、北京、广州的天气,但网页只能查当天的,只有保存到硬盘的数据才能永远回头翻
或者如果想每天自动执行一遍呢? 又或者如何自动发邮件提醒你带伞呢? --都是可以的, 这需要更多耐心和学习

但因为bash处理网页比较麻烦,而且也不是今天的主题语言。

关键组件

python

python简单灵活的语法又有丰富的生态, 适合作为入门爬虫的编程语言

Beautiful Soup

早期使用python自带的request库, 就能简单地实现一个爬虫软件

主角scrapy

An open source and collaborative framework for extracting the data you need from websites. In a fast, simple, yet extensible way.

scrapy 是一套完整框架, 实现了爬取网站的许多细节工作,让我们不需要了解代码,就能爬网站。其开发商还提供爬虫托管服务, 可以将你的爬虫做成在线服务, 而bs只是一个库

简单介绍 * 目录结构 * 配置

scrapy选择器

选择器用来指定网页的位置,scrapy支持的选择器有两种: * css * xpath

自己先在源码上找规律,也可以简单地使用浏览器提供的复制选择器功能

先手动验证, 获取列表的所有详情页链接

scrapy shell "https://www.douban.com/group/search?cat=1013&q=深圳租房"  
# xpath方式
response.xpath('//td[has-class("td-subject")]//a/@href').getall()
# css方式
response.css('td.td-subject').css('a::attr(href)').getall()

先手动验证, 获得下一页

response.css('span.next').css('a::attr(href)').get()
# 控制台->‘Copy CSS Selector’
# .next > a:nth-child(2)
response.css('.next > a::attr(href)').get()

详细

数据处理

爬到的数据可能乱和重复, 可以加个清洗的步骤, 在爬到数据后对数据过滤,删除不要的数据

# 删除重复条目
class DuplicatesPipeline(object):

    def __init__(self):
        self.ids_seen = set()

    def process_item(self, item, spider):
        if item['title_digist'] in self.ids_seen:
            raise DropItem("Duplicate item found: %s" % item)
        else:
            self.ids_seen.add(item['title_digist'])
            return item

具体项目 爬豆瓣租房

使用爬虫清理百度搜索结果

百度搜索的有效结果只占少部分,其他是广告,视频和推荐词

用爬虫来提取有效结果

# 标题
response.css('#content_left > div.result').css('a:nth-child(1)::text').getall()
# 结果
response.css('#content_left > div.result').css('a:nth-child(1)::attr(href)').get()

部署成在线服务

pip install shub

shub login
Insert your Zyte Scrapy Cloud API Key: <API_KEY>

# Deploy the spider to Zyte Scrapy Cloud
shub deploy

# Schedule the spider for execution
shub schedule blogspider 
Spider blogspider scheduled, watch it running here:
https://app.zyte.com/p/26731/job/1/8

# Retrieve the scraped data
shub items 26731/1/8

其他问题

登录

不是所有页面都能像豆瓣这样简单直接访问,有的需要登陆,这时需要设置cookie
可将浏览器的访问的请求复制为curl请求,再将curl请求转换为scrapy请求, 有工具curl2scrapy

爬携程机票,但这样只能看最新或者最便宜的机票,无法自动订票

curl 'https://flights.ctrip.com/international/search/api/lowprice/calendar/getCalendarDetailList?v=0.4625948281116462' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0' -H 'Accept: application/json' -H 'Accept-Language: zh-CN,en-US;q=0.5' --compressed -H 'Content-Type: application/json;charset=utf-8' -H 'transactionID: 0feb04e3db5648acb409a9efaf68eb99' -H 'scope: d' -H 'Cache-Control: no-cache' -H 'Origin: https://flights.ctrip.com' -H 'Connection: keep-alive' -H 'Referer: https://flights.ctrip.com/online/list/oneway-wuh0-szx?_=1&depdate=2021-12-08&cabin=y_s&adult=1&searchid=j1043107111-1628648957090-97788Tk-0&containstax=1' -H 'Cookie: _bfa=1.1638949895953.3tfx42.1.1638949895953.1638949895953.1.2; _bfs=1.2; FlightIntl=Search=[%22WUH|%E6%AD%A6%E6%B1%89(%E5%A4%A9%E6%B2%B3%E5%9B%BD%E9%99%85%E6%9C%BA%E5%9C%BA)(WUH)|477|WUH|WUH|480%22%2C%22SZX|%E6%B7%B1%E5%9C%B3(SZX)|30|SZX|480%22%2C%222021-12-09%22]; _RF1=116.30.223.163; _RSG=mtaCn0GabiBpwk2uaRD22A; _RDG=28d46524d5c5f727d530fed50e21ed2094; _RGUID=bb259100-3132-4df0-a082-35b9380aeeb3; _bfaStatus=send; MKT_Pagesource=PC; _bfaStatusPVSend=1; _bfi=p1%3D10320673302%26p2%3D10320673302%26v1%3D2%26v2%3D1' -H 'Sec-GPC: 1' -H 'TE: Trailers' --data-raw '{"cabin":"Y_S","flightWay":"S","flightSegmentList":[{"arrivalCityCode":"SZX","departureCityCode":"WUH","departureDate":"2021-12-08"}]}'

代理ip

爬取的时候莫名其妙 IP 就被网站封掉了,毕竟各大网站也不想自己的数据被轻易地爬走。 对于爬虫来说,为了解决封禁 IP 的问题,一个有效的方式就是使用代理,使用代理之后可以让爬虫伪装自己的真实 IP,如果使用大量的随机的代理进行爬取,那么网站就不知道是我们的爬虫一直在爬取了,这样就有效地解决了反爬的问题。

阿布云

模拟用户代理useragent

User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。

可以使用自己浏览器的UA,也可以用百度蜘蛛的UA列表
但目标网站是否允许就难说了, 需要多试一试

模拟浏览器

Selenium 是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。

cd ~/github/SeleniumLogin
python SeleniumLogin/core/taobao.py
def login(self, username, password, chromedriverpath, **kwargs):
    # 基本设置
    browser = webdriver.Chrome(options=self.chrome_opts)
    browser.get(self.login_url)
    driver_wait = WebDriverWait(browser, 60)
    # 点击'亲, 请登录', 进入登录界面
    button = driver_wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'h')))
    button.click()
    # 输入用户名密码
    username_sender = driver_wait.until(EC.presence_of_element_located((By.ID, 'fm-login-id')))
    username_sender.send_keys(username)
    password_sender = driver_wait.until(EC.presence_of_element_located((By.ID, 'fm-login-password')))
    password_sender.send_keys(password)
    time.sleep(2)
    # 检查是否出现了滑动验证码
    try:
        slider = browser.find_element_by_xpath("//span[contains(@class, 'btn_slide')]")
        if slider.is_displayed():
            ActionChains(browser).click_and_hold(on_element=slider).perform()
            ActionChains(browser).move_by_offset(xoffset=258, yoffset=0).perform()
            ActionChains(browser).pause(0.5).release().perform()
    except:
        pass
    # 点击登录按钮
    #button = driver_wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'password-login')))
    #button.click()
    # 返回必要的数据
    infos_return = {'username': username}
    return infos_return, browser

headless

Splash HTTP API

引用

http://c.biancheng.net/view/2011.html https://github.com/CharlesPikachu/SeleniumLogin

前言

无论书写还是口语,无论英文还是中文,当用名词修饰名词时,就感觉容易找不到主体(subject) ,但这种用法有很常见,特别是中文。比如电脑/人脑,说明书/漫画书,都是用名词修饰名词,也可以说是造了新(名)词,在英文中有很多名词+名词合成的派生词, 例如bookstore。大家接受这种词因为没有歧义,大家都知道前面的名词是用来修饰后面的词。例如说电脑,大家都知道指“带电的脑”,不会以为指“脑里面的电”。bookstore 则都认为指 store of books(有书的店),不会认为是 book of stores(有店的书)。

但如果句子比较复杂时,就容易有歧义了。

举例中文

当长一点的句子,有词修饰词的情况时,就会可能有歧义,特别是比较陌生时,不一定能靠语句分析出主体,而熟悉的东西可以靠经验储备来辅助

”新的证据表格“: 1. 新修饰的表格,表格有新旧之分,新表格和表格都用于记录证据 2. 新修饰的证据,证据有新旧之分,表格专门用来记录新证据

举例英文

new evidence table 这里有歧义: 新表格还是新证据?

正确的说法 * new table of evidence, 新表格 * new evidence in the table, 新证据

英文中的名词修饰名词

英文中的名词

Noun-adjunct-维基 知乎

多个of 情况

是不推荐的

总结

对于英文而言,名词修饰名词的情况比较少见,除非是常用的或者派生词。用“名词 of 名词”是比较合理的。而当多个of时, 虽然语法上没错,但是容易产生歧义,这时需要调整语句结构来消除歧义。中文同理。

容器技术

容器是利用'cgroup'和'namespace' 实现软件级的虚拟化,产品有lxc和docker。不同与全虚拟化和半虚拟化,这些是硬件辅助的半虚拟化Zen(Zen也有全虚拟化)和全虚拟化kvm
因为容器使用主机内核,不存在特权指令翻译,所以从性能上比较,容器是最高效的,其次是kvm, Zen

cgroup和namespace

这两个是容器技术的根基,包括Docker, lxc。实际上docker开始用的lxc,后来用go重新写了libcontainer来实现控制这两个东西
简单来说,cgroup实现对进程组的资源的限制和监控,而namespace如同c++里面类似,实现了进程组之间的隔离

但除此之外, 虚拟化系统时则还需要rootfs支持

LXC和Docker

lxc代码合入了内核,其原理和Docker非常类似, 不过Docker有比较丰富的全套工具和方便的api

nsenter命令的使用

容器技术都使用到了namespace, 而nsenter可以操作namespace, 进入容器内部,以下都一docker容器为例

使用命令, 获取到容器的首进程pid

docker inspect --format '{{.State.Pid}}' containername
3015

然后可以使用nsenter来进入容器,如同docker exec 一样

$ pwd
/home/user
$ sudo nsenter -t 3015 -m  pwd
/

-m表示挂载命名空间,这样就能访问docker里的进程的文件系统,所以能正确打印出容器进程的当前目录
-t表示目标进程,nsenter可以获取以下命名空间。 跟通过执行ls /proc/self/ns所看到的差不多,这里self是命令本身pid

/proc/pid/ns/mnt    the mount namespace
/proc/pid/ns/uts    the UTS namespace
/proc/pid/ns/ipc    the IPC namespace
/proc/pid/ns/net    the network namespace
/proc/pid/ns/pid    the PID namespace
/proc/pid/ns/user   the user namespace
/proc/pid/ns/cgroup the cgroup namespace
/proc/pid/ns/time   the time namespace
/proc/pid/root      the root directory
/proc/pid/cwd       the working directory respectively
上面的每项都有对应的参数,而'-a'表示进入上面的所有命名空间

c++的basic_string优化

Small-string optimization is basically

union { 
  struct { char*, size_t, size_t; }
  struct { char[23]; bool; } 
} 
- but with bit-tricks instead. When allocated, the local buffer's space is reused for size and capacity. It's not wasted. But gdb just shows both sides of the union, which can be confusing.

c++的信号和异常

信号和异常都是c++程序会遇到的,以前常把信号也当异常来看待,当其实两者是截然不同的东西

信号

信号不仅是对c++程序,而是对所有linux程序,是内核在进程由内核态转用户态时,检查信号槽并调用信号处理函数(用户定义或者默认)。
有些信号可以由程序处理(常见退出信号SIGINT),而有些无法处理(常见退出信号SIGABT, 段错误信号SIGSEGV),而大多信号默认是退出进程。

异常

c++的异常是c所没有的,异常一般在出现逻辑错误运行时错误时要抛出异常,如果没有异常处理函数,则coredump。

触发方式

触发信号用raise,而异常用trow

int raise(int sig);

crontab manual

https://www.computerhope.com/unix/ucrontab.htm#command-entries

使用systemd-run 替换

# systemd-run --on-active=30 /bin/touch /tmp/foo
# systemd-run --on-active="12h 30m" --unit someunit.service

docker 存储

docker 有3种方式使用系统存储:

  • bind: 宿主机目录映射方式,启动容器命令添加参数-v dir1:dir2。将宿主机的目录dir1挂载到容器的dir2目录,实际执行的mount -o bind类似命令
  • volume: 由docker管理的存储,先创建后使用,且可以在多个容器中共享,是官方推荐的方式
  • tmpfs: 使用宿主机的内存作为存储,这种使用内存文件系统来挂载到容器目录, 使用参数--mount type=tmpfs,destination=/dir, tmpfs是退出即释放的一种文件系统

docker使用的文件系统

docker使用的文件系统经过很多变化,而且在各发行版下可能不同,但目前主流的是overlay2,执行docker info 查看当前使用的是overlay2

sudo docker info | grep  Storage                                                                                                                                              
 Storage Driver: overlay2

除了overlay2,还有aufs(ubuntu),devicemapper(centos),btrfs和zfs。他们的实现都不同,都能支持分层和支持写时复制(Cow/copy-on-write),而他们实现的方式有区别,所以效率也有区别

  • 分层: 镜像都是分层的,在Dockerfile构建时,每次执行COPY/RUN时,都会增加一层
  • 写时复制: 在容器或者DockerFile执行修改操作时,包括权限修改,会将lower layer 的文件复制到container层再修改

而容器就是在镜像顶层压栈了一个可写层,而且是临时的,当容器销毁时,这层的文件也会删除

overlay的优势

  1. page caching, 可以在多个不同实例之间共享
  2. 不同层之间,相同文件使用硬连接, 节省inode 和 大小

写时复制 copy-up 会导致第一次写时造成延迟,特别是大文件,拷贝起来费时。 但第二次就不会延时, 而且overlay2 有caching, 相比其它文件系统,更减少延时

overlay的问题

  1. 实现不够完全, 例如没有实现uname
  2. 先只读打开一个文件 open(read), 再读写打开相同文件open(write), 两个fd 会对应2个不同文件, 第一个对应的lower的文件,第二个造成写时复制,对应容器里的文件。
  3. 规避方法是先执行touch 操作。 现实的例子是 yum 需要安装yum-plugin-ovl。 但这个只有7.2才支持, 之前的话就需要先touch /var/lib/rpm/*

最佳实践

  1. 使用ssd
  2. 对于写操作比较多的场景,例如数据库,应使用映射文件(bind)或者volume。这样跳过了overlay的复杂操作,直接使用主机的文件系统

overlay的增删改

当运行docker容器时查看挂载

overlay on /var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B74PWZCBMRCWXFH5UL2ZXB5WEU:/var/lib/docker/overlay2/l/WNHICVPVSDNUGSCZW435TPSMOK,upperdir=/var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/diff,workdir=/var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/work)
docker 将镜像的文件挂载为只读, 将容器层挂载为可读可写。 文件系统可以分为2部分 upper(容器层) + lower (镜像层)

  • 当在容器里执行写时, 如果文件不存在, 会依次遍历lower。如果都不存在就会在upper层创建文件
  • 读也相同
  • 删除时会创建一个without 来隐藏, 这是为什么即使删除容器里的文件, 镜像还是会增大。
  • 删除目录情况也差不多

特殊情况

在保存容器后(docker commit),会多一层,里面包含了修改的文件,以及删除后生成的without文件,然后生成镜像

但对于以下特殊目录文件不会提交, 因为这些文件是运行时docker 要根据用户配置进行修改的。

  1. /etc/hostname
  2. /etc/hosts
  3. /etc/resov.conf

例如docker 的link选项,会在容器的hosts 文件里定义对应的容器名->容器ip

手动mount overlayfs的例子

  1. 原本目录,文件都分散在不同目录ABC
    .
    ├── A
    │   ├── aa
    │   └── a.txt
    ├── B
    │   ├── a.txt
    │   └── b.txt
    ├── C
    │   └── c.txt
    └── worker
        └── work [error opening dir]
    
  2. overlay 挂载到/tmp/test目录 sudo mount -t overlay overlay -o lowerdir=A:B,upperdir=C,workdir=worker /tmp/test/
  3. 查看test目录
    /tmp/test/
    ├── aa
    ├── a.txt
    ├── b.txt
    └── c.txt
    
    mount  | grep 'overlay'
    overlay on /tmp/test type overlay (rw,relatime,lowerdir=A:B,upperdir=C,workdir=worker)
    

参考

https://docs.docker.com/storage/storagedriver/

起因

遇到过一个面试题, 以下程序打印几个'A'

#include <stdio.h>
#include <unistd.h>

int main() {
  printf("A");
  fork();
  printf("A");
}
答案是4个,而以下是3个
#include <stdio.h>
#include <unistd.h>

int main() {
  printf("A\n");
  fork();
  printf("A");
}
再把上面的编译后的可执行文件重定向到文件, 则有4个'A'
#./a.out > r.log
#cat r.log
原因就是缓冲

Linux 缓冲介绍

分为无缓冲、行缓冲和全缓冲

演示

发现有些程序可以输出到屏幕,重定向到文件后,执行ctl+c退出后却没有任何内容,例如以下

#include <stdio.h>
#include <unistd.h>

int main(void) {
  while (1) {
    printf("Hello World\n");
    sleep(1);
  }
}
这个程序是死循环所以需要手动结束,且一直打印helloword。但如果将输出重定向到文件,

#./a.out > r.log
当手动退出时,文件依旧是空的

shell 文件操作

读行

while read line ;do
#or while read -r line ;do
echo $line
done < $1
cat $1 | while read line ;do
echo $line
done

使用for时,结果略有不同, for以空格为一行

for line in $(cat $1) ;do
echo $line
done

reference

https://bash.cyberciti.biz/guide/Reads_from_the_file_descriptor_(fd)

huawei change rom

reference

https://forum.xda-developers.com/t/guide-rom-8-1-lineageos-15-1-huawei-mediapad-m3-8-4-btv-w09-btv-dl09.4085995/