为了账号安全,请及时绑定邮箱和手机立即绑定
3. start 和 end

有的同学可能早就按耐不住自己躁动的内心:道理我都懂,语法我也都知道,这个start和end的意思我小学也背过,但是我就是想知道start和end究竟是控制着哪方面的开始和结束!帧动画的最后一帧显示不出来到底和他俩有没有点啥关系呢?答案是:当然有关系!咱们正常人肯定都是这么想的:一个动画我分为了七步,假如我让动画运行七秒,那么每一秒钟就要走一步,七秒过后我应该会看见七帧。这么想很正常对吧!听起来挺是那么回事的啊!但实际上是大家的认知和电脑的认知不一致,所以导致了这场误会。那么接下来我就带领大家一起揭晓一下人类和电脑互相之间都是怎么定义帧动画的:/* 定义动画:动画名(change-color) */ @keyframes change-color { /* 第1步 */ from { color: red } /* 红 */ /* 第2步 */ 16% { color: orange } /* 橙 */ /* 第3步 */ 32% { color: yellow } /* 黄 */ /* 第4步 */ 48% { color: green } /* 绿 */ /* 第5步 */ 64% { color: cyan } /* 青 */ /* 第6步 */ 80% { color: blue } /* 蓝 */ /* 第7步 */ to { color: purple } /* 紫 */}假如我们用steps(1)来运行这个动画,那么:人类的想法:​ 第一步:红色​ 第二步:橙色​ 第三步:黄色​ 第四步:绿色​ 第五步:青色​ 第六步:蓝色​ 第七步:紫色而电脑的想法:​ 第一步:红色到橙色​ 第二步:橙色到黄色​ 第三步:黄色到绿色​ 第四步:绿色到青色​ 第五步:青色到蓝色​ 第六步:蓝色到紫色steps()就是控制着这第一步到第二步、第N步到第N+1步。steps(1)就代表了第一步到第二步:红色到橙色之间用一步就走完。同理,steps(10)就代表了第一步到第二步:红色到橙色之间之间要慢慢过渡10次。用一张图片来方便大家理解:图片就可以很明显看出steps()控制的到底是哪个地方。仔细数一数,steps()是不是要比红橙黄绿青蓝紫(我们定义过的帧)少一个啊?所以我们最后一帧才会显示不出来,那么start和end又代表的是什么含义呢?从图中可以看出每一个steps()里面都包含了两个步骤,start和end就是控制这个的。假如说从红到橙需要一秒钟,那么start就表示第一秒钟的开始阶段就从红变到橙色了,由于第一秒的一开始(也就相当于零秒)就红变橙了,也就是说红色还没来得及展示就变成橙色了,所以用户压根就看不见这个红色,结果就是start会丢失第一帧。end也是同理,红色持续一秒,在第一秒的最后阶段才会变成橙色,这样的话我们就看得见红色了。然后来到第二帧,第二帧是从橙变黄。橙色持续一秒然后在这一秒的最后阶段变黄、黄变绿、绿变青、青变蓝、然后就来到了最后一帧:蓝变紫。蓝色在本次动画的最后一秒一开始就持续一秒,然后紫色在最后一秒的最后阶段才会出现,但是到了最后一秒得最后阶段就意味着本次动画即将结束了,紫色也是刚出现就即将消失,所以用户也看不见紫色,结局就是end会丢失最后一帧。这里附上一张w3c的step工作原理图:拿 steps(1, end) 那张图来举例:假如第一步是从红到黄,那么图中的实心小圆点代表了红色,然后圆点后面的线段代表红色持续的时间,空心小圆点代表该变成黄色但还没变成黄色,直到下一个实心小圆点才会变成黄色,依此类推。从这张图的走势就可以很好的看出start和end的各自丢帧情况,start的第一帧是空心的,end的最后一帧虽然是实心的,但后面已经没有了线段,也就是代表最后一帧没有任何的运行时间,所以它们分别会丢失第一帧和最后一帧。

3. 示例程序

