前言
在使用爬虫爬取数据的时候,当需要爬取的数据量比较大,且急需很快获取到数据的时候,可以考虑将单线程的爬虫写成多线程的爬虫。下面来学习一些它的基础知识和代码编写方法。
一、进程和线程
进程可以理解为是正在运行的程序的实例。进程是拥有资源的独立单位,而线程不是独立的单位。由于每一次调度进程的开销比较大,为此才引入的线程。一个进程可以拥有多个线程,一个进程中可以同时存在多个线程,这些线程共享该进程的资源,线程的切换消耗是很小的。因此在操作系统中引入进程的目的是更好地使多道程序并发执行,提高资源利用率和系统吞吐量;而引入线程的目的则是减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
下面用简单的例子进行描述,打开本地计算机的”任务管理器”如图1所示,这些正在运行的程序叫作进程。如果将一个进程比喻成一个工作,指定10个人来做这份工作,这10个人就是10个线程。因此,在一定的范围内,多线程效率比单线程效率更高。
图1.任务管理器
二、Python中的多线程与单线程
在我们平时学习的过程中,使用的主要是单线程爬虫。一般来说,如果爬取的资源不是特别大,使用单线程即可。在Python中,默认情况下是单线程的,简单理解为:代码是按顺序依次运行的,比如先运行第一行代码,再运行第二行,依次类推。在前面章节所学习知识中,都是以单线程的形式实践的。
举个例子,批量下载某网站的图片,由于下载图片是一个耗时的操作,如果依然采用单线程的方式下载,那么效率就会特别低,意味着需要消耗更多的时间等待下载。为了节约时间,这时候我们就可以考虑使用多线程的方式来下载图片。
threading模块是Python中专门用来做多线程编程的模块,它对thread进行了封装,使用更加方便。例如需要对写代码和玩游戏两个事件使用多线程进行,案例代码如下。
import threading
import time
# 定义第一个
def coding():
for x in range(3):
print('%s正在写代码\n' % x)
time.sleep(1)
# 定义第二个
def playing():
for x in range(3):
print('%s正在玩游戏\n' % x)
time.sleep(1)
# 如果使用多线程执行
def multi_thread():
start = time.time()
# Thread创建第一个线程,target参数为函数命
t1 = threading.Thread(target=coding)
t1.start() # 启动线程
# 创建第二个线程
t2 = threading.Thread(target=playing)
t2.start()
# join是确保thread子线程执行完毕后才能执行下一个线程
t1.join()
t2.join()
end = time.time()
running_time = end - start
print('总共运行时间 : %.5f 秒' % running_time)
# 执行
if __name__ == '__main__':
multi_thread() # 执行单线程
运行结果如图2所示:
图2.多线程运行结果
那么执行单线程会消耗多少时间,案例代码如下所示。
import time
# 定义第一个
def coding():
for x in range(3):
print('%s正在写代码\n' % x)
time.sleep(1)
# 定义第二个
def playing():
start = time.time()
for x in range(3):
print('%s正在玩游戏\n' % x)
time.sleep(1)
end = time.time()
running_time = end - start
print('总共运行时间 : %.5f 秒' % running_time)
def single_thread():
coding()
playing()
# 执行
if __name__ == '__main__':
single_thread() # 执行单线程
运行结果如图3所示:
图3.单线程运行结果
经过以上多线程和单线程的运行结果,可以看出多线程中写代码和玩游戏是一起执行的,单线程中则是先写代码再玩游戏。从时间上来说,可能只有细微的差距,当执行工作量很大的时候,便会发现多线程消耗的时间会更少,从这个案例中我们也可以知道,当所需要执行的任务并不多的时候,只需要编写单线程即可。
三、单线程改为多线程
以某直播的图片爬取为例,案例代码如下:
import requests
from lxml import etree
import time
import os
dirpath = '图片/'
if not os.path.exists(dirpath):
os.mkdir(dirpath) # 创建文件夹
header = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'
}
def get_photo():
url = 'https://www.huya.com/g/4079/' # 目标网站
response = requests.get(url=url, headers=header) # 发送请求
data = etree.HTML(response.text) # 转化为html格式
return data
def jiexi():
data = get_photo()
image_url = data.xpath('//a//img//@data-original')
image_name = data.xpath('//a//img[@class="pic"]//@alt')
for ur, name in zip(image_url, image_name):
url = ur.replace('?imageview/4/0/w/338/h/190/blur/1', '')
title = name + '.jpg'
response = requests.get(url=url, headers=header) # 在此发送新的请求
with open(dirpath + title, 'wb') as f:
f.write(response.content)
print("下载成功" + name)
time.sleep(2)
if __name__ == '__main__':
jiexi()
如果需要修改为多线程爬虫,只需要修改主函数即可,例如创建4个线程进行爬取,案例代码如下所示:
if __name__ == "__main__":
threads = []
start = time.time()
# 创建四个进程
for i in range(1, 5):
thread = threading.Thread(target=jiexi(), args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end = time.time()
running_time = end - start
print('总共消耗时间 : %.5f 秒' % running_time)
print("全部完成!") # 主程序
数据结构与算法,是理论和实践必须紧密结合的一门学科,有关数据结构和算法同类的课程或书籍,有些只是名为“数据结构”,而非“数据结构与算法”,它们在内容上并无很大区别。
实际上,数据结构和算法,没有必要也无法严格区分,两者是“你中有我,我中有你”的关系。或者,将数据结构算做算法的一个分支也未尝不可,比如著名教材《算法导论》,就包含大量数据结构的内容。本书中涉及的问题,如果需要将数据以比较复杂的方式组织起来,就归类为数据结构;否则就归类为算法。
目前,程序设计课程在中学已经较为普及,在许多大中专院校更是理科生的必修课。社会上开办编程培训班亦十分流行。许多没有经过系统的计算机专业学习的学生,经过培训后若能掌握一两门语言,学会一些前端后端应用的开发技能,虽然这样理论基础薄弱,也能求得一份程序员的职位。
然而,要成为一名优秀的程序员,有一门课程是没有捷径可以绕过去的,那就是“数据结构与算法”。优秀的公司是不会放心将重要的任务交给不懂数据结构和算法的程序员的,因为那些程序员没有效率的观念,一不小心就可能写出肆意挥霍计算资源的程序,让公司付出真金白银的代价。比如,低效的后端将导致公司需要购买更多的服务器才能提供服务,甚至在访问量高时导致系统崩溃。如果有程序员信誓旦旦地说他的工作不需要用到数据结构和算法,那多半是因为他的水平不足以使他接触到需要数据结构和算法的任务。
总之,计算机专业的人员需要掌握好数据结构与算法,自不必说,非计算机专业的人员,不论打算转行,还是已经转行做了程序员,都应该学好这门课程。即便不做程序员,如果经常需要用编程来解决工作中的问题,学习这门课程也大有裨益。
1. A + B
题目
输入两个整数,求这两个整数的和是多少。
输入格式
输入两个整数A,B
,用空格隔开
输出格式
输出一个整数,表示这两个数的和
数据范围
0≤A,B≤108
样例输入:
3 4
样例输出:
7
AC代码C:
#include <stdio.h>
int main()
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",a+b);
return 0;
}
2.栈
题目
栈是计算机中经典的数据结构,简单的说,栈就是限制在一端进行插入删除操作的线性表。
栈有两种最重要的操作,即 pop(从栈顶弹出一个元素)和 push(将一个元素进栈)。
栈的重要性不言自明,任何一门数据结构的课程都会介绍栈。
宁宁同学在复习栈的基本概念时,想到了一个书上没有讲过的问题,而他自己无法给出答案,所以需要你的帮忙。
宁宁考虑的是这样一个问题:一个操作数序列,从 1,2,一直到 n,栈 的深度大于 n。
现在可以进行两种操作,
将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push
操作)。将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop
操作)。使用这两种操作,由一个操作数序列就可以得到一系列的输出序列。
你的程序将对给定的 n,计算并输出由操作数序列 1,2,…,n经过操作可能得到的输出序列的总数。
输入格式
输入文件只含一个整数 n
。
输出格式
输出文件只有一行,即可能输出序列的总数目。
数据范围
1≤n≤18
输入样例:
3
输出样例:
5
AC代码C:
3.大小写翻转
AC代码C:
#include<stdio.h> //包含头文件stdio.h
#include<string.h> //包含头文件string.h
int main() //程序从这里开始执行
{
char a[100]; //定义一个字符数组a,长度为100
int i=0; //定义整型变量i并初始化为0
gets(a); //输入字符串,并存储在a数组中
while(a[i]!='\0') //循环语句,判断a数组中第i个元素是否为'\0'(即是否到了字符串结尾)
{
if(a[i]>='a'&&a[i]<='z') //如果a数组中第i个元素是小写字母
{
a[i]=a[i]-32; //将其转换为对应的大写字母
}else if(a[i]>='A'&&a[i]<='Z') //如果a数组中第i个元素是大写字母
{
a[i]=a[i]+32; //将其转换为对应的小写字母
}
i++; //i自增1
}
puts(a); //输出转换后的字符串
getchar(); //等待用户输入任何字符后结束程序
return 0; //程序正常结束
}
4.非素数个数
求 [a,b]
之间的非素数个数。
特别的,1 也算作素数。
输入格式
输入包含多组测试数据。
每组数据占一行,包含两个整数 a,b
。
输出格式
每组数据输出一行答案,表示非素数的个数。
数据范围
1≤a≤b≤107
,
输入最多包含 10组数据。
输入样例:
1 10
1 100
输出样例:
5
74
不完全AC代码C:
完全的我不会写,先留着吧,因为我是个lj
#include <stdio.h>
// 判断一个数是否为素数,返回值为0或1
int hanshu1(int x)
{
for (int i = 2; i * i <= x; i++) // 枚举小于等于sqrt(x)的所有数
{
if (x % i == 0) return 1; // 如果x能被i整除,说明x不是素数,返回1
}
return 0; // 否则返回0,表示x是素数
}
int main()
{
int a, b;
while (scanf("%d%d", &a, &b) != EOF) // 读入多组数据,直到读取到文件结尾
{
int count = 0; // 统计[a,b]中的素数个数
for (int i = a; i <= b; i++) // 枚举区间[a,b]中的所有数
{
if (hanshu1(i)) count++; // 如果i是素数,计数器加1
}
printf("%d\n", count); // 输出素数个数
}
return 0;
}
该代码主要使用了两个函数:hanshu1
和main
。hanshu1
函数用于判断一个数是否为素数,main
函数用于读入多组数据,统计每组数据中区间[a,b]中的素数个数。
在hanshu1
函数中,使用了一个循环来枚举小于等于sqrt(x)的所有数i,并判断x是否能被i整除。如果x能被i整除,说明x不是素数,返回1;否则返回0,表示x是素数。
在main
函数中,使用了一个循环来读入多组数据,并对每组数据进行相同的操作。首先定义一个计数器count,用于统计区间[a,b]中的素数个数。然后使用另一个循环来枚举区间[a,b]中的所有数i,如果i是素数,计数器count加1。最后输出计数器count的值,即为区间[a,b]中的素数个数。
5.母牛的故事
题目描述
有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?
输入格式
输入数据由多个测试实例组成,每个测试实例占一行,包括一个整数n(0<n<55),n的含义如题目中描述。
n=0表示输入数据的结束,不做处理。
输出格式
对于每个测试实例,输出在第n年的时候母牛的数量。
每个输出占一行。
样例输入
2
4
5
0
样例输出
2
4
6
AC代码C:
#include <stdio.h>
// 递归函数,计算斐波那契数列的第n项,返回值为整数
int hanshu(int x)
{
if (x <= 4) return x; // 当n<=4时,直接返回n的值
else // 当n>4时,递归计算前两项的和
return hanshu(x - 3) + hanshu(x - 1);
}
int main()
{
int a;
while (scanf("%d", &a)&&a) // 循环读入多组数据
printf("%d\n", hanshu(a)); // 对每组数据进行相同的操作,输出斐波那契数列的第n项
return 0;
}
该代码定义了一个递归函数hanshu
,用于计算斐波那契数列的第n项。在函数中,当n<=4时,直接返回n的值;当n>4时,递归计算前两项的和。
该代码还包括一个循环语句,用于读入多组数据,并对每组数据进行相同的操作。使用scanf
函数从标准输入中读入一个整数a,并调用函数hanshu
计算斐波那契数列的第a项。最后使用printf
函数输出计算结果。
6.用筛法求之N内的素数
AC代码C:
#include<stdio.h>
int main()
{
int n;
scanf("%d",&n);
int i,j;
for(i=2;i<=n;i++)
{
for(j=2;j*j<=i;j++)
if(i%j==0) break;
if(j>sqrt(i)) printf("%d\n",i);
}
}
7.C语言训练-计算一个整数N的阶乘
#include<stdio.h>
int main()
{
int a,t=1;
scanf("%d",&a);
for(int i = 1;i<=a;i++)
t*=i;
printf("%d",t);
return 0;
}
i++和++i的区别
1、首先,单独拿出来说,i++和++i的意思是一样的,就是i = i + 1。
2、如果当做运算符来说,就是a = i++ 和 a = ++i这样的形式,情况就不一样了。
a = i++的意思是,先把i的值赋给a,即a = i,再执行i = i + 1;
a = ++i是先执行 i = i+1,再把i的值赋给a;
举个例子来说,如果一开始i=4。
那么执行a=i++这条语句之后,a=4,i=5;
那么执行a=++i这条语句之后,i=5,a=5;
同理,i–和–i的用法也是一样的。
3、另外在循环体中的区别(重要)
①for循环中,for(int i = 0;i < 6;i++)和for(int i = 0;i < 6;++i)效果一样
② while(i++)是先用i的初始化值做循环变量再i+1
而while(++i)是先用i的初始值+1,再循环
8.输人两个正整数m和n,求其最大公约数和最小公倍数
最大公约数和最小公倍数之间的性质:两个自然数的乘积等于这两个自然数的最大公约数和最小公倍数的乘积。
AC代码C:
#include <stdio.h>
int main()
{
int p,r,m,n,temp;
printf("请输入两个正整数m,n:");
scanf("%d%d",&m,&n);
if(n<m)
{
temp=n;
n=m;
m=temp;
}
p=n*m;
while(m!=0)
{
r=n%m;
n=m;
m=r;
}
printf("最大公布数:%d\n",n);
printf("最小公倍数:%d\n",p/n);
}
9.输人一行字符,分别统计出其中英文字母、空格、数字和其他字符的个数
AC代码C:
这段代码中的 while 循环是一个字符输入循环,其条件为 (cc=getchar())!='\n'
。
在每次循环中,getchar()
函数用于从输入流中读取一个字符,并将其赋值给变量 cc
。然后,条件表达式 (cc=getchar())!='\n'
检查读取的字符是否为换行符(\n
)。如果不是换行符,则循环继续执行;如果是换行符,则循环结束。
这样的循环结构可以用来逐个读取用户输入的一行字符,直到遇到换行符为止。每次读取一个字符后,根据字符的类型进行相应的统计操作,如统计英文字母数目、空格数目、数字数目和其他字符数量。
#include<stdio.h>
int main()
{
char cc;
int a=0,b=0,c=0,d=0;
printf("请输入一行字符:");
while((cc=getchar())!='\n')
{
if(cc>='a'&&cc<='z'||cc>='A'&&cc<='Z')
{
a++;
}
else if(cc == ' ')
{
b++;
}
else if(cc>='0'&&c<='9')
{
c++;
}
else
{
d++;
}
}
printf("英文字母数目是:%d\n 空格数目:%d\n 数字数目:%d\n 其他字符数量:%d\n",a,b,c,d);
return 0;
}
10.求 S n S_n Sn=a+aa+aaa+…+ a a + ⋯ + a ⏞ n 个 a \overbrace{aa+\dots+a}^{n个a} aa+⋯+a n个a 之值,其中a是一个数字,n表示a的位数,a、n由键盘输入
AC代码C:
可以使用math.h头文件中的pow函数来计算10的i次幂
#include<stdio.h>
#include<math.h>
int main()
{
int n;
double a,sum,totle_sum;
printf("输入a的值以及n的值");
scanf("%lf %d",&a,&n);
for(int i=0;i<n;i++)
{
sum+=a*pow(10,i);
totle_sum+=sum;
}
printf("总和是:%lf\n",totle_sum);
}
11.求 ∑ n = 1 20 n ! \sum\limits_{n=1}^{20}n! n=1∑20n! (即求1!+2!+3!+4!+…+20!)
在C语言标准中,double类型的取值范围通常是-1.7E308到1.7E308之间,而 int类型的取值范围通常是-32768到32767之间(这些范围可能会因为不同的编译器或平台而有所不同)。因此,如果需要处理比较大的数值,使用 double类型可以更好地满足需求。
AC代码C:
#include<stdio.h>
int main()
{
double sum=0;
for(int i=0;i<=20;i++)
{
double single_sum = 1;
for(int j = i;j>0;j--)
{
single_sum*=j;
}
sum+=single_sum;
}
printf("阶乘和为:%lf\n",sum);
}
12.求和 ∑ k = 1 100 k \sum\limits{k=1}^{100}k ∑k=1100k + ∑ k = 1 50 k 2 \sum\limits{k=1}^{50}{k}^2 ∑k=150k2+ ∑ k = 1 10 1 k \sum\limits_{k=1}^{10}{\frac{1}{k}} k=1∑10k1
1.0/k表示1.0除以k,其中1.0是一个浮点数常量,而k可以是整数或浮点数。结果将会是一个浮点数,即使k是整数类型。
1/k表示整数除法,其中k必须是整数类型。如果k是整数类型,那么1/k将会进行整数除法运算,结果将会是整数类型。在整数除法中,会将小数部分舍去,只保留整数部分。
AC代码C:
#include<stdio.h>
int main()
{
double sum=0,sum1=0,sum2=0,sum3=0;
for(int k=1;k<=100;k++)
{
sum1+=k;
if(k<=50)
{
sum2+=k*k;
}
if(k<=10)
{
sum3+=1.0/k;
}
}
sum=sum1+sum2+sum3;
printf("三种情况求和结果:%lf\n",sum);
}
13.输出所有的“水仙花数”,所谓“水仙花数”是指一个3位数,其各位数字立方和等于该数本身。
如果操作数都是整数类型,则执行整数除法。整数除法将返回一个整数值,省略小数部分,只保留整数部分。例如,5 / 2将返回2,而不是2.5。
如果操作数中至少有一个是浮点数类型,则执行浮点数除法。浮点数除法将返回一个浮点数值,保留小数部分。例如,5.0 / 2将返回2.5。
AC代码C:
#include<stdio.h>
int main()
{
int a,b,c;
for(int i=100;i<=999;i++)
{
a=i/100;
b=(i/10)%10;
c=i%10;
if(a*a*a+b*b*b+c*c*c==i)
{
printf("%d\n",i);
}
}
}
14.一个数如果恰好等于它的因子之和,这个数就称为“完数”。
例如,6的因子为1,2,3,而6=1+2+3,因此6是“完数”。编程序找出1000之内的所有完数,并按下面格式输出其因子:
6 its factors are 1,2,3
AC代码C:
#include<stdio.h>
int main()
{
int a,factor,sum;
for(a=2;a<=1000;a++)
{
sum =1;
for(factor=2;factor<=a/2;factor++)
{
if(a%factor==0)
{
sum+=factor;
}
}
if(sum==a)
{
printf("%d因子是:1,",a);
for(factor=2;factor<=a/2;factor++)
{
if(a%factor==0)
{
printf("%d,",factor);
}
}
printf("\n");
}
}
}
15.有一个分数序列,求出这个数列的前20项之和。
2 1 \frac{2}{1} 12, 3 2 \frac{3}{2} 23, 5 3 \frac{5}{3} 35, 8 5 \frac{8}{5} 58, 13 8 \frac{13}{8} 813, 25 13 \frac{25}{13} 1325,…
AC代码C:
#include<stdio.h>
int main()
{
double a=2,b=1,sum=0;
double temp;
int count;
scanf("%d",&count);
for(int i=0;i<count;i++)
{
sum+=a/b;
temp=a;
a=a+b;
b=temp;
}
printf("前%d项之和:%lf\n",count,sum);
}
16.一个球从100m高度自由落下,每次落地后反弹回原高度的一半,再落下,再反弹。
求它在第10次落地时共经过多少米,第10次反弹多高。
不需要计算第10次的反弹高度,所以减去文章来源:https://www.toymoban.com/news/detail-647337.html
AC代码C:
#include<stdio.h>
int main()
{
double a=100;
double sum=0;
for(int i=0;i<10;i++)
{
sum+=a;
a=a/2;
sum+=a;
}
sum=sum-a;
printf("小球供经历%lf米,第10次反弹%lf米\n",sum,a);
return 0;
}
17.猴子吃桃问题。
猴子第1天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第2天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,就只剩一个桃子了。求第1天共摘多少个桃子。
前一天的桃子数量 = (后一天桃子数量+1) * 2文章来源地址https://www.toymoban.com/news/detail-647337.html
AC代码C:
#include<stdio.h>
int main()
{
int day=9;
int day_count;
int count=1;
while(day>0)
{
day_count=(count+1)*2;
count = day_count;
day--;
}
printf("total count:%d\n",count);
return 0;
}
18.有一个已经排好序的数组,要求输入一个数后,按原来顺序的规律将它插入数组中
AC代码C:
#include<stdio.h>
int main()
{
int num;
int shuzu[10]={1,2,3,4,5,6,7,8,9,10};
printf("请输入一个整数:");
scanf("%d",&num);
printf("%d\n",num);
int end=9;
while(end>=0&&num<shuzu[end])
{
shuzu[end+1]=shuzu[end];
end--;
}
shuzu[end+1]=num;
for(int i=0;i<11;i++)
{
printf("%d\n",shuzu[i]);
}
printf("\n");
}
到了这里,关于Python爬虫:单线程、多线程、多进程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!