跳转至

2021

堆排序

堆排类似选择排序,只排序未排序的部分,但堆排比较方式不同,每次只比较log(n)次
最终的堆排时间复杂度为O(nlogn)

关键下标

堆一般用数组来表示,但是关系为

  • 当以1为起点时,若父节点是n
  • 左子节点=n*2
  • 右子节点=n*2 + 1

  • 若节点为n, 则其父节点为n/2

  • 若元素为m个,则最后一个根节点(非叶节点)为m/2

  • 当以0为起点时,若父节点是n

  • 左子节点=(n+1)2-1 = n2 + 1
  • 右子节点=(n+1)2+1 -1 = n2 + 2

  • 若节点下标为n, 其父节点为(n+1)/2 - 1 = (n-1)/2

  • 若元素为m个,则最后一个根节点(非叶节点)为(m+1)/2-1 = (m-1)/2

可见,若以1为起点,要简洁很多

建立堆

  1. 从第一个非叶子节点开始,比较父子节点中找的最值,如果需要调整则交换父子节点再递归遍历下一层:
  2. 建立最小堆时,每次比较出最大值,一遍之后,堆顶为最大
  3. 建立最大堆时,每次比较出最小值,一遍之后,堆顶为最小
  4. 比较到顶部节点时,完成第一次遍历,找到了顶部的最值
  5. 将堆顶的最值与尾部交换,遍历范围-1,然后从堆顶开始比较
  6. 重复3动作,直到范围为1

3的动作类似选择排序,只遍历未排序的部分

最大堆和最小堆

最小堆可以求得topK的数,反过来最大堆可以求得topK的数

topK

堆排序通常用来实现topK,适用于限制内存的情况。方法是建立一个堆大小为K: 先用头部K个数据建立最小堆,后续的数字与堆顶(最小)比,如果跟小则跳过,否则替换然后执行thiftdown。其时间复杂读为O)KlogK + NlogK), 只需要一次建堆,后续都是比较。空间复杂度为K。

也可以则使用分治法,将数据分为多组,每组求其top k。最后合并起来再排序,求最终的topK。 分治法可以配合线程一起使用加快速度。

// 使用最小堆来实现topK
void thiftdown(int *heap, int root, int size) {
  int m = root;
  int l = root * 2 + 1;
  int r = root * 2 + 2;

  if (l < size && heap[l] > heap[m]) {
    m = l;
  }
  if (r < size && heap[r] > heap[m]) {
    m = r;
  }

  if (m != root) {
    int tmp = heap[root];
    heap[root] = heap[m];
    heap[m] = tmp;

    thiftdown(heap, m, size);
  }
}

void adjust(int *heap, int root, int size) {
  int m = root;
  int l = root * 2 + 1;
  int r = root * 2 + 2;

  if (l < size && heap[l] < heap[m]) {
    m = l;
  }
  if (r < size && heap[r] < heap[m]) {
    m = r;
  }

  if (m != root) {
    int tmp = heap[root];
    heap[root] = heap[m];
    heap[m] = tmp;

    adjust(heap, m, size);
  }
}

void heapify(int *heap, int size) {
  for (int i = (size - 1) / 2; i >= 0; i--) {
    thiftdown(heap, i, size);
  }

  while (size > 0) {
    int tmp = heap[0];
    heap[0] = heap[size - 1];
    heap[size - 1] = tmp;
    size--;
    thiftdown(heap, 0, size);
  }
}

void add(int *heap, int val, int size) {
  if (heap[0] >= val) {
    return;
  }
  heap[0] = val;
  adjust(heap, 0, size);
}

使用方法

heapify(m, 5);
for (int i = 5; i < 49; i++) {
  add(m, n[i], 5);
}
return m[0];

海量数据求topK

常见问题类似: 1. 求许多重复整数中,重复第K多的正整数。 2. 求文本中重复出现的词语次数,重复第K多的”, 3. ip中访问最K多的”。

这些问题需要多一步统计,统计出现次数,然后再使用堆来查找topK。

  1. 使用数组统计,如果目标都是正整数,如问题1
  2. 使用map统计,int表示出现次数,如果目标字符串,如问题2和问题3

统计完成后,将目标和计数放入结构体,再使用堆来找topK

{
  目标
  次数
}

总结

topK 问题除了堆排序以外,还常用快排来找。

DP 四板斧

以跳台阶的题目为例, 题目是以每次跳1层或2层的方式跳到n台阶, 问一共有多少种跳法
开始我觉得这是求路径个数,而通常dp算最大或最小值, 不适用。
但后来换个角度想,求最大最小必须得试过所有方法才得出来的, 而将所有的试过的方法计数,就是这题的解