#include <stdio.h>#include <string.h>#define StudentNumbers 50#define NameLength 50typedef struct{ int id; char name[NameLength]; int age; int score; int flag;} Student;int add(Student student, Student Students[]);int del(int id, Student students[]);int display(Student students[]);int update(int id, Student students[]);int search(char name[], Student students[]);int main(){ int id = -1; char name[NameLength]; int choice = 0; int stop = 0; Student students[StudentNumbers]; Student student; for (int i = 0; i < StudentNumbers; i++) { students[i].id = i; students[i].flag = 0; } while (stop == 0) { printf("-------------------------\n"); printf("* 学生管理系统 *\n"); printf("-------------------------\n"); printf("1 添加\n"); printf("2 修改成绩\n"); printf("3 查询\n"); printf("4 删除\n"); printf("5 显示学生列表\n"); printf("0 退出程序\n"); printf("请直接输入数字选项:"); scanf("%d", &choice); switch (choice) { case 1: printf("请输入学生姓名:"); scanf("%s", student.name); printf("请输入学生的年龄:"); scanf("%d", &student.age); printf("请输入学生成绩:"); scanf("%d", &student.score); add(student, students); break; case 2: printf("请输入要修改成绩的学生编号:"); scanf("%d", &id); update(id, students); break; case 3: printf("请输入要查找的学生姓名:"); scanf("%s", name); search(name, students); break; case 4: printf("请输入要删除的学生编号:"); scanf("%d", &id); del(id, students); break; case 5: display(students); break; case 0: stop = 1; break; default: printf("输入选项有误\n"); break; } } return 0;}int add(Student student, Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (students[i].flag == 0) { strcpy(students[i].name, student.name); students[i].age = student.age; students[i].score = student.score; students[i].flag = 1; return 0; } } return 1;}int del(int id, Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (students[i].id == id) { students[i].flag = 0; return 0; } } return 1;}int display(Student students[]){ printf("******************\n"); printf("学生列表\n"); printf("******************\n"); for (int i = 0; i < StudentNumbers; i++) { if (students[i].flag == 1) { printf("学生编号:%d,学生姓名:%s,年龄:%d,成绩:%d\n", students[i].id, students[i].name, students[i].age, students[i].score); } } printf("******************\n"); return 0;}int update(int id, Student students[]){ int score = -1; printf("请输入新的成绩:"); scanf("%d", &score); for (int i = 0; i < StudentNumbers; i++) { if (students[i].id == id) { students[i].score = score; return 0; } } return 1;}int search(char name[], Student students[]){ for (int i = 0; i < StudentNumbers; i++) { if (strcmp(name, students[i].name) == 0) { printf("学生编号: %d,学生姓名: %s,年龄: %d,成绩: %d\n", students[i].id, students[i].name, students[i].age, students[i].score); return 0; } } printf("没有查找到相关学生信息。\n"); return 1;}很多人可能会第一次接触这么长的程序,会产生畏惧的心理。其实不用担心。要相信自己可以看懂的。我们分开来讲解一下。在程序的最开始我们需要引入程序中可能需要使用的函数的头文件。这里我们因为要使用 printf 、 scanf 等,所以需要 stdio 函数库。因为要使用 strcpy 、 strcmp 函数,所以需要 string 函数库。#include <stdio.h>#include <string.h>为了便于程序中的维护,不用在很多出修改共用的数值。所以这里定义了一个常量#define StudentNumbers 50#define NameLength 50为了存储学生的信息。我们用了 struct 来定义学生的信息。里面包含学生的编号 id ,姓名 name 这是一个字符串,年龄 age ,成绩 score ,标志位 flag 这个变量是用来表示是否有学生信息存储在该位置的。不过这里我们使用了之前没有介绍的一个 typedef 。这个关键字使用的好处是使得后面使用这个 struct 的时候不用每次都用关键字 struct 来定义,只要用这个结构的名称直接定义就可以了,如同我们定义整数等内置类型一样方便。typedef struct{ int id; char name[NameLength]; int age; int score; int flag;} Student;为了便于维护,我们没有按照函数出现的顺序来写。不过 C 语言一直秉承着先定义再使用的原则。所以。如果你使用的函数没有在使用前出现,而是在后面的话,那么你就需要先让编译器知道这个函数的基本情况。这个时候我们会先把函数的定义写在前面。我们可以看到下面我们定义了这个系统的功能。每个功能我们都会写一个函数。其实不写这些函数,把所有的功能写在 main 函数内部也是可以的。但是这样会在维护上存在问题。进行测试也会变得困难。int add(Student student, Student Students[]);int del(int id, Student students[]);int display(Student students[]);int update(int id, Student students[]);int search(char name[], Student students[]);这里定义了一些需要使用的变量。stop 变量是用来控制程序循环的,也就是控制程序在什么时候可以结束循环的。我们定义了一个 Student 的数组,用来存储学生的信息。用一个单独的变量来存储单条的学生信息。int id = -1;char name[NameLength];int choice = 0;int stop = 0;Student students[StudentNumbers];Student student;这里我们通过循环来初始化我们的数组。for (int i = 0; i < StudentNumbers; i++){ students[i].id = i; students[i].flag = 0;}循环语句如果在不改变条件的情况下会一直循环。确保我们的系统可以一直运行。while (stop == 0)在接收到输入后。我们就会通过 switch 来进行相应的匹配。完成对应的操作。这比使用大量的 if 语句简约了很多。switch (choice)在子程序中,也就是实现增、删、改、查这些功能程序中。我们用了循环语句来访问数组中的元素。同时,利用了判断语句与特定的变量,来判断该位置是否存有学生信息。运行结果:utopia@DESKTOP:~$ ./test-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:1请输入学生姓名:张三请输入学生的年龄:22请输入学生成绩:100-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:1请输入学生姓名:李四请输入学生的年龄:21请输入学生成绩:90-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:1请输入学生姓名:王二请输入学生的年龄:23请输入学生成绩:99-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:5******************学生列表******************学生编号:0,学生姓名:张三,年龄:22,成绩:100学生编号:1,学生姓名:李四,年龄:21,成绩:90学生编号:2,学生姓名:王二,年龄:23,成绩:99******************-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:2请输入要修改成绩的学生编号:1请输入新的成绩:80-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:5******************学生列表******************学生编号:0,学生姓名:张三,年龄:22,成绩:100学生编号:1,学生姓名:李四,年龄:21,成绩:80学生编号:2,学生姓名:王二,年龄:23,成绩:99******************-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:4请输入要删除的学生编号:1-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:5******************学生列表******************学生编号:0,学生姓名:张三,年龄:22,成绩:100学生编号:2,学生姓名:王二,年龄:23,成绩:99******************-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:1请输入学生姓名:张五请输入学生的年龄:20请输入学生成绩:70-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:5******************学生列表******************学生编号:0,学生姓名:张三,年龄:22,成绩:100学生编号:1,学生姓名:张五,年龄:20,成绩:70学生编号:2,学生姓名:王二,年龄:23,成绩:99******************-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:3请输入要查找的学生姓名:张五学生编号: 1,学生姓名: 张五,年龄: 20,成绩: 70-------------------------* 学生管理系统 *-------------------------1 添加2 修改成绩3 查询4 删除5 显示学生列表0 退出程序请直接输入数字选项:在程序中,我们首先添加了 3 条学生的记录。然后我们进行了列表显示。接着,我们尝试修改了其中一个学生成绩,并再次查看列表,发现成绩修改生效了。然后,我们删除了一个学生,列表显示结果其已经被删除了。然后我们又尝试添加了一个学生。列表显示结果添加成功。最后我们按照姓名查找了一个学生。

1. 基于 Cookie 的自动登录

