简单上手python爬虫

犇犇犇犇

2020-08-13 00:09:34

Tech. & Eng.

有没有一个时刻,
看着页面上大量精美的动漫图,
却一张张下载点到手

有没有一个时刻,
从网上整理寻找资料,
却看着长长的页面加载进度条感到头

有没有一个时刻,
玩着洛谷冬日画板,
看着别人画着一张张图片,
自己只能一个一个,从这点到

面对如此繁琐重复的工作
不妨把他交给电脑来完成 !

今天,
就让我们来写一只爬虫,
替我们在虚拟的网络上爬呀爬呀

没有人发现每段段尾是押韵的吗

目录:

-2:什么是爬虫
-1:python or c++
0:前置知识:关于python
1:第一个任务--下载网页
2:下载一张图片吧
3:第二个任务--有道翻译

$\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:第一个任务--下载网页

这段代码在文章开头出现过了。代码不长,先贴上来了吧。

import requests # 引入requsets库
response = requests.get('https://www.baidu.com/') # 不带参数的get请求
response.encoding = 'utf-8' # 用utf-8解码
print(response.text) # 输出

我们发现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中猜测的响应内容编码方式

那么这些到底是什么意思呢

我们可以执行一下下面的代码

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)

结果如下:
分析下内容:
status_code表示请求状态。200则表示请求成功。如果status_code是404或者502的话就表示请求失败了。

content返回的是响应内容的二进制形式,我们可以发现开头有一个b字母,同时还有一些类似\xe7\x99的乱码。这是字节字符串的标志。

text大部分与content一样,这两个的区别是text用猜测的编码方式将content内容编码成字符串。如果页面是纯ascii码,这那么content与text的结果是一样的,对于其他的文字(比如中文),需要编码才能正常显示。否则就会出现乱码。当然我们也可以使用response.content.decode('utf-8')来手动解码。

我们输出一下content和text的类型,可以发现他们是不同的类型。一个是bytes(字节字符串)类型,另一个是str(字符串)类型。text是现成的字符串,可以当成字符串直接使用;content还要编码。但是text是根据猜测的响应内容编码方式进行解码 (下文的response.encoding)。有的时候系统会判断失误,这时候我们需要手动输入解码方式来进行解码。

response.encodingHTTP header中猜测的响应内容编码方式。python会从header中的charset提取的编码方式(如下图所示),若header中没有charset字段则会默认为ISO-8859-1,这也是上文说的系统判断失误,无法正确解码的原因。这时候我们需要手动输入解码方式解码。

apparent_encoding从网页的内容中分析网页编码的方式,所以apparent_encoding比encoding更加准确。python会根据encoding中存的内容进行解码。所以可以采用 response.encoding=response.apparent_encoding。 当然手动输入解码方式是最靠谱的。

2:下载一张图片吧

既然python能下载网页,那么python能不能下载图片呢?其实原理是一样的,把图片网页下载下来,然后把它写入文件就可以了。当然,这里直接用字节(byte)的方式写入,所以要用content

这里给一个随机图片网址 https://api.ixiaowai.cn/api/api.php

打开就会自动随机跳转动漫图。

先放上本人的代码:

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,把它变成原始字符串,就不需要两个\了。

什么,你说要批量下载?加一个循环不就好了吗

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

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的访问,直接把我们拒之门外了。这也是上文的爬虫无法正常访问洛谷或者知乎的原因。

那么这就真的能阻止爬虫访问吗?


我们可以发现上面那个故事中,老和尚通过马蹄声来辨别商人与强盗。那么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}

3.2:使用有道翻译

好啦,那么具体怎么操作呢?我们进入正题:
这里用谷歌浏览器来示例一下。其他浏览器操作基本相同

  1. 将浏览器全屏显示,首先打开有道翻译,右键,检查。(有的浏览器是审查元素,或者直接按F12按钮)

  1. 我们可以发现有我们熟悉的Elements,我们的网页源代码。这里我们选择Network选项。

  1. 点击一下翻译按钮,我们发现这里多出来了很多请求。这就是浏览器与服务器的通信内容了。

  1. 点击这些内容,我们可以发现右边出现了请求的详细信息。


主要操作方式依次为右键->检查元素->网络(Network)

这些浏览器拦截的通信内容。这里 Request Method 就是请求方式。我们发现这里有 get 和 post 两种。简单点说,get 就是我们从服务器获得数据。post 就是指我们向服务器提交数据。虽然在实际过程中 get 也可以用来提交数据。
我们在翻译的时候当我们点开翻译按钮的时候,明显我们向服务器提交了我们要翻译的内容,所以我们应该选择 post 。

  1. 拉到底,我们可以发现有 From Data 。这就是浏览器提交的内容了。果然, i 这一项的值是 Hello World 。终于找到组织了,就是这个请求。

  1. 点击右边的Preview。终于看到了我们的结果。你好,世界。这就是服务器返回给我们的结果了。下面我们要做的就是用python模拟上面整个过程。

