知网硕博类论文url爬虫
1.概述
手写一个对知网的所有的硕博类论文的 URL 分地区和学科进行爬取的爬虫,将爬虫托管在服务器上运行,并将得到的初步结果保存在 txt 文件上,处理错误日志.
没有用Scrapy框架
2.准备工作
2.1 操作系统
ubuntu16.04 64 位 , windows 64 位
2.2 开发工具
Pycharm , Fiddler 调试工具, Chrome , putty (连接服务器), winscp(传输文件)
3.逻辑分析
3.1 页面分析
代码是根据第二个入口里面的逻辑写的 肯定是要重写了
地区和学科的映射关系如下:
真的找不出是什么规律
'福建': '华东(南)',
'上海': '华东(南)',
'江西': '华东(南)',
'浙江': '华东(南)',
'新疆': '西北',
'宁夏': '西北',
'陕西': '西北',
'青海': '西北',
'甘肃': '西北',
'湖北': '华中',
'河南': '华中',
'湖南': '华中',
'海南': '华南',
'广西': '华南',
'广东': '华南',
'江苏': '华东(北)',
'安徽': '华东(北)',
'山东': '华东(北)',
'瑞典': '其他国家',
'日本': '其他国家',
'澳大利亚': '其他国家',
'英国': '其他国家',
'德国': '其他国家',
'美国': '其他国家',
'天津': '华北',
'内蒙古': '华北',
'山西': '华北',
'河北': '华北',
'北京': '华北',
'黑龙江': '东北',
'辽宁': '东北',
'吉林': '东北',
'香港': '港澳台',
'西藏': '西南',
'四川': '西南',
'重庆': '西南',
'云南': '西南',
'贵州': '西南',
3.2 源码分析
Ctrl+U 查看源码
定位元素,右键检查,打开 Chrome 调试器,可以观察到对应的代码
按照静态网址的做法,首先复制需要分析的字符串,观察在源码中的位置
可以发现两者呈现的结果不一样,原因在于<div class=bodymain>
这个结果是动态呈现的,通过一段js代码实现
对于动态网页,有两种解决方式:
1.selenium + chromedriver 模拟浏览器行为
2.分析网页请求,得到是哪个接口获取这方面的数据,然后再用 post 的方法发送得到 response
采用第二种.
3.3 Fiddler 调试
可以得到相应的header 和 webforms
分析得到 form 中有关联的是 SearchStateJson , index
同理,点击大学可以分析到关联的 SearchStateJson 中的 value 字段
相应能够构造出地区,大学的代码,详见代码
大学页面用相似的办法处理,也是动态加载,得到学科代码subCode,页码pIDx
4.编写代码
1.构造查询大学请求,遍历地区,得到大学的列表
2.利用大学列表构造相应大学的 URL (make_university_url)
3.在新的 URL 内查询学科,得到相应论文的数据段
4.解析页面(parse_page)
具体实现:
#!/usr/bin/python
# vim: set fileencoding=utf-8 :
import threading
import random
import re
import io
from bs4 import BeautifulSoup
from lxml import etree
import time
import requests
get_province = {
'0004': '福建',
'0025': '上海',
'0017': '江西',
'0031': '浙江',
'0029': '新疆',
'0020': '宁夏',
'0024': '陕西',
'0021': '青海',
'0005': '甘肃',
'0013': '湖北',
'0011': '河南',
'0014': '湖南',
'0009': '海南',
'0007': '广西',
'0006': '广东',
'0016': '江苏',
'0001': '安徽',
'0022': '山东',
'0036': '瑞典',
'0035': '日本',
'0037': '澳大利亚',
'0033': '英国',
'0038': '德国',
'0034': '美国',
'0027': '天津',
'0019': '内蒙古',
'0023': '山西',
'0010': '河北',
'0002': '北京',
'0012': '黑龙江',
'0018': '辽宁',
'0015': '吉林',
'0032': '香港',
'0039': '西藏',
'0026': '四川',
'0003': '重庆',
'0030': '云南',
'0008': '贵州'
}
get_zone = {
'福建': '华东(南)',
'上海': '华东(南)',
'江西': '华东(南)',
'浙江': '华东(南)',
'新疆': '西北',
'宁夏': '西北',
'陕西': '西北',
'青海': '西北',
'甘肃': '西北',
'湖北': '华中',
'河南': '华中',
'湖南': '华中',
'海南': '华南',
'广西': '华南',
'广东': '华南',
'江苏': '华东(北)',
'安徽': '华东(北)',
'山东': '华东(北)',
'瑞典': '其他国家',
'日本': '其他国家',
'澳大利亚': '其他国家',
'英国': '其他国家',
'德国': '其他国家',
'美国': '其他国家',
'天津': '华北',
'内蒙古': '华北',
'山西': '华北',
'河北': '华北',
'北京': '华北',
'黑龙江': '东北',
'辽宁': '东北',
'吉林': '东北',
'香港': '港澳台',
'西藏': '西南',
'四川': '西南',
'重庆': '西南',
'云南': '西南',
'贵州': '西南',
}
def get_university_response(value, page_index):
url = "http://navi.cnki.net/knavi/Common/Search/PPaper"
headers = {
'Accept': "text/plain, */*; q=0.01",
'Accept-Encoding': "gzip, deflate",
'Accept-Language': "zh-CN,zh;q=0.9",
'Content-Length': "976",
'Content-Type': "application/x-www-form-urlencoded",
'Cookie': "cnkiUserKey=a96dda55-d4ee-2b66-7221-c2be612e4129; UM_distinctid=163626642d47c-0d7118a6fbe2c8-3961430f-1fa400-163626642d5ae6; Ecp_ClientId=3180515165106096065; ASP.NET_SessionId=fn4hjrmxg3nv4ng2o3tqbt41; SID_navi=1201620; Ecp_session=1; LID=WEEvREcwSlJHSldRa1FhdXNXa0hIb2hxbUg0bGRtZ01sK0lLNitEZ0d1az0=$9A4hF_YAuvQ5obgVAqNKPCYcEjKensW4IQMovwHtwkF4VYPoHbKxJw!!",
'Host': "navi.cnki.net",
'Origin': "http://navi.cnki.net",
'Proxy-Connection': "keep-alive",
'Referer': "http://navi.cnki.net/knavi",
'User-Agent': "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36",
'X-Requested-With': "XMLHttpRequest",
'Cache-Control': "no-cache",
}
form_data = {
'SearchStateJson': '{"StateID":"","Platfrom":"","QueryTime":"","Account":"knavi","ClientToken":"","Language":"","CNode":{"PCode":"CDMD","SMode":"","OperateT":""},"QNode":{"SelectT":"","Select_Fields":"","S_DBCodes":"","QGroup":[{"Key":"Navi","Logic":1,"Items":[],"ChildItems":[{"Key":"PPaper","Logic":1,"Items":[{"Key":1,"Title":"","Logic":1,"Name":"AREANAME","Operate":"","Value":\"' + str(
value).rjust(4,
'0') + '?","ExtendType":0,"ExtendValue":"","Value2":""}],"ChildItems":[]}]}],"OrderBy":"RT|","GroupBy":"","Additon":""}}',
'displaymode': 1,
'index': 1,
'pagecount': 21,
'pageindex': page_index,
# 'random': 0.350068384544433,
'random': random.random(),
}
response = requests.request("POST", url, data=form_data, headers=headers, timeout=30)
return response.text
def get_university_id(html_text):
code_list = re.findall(r"baseid=\w+", html_text)
title_list = re.findall(r"title=\"[\u4e00-\u9fa5]*·?[\u4e00-\u9fa5]*\(?[\u4e00-\u9fa5A-Za-z\s]*\)?[^\x00-\xff]?\">",
html_text)
if (len(title_list) > 0):
del title_list[0]
# print(code_list)
# print(title_list)
code_list1 = []
title_list1 = []
for i in code_list:
code_list1.append(str(i)[7:])
for i in title_list:
title_list1.append(str(i)[7:-2])
return code_list1, title_list1
def write_in_file(code_list1, title_list1):
fpath = '/root/university_id_name.out'
with io.open(fpath, 'a', encoding='utf-8') as f:
for i in range(len(code_list1)):
f.write(code_list1[i] + '_' + title_list1[i] + '\n')
def make_university_url(code_list):
root_url = 'http://navi.cnki.net/knavi/PPaperDetail?pcode=CDMD&logo='
for i in code_list:
url = root_url + i[0:5]
value = i[-4:]
# print(url)
for subCode in range(1, 13): # (1,13) 学科编号
for pIDx in range(0, 1): # (需要额外判断) 页面下标
get_article_by_subject_response(url, subCode, pIDx, value)
# page_index 下标从 0 开始
def get_article_by_subject_response(url, subCode, pIDx, value):
post_url = 'http://navi.cnki.net/knavi/PPaperDetail/GetArticleBySubject'
headers = {
'Accept': "*/*",
'Accept-Encoding': "gzip, deflate",
'Accept-Language': "zh-CN,zh;q=0.9",
'Content-Length': "75",
'Content-Type': "application/x-www-form-urlencoded; charset=UTF-8",
# 'Cookie': "cnkiUserKey=a96dda55-d4ee-2b66-7221-c2be612e4129; UM_distinctid=163626642d47c-0d7118a6fbe2c8-3961430f-1fa400-163626642d5ae6; Ecp_ClientId=3180515165106096065; ASP.NET_SessionId=fn4hjrmxg3nv4ng2o3tqbt41; SID_navi=1201620; Ecp_session=1; LID=WEEvREcwSlJHSldRa1FhdXNXa0hIb2hyQlVyRFlJczA2Mm1YYzF4RzNGUT0=$9A4hF_YAuvQ5obgVAqNKPCYcEjKensW4IQMovwHtwkF4VYPoHbKxJw!!; Ecp_IpLoginFail=18052136.17.61.215",
'Host': "navi.cnki.net",
'Origin': "http://navi.cnki.net",
'Proxy-Connection': "keep-alive",
'Referer': "http://navi.cnki.net/knavi/PPaperDetail?pcode=CDMD&logo=GHAGU",
'User-Agent': "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36",
'X-Requested-With': "XMLHttpRequest",
'Cache-Control': "no-cache",
}
form_data = {
'pcode': 'CDMD',
'baseID': url[-5:],
'subCode': str(subCode).rjust(2, '0'),
'orderBy': 'RT|DESC',
'scope': '%u5168%u90E8',
'pIdx': pIDx
}
try:
response = requests.request('POST', post_url, data=form_data, headers=headers, timeout=30)
except:
print('Error:'+pIDx)
parse_page(response.text, value, subCode)
# 获取页码数
soup = BeautifulSoup(response.text, 'html.parser')
pIDx_max = soup.find('span', attrs={'id': 'partiallistcount'}).string
pIDx_max = int(pIDx_max)
for pIDx in range(1, int(pIDx_max / 20 + 2)):
form_data = {
'pcode': 'CDMD',
'baseID': url[-5:],
'subCode': str(subCode).rjust(2, '0'),
'orderBy': 'RT|DESC',
'scope': '%u5168%u90E8',
'pIdx': pIDx
}
try:
response = requests.request('POST', post_url, data=form_data, headers=headers, timeout=30)
parse_page(response.text, value, subCode)
except:
with io.open('/root/ErrorLog.out','a',encoding='utf-8') as f:
f.write(url+'\n')
def parse_page(response_text, value, subCode):
with io.open('/root/response_text.out', 'w', encoding='utf-8') as f:
f.write(response_text)
soup = BeautifulSoup(response_text, 'html.parser')
tag_list = soup.find_all('td', attrs={'class': 'name'})
if (len(tag_list) == 0):
time.sleep(1)
return
if (len(tag_list) > 0):
del tag_list[0]
# print(tag_list) type is bs4.element.Tag
with io.open('/root/result.out', 'a', encoding='utf-8') as f:
if (len(tag_list) > 0):
for tag in tag_list:
string = tag.a['href']
indexx = string.find('sfield=FN&')
province = get_province[value]
zone = get_zone[province]
f.write('http://kns.cnki.net/kcms/detail/detail.aspx?' + string[
indexx:] + ' ' + province + ' ' + zone + ' ' + str(
subCode) + '\n')
def main():
code_list = []
title_list = []
for value in range(1, 40): # (1,40) 大学代码遍历
for page_index in range(1, 4): # 检索大学页面下标
html_text = get_university_response(value, page_index)
code_list1, title_list1 = get_university_id(html_text)
code_list1 = [i + '_' + str(value).rjust(4, '0') for i in code_list1] # new
code_list.extend(code_list1)
title_list.extend(title_list1)
# 获取所有大学的代码和名字 大学代码均为5位
# write_in_file(code_list, title_list) # 写入文件 university_id_name.txt
# print(code_list) # GJUYU_0025
thread = []
for i in range(10):
t = threading.Thread(target=make_university_url, args=(code_list))
thread.append(t)
make_university_url(code_list) # 构造大学url
main()
5.服务器托管
利用 winscp 将代码传输到相应服务器,Tmux 分屏实现托管
Tmux 命令可参考博客
终端利器 - tmux配置与使用
tmux
python3 cnki_2.py
按Ctrl+B D #返回主界面
6.生成结果
19 小时跑完所有结果(450MB),考虑到单线程还是比较快的
ErrorLog.out保存错误日志
response_rext.out保存最后一次的response
result.out为最终结果
7.后记
整个过程坑点满满,随便写几个:
1.大学的名字格式千奇百怪,还有国外的,写正则换了好几次
2.有某个大学的括号是全角半角的混用的…知网前端程序员背锅
3.cookie是不必要的,甚至会因为点击频率太高,导致得不到对应的respsonse
4.搬运到服务器上遇到了一点编码上的小问题
启发:
熟练了 post 方法处理动态页面
Fiddler 实在太强大了,另外 postman 不太会用
后续:
开发速度很快,但被明示结果的呈现太简单暴力…应该是要分文件夹,细分文件夹,以及得到更多的字段,然后开始要解析具体的论文页面