如果是想基于基本的 API 方式登录,我们会面临两大难点:手机验证码校验 ,如下图所示:起点网站登录手机发送验证码滑动验证码校验,如下图所示:起点网站登录滑动验证码绕过这些校验的方法超过了本教程的知识范围,故我们不再次详细讨论。好在起点网支持自动登录过程,也就是 Cookie 登录:起点网支持自动登录第一次手动登录起点,选择自动登录后,起点网站返回的 Cookie 信息就会保存至本地。下次再访问起点网时,通过请求带上该 Cookie 信息就能正确识别用户,实现自动登录过程。Cookie 存在本地,就存在被代码读取的可能。通常而言,我们来使用 Python 中的 browsercookie 库可以获取浏览器的 cookie,目前它只支持 Chrome 和 FireFox 两种浏览器。不过对于 Chrome 80.X 版本的浏览器,其中的 cookie 信息被加密了,我们无法按照早期的操作进行 cookie 读取。不过网上这个博客给出了一个解密 Cookie 的代码,我们拿过来简单改造下,做成一个辅助模块:# 参考文档:https://blog.csdn.net/u012552769/article/details/105001108import sqlite3import urllib3import osimport jsonimport sysimport base64from cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesurllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)def dpapi_decrypt(encrypted): import ctypes import ctypes.wintypes class DATA_BLOB(ctypes.Structure): _fields_ = [('cbData', ctypes.wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_char))] p = ctypes.create_string_buffer(encrypted, len(encrypted)) blobin = DATA_BLOB(ctypes.sizeof(p), p) blobout = DATA_BLOB() retval = ctypes.windll.crypt32.CryptUnprotectData( ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout)) if not retval: raise ctypes.WinError() result = ctypes.string_at(blobout.pbData, blobout.cbData) ctypes.windll.kernel32.LocalFree(blobout.pbData) return resultdef aes_decrypt(encrypted_txt): with open(os.path.join(os.environ['LOCALAPPDATA'], r"Google\Chrome\User Data\Local State"), encoding='utf-8', mode="r") as f: jsn = json.loads(str(f.readline())) encoded_key = jsn["os_crypt"]["encrypted_key"] encrypted_key = base64.b64decode(encoded_key.encode()) encrypted_key = encrypted_key[5:] key = dpapi_decrypt(encrypted_key) nonce = encrypted_txt[3:15] cipher = Cipher(algorithms.AES(key), None, backend=default_backend()) cipher.mode = modes.GCM(nonce) decryptor = cipher.decryptor() return decryptor.update(encrypted_txt[15:])def chrome_decrypt(encrypted_txt): if sys.platform == 'win32': try: if encrypted_txt[:4] == b'x01x00x00x00': decrypted_txt = dpapi_decrypt(encrypted_txt) return decrypted_txt.decode() elif encrypted_txt[:3] == b'v10': decrypted_txt = aes_decrypt(encrypted_txt) return decrypted_txt[:-16].decode() except WindowsError: return None else: raise WindowsErrordef get_cookies_from_chrome(domain, key_list): sql = f'SELECT name, encrypted_value as value FROM cookies where host_key like "%{domain}%"' filename = os.path.join(os.environ['USERPROFILE'], r'AppData\Local\Google\Chrome\User Data\default\Cookies') con = sqlite3.connect(filename) con.row_factory = sqlite3.Row cur = con.cursor() cur.execute(sql) cookie_dict = {} for row in cur: if row['value'] is not None: name = row['name'] value = chrome_decrypt(row['value']) if value is not None and name in key_list: cookie_dict[name] = value return cookie_dictTips:上述这段代码不用纠结细节,前面函数的主要是替 get_cookies_from_chrome() 函数服务的,而该函数的输入要搜索的网站以及提取相应网站 cookie 信息中的某个具体字段,返回相应的结果。本人 Python 3.8.2 安装的是 win32 版本,该段代码亲测有效。来看看起点中文网给读者生成的 cookie 数据,我们调用上面的获取 cookie 信息的代码来从中提取相应数据:起点的cookie信息print(get_cookies_from_chrome('qidian.com', '_csrfToken'))print(get_cookies_from_chrome('qidian.com', 'e1'))print(get_cookies_from_chrome('qidian.com', 'e2'))执行上述代码我们可以得到如下结果:PS C:\Users\spyinx> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/test_cookie.py{'_csrfToken': 'YJklLmhMNpEfuSmqZZGaK72D4sUVJty52gyKwXXX'}{'e1': '%7B%22pid%22%3A%22qd_p_qidian%22%2C%22eid%22%3A%22qd_A08%22%2C%22l1%22%3A1%7D'}{'e2': '%7B%22pid%22%3A%22qd_p_qidian%22%2C%22eid%22%3A%22qd_A10%22%2C%22l1%22%3A1%7D'}这说明我们通过前面的代码能争取获取到起点网保存在 Chrome 浏览器中的 cookie 信息。因此,前面的代码将作为我们读取起点用户登录 Cookie 的重要辅助模块。Tips:这个测试只能在装了 Chrome 浏览器的 Windows 系统上进行测试,或者是 Linux 的桌面版我们首先来创建一个起点爬虫:PS C:\Users\Administrator\Desktop> scrapy startproject qidian_spider接下里我们来看看要提取的我的书架的信息:我的书架书籍信息对应的 items.py 的内容如下:# https://docs.scrapy.org/en/latest/topics/items.htmlimport scrapyclass QidianSpiderItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() # 分组 category = scrapy.Field() # 小说名 name = scrapy.Field() # 最新章节 latest_chapter = scrapy.Field() # 作者 author = scrapy.Field() # 更新时间 update_time = scrapy.Field() # 阅读进度 progress_status = scrapy.Field()接下来,在爬虫部分需要请求该页面然后提取相应的数据,我们的爬虫代码如下:"""获取用户书架数据"""import jsonfrom urllib import parsefrom scrapy import Requestfrom scrapy.spiders import Spiderfrom .get_cookie import get_cookies_from_chromefrom ..items import QidianSpiderItemclass BookCaseSpider(Spider): name = "bookcase" # 构造函数 def __init__(self): # 最重要的就是这个获取起点的cookie数据了,这里保存了之前用户登录的cookie信息 self.cookie_dict = get_cookies_from_chrome( "qidian.com", ["_csrfToken", "e1", "e2", "newstatisticUUID", "ywguid", "ywkey"] ) def start_requests(self): url = "https://my.qidian.com/bookcase" # http请求时附加上cookie信息 yield Request(url=url, cookies=self.cookie_dict) def parse(self, response): item = QidianSpiderItem() books = response.xpath('//table[@id="shelfTable"]/tbody/tr') for book in books: category = book.xpath('td[@class="col2"]/span/b[1]/a[1]/text()').extract_first() name = book.xpath('td[@class="col2"]/span/b[1]/a[2]/text()').extract_first() latest_chapter = book.xpath('td[@class="col2"]/span/a/text()').extract_first() update_time = book.xpath('td[3]/text()').extract_first() author = book.xpath('td[@class="col4"]/a/text()').extract_first() progress_status = book.xpath('td[@class="col5"]/a/text()').extract_first() item['category'] = category item['name'] = name item['latest_chapter'] = latest_chapter item['update_time'] = update_time item['author'] = author item['progress_status'] = progress_status print(f'get item = {item}') yield item最重要的方法就是那个获取 cookie 信息的方法了,正是靠着这个 cookie,我们才能获取相应的用户书架的网页并提取相应的书籍信息。接下来,简单实现一个 item pipeline 用于保存书架上的书籍信息,该代码位于 scrapy/pipelines.py 文件中,默认的 pipelines 都写会在这里:# 源码位置:scrapy/pipelines.py# Define your item pipelines here## Don't forget to add your pipeline to the ITEM_PIPELINES setting# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.htmlimport json# useful for handling different item types with a single interfacefrom itemadapter import ItemAdapterclass QidianSpiderPipeline: def open_spider(self, spider): self.file = open("bookcase.json", 'w+', encoding='utf-8') def close_spider(self, spider): self.file.close() def process_item(self, item, spider): data = json.dumps(dict(item), ensure_ascii=False) self.file.write(f"{data}\n") return item最后别忘了在 settings.py 中添加这个 item pipeline:ITEM_PIPELINES = { 'qidian_spider.pipelines.QidianSpiderPipeline': 300,}我们运行下这个爬虫,看看是否能抓到我们想要的数据:PS C:\Users\Administrator\Desktop> scrapy crawl bookcase最后的结果如下:获取用户的书架上书籍信息这样,我们就成功实现了用户登录后的访问动作。接下来我们在这个基础上进一步扩展,实现清除书架上所有的书籍,类似于淘宝的一键清除购物车。

5. 优化快速排序算法