写出递归

dp第一步要先写出递归来, 写递归得写收敛条件
这里的收敛条件是当传参小于0,则说明解法失败(多跳了),返回0。 当恰好为0则解法有效返回1

递归体就是跳1层和跳2层

static int jump(int number) {
  if (number < 0) {
    return 0;
  } else if (number == 0) {
    return 1;
  }
  return jump(number - 1) + jump(number - 2);
}

int jumpFloor(int number) { 
  return jump(number - 1) + jump(number - 2); 
}

用缓存来优化递归

递归显然会重复计算, 可以用缓存计算结果来避免, 当有缓存时使用缓存,否则才去计算, 最后将结果缓存

并且使用缓存时应该指定尾递归的值

int jump(int number, int *cache) {
  if (number < 3) {
    return cache[number];
  }
  // 缓存没命中就计算
  int ret1 =
      cache[number - 1] != -1 ? cache[number - 1] : jump(number - 1, cache);
  int ret2 =
      cache[number - 2] != -1 ? cache[number - 2] : jump(number - 2, cache);
  cache[number] = ret1 + ret2;
  return cache[number];
}

int jumpFloor(int number) {
  int cache[number + 1];
  for (int i = 0; i < number + 1; i++) {
    cache[i] = -1;
  }

  cache[0] = 0;
  cache[1] = 1;
  cache[2] = 2;
  return jump(number, cache);
}

递归改for循环

递归的调用流程是自上而下,先计算(n)->计算(n -1)->再计算(n-2) ... 0
现在要反过来, 先计算(0)->计算(1)->计算(2) ... n 即自下而上

int jumpFloor(int number) {
  if (number < 3) {
    return number;
  }

  // 之前要初始化为了判断, 而现在显然下标大于2的值是要计算出来的,不需要读取  
  // 所以不需要初始化
  int cache[number + 1];
  cache[1] = 1;
  cache[2] = 2;

  for (int i = 3; i <= number; i++) {
    cache[i] = cache[i - 1] + cache[i - 2];
  }

  return cache[number];
}

缩小缓存大小

仔细看, 其实每次计算只涉及最大的三个数, 所以数组并不需要那么大,只用3个单元就足够,故直接写成3个变量

  int jumpFloorD(int number) {
    if (number < 3) {
      return number;
    }

    int total = 0;
    int minors2 = 1;
    int minors1 = 2;

    for (int i = 3; i <= number; i++) {
      total = minors1 + minors2;
      minors2 = minors1;
      minors1 = total;
    }

    return total;
  }

这样DP解法就出来了, 这个套路是我从力扣上学到的, 非常有效简单

总结

从上可见,你需要训练如何发现递归关系,剩下的工作就是缓存递归调用的结果,最后是将从上到下的解决思路变为从下到上。这样我们就可以用O(N)的时间复杂度去遍历

参考

https://leetcode.com/discuss/study-guide/1490172/dynamic-programming-is-simple

引题

在stackoverflow上搜到一个有趣帖子
讨论了指针解引用和数组下标访问的区别,而根本原因就是指针有时不同于数组,这是《c专家编程》有仔细讨论的,故再看了一遍

问题

帖子的问题可以用类型不同指针在extern声明时不同于数组来直接了当地解释。而类型不匹配如何导致程序崩溃的呢

//file file1.c

int a[2] = {800, 801};
int b[2] = {100, 101};
//file file2.c

extern int a[2];

// here b is declared as pointer,
// although the external unit defines it as an array
extern int *b; 

int main() {

  int x1, x2;

  x1 = a[1]; // ok
  x2 = b[1]; // crash at runtime, segment fault

  return 0;
}

当声明为extern 数组时,编译后符号表保存了数组的地址,那么a[1]解引用只是在数组地址上加偏移量a+1得到新地址,然后取新地址的内容,所以数组的地址提前可知
当声明为extern 指针时,编译后符号表保存的是符号b,让编译器以指针的方式处理b,所以b是一个指针,其指针地址被赋值为数组的内容(101100)。在运行时,先取出b的地址为(101100),
再加上偏移量得到新地址(101101), 最后访问这个非法地址导致崩溃

因为当声明为extern int *b时,在file1中的b应该是存放一个内存地址而实际存放的是数组内容,就导致程序把数组的内容当地址访问。

打印指针地址

