针对源代码和检查元素不一致的网页爬虫——利用Selenium、PhantomJS、bs4爬取12306的列车途径站信息
整个程序的核心难点在于上次豆瓣爬虫针对的是静态网页,源代码和检查元素内容相同;而在12306的查找搜索过程中,其网页发生变化(出现了查找到的数据),这个过程是动态的,使得我们在审查元素中能一一对应看到的表格数据没有显示在源代码中。这也是这次12306爬虫和上次豆瓣书单爬虫的最大不同点。
选择使用Selenium的PhantomJS模拟浏览器爬取源代码,这样获取到的datas包含了我需要的(查找搜索出的)途径站数据。
暂时把整个程序分为了这几个部分:
(1)提取列车Code和No信息;
(2)找到url规律,根据Code和No变化实现多个网页数据爬取;
(3)使用PhantomJS模拟浏览器爬取源代码;
(4)用bs4解析源代码,获取所需的途径站数据;
(5)用csv库存储获得的数据。
整体使用面向过程的书写方式。
(1)values_get()函数实现了从已有存储了列车信息的csv中逐次提取Code和No。(在这里有点刻意追求面向过程的函数,设置了每次提取都openfile再close。所以使用了tell一次readline完的游标位置,再seek次游标位置到下一次提取位置,实现关闭file后仍然可以接着上一次结束的seek位置继续操作)
(2)olddriver()函数包含PhantomJS和bs4两个部分。利用format来控制多个url,用PhantomJS、driver代替requests爬取网页源代码driver.get(url)。service_args可以配置模拟浏览器(优化加速),set_page_load_timeout()和set_script_timeout()+try except('window.stop()')设置超时(还未用上,存疑),最后用driver.quit()关闭使用完的PhantomJS避免内存爆炸。*这里存在很多优化模拟浏览器的方法,除了上述的配置、超时、quit,还包括在循环外提前打开PhantomJS来实现程序运行时间加速等方法,笔者还未理解透这些方法。这里贴出优化的参考链接:①https://blog.csdn.net/weixin_40284075/article/details/87190040②https://www.jianshu.com/p/8ec70859ae03还有PhantomJS的使用攻略①https://www.cnblogs.com/miqi1992/p/8093958.html②https://www.cnblogs.com/lizm166/p/8360388.html
为什么使用已经被Selenium抛弃的PhantomJS而不使用Headless Chrome?笔者也曾尝试过使用无头chrome,但爬到的源代码仍不包含我所需tbody数据。
丢失数据的源代码长这样(它只有tbody标签,没有标签内的数据)。
而检查元素里可以看到所需数据出现在tbody内:
虽然用PhantomJS确实可以爬取到所需的tbody数据,但是在后来循环url爬取多个列车信息时,可能是因为网站有反爬虫措施,或是PhantomJS的不稳定,导致了经常会出现丢失数据的情况(PhantomJS的作用失效了)。所以我添加了一句if datas==[],递归olddriver()来确保能爬到这班列车的信息。如图,失败率仍然很高。
相关推荐:《Python视频教程》
(3)最后是data_write_csv()写入数据到csv,这里用csv库直接把列表变为了csv文件(列表中的多个列表就是多行数据),以后多尝试用一下csv库,还是很好用的。
(4)在主程序调用各个函数时,要注意global全局变量的使用、函数return参数给其他函数使用。
(5)最后是几点自己的建议和猜想。首先如果12306真的有反爬虫,我们可以尝试像requests一样的伪装(在driver里没刻意伪装)或是换其他的网站来爬取。其次多注意:爬取网页查询搜索数据的方法,网页跳转等(或简易成爬取多个网页数据,如本例)。还有PhantomJS和headless Chrome,按理来说headless Chrome不会出现这样的错误。最后是提升爬虫运行速度的方法,(这次的爬取速度实在太慢了,10条信息平均要3分钟才能成功获得),除了对模拟浏览器的配置和优化,以及代码本身的优化(如file文件一直开着,不提取一次数据就开关file一次),我们是否可以尝试其他的源代码.get(url)获取方式?尝试多线程加速?尝试云服务器?
代码:
import urllib3
#import requests
from selenium import webdriver
from bs4 import BeautifulSoup
import csv
import time
start = time.time()
def values_get():#通过设置游标来实现:从上一次结束的地方继续读取
file = open('Code.csv','r')
global seekloc#全局游标
file.seek(seekloc)#设置游标位置
line = file.readline()
'''
if line == '':
break
'''
if line == '':
seekloc = -1
twovalue = line.strip('n').split(',')#csv转化为list
code, no = twovalue[0], twovalue[1]
seekloc = file.tell()#读取结束时游标的位置
file.close()
return code, no#code是列车号,no是长串
def olddriver():
#下文中将PhantomJS移除循环未果,任选择在循环中打开。
service_args=[]#PhantomJS优化
service_args.append('--load-images=no') ##关闭图片加载
driver = webdriver.PhantomJS(service_args=service_args)
driver.set_page_load_timeout(10) # 设置页面加载超时
driver.set_script_timeout(10) # 设置页面异步js执行超时
url = f'https://kyfw.12306.cn/otn/queryTrainInfo/init?train_no={no}&station_train_code={code}&date=2019-07-16'
try:
driver.get(url)
data = driver.page_source
except:
print('Timeout!')
driver.execute_script('window.stop()')
driver.quit()#这句可让PhantomJS关闭
#return data
#def beauti4soup():
#global data
soup = BeautifulSoup(data,'lxml')
table_datas = soup.find('table',{'id':'queryTable'})
datas = table_datas.findAll('tbody')[1].findAll('tr')
if datas == []:
print('Failed... Restart!')
olddriver()
#beauti4soup()
else:
print("It's OK! ")
midways = []
for data in datas:
midway = data.find('div',{'class':'t-station'}).get_text()#单个列车的信息爬取
midways.append(midway)
answer.append(midways)
return answer
def data_write_csv(file_name,datas):
file_csv = open(file_name,'w+')
#writer = csv.writer(file_csv,delimiter=',',quotechar=' ',quoting=csv.QUOTE_MINIMAL)csv库用法存疑
writer = csv.writer(file_csv)
for data in datas:
writer.writerow(data)
#---主程序开始---#
seekloc = 0#初始化游标
values_get()#运行一次values_get()把csv无用的第一行过滤掉
answer = []#存储所有途径站信息的list
'''
#这里三行尝试将PhantomJS放在循环外,提前开启,减少加载时间。
#结果:运行时间确实大幅减短,但遇到一次failed之后就一直failed。
#参考链接:https://blog.csdn.net/qingwuh/article/details/81583801
service_args=[]#PhantomJS优化
service_args.append('--load-images=no') ##关闭图片加载
driver = webdriver.PhantomJS(service_args=service_args)
'''
j = 1
while True:#循环爬取
code, no = values_get()
if j > 10:#十个一循环的测试
break
#if seekloc == -1:
# break
answer = olddriver()
j += 1
data_write_csv('Route.csv',answer)#存储数据
#---主程序结束---#
end = time.time()
print('Running time: {} Seconds'.format(end-start))
print("=================================")