对于优化快速排序,在这里我们只考虑最简单的一种优化思路,就是基准数的选择。前面的快排算法中,我们都是选择列表的第一个元素作为基准数,这在列表随机的情况下是没有什么问题的,但对本身有序的列表进行排序时,时间复杂度就会退化到 O(n2)O(n^2)O(n2)。我们来进行相关测试:# 冒泡排序算法import datetimeimport randomfrom sort_algorithms import get_num_position, quick_sort# python默认递归次数有限制,如果不进行设置,会导致超出递归最大次数import sys sys.setrecursionlimit(1000000)if __name__ == '__main__': # 对于设置10000,本人电脑会出现栈溢出错误 # nums = [random.randint(10, 10000) for i in range(6000)] nums = [i for i in range(6000)] start = datetime.datetime.now() quick_sort(nums, 0, len(nums) - 1) end = datetime.datetime.now() print('Running time: %s Seconds' % (end-start))第一个注释的语言是对 nums 列表随机生成,而第二个直接情况是直接生成有序的列表。我们分别看执行结果:# 随机列表快排PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.027907 Seconds# 有序列表快排PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:02.159677 Seconds可以看到明显的差别,差不多差了 80 倍,这确实是纯快排算法存在的一个问题。如果我们往这个有序列表中插入少量随机数,打破有序的情况,会看到性能会有较大提升。这个问题的根源在于基准数目的选择,对于有序的列表排序时每次都是选择的最小或者最大的值,这就是最坏的情况。一个显而易见的改进策略就是随机选择列表中的值作为基准数,另一种是从头 (left)、中 ([left + right] // 2) 和尾 (right) 这三个元素中取中间值作为基准数。我们分别进行测试:import randomdef get_base(nums, left, right): index = random.randint(left, right) if index != left: nums[left], nums[index] = nums[index], nums[left] return nums[left]def get_base_middle(nums, left, right): if left == right: return nums[left] mid = (left + right) >> 1 if nums[mid] > nums[right]: nums[mid], nums[right] = nums[right], nums[mid] if nums[left] > nums[right]: # nums[left]最大,nums[right]中间,所以交换 nums[left], nums[right] = nums[left], nums[mid] if nums[mid] > nums[left]: # 说明nums[left]最小, nums[mid]为中间值 nums[left], nums[mid] = nums[mid], nums[left] return nums[left]def get_num_position(nums, left, right): # base = nums[left] # 随机基准数 base = get_base(nums, left, right) # base = get_base_middle(nums, left, right) while left < right: # 和前面相同,以下两个while语句用于将基准数放到对应位置,使得基准数左边的元素都小于它,右边的数都大于它 while left < right and nums[right] >= base: right -= 1 nums[left] = nums[right] while left < right and nums[left] <= base: left += 1 nums[right] = nums[left] # 基准数的位置,并返回该位置值 nums[left] = base return left改进后的测试结果如下:# 有序列表-随机基准值PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.021890 Seconds# 随机列表-随机基准值PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.026948 Seconds# 有序列表-中位数基准值PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.012944 Seconds# 随机列表-中位数基准值 PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.020933 Seconds可以看到使用中位数基准值在有序列表情况下排序速度更快。最后还有一个简单的常用优化方案,就是当序列长度达到一定大小时,使用插入排序。# 改造前面的插入排序算法def insert_sort(nums, start, end): """ 插入排序 """ if not nums: return False for i in range(start + 1, end + 1): t = nums[i] for j in range(i - 1, start - 1, -1): k = j if nums[j] <= t: k = k + 1 break nums[j + 1] = nums[j] nums[k] = t return True # ...def quick_sort(nums, start, end): """ 快速排序算法 """ if (end - start + 1 < 10): # 在排序的数组小到一定程度时,使用插入排序 insert_sort(nums, start, end) return # 找到基准数位置,同时调整好数组,使得基准数的左边小于它,基准数的右边都是大于它 pos = get_num_position(nums, start, end) # 递归使用快排,对左边使用快排算法 quick_sort(nums, start, pos - 1) # 对右边元素使用快排算法 quick_sort(nums, pos + 1, end)下面是使用【随机基准+插入排序优化】的测试结果如下:# 有序列表-[随机基准+使用插入排序优化]PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.010962 Seconds# 无序列表-[随机基准+使用插入排序优化]PS C:\Users\spyinx\Desktop\学习教程\慕课网教程\算法慕课教程\code> & "D:/Program Files (x86)/python3/python.exe" c:/Users/spyinx/Desktop/学习教程/慕课网教程/算法慕课教程/code/test_algorithms.pyRunning time: 0:00:00.018935 Seconds可以看到这些小技巧在真正大规模数据排序时会有非常明显的效果。更多的优化方法以及测试,读者可以自行去实验和测试,这里就不再继续讲解了。

3.1 ANR 和死锁