如果在coredump之前,先打印b的地址,得到的是b数组的值为'0x6500000064', 正好指针长度是两个int之和。所以file2中把数组内容当成了指针地址,然后偏移一个int长度后,地址是'0x6500000068', 然后这个无效地址导致段错误。

printf("%p\n", (void*)b);

从汇编角度看这个问题

b存放的是内容,而程序以为是地址。因为实际上编译器不会为数组额外分配空间来存储指针, 但是要为指针分配空间。

int main() {
    int *a = 0;
    int b[4];
    *b = 0;
    // 为了显示栈大小
    foo();
}
从汇编代码可见,分配了32个字节的空间,指针8 + 4*4 =
        sub     rsp, 32
        mov     QWORD PTR -8[rbp], 0
        mov     DWORD PTR -32[rbp], 0

弱符号

浅谈内存管理

这里记录一下看书所得,应用程序的内存管理,即glibc的malloc和相关函数

内存管理的重要性

过去Linux通过默认的dlmalloc来申请内存,但不支持多线程,这导致的竞争和性能降低,而ptmalloc2支持多线程,也成为了Linux新的默认方式
ptmalloc2通过为每个线程单独维护一个帧,这样每个线程的申请和释放可以并行执行,但当线程过多时,线程也将共享帧。这种帧叫做per thread arena

主线程和线程的内存申请方式

主线程中使用brk来申请大块内存,其实brk不过是修改了寄存器的值。而且返回的大小通常要比malloc申请的多,如同测试中的在主线程中申请100个字节大小的内存,返回了132 KB。

55f7b402c000-55f7b404d000 rw-p 00000000 00:00 0                          [heap]
而在线程中,malloc使用mmap来申请内存,返回大小也比申请大小多,测试中的在线程中申请100字节,返回了64M的地址空间, 但只有8MB是可用的堆内存。
7fb738000000-7fb738021000 rw-p 00000000 00:00 0      
7fb738021000-7fb73c000000 ---p 00000000 00:00 0      

主线程中申请的地址大于Data Section的地址,说明使用的brk,而后者申请时用了mmap,对比附录文档可以看出32位和64位区别比较大

arena

主线程和线程使用不同的arena,这样主线程和线程同时申请内存时不存在竞争。但不是所有的线程都独享一份arena, arena的个数限制如下

For 32 bit systems:
     Number of arena = 2 * number of cores.
For 64 bit systems:
     Number of arena = 8 * number of cores.

当线程个数太多时,会公用主线程的arena

main arena 和 per thread arena

main arena 指在主线程(main)中使用的,而后者是子线程中使用,两者有很大区别
主线程只有一块heap,所以没有heapinfo结构,而子线程有。主线程的Arena header是全局变量,所以存在于libc.so的Data Segment中

线程的arena含有多个heap, 每个heap都有heapinfo结构,多个heapinfo组成链表。线程的多个heap共享一个malloc_state 也称为Arena header

Arena header包含了bin、top chunk等信息

每个heap有多个chunk,每个chunk有个malloc_chunk结构,其管理chunk的状态和其在heap中的偏移大小,有以下几种状态

  • Allocated chunk
  • Free chunk
  • Top chunk
  • Last Remainder chunk

Top chunk 处于arena的顶部,在freelist没有合适大小的chunk时使用,如果Top chunk比申请的大,则又分为两部分(新Top chunk和用户申请的chunk)

Last Remainder chunk, 是当用户请求small chunk时,会先遍历small bin和Unsorted bin, 如果都无法满足,则遍历其他bin。
如果找到的chunk还有剩余,则这个chunk变为Last Remainder chunk,且将被加入到Unsorted bin
这样的好处是多次申请small chunk时,会先向Unsorted binLast Remainder chunk申请, 所以小内存块会集中分布在Last Remainder chunk,提高小块内存的局部性

bin

当malloc的内存被free后(仅仅是例子中的100字节chunk),并不会释放给内核,而是进入到glibc的bin中,也叫做freelist
当下次申请内存时,会从bin中查找合适的chunk,而只有当没有合适的大小时,才向内核申请。

所以bin用来保存free过的chunk, 基于chunk的大小分为 - Fast bin - Unsorted bin - Small bin - Large bin

但不同的bin都使用链表来管理这些chunk

测试方法

获取程序运行时的pid, 假设为949293, 那么通过以下命令可以获取程序的内存结构

