一、项目需求
具体需求如下:
以“单位”为分类,“中国地震局”为关键词,跨选“期刊、教育期刊、特色期刊、博士、硕士、国内会议、国际会议、报纸、学术辑刊”几个数据库进行搜索,将搜索到的结果及详情页相关字段提取出来,存入一个CSV文件。
二、需求分析
了解了上面的具体需求,乍一看网站,似乎挺容易,实则不然!
且看采集步骤:
1、在搜索列表中提取文章详情页链接及其所在数据库存入任务文件;
2、多线程采集任务文件中的所有任务
简单看只有这两步,实际上难点就在第一步。
难点:
1、如何得到列表页
当按条件搜索得到列表后,想在列表里提取详情页链接,这时打开源文件发现没有文章列表。
不用怀疑,列表肯定是ajax加载的。
好吧,那就用Fiddler来找出ajax请求。
下面两图对比网站搜索列表页和Fiddler抓取的请求结果页,信息是一致的。
然后在Fiddler中模拟请求,发现Cookie是必须的。
经过多次调试,发现Cookie是有时效的,所以必须使用Cookie管理器动态获取Cookie提供给请求的headers。
2、如何得到列表分页
当要打开列表分页的时候,发现列表最多显示120个分页,而且列表上是没有显示分页路径的。所以就按年度分类得到细分列表再加载分页吧。
思路这么定了,那就是得找年度分类有什么特点和分页列表请求是什么了?
还是通过Fiddler来寻找是哪些请求吧,如下两图已经框出了整个过程索要执行的请求。
3、在加载分页过程中,出现验证码问题!
当连续加载了16个分页后,网站就出现了验证码,这说明列表分页页面Cookie有效加载分页数是16个。那解决这个问题的方法就是判断页面出现验证码的时候,要重新获取网站动态Cookie,然后再执行搜索,接着从上次出现验证码的页面开始加载。
好了,以上内容已经把列表页获取详情页链接的操作分析清楚了,那下来就编码实现吧。
三、编写代码
# coding: utf-8
# 抓取CNKI(http://cnki.net/)中单位为“中国地震局”的相关文献数据。
import re
import sys
from webscraping import common, download
from lxml import etree
from collections import deque
import time
import cookielib
import urllib2
from urlparse import urljoin
import csv
DELAY = 3
FIELDS = ['T1', 'A1', 'AD', 'AB', 'FU', 'K1', 'JF1', 'JF2', 'PY', 'SN', 'DT', 'A3', 'CN', 'CT', 'CP', 'DB', 'URL']
#FIELDS_DES = ['论文标题', '作者全名', '作者地址', '摘要', '基金', '关键词', '出版物中文名', '出版物英文名', \
#'出版年', 'ISSN号', '文献类型', '导师', '会议名称', '会议时间/出版日期', '会议地点', '数据库', 'URL']
NUM_THREADS = 10
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36'
def scraper():
found = {}
writer = common.UnicodeWriter('cnki_CEA.csv')
writer.writerow(FIELDS)
#writer.writerow(FIELDS_DES)
# 任务队列
tasks = deque()
#for year in range(1997, 2020):
for year in range(1998, 2020):
#for year in [2017]:
# 年份、起始页码
tasks.append((str(year), 1))
# 将搜索列表上的文章详情页链接写入文件,以追加的方式,url唯一性
url_writer = common.UnicodeWriter('art_urls.csv', mode='ab', unique=True, unique_by=[0])
url_writer.writerow(['art_url', 'DB'])
while tasks:
task_year, task_start_pagenum = tasks.popleft()
print 'Task year = {}, start_pagenum = {}'.format(task_year, task_start_pagenum)
# 通过Cookie管理器动态获取网站加载的cookie
cookie_jar = cookielib.MozillaCookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar))
D = download.Download(delay=DELAY, num_retries=1, opener=opener, proxy_file='proxies.txt')
# 每次加载页面获取页面cookie
get_cookie_url = 'http://kns.cnki.net/kns/brief/default_result.aspx'
cookie_text = D.get(get_cookie_url, read_cache=False)
# 初始化任务,通过ajax请求加载各年份下文章列表页,提取文章链接加入任务队列
# 1,执行搜索(9个数据库)(每次必须执行搜索,不能读取缓存)
#search_url = 'http://kns.cnki.net/kns/request/SearchHandler.ashx?action=&NaviCode=*&ua=1.11&PageName=ASP.brief_default_result_aspx&DbPrefix=SCDB&DbCatalog=%e4%b8%ad%e5%9b%bd%e5%ad%a6%e6%9c%af%e6%96%87%e7%8c%ae%e7%bd%91%e7%bb%9c%e5%87%ba%e7%89%88%e6%80%bb%e5%ba%93&ConfigFile=SCDBINDEX.xml&db_opt=CJFQ%2CCDFD%2CCMFD%2CCPFD%2CIPFD%2CCCND%2CCCJD&txt_1_sel=AF%24%25&txt_1_value1=%E4%B8%AD%E5%9B%BD%E5%9C%B0%E9%9C%87%E5%B1%80&txt_1_special1=%25&his=0&parentdb=SCDB'
# 只搜索“期刊”数据库的请求url
search_url = 'http://kns.cnki.net/kns/request/SearchHandler.ashx?action=&NaviCode=*&ua=1.11&PageName=ASP.brief_default_result_aspx&DbPrefix=SCDB&DbCatalog=%e4%b8%ad%e5%9b%bd%e5%ad%a6%e6%9c%af%e6%96%87%e7%8c%ae%e7%bd%91%e7%bb%9c%e5%87%ba%e7%89%88%e6%80%bb%e5%ba%93&ConfigFile=SCDBINDEX.xml&db_opt=CJFQ%2CCJRF%2CCJFN&txt_1_sel=AF%24%25&txt_1_value1=%E4%B8%AD%E5%9B%BD%E5%9C%B0%E9%9C%87%E5%B1%80&txt_1_special1=%25&his=0&parentdb=SCDB&__=Fri%20Sep%2028%202018%2009%3A12%3A58%20GMT%2B0800%20(%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4)'
search_result = D.get(search_url, read_cache=False)
# 2,get方法加载年份列表,提取年份和年份id构造不同年份下列表首页请求url,并将列表第一页中的文章链接写入文件
# http://kns.cnki.net/kns/group/doGroupLeft.aspx?action=1&Param=ASP.brief_default_result_aspx%23SCDB/%u53D1%u8868%u5E74%u5EA6/%u5E74%2Ccount%28*%29/%u5E74/%28%u5E74%2C%27date%27%29%23%u5E74%24desc/1000000%24/-/40/40000/ButtonView&cid=0&clayer=0
#get_year_url = 'http://kns.cnki.net/kns/group/doGroupLeft.aspx?action=1&Param=ASP.brief_default_result_aspx%23SCDB/%u53D1%u8868%u5E74%u5EA6/%u5E74%2Ccount%28*%29/%u5E74/%28%u5E74%2C%27date%27%29%23%u5E74%24desc/1000000%24/-/40/40000/ButtonView&cid=0&clayer=0'
# 只搜索“期刊”数据库时获取年份列表的请求url
get_year_url = 'http://kns.cnki.net/kns/group/doGroupLeft.aspx?action=1&Param=ASP.brief_default_result_aspx%23SCDB/%u53D1%u8868%u5E74%u5EA6/%u5E74%2Ccount%28*%29/%u5E74/%28%u5E74%2C%27date%27%29%23%u5E74%24desc/1000000%24/-/40/40000/ButtonView&cid=0&clayer=0&__=Fri%20Sep%2028%202018%2009%3A19%3A47%20GMT%2B0800%20(%E4%B8%AD%E5%9B%BD%E6%A0%87%E5%87%86%E6%97%B6%E9%97%B4)'
year_list = D.get(get_year_url, read_cache=False)
if year_list:
for year_id, year in re.compile(r'
page_num = 1
if year == task_year:
query_id = ''
# 构造年份列表页 &recordsperpage=50 每页显示50条
year_url = 'http://kns.cnki.net/kns/brief/brief.aspx?ctl={}&dest=%E5%88%86%E7%BB%84%EF%BC%9A%E5%8F%91%E8%A1%A8%E5%B9%B4%E5%BA%A6%20%E6%98%AF%20{}&action=5&dbPrefix=SCDB&PageName=ASP.brief_default_result_aspx&Param=%e5%b9%b4+%3d+%27{}%27&SortType=%e5%b9%b4&ShowHistory=1&recordsperpage=50'.format(year_id, year, year)
year_first_html = D.get(year_url, read_cache=False)
# 提取总页数
page_num = common.regex_get(year_first_html, r'\d+/(\d+)')
# 提取QueryID
query_id = common.regex_get(year_first_html, r'&QueryID=(\d+)&')
#open(year + '.html', 'w').write(year_first_html)
# 第一页
if task_start_pagenum == 1 and year_first_html:
root = etree.HTML(year_first_html)
art_table = root.xpath('//table[@class="GridTableContent"]')
if art_table:
for tr in art_table[0].xpath('./tr'):
art_url = tr.xpath('string(./td[2]/a/@href)')
if art_url:
db_name = tr.xpath('string(./td[6])').strip()
if year == '2019':
art_url_id = common.regex_get(art_url, r'&URLID=([^=\'"]+)')
art_url = 'http://kns.cnki.net/KCMS/detail/' + art_url_id + '.html'
print 'Found art_url : {}'.format(art_url)
if art_url not in found:
found[art_url] = 1
# 将文章链接, 所属数据库 写入文件
url_writer.writerow([art_url, db_name])
else:
dbcode = common.regex_get(art_url, r'DbCode=([A-Z]+)&')
dbname = common.regex_get(art_url, r'DbName=([^&=]+)&')
filename = common.regex_get(art_url, r'FileName=([^&=]+)&')
art_url = 'http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=' + dbcode + '&dbname=' + dbname + '&filename=' + filename
print 'Found art_url : {}'.format(art_url)
if art_url not in found:
found[art_url] = 1
# 将文章链接, 所属数据库 写入文件
url_writer.writerow([art_url, db_name])
# 构造列表分页请求
# http://kns.cnki.net/kns/brief/brief.aspx?curpage={}&RecordsPerPage=50&QueryID=16&ID=&turnpage=1&tpagemode=L&dbPrefix=SCDB&Fields=&DisplayMode=listmode&SortType=%e5%b9%b4&PageName=ASP.brief_default_result_aspx&ctl=d6a6b300-5785-4953-b005-47dcfffdcb51&Param=%e5%b9%b4+%3d+%272018%27
if page_num:
page_num = int(page_num)
if page_num > 1:
for page in range(task_start_pagenum + 1, page_num + 1):
print 'page', page
get_page_url = 'http://kns.cnki.net/kns/brief/brief.aspx?curpage={}&RecordsPerPage=50&QueryID={}&ID=&turnpage=1&tpagemode=L&dbPrefix=SCDB&Fields=&DisplayMode=listmode&SortType=%e5%b9%b4&PageName=ASP.brief_default_result_aspx&ctl={}&Param=%e5%b9%b4+%3d+%27{}%27'.format(page, query_id, year_id, year)
paging_html = D.get(get_page_url, read_cache=False)
if 'class="GridTableContent"' in paging_html:
root = etree.HTML(paging_html)
art_table = root.xpath('//table[@class="GridTableContent"]')
valid_num = 0
if art_table:
for tr in art_table[0].xpath('./tr'):
art_url = tr.xpath('string(./td[2]/a/@href)')
if art_url:
valid_num += 1
db_name = tr.xpath('string(./td[6])').strip()
dbcode = common.regex_get(art_url, r'DbCode=([A-Z]+)&')
dbname = common.regex_get(art_url, r'DbName=([^&=]+)&')
filename = common.regex_get(art_url, r'FileName=([^&=]+)&')
art_url = 'http://kns.cnki.net/KCMS/detail/detail.aspx?dbcode=' + dbcode + '&dbname=' + dbname + '&filename=' + filename
print 'Found art_url : {}'.format(art_url)
if art_url not in found:
found[art_url] = 1
# 将文章链接, 所属数据库 写入文件
url_writer.writerow([art_url, db_name])
common.logger.info('Total found art_urls {}---{}'.format(valid_num, get_page_url))
else:
#open('invalid_list.html', 'w').write(paging_html)
common.logger.error('Invalid page--{}'.format(get_page_url))
print 'Add task_year = {}, task_start_pagenum = {} into task queue'.format(task_year, page - 1)
tasks.appendleft((task_year, page - 1))
break
# 以下详情页字段提起代码比较简单,在此就不详述了..............................
以上就是本项目主要思路和核心代码,有感兴趣的同学可自行尝试,不懂的地方欢迎留言询问。
领取专属 10元无门槛券
私享最新 技术干货