错误报告有助于我们找出导致应用无响应 (ANR) 错误和死锁事件的原因。找出无响应的应用当某个应用在一定时间内没有响应(通常是由于主线程被阻塞或繁忙)时,系统会终止该进程并将堆栈转储到 /data/anr。要找出 ANR 背后的罪魁祸首,请为二进制事件日志中的 am_anr 执行 grep 命令。日志范例如下:grep "am_anr" bugreport-2015-10-01-18-13-48.txt10-01 18:12:49.599 4600 4614 I am_anr : [0,29761,com.google.android.youtube,953695941,executing service com.google.android.youtube/com.google.android.apps.youtube.app.offline.transfer.OfflineTransferService]10-01 18:14:10.211 4600 4614 I am_anr : [0,30363,com.google.android.apps.plus,953728580,executing service com.google.android.apps.plus/com.google.android.apps.photos.service.PhotosService]我们也可以为 logcat 日志(其中包含关于发生 ANR 时是什么在占用 CPU 的更多信息)中的 ANR in 执行 grep 命令。日志范例如下:grep "ANR in" bugreport-2015-10-01-18-13-48.txt10-01 18:13:11.984 4600 4614 E ActivityManager: ANR in com.google.android.youtube10-01 18:14:31.720 4600 4614 E ActivityManager: ANR in com.google.android.apps.plus10-01 18:14:31.720 4600 4614 E ActivityManager: PID: 3036310-01 18:14:31.720 4600 4614 E ActivityManager: Reason: executing service com.google.android.apps.plus/com.google.android.apps.photos.service.PhotosService10-01 18:14:31.720 4600 4614 E ActivityManager: Load: 35.27 / 23.9 / 16.1810-01 18:14:31.720 4600 4614 E ActivityManager: CPU usage from 16ms to 21868ms later:10-01 18:14:31.720 4600 4614 E ActivityManager: 74% 3361/mm-qcamera-daemon: 62% user + 12% kernel / faults: 15276 minor 10 major10-01 18:14:31.720 4600 4614 E ActivityManager: 41% 4600/system_server: 18% user + 23% kernel / faults: 18597 minor 309 major10-01 18:14:31.720 4600 4614 E ActivityManager: 32% 27420/com.google.android.GoogleCamera: 24% user + 7.8% kernel / faults: 48374 minor 338 major10-01 18:14:31.720 4600 4614 E ActivityManager: 16% 130/kswapd0: 0% user + 16% kernel10-01 18:14:31.720 4600 4614 E ActivityManager: 15% 283/mmcqd/0: 0% user + 15% kernel...10-01 18:14:31.720 4600 4614 E ActivityManager: 0.1% 27248/irq/503-synapti: 0%10-01 18:14:31.721 4600 4614 I ActivityManager: Killing 30363:com.google.android.apps.plus/u0a206 (adj 0): bg anr查找堆栈跟踪通常我们可以找到与 ANR 对应的堆栈跟踪。请确保 VM 跟踪上的时间戳和 PID 与我们正在调查的 ANR 相符,然后再检查进程的主线程。日志范例如下:VM TRACES AT LAST ANR (/data/anr/traces.txt: 2015-10-01 18:14:41)pid 30363 at 2015-10-01 18:14:11Cmd line: com.google.android.apps.plusBuild fingerprint: 'google/angler/angler:6.0/MDA89D/2294819:userdebug/dev-keys'ABI: 'arm'Build type: optimizedZygote loaded classes=3978 post zygote classes=27Intern table: 45068 strong; 21 weakJNI: CheckJNI is off; globals=283 (plus 360 weak)Libraries: /system/lib/libandroid.so /system/lib/libcompiler_rt.so /system/lib/libjavacrypto.so /system/lib/libjnigraphics.so /system/lib/libmedia_jni.so /system/lib/libwebviewchromium_loader.so libjavacore.so (7)Heap: 29% free, 21MB/30MB; 32251 objectsDumping cumulative Gc timingsTotal number of allocations 32251Total bytes allocated 21MBTotal bytes freed 0BFree memory 9MBFree memory until GC 9MBFree memory until OOME 490MBTotal memory 30MBMax memory 512MBZygote space size 1260KBTotal mutator paused time: 0Total time waiting for GC to complete: 0Total GC count: 0Total GC time: 0Total blocking GC count: 0Total blocking GC time: 0suspend all histogram: Sum: 119.728ms 99% C.I. 0.010ms-107.765ms Avg: 5.442ms Max: 119.562msDALVIK THREADS (12):"Signal Catcher" daemon prio=5 tid=2 Runnable | group="system" sCount=0 dsCount=0 obj=0x12c400a0 self=0xef460000 | sysTid=30368 nice=0 cgrp=default sched=0/0 handle=0xf4a69930 | state=R schedstat=( 9021773 5500523 26 ) utm=0 stm=0 core=1 HZ=100 | stack=0xf496d000-0xf496f000 stackSize=1014KB | held mutexes= "mutator lock"(shared held) native: #00 pc 0035a217 /system/lib/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char> >&, int, char const*, art::ArtMethod*, void*)+126) native: #01 pc 0033b03b /system/lib/libart.so (art::Thread::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char> >&) const+138) native: #02 pc 00344701 /system/lib/libart.so (art::DumpCheckpoint::Run(art::Thread*)+424) native: #03 pc 00345265 /system/lib/libart.so (art::ThreadList::RunCheckpoint(art::Closure*)+200) native: #04 pc 00345769 /system/lib/libart.so (art::ThreadList::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+124) native: #05 pc 00345e51 /system/lib/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+312) native: #06 pc 0031f829 /system/lib/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char> >&)+68) native: #07 pc 00326831 /system/lib/libart.so (art::SignalCatcher::HandleSigQuit()+896) native: #08 pc 003270a1 /system/lib/libart.so (art::SignalCatcher::Run(void*)+324) native: #09 pc 0003f813 /system/lib/libc.so (__pthread_start(void*)+30) native: #10 pc 00019f75 /system/lib/libc.so (__start_thread+6) (no managed stack frames)"main" prio=5 tid=1 Suspended | group="main" sCount=1 dsCount=0 obj=0x747552a0 self=0xf5376500 | sysTid=30363 nice=0 cgrp=default sched=0/0 handle=0xf74feb34 | state=S schedstat=( 331107086 164153349 851 ) utm=6 stm=27 core=3 HZ=100 | stack=0xff00f000-0xff011000 stackSize=8MB | held mutexes= kernel: __switch_to+0x7c/0x88 kernel: futex_wait_queue_me+0xd4/0x130 kernel: futex_wait+0xf0/0x1f4 kernel: do_futex+0xcc/0x8f4 kernel: compat_SyS_futex+0xd0/0x14c kernel: cpu_switch_to+0x48/0x4c native: #00 pc 000175e8 /system/lib/libc.so (syscall+28) native: #01 pc 000f5ced /system/lib/libart.so (art::ConditionVariable::Wait(art::Thread*)+80) native: #02 pc 00335353 /system/lib/libart.so (art::Thread::FullSuspendCheck()+838) native: #03 pc 0011d3a7 /system/lib/libart.so (art::ClassLinker::LoadClassMembers(art::Thread*, art::DexFile const&, unsigned char const*, art::Handle<art::mirror::Class>, art::OatFile::OatClass const*)+746) native: #04 pc 0011d81d /system/lib/libart.so (art::ClassLinker::LoadClass(art::Thread*, art::DexFile const&, art::DexFile::ClassDef const&, art::Handle<art::mirror::Class>)+88) native: #05 pc 00132059 /system/lib/libart.so (art::ClassLinker::DefineClass(art::Thread*, char const*, unsigned int, art::Handle<art::mirror::ClassLoader>, art::DexFile const&, art::DexFile::ClassDef const&)+320) native: #06 pc 001326c1 /system/lib/libart.so (art::ClassLinker::FindClassInPathClassLoader(art::ScopedObjectAccessAlreadyRunnable&, art::Thread*, char const*, unsigned int, art::Handle<art::mirror::ClassLoader>, art::mirror::Class**)+688) native: #07 pc 002cb1a1 /system/lib/libart.so (art::VMClassLoader_findLoadedClass(_JNIEnv*, _jclass*, _jobject*, _jstring*)+264) native: #08 pc 002847fd /data/dalvik-cache/arm/system@framework@boot.oat (Java_java_lang_VMClassLoader_findLoadedClass__Ljava_lang_ClassLoader_2Ljava_lang_String_2+112) at java.lang.VMClassLoader.findLoadedClass!(Native method) at java.lang.ClassLoader.findLoadedClass(ClassLoader.java:362) at java.lang.ClassLoader.loadClass(ClassLoader.java:499) at java.lang.ClassLoader.loadClass(ClassLoader.java:469) at android.app.ActivityThread.installProvider(ActivityThread.java:5141) at android.app.ActivityThread.installContentProviders(ActivityThread.java:4748) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4688) at android.app.ActivityThread.-wrap1(ActivityThread.java:-1) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1405) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke!(Native method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)查找死锁由于线程出现粘滞,死锁往往首先表现为 ANR。如果系统服务器发生死锁,监控程序最终会将其终止,从而导致日志中出现类似以下的条目:WATCHDOG KILLING SYSTEM PROCESS。对于用户来说,看到的是设备重新启动,但从技术上来讲这是运行时重启,而不是真正的设备重新启动。要查找死锁,请检查 VM 跟踪部分中是否存在以下模式:线程 A 在等待线程 B 占用的某些资源,而线程 B 也在等待线程 A 占用的某些资源。日志范例如下:"Binder_B" prio=5 tid=73 Blocked | group="main" sCount=1 dsCount=0 obj=0x13faa0a0 self=0x95e24800 | sysTid=2016 nice=0 cgrp=default sched=0/0 handle=0x8b68d930 | state=S schedstat=( 9351576559 4141431119 16920 ) utm=819 stm=116 core=1 HZ=100 | stack=0x8b591000-0x8b593000 stackSize=1014KB | held mutexes= at com.android.server.pm.UserManagerService.exists(UserManagerService.java:387) - waiting to lock <0x025f9b02> (a android.util.ArrayMap) held by thread 20 at com.android.server.pm.PackageManagerService.getApplicationInfo(PackageManagerService.java:2848) at com.android.server.AppOpsService.getOpsRawLocked(AppOpsService.java:881) at com.android.server.AppOpsService.getOpsLocked(AppOpsService.java:856) at com.android.server.AppOpsService.noteOperationUnchecked(AppOpsService.java:719) - locked <0x0231885a> (a com.android.server.AppOpsService) at com.android.server.AppOpsService.noteOperation(AppOpsService.java:713) at com.android.server.AppOpsService$2.getMountMode(AppOpsService.java:260) at com.android.server.MountService$MountServiceInternalImpl.getExternalStorageMountMode(MountService.java:3416) at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:3228) at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:3170) at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:3059) at com.android.server.am.BroadcastQueue.processNextBroadcast(BroadcastQueue.java:1070) - locked <0x044d166f> (a com.android.server.am.ActivityManagerService) at com.android.server.am.ActivityManagerService.finishReceiver(ActivityManagerService.java:16950) at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:494) at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2432) at android.os.Binder.execTransact(Binder.java:453)"PackageManager" prio=5 tid=20 Blocked | group="main" sCount=1 dsCount=0 obj=0x1304f4a0 self=0xa7f43900 | sysTid=1300 nice=10 cgrp=bg_non_interactive sched=0/0 handle=0x9fcf9930 | state=S schedstat=( 26190141996 13612154802 44357 ) utm=2410 stm=209 core=2 HZ=100 | stack=0x9fbf7000-0x9fbf9000 stackSize=1038KB | held mutexes= at com.android.server.AppOpsService.noteOperationUnchecked(AppOpsService.java:718) - waiting to lock <0x0231885a> (a com.android.server.AppOpsService) held by thread 73 at com.android.server.AppOpsService.noteOperation(AppOpsService.java:713) at com.android.server.AppOpsService$2.getMountMode(AppOpsService.java:260) at com.android.server.AppOpsService$2.hasExternalStorage(AppOpsService.java:273) at com.android.server.MountService$MountServiceInternalImpl.hasExternalStorage(MountService.java:3431) at com.android.server.MountService.getVolumeList(MountService.java:2609) at android.os.storage.StorageManager.getVolumeList(StorageManager.java:880) at android.os.Environment$UserEnvironment.getExternalDirs(Environment.java:83) at android.os.Environment.isExternalStorageEmulated(Environment.java:708) at com.android.server.pm.PackageManagerService.isExternalMediaAvailable(PackageManagerService.java:9327) at com.android.server.pm.PackageManagerService.startCleaningPackages(PackageManagerService.java:9367) - locked <0x025f9b02> (a android.util.ArrayMap) at com.android.server.pm.PackageManagerService$PackageHandler.doHandleMessage(PackageManagerService.java:1320) at com.android.server.pm.PackageManagerService$PackageHandler.handleMessage(PackageManagerService.java:1122) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.os.HandlerThread.run(HandlerThread.java:61) at com.android.server.ServiceThread.run(ServiceThread.java:46)