cat /proc/949632/maps
# address                 perms offset  dev    inode             pathname
55f7b378a000-55f7b378b000 r--p 00000000 103:02 4727634                   /tmp/a.out (deleted)
55f7b378b000-55f7b378c000 r-xp 00001000 103:02 4727634                   /tmp/a.out (deleted)
55f7b378c000-55f7b378d000 r--p 00002000 103:02 4727634                   /tmp/a.out (deleted)
55f7b378d000-55f7b378e000 r--p 00002000 103:02 4727634                   /tmp/a.out (deleted)
55f7b378e000-55f7b378f000 rw-p 00003000 103:02 4727634                   /tmp/a.out (deleted)
55f7b402c000-55f7b404d000 rw-p 00000000 00:00 0                          [heap]
7f4808000000-7f4808021000 rw-p 00000000 00:00 0 
7f4808021000-7f480c000000 ---p 00000000 00:00 0 
7f480ea3d000-7f480ea3e000 ---p 00000000 00:00 0 
7f480ea3e000-7f480f241000 rw-p 00000000 00:00 0 
...
* address 是起始地址-结束地址,差值为空间大小 * perm 是指权限,分为rwx[p,s], [p,s]指最后一位可以是private或者shared, 这些能通过mprotect命令修改(c中的const实现) * offset 仅对mmap的区域有效,指映射偏移量 * pathname 如果此区域是映射的文件,则显示文件名。如果映射的是匿名映射则显示空白, 匿名映射包括线程通过mmap申请的堆内存,而[heap]是main函数的堆

所以通过计算address可以知道glibc申请的内存情况

malloc的返回值有意义吗

在我本机(gcc (Debian 10.2.1-6) 10.2.1 20210110 12GB)上测试了一段程序 1. 如果一次性申请13GB,malloc返回失败 2. 如果每次只申请2GB但不释放,第10次申请(20GB)malloc返回成功 3. 如果每次只申请2GB但不释放且访问,第三次申请(6GB)malloc返回成功,但访问时OOM

所以说,malloc的返回值是没有太大意义的

void foo(size_t large) {
  char *buffer = (char *)malloc(large);
  if (buffer == NULL) {
    printf("error!\n");
    exit(EXIT_FAILURE);
  }
  printf("Memory allocated\n");
  //  for (size_t i = 0; i < large; i += 4096) {
  //    buffer[i] = 0;
  //  }
  printf("Travalse done\n");
}
int main() {
  // size_t large = 1099511627776;
  size_t large = 13958643712;
  for (int i = 0; i < 100; i++) {
    foo(2147483648);
  }

  return EXIT_SUCCESS;
}

其他

内存管理不仅存在应用程序(或者说glibc)中,Linux内核也有内存管理。也不只有glibc的内存malloc一种实现,还有

  • dlmalloc – General purpose allocator
  • ptmalloc2 – glibc
  • jemalloc – FreeBSD 的libc实现, 相比其他第三方,这个比较流行
  • tcmalloc – Google 为多线程场景开发
  • libumem – Solaris

malloc是glibc对ptmalloc2的包装,而ptmalloc2dlmalloc的重构,区别很大

最后总结,glibc的malloc通过两个系统调用函数brk和mmap来向内核申请内存,但内存释放后不会归还内核,而是放到freelist等待再次被利用

引用

https://man7.org/linux/man-pages/man5/proc.5.html https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/ https://lemire.me/blog/2021/10/27/in-c-how-do-you-know-if-the-dynamic-allocation-succeeded/

spdlog

spdlog 支持多spdlog::logger, 适用与多个隔离模块的日志分别保存

而且logger含有多个sink的vector。sink与文件对应,如果想把日志输出到多个文件,就创建多个sink

std::vector<sink_ptr> sinks_;

logger和sink的级别独立

// logger级别的判断
template<typename... Args>
inline void spdlog::logger::log(source_loc source, level::level_enum lvl, const char *fmt, const Args &... args)
{
    if (!should_log(lvl))
    {
        return;
    }

// sink级别的判断
inline void spdlog::logger::sink_it_(details::log_msg &msg)
{
#if defined(SPDLOG_ENABLE_MESSAGE_COUNTER)
    incr_msg_counter_(msg);
#endif
    for (auto &sink : sinks_)
    {
        if (sink->should_log(msg.level))
        {
            sink->log(msg);
        }
    }

所以可以支持不同的级别到不同的文件,例如logger设置为trace级别, sink1 设置为info级别, sink2设置为trace级别,这样就能创建两个日志文件

info.log  # 只保存info级别
full.log  # 保存所有级别(logger级别或者sink2的级别)

爬虫

这个截图是百度的爬虫爬取网站的记录。 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);