下面我们来分析一下刚才所有我们看到的这些内容

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的时候添加就行了

response = requests.post(url=url,data=data,headers=head)

这里的data和head是 字典 类型。

下面我们开始写代码吧。

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)

运行结果如下。

输出了一个字符串。我们发现"tgt"后面就是我们需要的内容了。我们把这个中文提取出来就好。
当然,我们可以根据一般处理字符串的方法去处理,但是这种方法不方便而且不美观。仔细观察一下,熟悉python数据结构的可以发现,当中的"{}","[]",":"提示我们,这不就是个 字典 吗。其实这是一个json格式,包含的是python可以识别的正常数据结构。
对于这种字符串就是数据结构的情况,python提供一个json库。用法很简单,可以使用json.loads(str)(str为字符串),也可以直接使用response.json()

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就是一个字典了。


name是一个字典,其中包含我们需要内容的是translateResult这一项,而这一项里面又包含两个空列表,两个空列表里面又套着一个字典,这个字典里面的tgt是我们需要的结果。可以结合一下上面这张图来理解。禁止套娃

下面给出完整代码:

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'])

我们用python成功地实现了翻译。

之前说我们爬虫可能无法正常访问洛谷,这里一样只要加上User-Agent就可以正常使用了

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就一直报错呢?


这其实就是编码问题。这是初学 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就会出现问题。也就是我们看到的乱码问题。

Unicode 是一个很大的字符集,所有的Unicode可以到 这里 查看。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/ 。至少本人测试的时候还是能用的)
建议如果可能尽量使用IP类型为https的代理ip。当然,如果自己有服务器或者其他代理ip当然是最好的选择。
这里再提供一个查看ip的网站 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来演示

代码:

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试一试。

成功访问运行界面如下。

我们发现我们的python成功使用了代理ip。

当然我们也可以交替使用代理ip,避免由于单个ip访问次数过快过高被封。实现方法也很简单,把所有ip放到一个列表里,每次随机使用。这里需要使用 random库。

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(不能吃!!)




每到元旦的时候,为了画洛谷画板,某群里便会有着一堆求cookie的人。那么cookie到底是什么?好吃吗?

别说,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

然后就能看到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 安装

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 的形式相同,生成的文件可以直接套用qwq

由于本人较菜,当时参考了一下他的写法,这里感谢一下神ouuan

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 安装

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读取失败

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库

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不一定。

>>> 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'>

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匹配。

>>> 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匹配。

>>> 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,}?,*?,+?,??属于非贪婪模式(忽略优先量词)

>>> 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

>>> 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用.*替代,还要注意一下这里的[(都是匹配字符,需要转义。

>>> 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() 列出所有括号内容的元组。

>>> 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 忽略大小写

    >>> 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

    >>> str = "HELLO WORLD\n123%a"
    >>> re.findall(r'^\w+',str)
    ['HELLO']
    >>> re.findall(r'^\w+',str,re.M)
    ['HELLO', '123']
  3. re.S .可匹配任何字符,包括换行符

    >>> 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 冗余模式,忽略正则表达式中的空格与#注释

    >>> 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
http://cplusplus.com/reference/regex/

8:最後の言葉

以上就是全文了(貌似上万个字了),首先感谢能坚持看到这里qaq
这篇文章稍微简述了一下爬虫的基本方法以及简单应用。其实爬虫还有很多更加高级的操作,同时网站也有很多反爬虫机制。
有些网站会利用浏览器执行js动态生成代码,导致下载的网页与审查元素所看到的代码不一致。有些利用分析用户行为来判断爬虫。当然,爬虫也有对付的办法。

我们可以使用 selenium 配合 webdriver ,使用 python 操控浏览器,模拟浏览器行为;可以配合 Fiddler 抓包来抓取手机APP的数据或者电脑程序的数据;还有爬虫框架 Scrapy 以及其他库函数比如 bs4(beautifulsoup) 解析网页 HTML ,多线程爬虫实现更加强大的功能。由于篇幅限制,这里就不说了)

如果本人没有AFO或者有时间,可能还会再写一篇文章。如果感兴趣可以自行百度搜索,网络上这方面的资料还是很多的,作者本人当时也是自行搜索博客学习的qaq

如果文中有哪里写错了或者有疑问欢迎私信或者评论指出qaq

写文章不易qwq
求评论qwq
求点赞qwq