3.1 concat () 函数

对于多个数据集的合并操作,concat () 函数提供了丰富的设置参数,满足我们灵活的合并需要,这里我们列举几个常用的参数进行详细讲解。pd.concat(objs, axis='0', join:'outer', ignore_index: 'False', keys='None', levels='None', names='None', verify_integrity: 'False', sort: 'False', copy:'True') 参数名说明 objs 要合并的数据列表,可以是 Series、 DataFrameaxis 合并的方向,axis=0 纵向合并 (默认),axis=1 横向合并 join 数据合并的方式,包含 inner 和 outer 两种,默认是 outerignore_index 忽略合并方向上轴的索引值,从 0 开始重新进行索引值排序,默认为 ignore_index=False下面我们通过代码程序进行详细学习这些参数的使用。1. axis 参数该参数用于设置数据合并的方向。# data_03,data_04,data_05 是上面从三个excel表中解析出的数据集# concat 函数,axis=0 是纵向上按行合并。data_res=pd.concat([data_03,data_04,data_05],axis=0)print(data_res)# --- 输出结果 --- 编程语言 推出时间 价格 月平均销售数量 主要销售区域 月份 发行地点0 java 1995年 45.6 NaN NaN NaN NaN1 python 1991年 67.0 NaN NaN NaN NaN2 C 1972年 33.9 NaN NaN NaN NaN3 js 1995年 59.5 NaN NaN NaN NaN4 php 2012年 69.9 NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN0 NaN 1995年 NaN 134.0 成都 NaN NaN1 NaN 2006年 NaN 231.0 北京 NaN NaN2 NaN 1972年 NaN 67.0 天津 NaN NaN0 NaN 1995年 NaN NaN NaN 12.0 广州1 NaN 2006年 NaN NaN NaN 2.0 上海2 NaN 1972年 NaN NaN NaN 4.0 南京3 NaN 2017年 NaN NaN NaN 5.0 北京# 输出解析:通过设置 axis=0 在纵向上合并数据,总的行数据量是3个数据集的总和,扩充了行数据。# concat 函数,axis=1 设置在横向上合并。data_res=pd.concat([data_03,data_04,data_05],axis=1)print(data_res)# --- 输出结果 --- 编程语言 推出时间 价格 推出时间 月平均销售数量 主要销售区域 推出时间 月份 发行地点0 java 1995年 45.6 1995年 134.0 成都 1995年 12.0 广州1 python 1991年 67.0 2006年 231.0 北京 2006年 2.0 上海2 C 1972年 33.9 1972年 67.0 天津 1972年 4.0 南京3 js 1995年 59.5 NaN NaN NaN 2017年 5.0 北京4 php 2012年 69.9 NaN NaN NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN NaN NaN# 输出解析:通过设置 axis=1 在横向上合并数据,总的列数据量是3个数据集的总和,扩充了列数据。2. join 参数该参数设置数据集合并的方式,有两个值:inner:数据集之间的交集,行合并时取列索引值的相同的数据,列合并时取行索引值相同的数据;outer:取数据集之间的并集,没有数据的用 NaN 进行填充,默认是这种合并方式。# data_03,data_04,data_05 是上面从三个excel表中解析出的数据集# concat 函数,axis=1,join="outer" 设置合并的方式。data_res=pd.concat([data_03,data_04,data_05],axis=1,join="outer")print(data_res)# --- 输出结果 --- 编程语言 推出时间 价格 推出时间 月平均销售数量 主要销售区域 推出时间 月份 发行地点0 java 1995年 45.6 1995年 134.0 成都 1995年 12.0 广州1 python 1991年 67.0 2006年 231.0 北京 2006年 2.0 上海2 C 1972年 33.9 1972年 67.0 天津 1972年 4.0 南京3 js 1995年 59.5 NaN NaN NaN 2017年 5.0 北京4 php 2012年 69.9 NaN NaN NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN NaN NaN# 输出解析:这里设置在横向上合并列数据,合并方式为 outer ,所以将所有数据集的行索引取了并集,data_03 的行索引值为0-5,data_04 的行索引值为0-2,data_5 的行索引值为0-3,他们的并集就是 data_03 的从0到5,对于 data_04 和 data_05 在对应的行索引上不存在数据的,则以 NaN 进行填充。# concat 函数,axis=0,join="outer" 设置合并的方式。data_res=pd.concat([data_03,data_04,data_05],axis=0,join="outer")print(data_res)# --- 输出结果 --- 编程语言 推出时间 价格 月平均销售数量 主要销售区域 月份 发行地点0 java 1995年 45.6 NaN NaN NaN NaN1 python 1991年 67.0 NaN NaN NaN NaN2 C 1972年 33.9 NaN NaN NaN NaN3 js 1995年 59.5 NaN NaN NaN NaN4 php 2012年 69.9 NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN0 NaN 1995年 NaN 134.0 成都 NaN NaN1 NaN 2006年 NaN 231.0 北京 NaN NaN2 NaN 1972年 NaN 67.0 天津 NaN NaN0 NaN 1995年 NaN NaN NaN 12.0 广州1 NaN 2006年 NaN NaN NaN 2.0 上海2 NaN 1972年 NaN NaN NaN 4.0 南京3 NaN 2017年 NaN NaN NaN 5.0 北京# 输出解析: 这里设置了在纵向上的行合并,合并方式为 outer,在列索引上取了并集,为{“编程语言”,“推出时间”,“价格”,“月平均销售数量”,“主要销售区域”,“月份”,“发行地点”},合并行中如果不存在对应列的数据,则以 NaN 进行填充。# concat 函数,axis=1,join="inner" 设置合并的方式。data_res=pd.concat([data_03,data_04,data_05],axis=1,join="inner")print(data_res)# --- 输出结果 --- 编程语言 推出时间 价格 推出时间 月平均销售数量 主要销售区域 推出时间 月份 发行地点0 java 1995年 45.6 1995年 134 成都 1995年 12 广州1 python 1991年 67.0 2006年 231 北京 2006年 2 上海2 C 1972年 33.9 1972年 67 天津 1972年 4 南京# 输出解析:这里设置了在横向上合并列数据,合并方式为 inner ,在行索引值中去交集,data_03 的行索引值为0-5,data_04 的行索引值为0-2,data_5 的行索引值为0-3,他们的交集也就是0到2,可以看到输出结果合并了列,取了三行数据。# concat 函数,axis=0,join="inner" 设置合并的方式。data_res=pd.concat([data_03,data_04,data_05],axis=0,join="inner")print(data_res)# --- 输出结果 --- 推出时间0 1995年1 1991年2 1972年3 1995年4 2012年5 1983年0 1995年1 2006年2 1972年0 1995年1 2006年2 1972年3 2017年# 输出解析:通过设置在行上进行数据合并,用的 inner 方式合并,在列的数据上,他们的交集只有“推出时间”,通过输出可以看到效果。通过上面的代码演示可以看到,因为 outer 取得是并集,合并结果中可能会出现 NaN 的填充数据,而 inner 取的是交集,合并数据结果集中不会出现 NaN 的缺失数据。3. ignore_index 参数该参数可以设置在合并方向上的索引值自动生成,从 0 开始的整数序列。# data_03,data_04,data_05 是上面从三个excel表中解析出的数据集# concat 函数,ignore_index 重新生成索引序列。data_res=pd.concat([data_03,data_04,data_05],axis=1,ignore_index=False)print(data_res)# --- 输出结果 ignore_index=False(默认的值)--- 编程语言 推出时间 价格 推出时间 月平均销售数量 主要销售区域 推出时间 月份 发行地点0 java 1995年 45.6 1995年 134.0 成都 1995年 12.0 广州1 python 1991年 67.0 2006年 231.0 北京 2006年 2.0 上海2 C 1972年 33.9 1972年 67.0 天津 1972年 4.0 南京3 js 1995年 59.5 NaN NaN NaN 2017年 5.0 北京4 php 2012年 69.9 NaN NaN NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN NaN NaNdata_res=pd.concat([data_03,data_04,data_05],axis=1,ignore_index=True)print(data_res)# --- 输出结果 ignore_index=True --- 0 1 2 3 4 5 6 7 80 java 1995年 45.6 1995年 134.0 成都 1995年 12.0 广州1 python 1991年 67.0 2006年 231.0 北京 2006年 2.0 上海2 C 1972年 33.9 1972年 67.0 天津 1972年 4.0 南京3 js 1995年 59.5 NaN NaN NaN 2017年 5.0 北京4 php 2012年 69.9 NaN NaN NaN NaN NaN NaN5 C++ 1983年 75.0 NaN NaN NaN NaN NaN NaN 输出解析:这里通过 ignore_index 参数设置的对比,可以看到在列索引上的索引值的变化。

