简单上手python爬虫

犇犇犇犇

2020-08-13 00:09:34

Personal

有没有一个时刻, 看着页面上大量精美的动漫图, 却一张张下载点到手**麻**? 有没有一个时刻, 从网上整理寻找资料, 却看着长长的页面加载进度条感到头**大**? 有没有一个时刻, 玩着洛谷冬日画板, 看着别人画着一张张图片, 自己只能一个一个,从这点到**那**? 面对如此繁琐重复的工作 不妨把他交给电脑来完成**吧** ! 今天, 就让我们来写一只爬虫, 替我们在虚拟的网络上爬呀爬呀**爬**。 ![](https://cdn.luogu.com.cn/upload/image_hosting/krv3v53y.png) ~~没有人发现每段段尾是押韵的吗~~ * 本文作者:[犇犇犇犇](https://www.luogu.com.cn/user/35998) * 考虑到大部分读者为OIer,本文正文对python水平有一定要求,不熟悉的可以看下下文关于python部分。 * 本文默认使用 **python3** ,python3 默认使用 unicode 编码,如使用 python2 可能会出现显示问题或者其他问题 * 由于前面的内容比较简单,感觉简单可以**直接跳到 1:下载网页部分** * ~~如果有dalao觉得写的太啰嗦可以只看代码部分和加粗部分~~。 * 本文较长,图片比较多,可能有的地方$\LaTeX$渲染或者图片显示需要时间,可以刷新页面或者等待。如果**真的**有地方Markdown或者$\LaTeX$或者图片挂了可以私信或评论qaq * 由于洛谷博客feature,有的时候美元符号会被误认为是LaTeX,从而导致格式全部乱套,所以本文会用¥符号代替。 * 也可以看看我的上一期洛谷日报,[【日报#284】RE:从零开始搭建自己的博客](https://www.luogu.com.cn/blog/35998/wordpress) * 欢迎到我的[个人博客](https://www.12cow.com/index.php/2020/03/12/re%ef%bc%9a%e4%bb%8e%e9%9b%b6%e5%bc%80%e5%a7%8b%e6%90%ad%e5%bb%ba%e8%87%aa%e5%b7%b1%e7%9a%84%e5%8d%9a%e5%ae%a2/)查看文章qwq(服务器没续费,链接挂了) * ~~来都来了,不点个赞再走吗/kel~~ ### 目录: -2:什么是爬虫 -1:python or c++ 0:前置知识:关于python 1:第一个任务--下载网页 2:下载一张图片吧 3:第二个任务--有道翻译 $\texttt{ }$3.0:服务器是怎么处理我们的发送的请求的? $\texttt{ }$3.1:前置芝士:URL的组成 $\texttt{ }$3.2:使用有道翻译 4:关于编码的那些事 $\texttt{ }$4.1:ASCII,utf-8,GB2312,unicode,ANSI有什么区别? $\texttt{ }$4.2:python编码问题的解决 5:代理IP 6:cookie(不能吃!!) $\texttt{ }$6.1 何为cookie $\texttt{ }$6.2 如何查看和使用cookie $\texttt{ }$6.3 洛谷冬日绘板 7:正则表达式 $\texttt{ }$7.1 正则表达式的核心函数 $\texttt{ }$7.2 元字符 $\texttt{ }$7.3 重复限定符 $\texttt{ }$7.4 贪婪与非贪婪 $\texttt{ }$7.5 分组与条件 $\texttt{ }$7.6 group的方法 $\texttt{ }$7.7 匹配方式(flags) 8:最後の言葉 ~~长文预警~~ ## -2:什么是爬虫 > 网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。 ——《百度百科》 百度百科的定义是给神仙们看的,简单点说,爬虫就是你造的一个机器,你可以把它放到你需要的目标网站上,在网络上爬,让它把你需要的信息带回来。同时你也可以让爬虫代替你在网页上工作,比如帮你点洛谷画板。 爬虫在我们的生活中十分常见,网络上有各种各样的爬虫,有好有坏。比如百度就用了一种爬虫,把他放到各个网站上,根据你的搜索关键词,告诉你可能能解决你问题的网站。 那么爬虫安全吗?如果正常使用爬虫,那么大部分情况下是安全的。 还有网站一般会有标明哪些不能爬,比如洛谷就有[https://www.luogu.com.cn/robots.txt](https://www.luogu.com.cn/robots.txt) 告诉我们/record/和/recordnew/ 也就是提交记录是明确不能爬的 ![](https://cdn.luogu.com.cn/upload/image_hosting/3cxxns84.png) 爬虫多种多样,这里只介绍比较简单和常见的爬虫写法。~~关键是本人太菜~~ ## -1:python or c++ ![](https://cdn.luogu.com.cn/upload/image_hosting/asxtjwtv.png) python好还是c++好,这个是一个难以回答的问题。只能说python与c++擅长了领域不同,哪个更适合罢了。 我们不妨来看看用c++和python分别实现一个下载网页源代码的爬虫 ### c++版 ```cpp #include <stdio.h> #include <windows.h> #include <conio.h> #include <sstream> #include <bits/stdc++.h> using namespace std; #ifdef URLDownloadToFile #undef URLDownloadToFile #endif typedef int(__stdcall *UDF)(LPVOID,LPCSTR,LPCSTR,DWORD,LPVOID); UDF URLDownloadToFile = (UDF)GetProcAddress(LoadLibrary("urlmon.dll"),"URLDownloadToFileA"); void UTF8ToANSI(char *str) { int len = MultiByteToWideChar(CP_UTF8,0,str,-1,0,0); WCHAR *wsz = new WCHAR[len+1]; len = MultiByteToWideChar(CP_UTF8,0,str,-1,wsz,len); wsz[len] = 0; len = WideCharToMultiByte(CP_ACP,0,wsz,-1,0,0,0,0); len = WideCharToMultiByte(CP_ACP,0,wsz,-1,str,len,0,0); str[len] = 0; delete []wsz; } HANDLE hOutput; char name[32]; int cnt[8]; int main() { int uid,len,i = 0; DWORD unused; char url[128],user[16],*file,*ptr; HANDLE hFile; hOutput = GetStdHandle(STD_OUTPUT_HANDLE); char ss[128]; cin>>ss; sprintf(url,ss); URLDownloadToFile(0,url,"download.tmp",0,0); hFile = CreateFile("download.tmp",GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); len = GetFileSize(hFile,0); file = new char[len+3]; ReadFile(hFile,file,len,&unused,0); file[len] = file[len+1] = 0; CloseHandle(hFile); UTF8ToANSI(file);; memset(cnt,0,sizeof(cnt)); cout<<file<<endl; DeleteFile("download.tmp"); delete []file; return 0; } ``` 一共近50行代码 ### python版 ```python import requests str = input() response = requests.get(str) response.encoding = 'utf-8' print(response.text) ``` 就5行代码 此时,python简洁的优势就可以体现出来了。python还有大量库的支持,比如处理图片可以直接调用image库,抓取内容可以使用正则表达式。而相比与c++,python的语法更加简洁,方便调试。 但是c++跑的快啊。可是,在爬虫方面,有时候我们甚至还需要避免服务器封ip,手动降速,手动wait的情况。在爬虫方面c++的运行速度的优势没有用武之地,而代码和调试难度反而成了弊端。 python和c++也没有明确的优劣之分,只是在爬虫方面,python更适合罢了。当然,如同上面的c++代码,c++也是可以写爬虫的,只不过如此大的调试难度感到得不偿失。事实上,使用python把我们需要的数据拉下来,在本地使用c++或者其他工具进一步处理也是很常见的。 ## 0:前置知识:关于python 简单介绍下python,会使用python的可以**跳过**这部分。 因为笔者写这篇文章的时候不是很熟悉 python,所以自己记录了些笔记函数,稍微整理了一下,可能对不熟悉 python 的有点用处) 这部分涉及到很多 python 的基础操作,和后文的爬虫关系不大,所以如果觉得这里东西太多直接跳过也没问题,如果遇到后面看不懂的操作回来查表也行qaq 首先,你需要一个 python,~~这不废话~~ 安装python,可以通过[python官网](https://www.python.org/)下载 python官网速度太慢,所以这里也给一个百度网盘链接:https://pan.baidu.com/s/1e4UtCUENEBsk9Rie8rIucQ 提取码:82ve 需要自取 由于 python2 会默认使用 **ASCII 编码**,python3 会默认使用 **utf-8 编码**。所以python2更加容易由于编码错误而报错。所以建议使用更加高版本的python。本文python版本为 python3.8 。(上面给了百度网盘的安装包) python安装会自带pip,这里爬虫需要用到**requests库**。 可以通过cmd(命令提示符)输入 `pip install requests` 来获取requests库 如果出现了 `Successfully installed requests` 那么就能安装成功了。 如果python安装或pip路径配置出现问题,可以自行搜索解决方案,这里不详细说明了。 ------------ 下面自己简述一下可能会用到的python基本命令。可以暂时跳过,看到后面代码看不懂的部分可以回来找。 **输入和输出** python的输入为input(),输出为print() python可以自动将字符串与变量互相转化,比如输入int可使用 int(input()) 。python不需要提前定义变量。 **循环与代码块** python使用`:`和缩进表示代码块,不需要使用`{}` ```python for i in [1,2,3,4,5]: print(i,end=" ") ``` python常用的数据结构有列表,元组,字典,字符串,集合 **列表和元组** 列表是python常用的数据结构,用`[]`表示,相当于一个加强版数组,下标从0开始。创建方式为 `list1=[1,2,3,4,5]`,`list2 = ["a", "b", "c", "d"]`。可以通过下标访问和更新,`print(list1[0])` , `list1[0]=3` 若 list1=[1,2,3,4,5],list2 = ["a", "b", "c", "d"] 这里用一个表格来表示操作 | 函数名称 | 作用 | 结果示例 | | :----------: | :----------: | :----------: | | len(list1) | 列表长度 | 5 | | list1+list2 | 列表拼接 | [1,2,3,4,5,"a", "b", "c", "d"] | | 2 in list1: | 判断2是否在list1中 | True | | for i in list1: print(i,end=" ") | 遍历列表 | 1 2 3 4 5 | | list1[-2] | 倒数第2个元素 | 4 | | list[2:] | 切片操作,下标2右边的元素 | [3,4,5] | | list1[2:4] | 切片操作,下标从2到3(4-1) | [3,4] | | list1[:3] | 切片操作,下标2(3-1)左边的所有元素 | [1,2,3] | | list[a:b] | **切片**基本类型,截取一部分,下标从a到b-1,遵循**左闭右开**原则 | | | list1.append(2) | 在末尾添加 | [1,2,3,4,5,2] | | list1.count(2) | 统计出现的次数 | 1 | | list1.index(3) | 第一个值为3的位置(下标) | 2 | | list1.insert(2,6) | 下标为2的位置插入6 | [1,2,6,3,4,5] | | list1.pop() | 弹出最后一个元素 | [1,2,3,4] | | list1.remove(2) | 弹出第一个值为2的元素 | [1,3,4,5] | | list1.reverse() | 元素反转 | [5,4,3,2,1] | | list1.sort() | 列表排序 | [1,2,3,4,5] | | del list1[2] | 将下标为2的元素删除 | [1,2,4,5] | 元组相当于一个不能修改元素的列表,使用`()`创建。`tuple1=(1,2,3,4,5)`操作和列表类似,也支持切片,拼接,下标访问,遍历。但是不支持修改操作。 **字典** 字典可以理解为c++中的map,定义方式为`d={key1:value1,key2:value2}` 访问使用d[key]。 d[key]=value来更新和del d[key]来删除。其中key不可为列表,但可以是元组。 if key in dict:来判断key是否在字典里。 d.keys()来返回所有的key。 大部分requests在调用参数的时候都会使用**字典**类型 **字符串** 字符串部分和c++比较类似,使用 `str='abcde'` 创建,下标从0开始 字符串也支持列表中的切片操作,转义字符和c++一样,支持通过`+`来拼接字符串,`str()`来转化为字符串形式。 **文件操作** python可以使用`f=open(file, mode)`进行文件操作 file为(相对或者绝对路径),mode为文件打开模式,"r"为读入,"w"为写入(只能写入字符串类型),"a"为追加写入,"wb+"以二进制读写模式打开(写入byte类型) 打开模式中'w'与'wb+'的区别与应用会在之后文章提到。 f.read()读入文件,f.write()写入文件 但是每次更改目录过于繁琐 也可以使用 `with open(file, mode) as f:` 来打开文件 这种方法优点是可以不用每次关闭文件。 $\color{black}\colorbox{lightgreen}{一个小小的tips}$ 列表,元组和字典分不清楚? 判断字符串,列表,元组和字典的一种可能有用的方法: 看到`""`是字符串,`[]`是列表, `()`是元组, `{}`是字典 上面简述了下python基本语法,下面我们就开始吧。 =-=-=-=-=-=-=-=-=-=-(我是分割线)-=-=-=-=-=-=-=-=-=-= ## 1:第一个任务--下载网页 这段代码在文章开头出现过了。代码不长,先贴上来了吧。 ```python import requests # 引入requsets库 response = requests.get('https://www.baidu.com/') # 不带参数的get请求 response.encoding = 'utf-8' # 用utf-8解码 print(response.text) # 输出 ``` ![](https://cdn.luogu.com.cn/upload/image_hosting/fp2u7q2n.png) 我们发现python成功的输出了网页源代码。 但是我们发现这段代码貌似并不能下载所有网页。比如下载洛谷和知乎貌似直接404了?这个是怎么回事呢?这点在后文会讲到。 requests.get()会返回一个response类 即为`requests.models.Response`类 这里介绍下response类一些最基本的用法 | 代码 | 说明 | | :---------- | :---------- | | response.status_code | HTTP请求的返回状态 | | response.content | HTTP响应内容的二进制形式 | | response.text | HTTP响应内容的字符串形式 | | response.apparent_encoding | 从内容中分析出的响应内容编码方式(备选编码方式) | | response.encoding | 从HTTP header中猜测的响应内容编码方式 | 那么这些到底是什么意思呢 ![](https://cdn.luogu.com.cn/upload/pic/47361.png) 我们可以执行一下下面的代码 ```python import requests response = requests.get('https://www.baidu.com/') print(response.status_code) print(response.content) print(response.text) print(response.apparent_encoding) print(response.encoding) ``` 结果如下: ![](https://cdn.luogu.com.cn/upload/image_hosting/8z6ds7jt.png) 分析下内容: **status_code**表示**请求状态**。200则表示请求成功。如果status_code是404或者502的话就表示请求失败了。 **content**返回的是**响应内容的二进制形式**,我们可以发现开头有一个b字母,同时还有一些类似\xe7\x99的乱码。这是字节字符串的标志。 **text**大部分与content一样,这两个的区别是text用猜测的编码方式**将content内容编码成字符串**。如果页面是纯ascii码,这那么content与text的结果是一样的,对于其他的文字(比如中文),需要编码才能正常显示。否则就会出现乱码。当然我们也可以使用response.content.decode('utf-8')来手动解码。 ![](https://cdn.luogu.com.cn/upload/image_hosting/t8o6vlei.png) 我们输出一下content和text的类型,可以发现他们是不同的类型。一个是bytes(字节字符串)类型,另一个是str(字符串)类型。text是现成的字符串,可以当成字符串直接使用;content还要编码。但是text是根据猜测的响应内容编码方式进行解码 (下文的response.encoding)。有的时候系统会判断失误,这时候我们需要手动输入解码方式来进行解码。 **response.encoding**从**HTTP header中猜测的响应内容编码方式**。python会从header中的**charset**提取的编码方式(如下图所示),若header中没有charset字段则会默认为ISO-8859-1,这也是上文说的系统判断失误,无法正确解码的原因。这时候我们需要手动输入解码方式解码。 ![](https://cdn.luogu.com.cn/upload/image_hosting/m3ugc8l3.png) **apparent_encoding**会**从网页的内容中**分析网页编码的方式,所以apparent_encoding比encoding更加准确。python会根据encoding中存的内容进行解码。所以可以采用 `response.encoding=response.apparent_encoding`。 当然手动输入解码方式是最靠谱的。 ## 2:下载一张图片吧 既然python能下载网页,那么python能不能下载图片呢?其实原理是一样的,把图片网页下载下来,然后把它写入文件就可以了。当然,这里直接用字节(byte)的方式写入,所以要用**content**。 这里给一个随机图片网址 `https://api.ixiaowai.cn/api/api.php` ![](https://api.ixiaowai.cn/api/api.php) 打开就会自动随机跳转动漫图。 先放上本人的代码: ```python import requests response = requests.get('https://api.ixiaowai.cn/api/api.php') #下载图片 with open("1.jpg","wb+") as f: # 因为这里是以字节的形式写入,所以写入模式要用wb+。如果用w只能写入字符串(str) f.write(response.content) ``` 我们发现python同目录下出现了一个1.jpg的文件,就是我们要下载的图片了。 注:这里的 1.jpg 是相对路径,可以理解为同目录下创建文件,这里也可以直接改成绝对路径比如 `with open("D:\\qwq\\1.jpg","wb+") as f:` 这就是在D盘qwq文件夹下写文件。由于`\`(反斜杠)是转义字符,需要用`\`把`\`这个符号给转义一下,这里和c++的printf是一样的。python也可以在"前加r,把它变成原始字符串,就不需要两个\了。 什么,你说要批量下载?加一个循环不就好了吗 ```python import requests for i in range(10): # 这里range(10)可以理解为从0循环到9,下载10张图。这个数字可以随便改 response = requests.get('https://api.ixiaowai.cn/api/api.php') # 每次下载图片 with open(str(i)+".jpg","wb+") as f: # str(i)+'.jpg'是字符串拼接。每次写入一个新的文件。 f.write(response.content) ``` 这里再放一个去重复图片版本的,可能写的比较丑qwq 输入图片数量 n 即可开始下载,支持读取本地图片并去除重复图片qwq ```python import requests import os import time s = {} print("请输入下载数量:") n = int(input()) cnt = 0 for i in range(1,n+1): if os.path.exists(str(i)+".jpg"): # 已经存在本地图片 with open(str(i)+".jpg","rb+") as f: t = f.read() s[t]=1 # 记录 print(str(i)+'.jpg finish') continue response = requests.get('https://api.ixiaowai.cn/api/api.php') cnt = cnt + 1 if cnt % 100 == 0: # 达到已经数量后停止一段时间 print(str(cnt)+" pictures have been downloaded , waiting...") time.sleep(30) num = 0 while response.content in s: # 下载到了重复图片 response = requests.get('https://api.ixiaowai.cn/api/api.php') cnt = cnt + 1 if cnt % 100 == 0: print(str(cnt)+" pictures have been downloaded , waiting...") time.sleep(30) num = num + 1 print("repeat * "+str(num)) s[response.content]=1 with open(str(i)+".jpg","wb+") as f: f.write(response.content) print(str(i)+'.jpg finish') ``` 是不是很简单qaq ## 3:第二个任务--有道翻译 桥豆麻袋,之前不是才刚下载了网页吗。怎么突然开始这么复杂了? 翻译吗?我们需要先在左边输入翻译的内容,然后点翻译,然后再右边把内容复制下来。这个python能弄吗? 嗯。没错。对于在前端的我们操作步骤确实是这样。可是对于浏览器和处理这些信息的服务器来说,也是这样操作的吗?在我们点下那个神奇的翻译键的时候,浏览器到底干了什么? ### 3.0:服务器和浏览器是怎么处理我们的发送的请求的? 先来讲一个故事。从前有座山,山上有座庙,~~庙里有个老和尚在给小和尚讲故事。~~ 庙里有一个老和尚,十分有钱,而且特别喜欢收藏各种藏品。于是每周,一个商人便会敲开和尚家的门,与和尚做交易。商人背着各种各样的藏品来到寺庙,老和尚选走自己喜欢的藏品,把钱交给商人。商人带着钱高兴地回去了。 于此同时,在寺庙旁边住着一个强盗。他看到老和尚有那么多钱,于是想冒充商人,抢劫老和尚。算准了商人应该会来的时间,在那天他敲了老和尚的门。可是老和尚也不傻啊,每次商人来之前,他都会听到马蹄声。但是这次却很奇怪。老和尚想了想,越想越感觉不对。最终还是没有开门。 ~~故事纯属我瞎编的,本人文笔不好,体谅一下~~ 这个故事看似没什么关系,但是类比一下,我们可以发现:那个商人就是我们的浏览器,老和尚就是服务器。浏览器带着我们发送的请求,把请求交给服务器,而服务器把结果重新交还给浏览器,浏览器把请求结果带回来,交给我们。 而这个强盗就是我们的python。因为大量的爬虫访问会让服务器压力很大。所以服务器自然不欢迎大量的爬虫访问。所以有些服务器一判断出这个是python的访问,直接把我们拒之门外了。这也是上文的爬虫无法正常访问洛谷或者知乎的原因。 那么这就真的能阻止爬虫访问吗? ![](https://cdn.luogu.com.cn/upload/image_hosting/izbhomzs.png) 我们可以发现上面那个故事中,老和尚通过马蹄声来辨别商人与强盗。那么python伪装成正常浏览器访问不就行了? ### 3.1:前置芝士:URL的组成 这里插播一下网址url的组成,可以大概了解一下下文各种链接的组成。看不懂可以暂时跳过 我们在日常上网中可以看到各式各样的网址,比如`https://www.baidu.com/`,`https://www.luogu.com.cn/user/35998`,`https://www.luogu.com.cn/discuss/lists?forumname=service` 这种网址虽然看上去完全不同,甚至还有的带有?以及: 但是其实所有url都是下面这种基本形式组成的 $protocol://hostname[:port]/path/[;parameters][?query]#fragment$ 分个段 $\color{red}{protocol://hostname[:port]} \color{purple}{/path/}\color{green}{[;parameters]}\color{blue}{[?query]}\color{gray}{\#fragment}$ * protocol为协议,有http,https,ftp,file * hostname为地址,可以为域名,也可以为直接的ip地址 * port为端口,http默认为80端口,https默认端口号443 * path为路径。网站和本地文件一样,也有目录和文件。文件在服务器中的指定路径即为path * parameter为参数,向服务器传的参数,跟在?后面。比如洛谷提交记录即为 `https://www.luogu.com.cn/record/list?user=uid` 我们必须给变量user一个值uid,服务器把内容再传回来 * query查询,从服务器那里查询内容 * fragment为片段,可以直接达到网页指定位置。可以在百度百科中看到,比如 `https://baike.baidu.com/item/C%2B%2B#学习指南` ### 3.2:使用有道翻译 好啦,那么具体怎么操作呢?我们进入正题: 这里用谷歌浏览器来示例一下。其他浏览器操作基本相同 1. 将浏览器全屏显示,首先打开[有道翻译](http://fanyi.youdao.com/),右键,检查。(有的浏览器是审查元素,或者直接按F12按钮) ![](https://i.loli.net/2020/07/10/NYyifa9wjWsE64D.jpg) 2. 我们可以发现有我们熟悉的Elements,我们的网页源代码。这里我们选择Network选项。 ![](https://i.loli.net/2020/07/10/2ZjBuD9SKA4EHwJ.jpg) 3. 点击一下翻译按钮,我们发现这里多出来了很多请求。这就是浏览器与服务器的通信内容了。 ![](https://i.loli.net/2020/07/10/JvuajT7fAHqZIbn.jpg) 4. 点击这些内容,我们可以发现右边出现了请求的详细信息。 ![](https://i.loli.net/2020/07/10/VkWj2NcMEGIu3po.jpg) 主要操作方式依次为右键->检查元素->网络(Network) 这些浏览器拦截的通信内容。这里 **Request Method** 就是请求方式。我们发现这里有 get 和 post 两种。简单点说,get 就是我们从服务器获得数据。post 就是指我们向服务器提交数据。虽然在实际过程中 get 也可以用来提交数据。 我们在翻译的时候当我们点开翻译按钮的时候,明显我们向服务器提交了我们要翻译的内容,所以我们应该选择 post 。 ![](https://i.loli.net/2020/07/10/2FgjU8aIRxLSENV.jpg) 5. 拉到底,我们可以发现有 From Data 。这就是浏览器提交的内容了。果然, i 这一项的值是 Hello World 。终于找到组织了,就是这个请求。 ![](https://i.loli.net/2020/07/10/PVgq5ycwkUDBNsl.jpg) 6. 点击右边的Preview。终于看到了我们的结果。你好,世界。这就是服务器返回给我们的结果了。下面我们要做的就是用python模拟上面整个过程。 下面我们来分析一下刚才所有我们看到的这些内容 ![](https://i.loli.net/2020/07/10/haSKpLJj5rU4zgB.jpg) **Request URL**是处理我们请求的真实地址 **Request Method**主要有get和post两种,上面说过了 **Status Code** 这里200表示请求成功。如果404就是网页找不到了。 **Remote Address**服务器ip地址和端口号 下面**Request Headers**是客户端发送请求的headers。服务器通过这个headers来判断是否是非人类的访问。一般通过 **User-Agent** 这一项来识别是浏览器访问还是代码访问。 这个User-Agent包含我们的系统,浏览器版本号。如果我们用python,那么这个User-Agent就是python+版本号,那么服务器就很容易识别出来把我们屏蔽。当然这个User-Agent可以也可以用python自定义。 下面**From Data** 是表单数据,post提交的内容。通过这些冒号我们可以发现其实这是一个 字典 。i这一项对应的是我们翻译的内容。 requests自定义headers和提交数据也很简单,在post的时候添加就行了 ```python response = requests.post(url=url,data=data,headers=head) ``` 这里的data和head是 **字典** 类型。 下面我们开始写代码吧。 ```python import requests url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule" # 由于有道翻译feature,translate后面的_o要去掉 data = { "from":"AUTO", "to":"AUTO", "smartresult":"dict", "client":"fanyideskweb", "salt":"15801391750396", "sign":"74bbb50b1bd6c62fbff24be5f3787e2f", "ts":"1580139175039", "bv":"e2a78ed30c66e16a857c5b6486a1d326", "doctype":"json", "version":"2.1", "keyfrom":"fanyi.web", "action":"FY_BY_CLICKBUTTION", "i":"Hello world!" } # data就是上面的From Data,这里我们把他写成字典形式 head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} # 把浏览器的User-Agent贴进来,这里head也是一个字典 response = requests.post(url=url,data=data,headers=head) # requests标准形式 print(response.text) ``` 运行结果如下。 ![](https://i.loli.net/2020/07/10/EYiLdmRflOyZXou.jpg) 输出了一个字符串。我们发现"tgt"后面就是我们需要的内容了。我们把这个中文提取出来就好。 当然,我们可以根据一般处理字符串的方法去处理,但是这种方法不方便而且不美观。仔细观察一下,熟悉python数据结构的可以发现,当中的"{}","[]",":"提示我们,这不就是个 字典 吗。其实这是一个json格式,包含的是python可以识别的正常数据结构。 对于这种字符串就是数据结构的情况,python提供一个json库。用法很简单,可以使用`json.loads(str)`(str为字符串),也可以直接使用`response.json()`。 ```python import requests import json url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule" data = { "from":"AUTO", "to":"AUTO", "smartresult":"dict", "client":"fanyideskweb", "salt":"15801391750396", "sign":"74bbb50b1bd6c62fbff24be5f3787e2f", "ts":"1580139175039", "bv":"e2a78ed30c66e16a857c5b6486a1d326", "doctype":"json", "version":"2.1", "keyfrom":"fanyi.web", "action":"FY_BY_CLICKBUTTION", "i":"Hello world!" } head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} response = requests.post(url=url,data=data,headers=head) name = response.json() print(name) ``` 现在name就是一个字典了。 ![](https://i.loli.net/2020/07/10/avc4PIBsZWoTlNH.jpg) name是一个字典,其中包含我们需要内容的是translateResult这一项,而这一项里面又包含两个空列表,两个空列表里面又套着一个字典,这个字典里面的tgt是我们需要的结果。可以结合一下上面这张图来理解。~~禁止套娃~~ 下面给出完整代码: ```python import requests import json url = "http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule" data = { "from":"AUTO", "to":"AUTO", "smartresult":"dict", "client":"fanyideskweb", "salt":"15801391750396", "sign":"74bbb50b1bd6c62fbff24be5f3787e2f", "ts":"1580139175039", "bv":"e2a78ed30c66e16a857c5b6486a1d326", "doctype":"json", "version":"2.1", "keyfrom":"fanyi.web", "action":"FY_BY_CLICKBUTTION", } data['i']=input() # 输入中文 head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} response = requests.post(url=url,data=data,headers=head) name = response.json() print(name['translateResult'][0][0]['tgt']) ``` ![](https://i.loli.net/2020/07/10/3AlIEKPTzvo7Bi2.jpg) 我们用python成功地实现了翻译。 之前说我们爬虫可能无法正常访问洛谷,这里一样只要加上User-Agent就可以正常使用了 ```python import requests url=input() head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} response = requests.get(url=url,headers=head) response.encoding = 'utf-8' print(response.text) ``` ## 4:关于编码的那些事 $\color{red}\texttt{UnicodeDecodeError:}\texttt{ }\texttt{'utf-8'}\texttt{ }\texttt{codec}\texttt{ }\texttt{can't}\texttt{ }\texttt{decode}\texttt{ }\texttt{byte}\texttt{ }\texttt{0xe9}\texttt{ }\texttt{in}\texttt{ }\texttt{position}\texttt{ }\texttt{0:}\texttt{ }\texttt{invalid}\texttt{ }\texttt{continuation}\texttt{ }\texttt{byte}$ $\color{red}\texttt{UnicodeDecodeError:}\texttt{ }\texttt{'ascii'}\texttt{ }\texttt{codec}\texttt{ }\texttt{can't}\texttt{ }\texttt{decode}\texttt{ }\texttt{byte}\texttt{ }\texttt{0xce}\texttt{ }\texttt{in}\texttt{ }\texttt{position}\texttt{ }\texttt{0:}\texttt{ }\texttt{ordinal}\texttt{ }\texttt{not}\texttt{ }\texttt{in}\texttt{ }\texttt{range(128)}$ `\u4f60\u597d\uff0c\u4e16\u754c` 这里\u是干什么用的? `\xe4\xbd\xa0\xe5\xa5\xbd` 这里\x又是什么? 有时候我们把python爬虫抓取下来的资源保存再记事本中,想用熟悉的c++进行进一步处理。但是为什么我的c++无法读取? 使用python的过程中,经常有可能会出现一些神奇的错误。明明代码看着完全没问题,甚至在换到一些在线IDE上就能跑出来,可是为什么我的python就一直报错呢? ![](https://cdn.luogu.com.cn/upload/image_hosting/23enytbj.png) 这其实就是编码问题。这是初学 Python 的容易出现的一个问题之一。 使用python3和高版本的python可以大概率避免这些问题,因为 python2 会默认使用 ASCII 编码,python3 会默认使用 utf-8 编码。 那么ASCII,utf-8,GB2312,unicode。这些到底是什么东西,有什么区别呢? ### 4.1:ASCII,utf-8,GB2312,unicode,ANSI有什么区别? 这还要从编码和计算机的发展说起。 最早的编码是**ASCII编码**。ASCII码我们都很熟悉,它对应了英语字符与二进制位。**ASCII使用一个字节**,一个字节有8个二进制位。在英语中,128个符号(7个二进制位)就可以满足,所以一直将1个字节的最高位(第8位)闲置(默认为0),其他7位用于编码。后来才扩展了最高位,共可以表示256个符号。在表示英语,ASCII码绰绰有余。 但是世界上并不是只有英语一种语言,对于其他语言,比如汉字,明显256位根本不够。于是每个国家开始自己定自家的编码。这种表示方法简体中文叫做**GB2312**, 繁体中文叫Big5,日文叫Shift_JIS。这种编码统称为**ANSI**。ANSI只是一种代称,在英文操作系统中 ANSI 编码代表 ASCII,在简体中文操作系统 ANSI 编码代表 GB2312,在繁体中文操作系统相当于Big5 。 虽然这种方法的确解决了其他语言编码的问题,但是缺点也很明显。**不同 ANSI 编码之间互不兼容**,所以我们无法将两种语言同时保存在同一个 ANSI 编码的文本中。这种编码**只有当前操作语言操作系统有效**,而且与unicode , utf-8编码无关。汉字用两个字节表示一个字符。理论上最多可以表示 256 x 256 = 65536 个符号。GB2312是中国自己制定的编码,后来加入繁体发展成了GBK,最后加入日语和韩语成为GB18030。ANSI可以认为是ASCII的一种扩充,所以ANSI码前127个与ASCII码相同。 但是这种编码明显有局限性,不利于全球公用。所以迫切需要一种能表达全球所有语言的编码。于是**Unicode**诞生了,Unicode又称为"万国码",,它为**每种语言中的每个字符设定了统一并且唯一的二进制编码**,以满足跨语言、跨平台进行文本转换、处理的要求。因为Python的诞生比Unicode标准发布的时间早,所以Python2只支持ASCII编码,处理Unicode就会出现问题。也就是我们看到的乱码问题。 ![](https://cdn.luogu.com.cn/upload/image_hosting/7zmqgx15.png) Unicode 是一个很大的**字符集**,所有的Unicode可以到 [这里](https://home.unicode.org/) 查看。Unicode 只是一个符号集,给每一个字符一个ID,比如汉字'我'的ID(码位)就是25105,记作U+6211(25105的十六进制为0x6211)但是并没有规定怎么将码位转换为字节序列来给计算机储存。Unicode只给了每一个汉字一个ID,比如给了'我'这个字一个ID 25105 , 但是并没有规定怎么把这个25105给转化成二进制储存到计算机里。Unicode 与 UTF-8 的区别就是 **UTF-8** 是一种**编码规则**,也就是 Unicode 的一种实现方式。Unicode 还包含 UTF-16、UTF-32 等编码。UTF-8、UTF-16 等等这些编码规则负责将这些 Unicode 的 ID 转成二进制码储存到计算机里。 最开始的 Unicode 实现方式十分暴力。明显,一个字节只能储存256个符号,完全储存不下Unicode如此庞大的字符数。既然一个字节存不下,那就用三个或四个字节来表示一个字符啊。看似可行,但是比如英语字母只需要1个字节即可表示,为了填充每个字符4个字节的位置,前3个字节必然都是0,只有最后一个字节是有效内容。这会使纯英语或其他语言的文本文件的大小多出好多倍,对于存储来说是极大的浪费。 随着互联网的出现,对统一的编码方式需求越来越大,这使得 Unicode 开始推广,同时也产生了一种 UTF-8 的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式,很多网页的源码上会有类似 charset="UTF-8" 的信息,表示该网页正是用的UTF-8编码。 UTF-8 最大的一个特点,就是它是一种**变长的**编码方式。它可以使用1~4 个字节表示一个符号。 **UTF-8 的编码规则**:(好像和本文的主题没啥关系啊,如果感兴趣的可以看一下qaq) 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。 2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。 如下表所示,x表示可用编码的位。 | Unicode符号范围(十六进制) | UTF-8编码方式(二进制) | | :---------- | :---------- | | 0-7F | 0xxxxxxx | | 80-7FF | 110xxxxx 10xxxxxx | | 800-FFFF | 1110xxxx 10xxxxxx 10xxxxxx | | 10000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 简单点说,对于 UTF-8编码 开头有多少个连续的1,这个字符就占多少个字节。 举个例子,我们来讲一下怎么把汉字'我'转化成UTF-8编码值 首先我们先获取'我'的Unicode值,输入ord('我') ``` >>> ord('我') 25105 ``` 得到25105,我们把25105转为十六进制,使用hex函数 ``` >>> hex(25105) '0x6211' ``` 25105的16进制为6211,在范围800-FFFF之间,所以用3个字节表示。 现在我们已经确定了'我'的形式应该为1110xxxx 10xxxxxx 10xxxxxx,所以下一步我们只要把这些x给填上就可以了。这些x形式位4+6+6的形式,一共要填16位。 我们把25105转为二进制。输入bin(25105) ``` >>> bin(25105) '0b110001000010001' ``` 得到二进制为110001000010001。可是这里只有15个位,而我们要填16个数,所以在开头补一个0,得到$\color{red}{0}\color{black}{110001000010001}$ 把他分割成4,6,6的形式 $0110$ $001000$ $010001$,把这些数填进1110xxxx 10xxxxxx 10xxxxxx的x中 得到$1110\color{red}0110$ $10\color{red}001000$ $10\color{red}010001$ 11100110,10001000,10010001这三个数分别转为16进制得到e6 88 91 ``` >>> hex(0b11100110) '0xe6' >>> hex(0b10001000) '0x88' >>> hex(0b10010001) '0x91' ``` 这样我们就得到了'我'的UTF-8编码值,测试一下。 ``` >>> b'\xe6\x88\x91'.decode("utf-8") '我' ``` 实际上大部分汉字都使用3个字节。 ### 4.2:python编码问题的解决 处理编码问题最常见的命令是encode与decode。 decode的作用是将二进制数据解码成unicode。 encode的作用是将unicode编码编码成二进制数据。 简单说,decode就是“解密”,而encode就是“加密”。 python对于字符串拼接时候报错,比如string=string1+string2这种类型的,python2要求这两个字符串都是一样的编码方式。比如普通字符串和 Unicode 字符串进行拼接就会报错。这时候需要将普通字符串使用string.decode('utf-8')来转成一样的类型。 如果无法显示unicode中文,比如'\u4f60\u597d\uff0c\u4e16\u754c'这时候可以使用decode('unicode_escape')来解码,也可以使用eval函数,在字符串前加上u,告诉编译器这是unicode编码。`eval("u"+"\'"+string+"\'")`。 当不同的编码系统进行相互转换的时候,可以利用 Unicode 做一个中介。把其他编码先decode成unicode,再把unicode编码成其他编码,比如GB2312。 文件操作时使用编码操作 ``` f1 = open("test.txt", encoding="") ``` encoding这里加上文件的编码就行了。 对于python编码,使用python3可以避免大部分问题。 ## 5:代理IP 上文说了我们可以通过添加User-Agent来伪装成正常的人类点击。但是,服务器还是有一种暴力的方法来判断是不是人类的访问。记录ip访问量,设定一个阈值。如果访问量超过一个阈值,直接把它掐掉。因为爬虫一般访问速度都远远大于人类。 判断是人类还是程序,大部分网站的处理方法都是使用验证码。这种验证码对于人类没有问题。正常人类可以输入验证码,但是我们的python...就没这么好办了...... 为了避免触发阈值,一般有两种做法。 第一种是降速,降低爬虫速度,让它看起来更加像是人类的访问。实现方法很简单,每次访问完成后`time.sleep(5)`,等待个几秒钟就好。虽然处理简单,但是缺点很明显,就是访问速度太慢了,工作效率低下。 第二种就是使用ip代理。~~kkksc03:把你ip扬了。~~ 代理是什么?代理就像一个跑腿的。既然我去的次数太多,被限制了,那我就换一个跑腿的帮我访问。每次我们把需要访问的内容给代理,然后代理访问服务器,再把传回来的结果原封不动地告诉你。就相当于每次换ip访问,那么每个ip访问的速度就很慢了。服务器看到的访问地址是代理ip的地址。 代理ip(proxy)的形式是 `IP类型://代理ip:端口号` IP类型主要有http和https。 首先我们要找到代理ip。这里提供一个貌似可用的提供免费代理ip的网站 [http://www.xiladaili.com/](http://www.xiladaili.com/) 。至少本人测试的时候还是能用的) 建议如果可能尽量使用IP类型为https的代理ip。当然,如果自己有服务器或者其他代理ip当然是最好的选择。 这里再提供一个查看ip的网站 [https://www.myip.com/](https://www.myip.com/) 。访问就能看到自己的ip。 使用代理也很简单,建一个porxies的字典,在访问的时候加上这个函数就行了。如果是http那么key就是http。如果是https那么key就是https。 http:`proxies={"http":"http://代理ip:端口号"}` https:`proxies={"https":"https://代理ip:端口号"}` 访问时 `response = requests.get(url=url,proxies=proxies)` 这里就用上面网站的第一个https类型的代理ip来演示 ![](https://i.loli.net/2020/07/10/fOgGSvKDMEH5RIP.jpg) 代码: ```python import requests url='https://www.myip.com/' proxies={ "https":"https://184.82.128.211:8080/" } head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} response = requests.post(url=url,headers=head,proxies=proxies) print(response.text) ``` 这里也可以输出一下 `response.status_code` 查看访问状态,成功的话状态码应为200。如果出错的话可以多换几个ip试一试。 成功访问运行界面如下。 ![](https://i.loli.net/2020/07/10/2Cb3lgrjc8KSJGw.jpg) 我们发现我们的python成功使用了代理ip。 当然我们也可以交替使用代理ip,避免由于单个ip访问次数过快过高被封。实现方法也很简单,把所有ip放到一个列表里,每次随机使用。这里需要使用 random库。 ```python import requests import random url='https://www.myip.com/' proxies={} ip = ["https://184.82.128.211:8080/","https://103.60.137.2:22589/","https://89.28.53.42:8080/"] # 保存可用的代理ip proxies["https"]=random.choice(ip) # 随机使用 head = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36"} response = requests.post(url=url,headers=head,proxies=proxies) print(response.text) ``` ## 6:cookie(不能吃!!) ![](https://i.loli.net/2020/07/10/fhNtgC1LlJ6qePw.png) ![](https://i.loli.net/2020/07/10/fPzYv7qkxi9CQan.jpg) ![](https://i.loli.net/2020/07/10/JHnxlioUYeqBE9X.jpg) ![](https://cdn.luogu.com.cn/upload/image_hosting/068sml97.png) 每到元旦的时候,为了画洛谷画板,某群里便会有着一堆求cookie的人。那么cookie到底是什么?好吃吗? ![](https://i.loli.net/2020/07/11/6mEawDtexVgKjrz.jpg) ~~别说,cookie(曲奇)还是挺好吃的~~ ### 6.1 何为cookie > Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息。 ——《百度百科》 看着很复杂的样子,打个比方,有一天,你和你的npy去酒店度假。你们去酒店做了登记,于是酒店发给了你一张房卡。而这张房卡可以认为就是我们的cookie,它**保留在你的手上**,你可以凭借这张房卡自由地进出房间,它告诉房间你是这件房间的主人,避免每次回酒店都要重新登记一次(~~这不废话~~),同时也防止了无关人员进出你的房间。但是这张卡只有当晚才有效,到了第二天必须要重新付钱登记才可以继续使用,所以有的cookie是**暂时的**,cookie暂时保留在我们手上,但是过了一定时间需要重新登录。 cookie的应用十分广泛。比如我们登陆了洛谷,这时候我发了请求,告诉服务器我的账户和密码,然后我打开题目开始写题。当我们写完题准备提交的时候,浏览器把代码提交给服务器。那么问题来了,服务器怎么知道这份代码是谁提交的呢?cookie就很好地解决了这个问题,登录洛谷的时候服务端给客户端发送一个cookie,使用的时候客户端发送cookie证明这是”我“ 一般来说,cookie的使用流程为 1. 服务器生成cookie发送给客户端 2. 客户端保存cookie以便以后使用 3. 每次请求客户端将cookie发送给服务器 ### 6.2 如何查看和使用cookie 我们可以在浏览器直接查看cookie。这里以谷歌浏览器为例。 首先我们先**登录目标网站**,然后点击 F12(可能有些笔记本需要同时按fn键) - Application - cookie ![](https://cdn.luogu.com.cn/upload/image_hosting/qgxmbesz.png) 然后就能看到cookie了。 python使用过程中,cookie是一个**字典**,如上图,key为左侧Name , Value为右侧Value。 我们可以把他变成字符串的形式给爬虫使用,不同的项之间用`;`连接。形式为`"Name=Value;第二项,形式同上..."` 比如洛谷的就可以用`"_uid=xxx;__client_id=xxx"`这两项来登录 ``` headers={ "cookie":cookies } response = requests.post("请求网址",headers=headers) ``` 这里给一个通过洛谷cookie登录洛谷的示例,需要用到re库,可以 cmd 输入 `pip install re` 安装 ```python import requests import re uid = input("_uid=") client = input("__client_id=") string = "_uid="+uid+";__client_id="+client # 这里的string为拼接的cookie headers={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" ,"cookie":string # 使用cookie } response = requests.get("https://www.luogu.com.cn",headers=headers) response.encoding = 'utf-8' # 解码 s = response.text # 获取网页源代码 # 以下程序为正则表达式,在后文会提到,作用为抓取登录洛谷后打卡页面的id p = re.search(r"<h2 style='margin-bottom: 0'>(.*?)</h2>",s) if p: s=p.group() p = re.search(r"target=\"_blank\">(.*?)</a>",s) print("成功登录 " + p.group(1)) else: p = re.search(r"<h2>欢迎回来,(.*?)</h2>",s) if p: p = re.search(r"target=\"_blank\">(.*?)</a>",s) print("成功登录 " + p.group(1)) else: ref = "https://www.luogu.com.cn/api/user/search?keyword="+uid response = requests.get(ref,headers=headers) response.encoding = 'utf-8' id = response.json() id = id['users'][0]["name"] print("登录失败","uid:",uid,"id:",id) ``` ### 6.3 洛谷冬日绘板 和之前一样,我们来看一下我们点击洛谷画板时我们的浏览器干了什么。 我们先使用黑色去涂画板的左上角,可以发现浏览器发送了一个post,其中x: 0 y: 0 color: 0。我们换成最后一个颜色,点击右下角涂色,我们可以发现发送的post为x: 799 y: 399 color: 31。于是我们可以发现这个画板大小为800\*400,其中左上角为坐标原点,颜色按顺序为0~31。当前画板的情况可以在 `https://www.luogu.com.cn/paintBoard/board` 查看。这上面其实就是一个32进制,对应着每个点颜色。 我们需要把我们的图画保存在board.json里,其中board形式为列表套列表,每个小列表为x,y,col保存每个点的信息。比如`[[1,1,0],[1,2,1]]`就是需要在坐标(1,1)涂黑色,在(1,2)涂白色。 我们还需要把cookie提前存在cookie.json里。洛谷只需要__client_id和_uid这两项。我们使用列表套字符串,形式为`["_uid=xxx;__client_id=xxx","_uid=xxx;__client_id=xxx"]`,每次画点按顺序使用cookie,使用完一轮以后等待冷却30s。 要注意的就是我们获取画板状态 ``https://www.luogu.com.cn/paintBoard/board`` 虽然画板的确实每行只有600个有效字符,但是由于行尾有换行符,所以实际上每行有601个字符。即x,y坐标实际的位置为 `x*601+y` 下面放一下本蒟蒻的代码。由于每年洛谷画板可能都有微小的变化,不保证每年都能用,上面已经说明了写法,私认为最稳定的做法还是自己写一个qaq 一下程序的board.json,cookie.json和 [ouuan](https://github.com/ouuan/LuoguPaintBoard) 的形式相同,生成的文件可以直接套用qwq 由于本人较菜,当时参考了一下他的写法,这里感谢一下神ouuan ```python import requests import json import time headers={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" ,"cookie":"" } with open("cookies.json","r") as load_f: cookie = json.load(load_f) # 引入cookie with open("board.json","r") as load_f: board = json.load(load_f) # 引入图画内容 def paint(x,y,c): # 涂色函数 data={ 'x':x, 'y':y, 'color':c, } # data为我们需要填充的颜色和坐标 print(x,y,c) global cur headers["cookie"]=cookie[cur] print(data,headers) response = requests.post("http://www.luogu.com.cn/paintBoard/paint",data=data,headers=headers) # 填色 pause=31 mark=0 t0 = time.time() while 1: headers["cookie"]=cookie[0] pboard = requests.get("http://www.luogu.com.cn/paintBoard/board",headers=headers) d=[] cnt=1 for point in board: if cnt>len(cookie): break x=point[0] y=point[1] c=point[2] if int(pboard.text[x*601+y],32) != c: cnt=cnt+1 d.append(point) cur=0 t = time.time()-t0 # print(t) if mark == 1: time.sleep(pause-t) mark=1 t0 = time.time() for point in d: headers["cookie"]=cookie[cur] x=point[0] y=point[1] c=point[2] paint(x,y,c) cur=cur+1 ``` 当然这里还需要生成一个board.json,这里给出一个把图片生成board的代码。把需要处理的图片重命名为1.jpg,放入python的同目录下。支持把图片压缩成x\*y。width,height,startx,starty四个参数需要自定义。 这里需要image库和json库。可以 cmd 输入 `pip install image` 安装 ```python from PIL import Image from colorsys import rgb_to_hsv import json import math colors=[(0, 0, 0),(255, 255, 255),(170, 170, 170),(85, 85, 85),(254, 211, 199),(255, 196, 206),(250, 172, 142),(255, 139, 131),(244, 67, 54),(233, 30, 99),(226, 102, 158),(156, 39, 176),(103, 58, 183),(63, 81, 181),(0, 70, 112),(5, 113, 151),(33, 150, 243),(0, 188, 212),(59, 229, 219),(151, 253, 220),(22, 115, 0),(55, 169, 60),(137, 230, 66),(215, 255, 7),(255, 246, 209),(248, 203, 140),(255, 235, 59),(255, 193, 7),(255, 152, 0),(255, 87, 34),(184, 63, 39),(121, 85, 72)] def dis(x,y): rmean = (x[0] +y[0])/2 r = x[0] - y[0]; g = x[1] - y[1] b = x[2] - y[2] return math.sqrt((((512+rmean)*r*r)/256) + 4*g*g + (((767-rmean)*b*b)/256)) def closest(col): minn=10000000000 for i in colors: sum=dis(col,i) if minn>sum: minn=sum ans=colors.index(i) return ans width=100 # 图片压缩后的宽度 height=100 # 图片压缩后的高度 startx=10 # 开始画的点的x坐标 starty=10 # 开始画的点的y坐标 f=open('board.json','w') lena = Image.open("1.jpg") picture = lena.resize((width, height),Image.ANTIALIAS) a = picture.load() d = list() for i in range(picture.width): for j in range(picture.height): d.append( (startx+i,starty+j,closest(a[i,j])) ) string=json.dumps(d) f.write(string) ``` 给出一个可以根据board.json生成的图片预览 ``` from PIL import Image import json colors=[(0, 0, 0),(255, 255, 255),(170, 170, 170),(85, 85, 85),(254, 211, 199),(255, 196, 206),(250, 172, 142),(255, 139, 131),(244, 67, 54),(233, 30, 99),(226, 102, 158),(156, 39, 176),(103, 58, 183),(63, 81, 181),(0, 70, 112),(5, 113, 151),(33, 150, 243),(0, 188, 212),(59, 229, 219),(151, 253, 220),(22, 115, 0),(55, 169, 60),(137, 230, 66),(215, 255, 7),(255, 246, 209),(248, 203, 140),(255, 235, 59),(255, 193, 7),(255, 152, 0),(255, 87, 34),(184, 63, 39),(121, 85, 72)] with open("board.json","r") as load_f: board = json.load(load_f) x=0 y=0 for point in board: x=max(x,point[0]) y=max(y,point[1]) lena = Image.new("RGB",(x+1,y+1)) for point in board: x=point[0] y=point[1] c=colors[point[2]] lena.putpixel((x,y),c) lena.show() ``` 以下程序可以加入cookie,并查看cookie是否有效。 需要在桌面新建一个cookie.json,第一次使用需要在里面写入`[]` (一个空的列表),否则会导致读取本地cookie读取失败 ```python import json import requests import re with open("cookies.json","r") as load_f: l = json.load(load_f) d = {} print("输入0保存并退出") for i in l: d[i]=1 while 1: client = input("__client_id=") if client == "0": break uid = input("_uid=") cookies = {"_uid":uid,"__client_id":client} str = "" for i in cookies: str = str+i+"="+cookies[i]+";" string = str[:-1] if string in d: print("该cookie已经存在,请勿重复添加") continue headers={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" ,"cookie":str } response = requests.get("https://www.luogu.com.cn",headers=headers) response.encoding = 'utf-8' # print(response.text) s = response.text p = re.search(r"<h2 style='margin-bottom: 0'>(.*?)</h2>",s) if p: s=p.group() p = re.search(r"target=\"_blank\">(.*?)</a>",s) l.append(string) d[string]=1 print("成功添加 " + p.group(1)) else: p = re.search(r"<h2>欢迎回来,(.*?)</h2>",s) if p: # print(p.group()) p = re.search(r"target=\"_blank\">(.*?)</a>",s) l.append(string) d[string]=1 print("成功添加 " + p.group(1)) else: print("添加失败") print(l) print(len(l)) string=json.dumps(l) with open('cookies.json','w') as f: f.write(string) ``` 再给一个根据cookie.json来登录洛谷,可以解决查看cookie是否失效的问题,需要re库 ```python import json import requests import re with open("cookies.json","r") as load_f: l = json.load(load_f) for string in l: headers={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" ,"cookie":string } response = requests.get("https://www.luogu.com.cn",headers=headers) response.encoding = 'utf-8' # print(response.text) s = response.text p = re.search(r"<h2 style='margin-bottom: 0'>(.*?)</h2>",s) if p: s=p.group() p = re.search(r"target=\"_blank\">(.*?)</a>",s) print("成功登录 " + p.group(1)) else: p = re.search(r"<h2>欢迎回来,(.*?)</h2>",s) if p: # print(p.group()) p = re.search(r"target=\"_blank\">(.*?)</a>",s) print("成功登录 " + p.group(1)) else: p = re.search(r"uid=(.*?);",string) uid = p.group(1) ref = "https://www.luogu.com.cn/api/user/search?keyword="+uid response = requests.get(ref,headers=headers) response.encoding = 'utf-8' id = response.json() id = id['users'][0]["name"] print("登录失败","uid:",uid,"id:",id) print(l) print(len(l)) ``` $\color{black}\colorbox{lightgreen}{一个小小的tips}$ cookie的使解决了爬虫来"登录"账户的问题。但既然cookie可以用来画洛谷画板,那么有什么事情干不了呢?有了cookie正如同有了你的账户密码,在有效时间内可以干任何事情,**cookie是敏感信息**,所以还是尽量不要随便把cookie给不值得信任的人。 ~~所以有好心人能元旦的时候给我cookie吗,仅洛谷画板使用,有意向者可以加QQ3473131422~~ ## 7:正则表达式 在我们使用requests下载网页的时候,我们自然需要对下载的字符串进行处理,提取需要的信息。这便是一个字符串匹配的问题。 算法竞赛中,有许多处理字符串的算法比如KMP,~~自动AC机~~ AC自动机。但是这里不是算法竞赛,我们duck不必写这些字符串算法。python有一个强大的工具叫做正则表达式,她可以替代大篇幅的代码来做字符串匹配。 使用正则表达式需要一个"模式串"和一个"待匹配字符串" 比如我们下载了一个字符串 `<div class="am-u-sm-12 lg-small">&nbsp;<br>你已经在洛谷连续打卡了 <strong>666</strong> 天<br> </div>` 我们需要获得打卡的天数,也就是说我们需要匹配满足`<strong>xxx</strong>`的字符串,其中 xxx 是我们需要的内容。所以上面整个字符串为待匹配字符串,而我们需要找到满足形式为`<strong>xxx</strong>` 的字符串,这个字符串为"模式串",也就是正则表达式。简单点说,正则表达式就是一种符合某个模式(规则)的文本。 python正则表达式需要使用re库 `import re` ### 7.1 正则表达式的核心函数 常用的re函数有常用的正则表达式函数有re.match(),re.search(),re.findall(),re.compile()。 这类函数的形式都是一样的,拿 re.match 来举例 **re.match(pattern,string,flag=0)** **patter**为正则表达式,可以理解为我们需要找的字符。**string**为待匹配字符串。**flag**为标志位,控制匹配的方式,例如匹配的时候是否区分大小写。 re.match():**从头开始匹配**,如果遇到一个无法匹配的字符,则返回None,否则返回匹配结果。 re.search():**匹配整个字符串**,返回**第一个**匹配到的位置,没有返回None。 re.findall():匹配整个字符串,返回一个**列表**包含**所有**能匹配的字符串 re.compile():编译正则表达式。把字符串编译成正则表达式对象。可以**直接作为pattern使用**。 match与search的区别是 match必须从头开始匹配,如果找到一个字符不符合正则表达式,直接返回None。search是匹配整个字符串,匹配到的字符串**不一定要从头开始**。简单点说,**match匹配必须包含第一个字符**,而search不一定。 ```python >>> import re # 引入re库 >>> str = "Hello Hello world" # 待匹配字符串 >>> re.match(r"Hello",str) # 从str中从头开始匹配 Hello <re.Match object; span=(0, 5), match='Hello'> # 找到了,下标从0到4 >>> re.search(r"Hello",str) # 从str中匹配 Hello <re.Match object; span=(0, 5), match='Hello'> # 找到了,下标从0到4 >>> re.findall(r"Hello",str) # 从str中匹配所有的 Hello ['Hello', 'Hello'] # 返回一个列表,包含所有的匹配内容 >>> re.match(r"world",str) # 从str中从头开始匹配 world。str中第一个字符为H,与world匹配失败,直接返回None >>> re.search(r"world",str) # 从str中匹配 world <re.Match object; span=(12, 17), match='world'> # search不一定要从头开始,匹配下标为12-16 >>> p = re.search(r"world",str) # 把匹配的内容赋值给p >>> p.group(0) # 使用group(0)返回匹配的字符串内容 'world' >>> p.span() # 使用span返回匹配的字符串下标 (12, 17) >>> pattern=re.compile("Hello") # 编译成正则表达式 >>> re.match(pattern,str) # 可以直接调用,减少重新编译的时间 <re.Match object; span=(0, 5), match='Hello'> ``` * 模式串**前加上 r** 意思是**原始字符串**,原始字符串作用是**不转义反斜杠(\)**。例如 \n 代表换行符,但是在原字符串中就是"\n"这个字符串。在正则表达式中大部分情况下我们不需要用转义,使用原始字符串可以避免许多麻烦。当然在上面这个例子中没有涉及到`\`这个字符,所以加不加都是一样的。 * python字符串**下标从0开始**,search与match返回的下标满足**左闭右开**,即如果返回的坐标为(l,r),则实际位置为(l,r-1)。 ### 7.2 元字符 但是这样明显是不够的,实际应用中我们不可能只需要匹配类似Hello这种固定的字符串,比如我们需要匹配`<strong>xxx</strong>`,其中xxx为未知的字符。这时候我们可以用一些基本符号(元字符)来**代替**这些X。 这里用一个表格来列举一下常见的一些元字符。 | 符号 | 说明(可用来代替的字符) | 表达式 | 可匹配的解 | | :-----------: | :----------- | :----------- | :----------- | | . | 换行符以外的字符 | a.b | acb,asb,a2b | | ^ | 以...开始 | ^AK | AKIOI,AK123 | | 美元符号,这里会被识别成LaTeX,所以用¥代替 | 以...结束 | AK¥ | JohnVictorAK,321AK | | \b | 匹配单词边界,不匹配字符。单词边界指单词前后与空格间的位置 | asd\b | 123asd,不能匹配asd1(asd后不为空格) | | \d | 匹配数字1-9 | ab\dc | ab1c,ab2c,ab9c | | \D | 匹配非数字 | ab\Dc | abxc,ab&c | | \s | 匹配空白符(包括空格、制表符、换页符等) | ab\sc | ab c | | \S | 匹配非空白符 | ab\Sc | abyc,ab|c | | \w | 匹配字母、数字、下划线 | ab\wc | ab_c,ab1c | | [] | 匹配括号内的任意字符 | a[b,c,d,e]f | abf,acf,adf,aef | | \ | 转移字符,可以转义以上的元字符变成普通的字符 | a[b\\.\\\\]c | abc,a.c,a\c | 这样对于上面那种情况,`<strong>xxx</strong>`中的 x 为数字,可以用`\d`匹配。 ```python >>> str = "<div class=\"am-u-sm-12 lg-small\">&nbsp;<br>你已经在洛谷连续打卡了 <strong>666</strong> 天<br> </div>" >>> p=re.search(r"<strong>\d\d\d</strong>",str) >>> p.group(0) '<strong>666</strong>' ``` 又例如我们需要匹配一个ip地址 我们首先需要找出ip地址的特性。它是由https://地址.地址.地址.地址:端口号 的形式。这里地址和端口号为数字,可以用.或者\d匹配。 ```python >>> str = r"Welcome to visit https://129.226.190.205:443/" >>> p=re.search(r"https://\d\d\d\.\d\d\d\.\d\d\d\.\d\d\d:\d\d\d/",str) # 这里ip地址中的.由于不是元字符,而是做普通字符用,所以要转义 >>> p <re.Match object; span=(17, 45), match='https://129.226.190.205:443/'> ``` 这也是我们使用简单正则表达式的一般步骤。 1. 首先先分析我们需要匹配的字符串的特殊规律。 2. 然后再写出正则表达式,其中需要匹配的使用元字符代替。这里要注意就是类似于.以及\这些字符不作为转义字符时,前面要加\把他们转义成普通字符。 3. 进行匹配,这里我们需要根据情况选择match,search,findall三种匹配方式。 ### 7.3 重复限定符 有了元字符,我们可以完成对于大部分字符串的匹配。但是有的时候,我们会遇到字符大量重复,或者是只需要匹配夹在两个特定字符串中间的所有内容,根本不知道中间有多少个字符。 为了解决这些重复的问题,我们可以使用重复限定符,把他放在字符后面,表示字符的重复次数,让表达式看起来更加简洁。 同样的,用一个表格来表示常用的重复限定符 | 符号 | 说明 | 表达式 | 可匹配的解 | | :-----------: | :----------- | :----------- | :----------- | | * | 匹配0到多次 | abc* | ab,abccccccc | | + | 匹配1到多次 | abc+ | abc,abccccccc | | ? | 匹配0或1次 | abc? | ab,abc | | {m} | 匹配m次 | abc{3}de | abcccde | | {m,} | 匹配m或多次(包含m次) | abc{3,}de | abcccde,abcccccde | | {,m} | 匹配0到m次(包含m次) | abc{,3}de | abde,abcccde | | {n,m} | 匹配n到m次(包含n,m次) | abc{2,3}de | abccde,abcccde | ip地址的匹配可以简化为 `https://\d+\.\d+\.\d+\.\d+:\d+/` 匹配网页H1标签中的所有内容 `<h1>.*?</h1>` ### 7.4 贪婪与非贪婪 贪婪就是字面意思,匹配的越多越好。 比如,我们在aabaabb中找以a为开头,b为结尾的字符串。 贪婪模式匹配了所有的字符aabaabb。但是事实上,满足条件的字符串不止一个,我们只需要前3个字符aab就可以满足要求了。贪婪模式就是在能匹配的情况下越多越好,相反,非贪婪模式就是尽可能地少匹配。 通常,`{m,n}`,`{m,}`,`*`,`+`,`?`属于贪婪模式(匹配优先量词) `{m,n}?`,`{m,}?`,`*?`,`+?`,`??`属于非贪婪模式(忽略优先量词) ```python >>> str = "aabaabb" >>> re.search(r'a.*b',str) # *匹配,贪婪模式 <re.Match object; span=(0, 7), match='aabaabb'> >>> re.search(r'a.*?b',str) # *?匹配,非贪婪模式 <re.Match object; span=(0, 3), match='aab'> ``` ### 7.5 分组与条件 为了满足更加多的匹配需求,我们引入分组与条件。上面我们说了重复限定符。但是上面的重复限定符只能重复前面那个字符。分组就可以让我们对多个字符使用限定符。 我们用`()`来分组,分组中的内容可以看作**一个整体**。 特别如果在findall模式中分组,将返回**与分组匹配的文本列表**。如果使用了不只一个分组,那么**列表中的每项都是一个元组**,包含每个分组的文本。 放一段代码理解下qaq ```python >>> str = '<strong>666</strong>' >>> re.findall(r'<strong>.*</strong>',str) ['<strong>666</strong>'] >>> re.findall(r'<strong>(.*?)</strong>',str) ['666'] >>> ``` 我们发现,在.\*旁边加了括号,findall便会**只保留括号的内容**。 比如现在`'@[犇犇犇犇](/user/35998)'`,我们想要同时匹配出 犇犇犇犇 和 35998。 我们先来转化下形式`@[xxx](/user/xxx)`。在我们需要的xxx两旁加上括号,`@[(xxx)](/user/(xxx))`,然后把xxx用.\*替代,还要注意一下这里的`[`与`(`都是匹配字符,需要转义。 ```python >>> str = '@[犇犇犇犇](/user/35998)' >>> re.findall(r'@\[(.*?)\]\(/user/(.*?)\)',str) [('犇犇犇犇', '35998')] >>> str = '@[犇犇犇犇](/user/35998) @[JohnVictor](/user/254752)' >>> re.findall(r'@\[(.*?)\]\(/user/(.*?)\)',str) # 找到多个结果,每个结果为一个元组。 [('犇犇犇犇', '35998'), ('JohnVictor', '254752')] ``` 我们发现如果要匹配多个分组时,findall返回一个列表,每项都是一个元组,元组内为每个分组内容。 我们匹配网址的时候,会遇到http和https共存的情况,那么我们需要满足的条件时http或者https。这就需要**条件或**。 条件或格式为 `X|Y` 表示匹配X或Y,从左到右匹配,满足第一个条件就不会继续匹配第二个条件。 我们可以使用 `(http|https)://\d+\.\d+\.\d+\.\d+:\d+/`来同时匹配http与https。 ### 7.6 group的方法 既然findall可以返回每个分组内的内容,其实match和search也可以使用group函数来达到同样的效果。 group()与group(0)效果相同,就是匹配到的整个字符串。 group(1) 列出第一个括号内容,group(2) 列出第二个括号内容,group(n) 列出第n个括号内容。 groups() 列出所有括号内容的元组。 ```python >>> str = '@[犇犇犇犇](/user/35998)' >>> p=re.search(r'@\[(.*?)\]\(/user/(.*?)\)',str) >>> p.group() '@[犇犇犇犇](/user/35998)' >>> p.group(1) '犇犇犇犇' >>> p.group(2) '35998' >>> p.groups() ('犇犇犇犇', '35998') ``` ### 7.7 匹配方式(flags) 之前说match标准形式的时候说到match标准形式为re.match(pattern,string,flag=0) 现在来讲一下最后的那个flag有什么用。 常用的匹配方式如下: | 符号 | 说明 | | :-----------: | :-----------: | | re.I | 忽略大小写 | | re.M | 多行模式,`^`与美元符号会同时从每行进行匹配 | | re.S | `.`可匹配任何字符,包括换行符 | | re.X | 冗余模式,忽略正则表达式中的空格与#注释 | 代码示例部分: 1. re.I 忽略大小写 ```python >>> str = "HELLO WORLD" >>> re.search(r'hello',str) >>> re.search(r'hello',str,re.I) <re.Match object; span=(0, 5), match='HELLO'> ``` 2. re.M 多行模式,`^`与美元符号会同时从每行进行匹配 美元符号就是上文7.2中表格的第三行符号,表示以...结束。这里打美元符号貌似会被洛谷博客渲染成LaTeX,导致后面格式全部挂了qaq ```python >>> str = "HELLO WORLD\n123%a" >>> re.findall(r'^\w+',str) ['HELLO'] >>> re.findall(r'^\w+',str,re.M) ['HELLO', '123'] ``` 3. re.S `.`可匹配任何字符,包括换行符 ```python >>> str = "HELLO WORLD\n123%a" >>> re.findall(r'.+',str) ['HELLO WORLD', '123%a'] >>> re.findall(r'.+',str,re.S) ['HELLO WORLD\n123%a'] ``` 4. re.X 冗余模式,忽略正则表达式中的空格与#注释 ```python >>> str = 'aaa.bbb.ccc' >>> re.search(r'\..* \.',str) >>> re.search(r'\..* \.',str,re.X) <re.Match object; span=(3, 8), match='.bbb.'> ``` ------------ $\color{black}\colorbox{lightgreen}{一个小小的tips}$ 关于正则表达式暂时就讲到这里了,所以NOIP啥时候后能支持一下正则表达式啊(雾 经用户@stdtr1 提醒,c++11好像真的能用正则表达式qwq 给出两篇博客,有兴趣的可以自己研究) [https://www.cnblogs.com/jerrywossion/p/10086051.html](https://www.cnblogs.com/jerrywossion/p/10086051.html) [http://cplusplus.com/reference/regex/](http://cplusplus.com/reference/regex/) ## 8:最後の言葉 以上就是全文了(貌似上万个字了),首先感谢能坚持看到这里qaq 这篇文章稍微简述了一下爬虫的基本方法以及简单应用。其实爬虫还有很多更加高级的操作,同时网站也有很多反爬虫机制。 有些网站会利用浏览器执行js动态生成代码,导致下载的网页与审查元素所看到的代码不一致。有些利用分析用户行为来判断爬虫。当然,爬虫也有对付的办法。 我们可以使用 selenium 配合 webdriver ,使用 python 操控浏览器,模拟浏览器行为;可以配合 Fiddler 抓包来抓取手机APP的数据或者电脑程序的数据;还有爬虫框架 Scrapy 以及其他库函数比如 bs4(beautifulsoup) 解析网页 HTML ,多线程爬虫实现更加强大的功能。由于篇幅限制,这里就不说了) 如果本人没有AFO或者有时间,可能还会再写一篇文章。如果感兴趣可以自行百度搜索,网络上这方面的资料还是很多的,作者本人当时也是自行搜索博客学习的qaq 如果文中有哪里写错了或者有疑问欢迎私信或者评论指出qaq 写文章不易qwq 求评论qwq 求点赞qwq