简单上手python爬虫
犇犇犇犇
2020-08-13 00:09:34
有没有一个时刻,
看着页面上大量精美的动漫图,
却一张张下载点到手**麻**?
有没有一个时刻,
从网上整理寻找资料,
却看着长长的页面加载进度条感到头**大**?
有没有一个时刻,
玩着洛谷冬日画板,
看着别人画着一张张图片,
自己只能一个一个,从这点到**那**?
面对如此繁琐重复的工作
不妨把他交给电脑来完成**吧** !
今天,
就让我们来写一只爬虫,
替我们在虚拟的网络上爬呀爬呀**爬**。
![](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"> <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\"> <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