3. 图书爬虫之代码实现

根据上面的分析,我们来实现相应的代码。首先是完成获取计算机的所有分类以及相应的 URL 地址:def get_all_computer_book_urls(page_url): """ 获取所有计算机分类图书的url地址 :return: """ response = requests.get(url=page_url, headers=headers) if response.status_code != 200: return [], [] response.encoding = 'gbk' tree = etree.fromstring(response.text, etree.HTMLParser()) # 提取计算机分类的文本列表 c = tree.xpath("//div[@id='wrap']/ul[1]/li[@class='li']/a/text()") # 提取计算机分类的url列表 u = tree.xpath("//div[@id='wrap']/ul[1]/li[@class='li']/a/@href") return c, u我们简单测试下这个函数:[store@server2 chap06]$ python3Python 3.6.8 (default, Apr 2 2020, 13:34:55) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linuxType "help", "copyright", "credits" or "license" for more information.>>> from china_pub_crawler import get_all_computer_book_urls>>> get_all_computer_book_urls('http://www.china-pub.com/Browse/')(['IT图书网络出版 [59-00]', '计算机科学理论与基础知识 [59-01]', '计算机组织与体系结构 [59-02]', '计算机网络 [59-03]', '安全 [59-04]', '软件与程序设计 [59-05]', '软件工程及软件方法学 [59-06]', '操作系统 [59-07]', '数据库 [59-08]', '硬件与维护 [59-09]', '图形图像、多媒体、网页制作 [59-10]', '中文信息处理 [59-11]', '计算机辅助设计与工程计算 [59-12]', '办公软件 [59-13]', '专用软件 [59-14]', '人工智能 [59-15]', '考试认证 [59-16]', '工具书 [59-17]', '计算机控制与仿真 [59-18]', '信息系统 [59-19]', '电子商务与计算机文化 [59-20]', '电子工程 [59-21]', '期刊 [59-22]', '游戏 [59-26]', 'IT服务管理 [59-27]', '计算机文化用品 [59-80]'], ['http://product.china-pub.com/cache/browse2/59/1_1_59-00_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-01_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-02_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-03_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-04_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-05_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-06_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-07_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-08_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-09_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-10_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-11_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-12_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-13_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-14_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-15_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-16_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-17_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-18_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-19_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-20_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-21_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-22_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-26_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-27_0.html', 'http://product.china-pub.com/cache/browse2/59/1_1_59-80_0.html'])可以看到这个函数已经实现了我们想要的结果。接下来我们要完成一个函数来获取对应分类下的所有图书信息,不过在此之前,我们需要先完成解析单个图书列表页面的方法:def parse_books_page(html_data): books = [] tree = etree.fromstring(html_data, etree.HTMLParser()) result_tree = tree.xpath("//div[@class='search_result']/table/tr/td[2]/ul") for result in result_tree: try: book_info = {} book_info['title'] = result.xpath("./li[@class='result_name']/a/text()")[0] book_info['book_url'] = result.xpath("./li[@class='result_name']/a/@href")[0] info = result.xpath("./li[2]/text()")[0] book_info['author'] = info.split('|')[0].strip() book_info['publisher'] = info.split('|')[1].strip() book_info['isbn'] = info.split('|')[2].strip() book_info['publish_date'] = info.split('|')[3].strip() book_info['vip_price'] = result.xpath("./li[@class='result_book']/ul/li[@class='book_dis']/text()")[0] book_info['price'] = result.xpath("./li[@class='result_book']/ul/li[@class='book_price']/text()")[0] # print(f'解析出的图书信息为:{book_info}') books.append(book_info) except Exception as e: print("解析数据出现异常,忽略!") return books上面的函数主要解析的是一页图书列表数据,同样基于 xpath 定位相应的元素,然后提取我们想要的数据。其中由于部分信息合在一起,我们在提取数据后还要做相关的处理,分别提取对应的信息。我们可以从网页中直接样 HTML 拷贝下来,然后对该函数进行测试:提取图书列表的网页数据我们把保存的网页命名为 test.html,放到与该代码同级的目录下,然后进入命令行操作:>>> from china_pub_crawler import parse_books_page>>> f = open('test.html', 'r+')>>> html_content = f.read()>>> parse_books_page(html_content)[{'title': '(特价书)零基础学ASP.NET 3.5', 'book_url': 'http://product.china-pub.com/216269', 'author': '王向军;王欣惠 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111261414', 'publish_date': '2009-02-01出版', 'vip_price': 'VIP会员价:', 'price': '¥58.00'}, {'title': 'Objective-C 2.0 Mac和iOS开发实践指南(原书第2版)', 'book_url': 'http://product.china-pub.com/3770704', 'author': '(美)Robert Clair (著)', 'publisher': '机械工业出版社', 'isbn': '9787111484561', 'publish_date': '2015-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥79.00'}, {'title': '(特价书)ASP.NET 3.5实例精通', 'book_url': 'http://product.china-pub.com/216272', 'author': '王院峰 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111259794', 'publish_date': '2009-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥55.00'}, {'title': '(特价书)CSS+HTML语法与范例详解词典', 'book_url': 'http://product.china-pub.com/216275', 'author': '符旭凌 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111263647', 'publish_date': '2009-02-01出版', 'vip_price': 'VIP会员价:', 'price': '¥39.00'}, {'title': '(特价书)Java ME 游戏编程(原书第2版)', 'book_url': 'http://product.china-pub.com/216296', 'author': '(美)Martin J. Wells; John P. Flynt (著)', 'publisher': '机械工业出版社', 'isbn': '9787111264941', 'publish_date': '2009-03-01出版', 'vip_price': 'VIP会员价:', 'price': '¥49.00'}, {'title': '(特价书)Visual Basic实例精通', 'book_url': 'http://product.china-pub.com/216304', 'author': '柴相花 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111263296', 'publish_date': '2009-04-01出版', 'vip_price': 'VIP会员价:', 'price': '¥59.80'}, {'title': '高性能电子商务平台构建:架构、设计与开发[按需印刷]', 'book_url': 'http://product.china-pub.com/3770743', 'author': 'ShopNC产品部 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111485643', 'publish_date': '2015-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥79.00'}, {'title': '[套装书]Java核心技术 卷Ⅰ 基础知识(原书第10版)+Java核心技术 卷Ⅱ高级特性(原书第10版)', 'book_url': 'http://product.china-pub.com/7008447', 'author': '(美)凯S.霍斯特曼(Cay S. Horstmann)????(美)凯S. 霍斯特曼(Cay S. Horstmann) (著)', 'publisher': '机械工业出版社', 'isbn': '9787007008447', 'publish_date': '2017-08-01出版', 'vip_price': 'VIP会员价:', 'price': '¥258.00'}, {'title': '(特价书)Dojo构建Ajax应用程序', 'book_url': 'http://product.china-pub.com/216315', 'author': '(美)James E.Harmon (著)', 'publisher': '机械工业出版社', 'isbn': '9787111266648', 'publish_date': '2009-05-01出版', 'vip_price': 'VIP会员价:', 'price': '¥45.00'}, {'title': '(特价书)编译原理第2版.本科教学版', 'book_url': 'http://product.china-pub.com/216336', 'author': '(美)Alfred V. Aho;Monica S. Lam;Ravi Sethi;Jeffrey D. Ullman (著)', 'publisher': '机械工业出版社', 'isbn': '9787111269298', 'publish_date': '2009-05-01出版', 'vip_price': 'VIP会员价:', 'price': '¥55.00'}, {'title': '(特价书)用Alice学编程(原书第2版)', 'book_url': 'http://product.china-pub.com/216354', 'author': '(美)Wanda P.Dann;Stephen Cooper;Randy Pausch (著)', 'publisher': '机械工业出版社', 'isbn': '9787111274629', 'publish_date': '2009-07-01出版', 'vip_price': 'VIP会员价:', 'price': '¥39.00'}, {'title': 'Java语言程序设计(第2版)', 'book_url': 'http://product.china-pub.com/50051', 'author': '赵国玲;王宏;柴大鹏 (著)', 'publisher': '机械工业出版社*', 'isbn': '9787111297376', 'publish_date': '2010-03-01出版', 'vip_price': 'VIP会员价:', 'price': '¥32.00'}, {'title': '从零开始学Python程序设计', 'book_url': 'http://product.china-pub.com/7017939', 'author': '吴惠茹 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111583813', 'publish_date': '2018-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥79.00'}, {'title': '(特价书)汇编语言', 'book_url': 'http://product.china-pub.com/216385', 'author': '郑晓薇 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111269076', 'publish_date': '2009-09-01出版', 'vip_price': 'VIP会员价:', 'price': '¥29.00'}, {'title': '(特价书)Visual Basic.NET案例教程', 'book_url': 'http://product.china-pub.com/216388', 'author': '马玉春;刘杰民;王鑫 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111272571', 'publish_date': '2009-09-01出版', 'vip_price': 'VIP会员价:', 'price': '¥30.00'}, {'title': '小程序从0到1:微信全栈工程师一本通', 'book_url': 'http://product.china-pub.com/7017943', 'author': '石桥码农 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111584049', 'publish_date': '2018-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥59.00'}, {'title': '深入分布式缓存:从原理到实践', 'book_url': 'http://product.china-pub.com/7017945', 'author': '于君泽 (著)', 'publisher': '机械工业出版社', 'isbn': '9787111585190', 'publish_date': '2018-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥99.00'}, {'title': '(特价书)ASP.NET AJAX服务器控件高级编程(.NET 3.5版)', 'book_url': 'http://product.china-pub.com/216397', 'author': '(美)Adam Calderon;Joel Rumerman (著)', 'publisher': '机械工业出版社', 'isbn': '9787111270966', 'publish_date': '2009-09-01出版', 'vip_price': 'VIP会员价:', 'price': '¥65.00'}, {'title': 'PaaS程序设计', 'book_url': 'http://product.china-pub.com/3770830', 'author': '(美)Lucas Carlson (著)', 'publisher': '机械工业出版社', 'isbn': '9787111482451', 'publish_date': '2015-01-01出版', 'vip_price': 'VIP会员价:', 'price': '¥39.00'}, {'title': 'Visual C++数字图像处理[按需印刷]', 'book_url': 'http://product.china-pub.com/2437', 'author': '何斌 马天予 王运坚 朱红莲 (著)', 'publisher': '人民邮电出版社', 'isbn': '711509263X', 'publish_date': '2001-04-01出版', 'vip_price': 'VIP会员价:', 'price': '¥72.00'}]是不是能正确提取图书列表的相关信息?这也说明我们的函数的正确性,由于也可能在解析中存在一些异常,比如某个字段的缺失,我们需要捕获异常并忽略该条数据,让程序能继续走下去而不是停止运行。在完成了上述的工作后,我们来通过对页号的 URL 构造,实现采集多个分页下的数据,最后达到读取完该分类下的所有图书信息的目的。完整代码如下:def get_category_books(category, url): """ 获取类别图书,下面会有分页,我们一直请求,直到分页请求返回404即可停止 :return: """ books = [] page = 1 regex = "(http://.*/)([0-9]+)_(.*).html" pattern = re.compile(regex) m = pattern.match(url) if not m: return [] prefix_path = m.group(1) current_page = m.group(2) if current_page != 1: print("提取数据不是从第一行开始,可能存在问题") suffix_path = m.group(3) current_page = page while True: # 构造分页请求的URL book_url = f"{prefix_path}{current_page}_{suffix_path}.html" response = requests.get(url=book_url, headers=headers) print(f"提取分类[{category}]下的第{current_page}页图书数据") if response.status_code != 200: print(f"[{category}]该分类下的图书数据提取完毕!") break response.encoding = 'gbk' # 将该分页的数据加到列表中 books.extend(parse_books_page(response.text)) current_page += 1 # 一定要缓一缓,避免对对方服务造成太大压力 time.sleep(0.5) return books最后保存数据到 MongoDB 中,这一步非常简单,我们前面已经操作过 MongoDB 的文档插入,直接搬用即可:client = pymongo.MongoClient(host='MongoDB的服务地址', port=27017)client.admin.authenticate("admin", "shencong1992")db = client.scrapy_manualcollection = db.china_pub# ...def save_to_mongodb(data): try: collection.insert_many(data) except Exception as e: print("批量插入数据异常:{}".format(str(e)))正是由于我们前面生成了批量的 json 数据,这里直接使用集合的 insert_many() 方法即可对采集到的数据批量插入 MongoDB 中。代码的最后我们加上一个 main 函数即可:# ...if __name__ == '__main__': page_url = "http://www.china-pub.com/Browse/" categories, urls = get_all_computer_book_urls(page_url) # print(categories) books_total = {} for i in range(len(urls)): books_category_data = get_category_books(categories[i], urls[i]) print(f"保存[{categories[i]}]图书数据到mongodb中") save_to_mongodb(books_category_data) print("爬取互动出版网的计算机分类数据完成")这样一个简单的爬虫就完成了,还等什么,开始跑起来吧!!

首页上一页1234下一页尾页
直播
查看课程详情
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号