以前、乗換案内をマウス操作するのが面倒でキーボードで使えるようにする話を書いた。複数の乗換案内を使うと、サイトによって出す経路は案外違うことがわかる。以前書いたYahoo乗換をスクレイピングするコードを拡張して、Navitimeとジョルダンの3つをまとめて検索するようにしてみた。(選定理由は、あちこちの検索を使ってみて、エンジンに違いがあると感じたのがこの3つだったため)

 

乗換案内の出力はどうあるべきか

複数の乗換案内を評価するには、同じ条件を複数のサイトに与えて、結果をまとめると便利だ。このためには、出力を共通化する必要がある。乗換案内の出力には様々な情報があるが、どのように整理するのが望ましいのかを考察してみる(個人の好き嫌いの範疇だが)。

 

まず、もっとも重要な情報は出発時刻、到着時刻だろう。時間が条件を満たさないならほかの要素がどんなに良くても意味がない。次に来るのが(先の2つから計算できる情報ではあるが)所要時間、次いで料金、乗換回数といったあたりか。細かい評価をするためには乗換駅での待ち時間などの情報を出力する必要がある。

 

リストを出力するときには、可変長になる要素はなるべく最後にするのが良い。そうしておけば、途中までは縦に同じ要素を並べて表示できる。

見やすさを考えると、人間向けには下に示すような出力が良いのではないかと思う(機械が扱うなら、発や着といった文字は不要だが、今回は人間が見ることを優先した)。駅名も固定長にしたいところだが、空港などで異様に長い名称が出ることがあり断念した。


Y1, 09:00 発, 11:42 着, T 2:42, C 13870
  ,         , 09:00 発, 東京 ,   JR新幹線のぞみ213号・新大阪行
  , 11:30 着, 11:38 発, 新大阪 ,   JR京都線・宝塚行
  , 11:42 着,         , 大阪 ,
  
Tは所要時間、Cは費用である。Y1はYahooの第1経路を示している。

こうしてテキストで出力をさせると、Webページの出力が肥大化していると感じる。特にNaviTimeは個々の枠がでかい上に広告が挿入されたりしてスクロール量が多い。今まで乗換案内の結果を示すときには、ブラウザの画面をコピーしていたが、Navitimeは大きすぎてコピーできなかったりした。冷静に考えると、画像で示す必然性はない。今後はテキストにしようと思う。

 

プログラミング中の発見、感想

・NavitimeのURLにwspeed=66とあるが、これは「かなりせかせか」を選んだときのパラメータである※1。これを見たとき、歩く速度を好きに指定できるのかと思ったのだが、適当な数字を入れると100(=普通)に置き換えられる。特定のいくつかの数値以外は機能しないようになっている。将来の拡張を考慮してこのようにしているのだろうか。
・Yahooは3件ごとにページが切り替わる。他と比べて1ページ読み込みが増えるため所要時間が長い。少し試してみたが全件出力させるオプションはないようで、不親切だと思う。
・プログラムは余計な文字を取り除く処理がかなりの部分を占めている。元のページがclassなどで構造化されていれば手間は減るのだが。特にNavitimeはclass名がleftなど情報の意味ではなく、表示方法を示していたりして非常に処理しづらかった。また、この手のプログラムは場当たり的になってしまい、サイトの仕様変更があると付き合って修正する必要がある。
・駅名や列車名が異様に長いときがあり、適当なところでカットしたいのだが、適切な切り方が見つからない。特に駅名の愛称のような部分はカットしたい。
・入力された駅名に合致する駅が複数あった場合の動作は悩ましい。ジョルダンは駅名選択の画面に飛ばされてしまうので、一番上の選択肢で検索するようにした。他2つは(勝手に)特定の駅を選択して実行される。
・「発」と「着」という文字を大量に扱うことになるが、発や着を含む駅名(発寒、新発田、御着など)があるので適当に作るとバグの原因になる。
・乗換不要の処理も面倒だった。個人的には乗換不要なのだから、表示も不要だと思う。ジョルダンとNavitimeは路線名が変わるだけでもいちいち乗換不要が登場するのでかなり目障りだ。
 
図 品川→羽田空港間の直通電車に乗る例。上からYahoo,ジョルダン、Navitime。直通しているのだから、京急蒲田を表示する必要はないと思う。
 
・Navitimeはホーム番号の表示が変だ。途中駅では到着ホームは表示しない(例では新大阪で新幹線が何番ホームに着くのかは表示しない)のだが、なぜか最終到着地の到着ホームだけは表示している(迎えに行くためだろうか、例では大阪駅6番ホーム)。しかも表示位置に一貫性がない。この「6番ホーム」を取り除くのはなかなか面倒だった。(Yahoo乗換では全部の列車名の所に発車、到着ホームを記載しており、一貫性がある)

コード

相変わらず、汚いコードだが載せておく。

・メイン関数のyahoo_sw=1 jorudan_sw=1 navitime_sw=1 が使うサイトの指定である。使う場合は1,使わない場合は0にする。

・ChromeのバージョンアップのたびにChromeDriverを更新するのが面倒なので、webdriver_managerを使うようにした。

・ある程度使っているが、ありとあらゆる状況に対応している自信はない。対応漏れはあると思われる。

 

import datetime
import requests
from bs4 import BeautifulSoup
import urllib3
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException

options = webdriver.ChromeOptions()
#ブラウザを表示させる場合、以下をコメントアウト
options.add_argument('--headless')

options.add_argument("-silent")
options.add_argument('log-level=3')


driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)

#yahoo
def yahoo_output(route_st_no):
	transfer_list=soup.find_all(class_='transfer')
	route_count=int(len(transfer_list)/2)
	for route_no in range(route_st_no,route_st_no+route_count):
		routename='route0'+str(route_no)
		one_route=soup.find(id=routename)#1ルート分切り出す
		fare=one_route.find(class_='fare').text
		t_start=fare.find(":")
		t_end=fare.find("円")
		fare=fare[t_start+1:t_end]
		fare=fare.replace(',','')
		time=one_route.find(class_='time').text
		t_start=time.find("発")
		st_time=time[max(0,t_start-5):t_start]
		t_end=time.find("着")
		ed_time=time[t_start+2:t_end]
		t_start=time.find("着")
		t_end=time.find("分")
		time=time[t_start+1:t_end]
		
		#2時間3分→2:03に整形
		time=time.replace('時間',':')
		time=time.replace('分','')
		if(time.find(':')==-1):
			time="0:"+time
		if(time.find(':')==len(time)-1):
			time=time.replace(':',':00')
		if(time.find(':')==len(time)-2):
			time=time.replace(':',':0')			
		
		print("Y"+str(route_no) +", "+str(st_time)+" 発, "+str(ed_time)+" 着, T "+str(time)+", C "+str(fare))
		sta=one_route.find_all(class_='station')
		tr=one_route.find_all(class_='transport')
		st_count=-1
		for i in sta:
			st_count=st_count+1
			st=i.text

			t_start=st.find(']')
			t_end=st.find('\n',t_start+4)
			stname=st[t_start+3:t_end]
			
			dep_time=str(st[7:12])+" 発"
			arr_time=str(st[1:6])+" 着"
			if(st.find('[dep]')>0):
				arr_time="        "
				dep_time=str(st[1:6])+" 発"
			if(st.find('[arr]')>0):
				dep_time="        "
				arr_time=str(st[1:6])+" 着"
				train=""
			else:
				train=tr[st_count].text
				t_start=train.rfind(']')
				t_end=train.find('\n',t_start)
				train=train[t_start+1:t_end]
			if(st.find('[direct]')==-1):
				print("  , "+str(arr_time)+", "+str(dep_time)+", "+str(stname)+" ,   "+str(train))

def jorudan_output():
	for b in range(0,len(time_elements)):
		xx="bR"+str(b+1)
		one_route=soup.find(id=xx)
		time_use=one_route.find(class_="data_total-time")
		
		time=time_use.text[4:]
		time=time.replace('時間',':')
		time=time.replace('分','')
		if(time.find(':')==-1):
			time="0:"+time
		if(time.find(':')==len(time)-1):
			time=time.replace(':',':00')
		if(time.find(':')==len(time)-2):
			time=time.replace(':',':0')

		time_tm=one_route.find(class_="data_tm").text
		time_tm=time_tm.replace('→','')
		time_tm=time_tm.replace('(','')
		time_tm=time_tm.replace(')','')
		time_tm=time_tm.replace('発',' 発,')
		time_tm=time_tm.replace('着',' 着')
		time_tm=time_tm.replace('  ',' ')
		time_tm=time_tm.replace('△','')
		
		#夜行だと日付が出るので削除
		d=time_tm.find("/")
		if(d>-1):
			time_tm=time_tm[:d-2]+time_tm[d+4:]
		d=time_tm.find("/")
		if(d>-1):
			time_tm=time_tm[:d-2]+time_tm[d+4:]
		time_tm=time_tm.replace('  ',' ')
		
		print("J"+str(b+1)+", "+time_tm+", T "+str(time)+", ",end="")
		cost_element=one_route.find(class_="data_total")
		
		fare = cost_element.text[3:]
		fare=fare.replace('\n','')
		fare=fare.replace(',','')
		index_end=fare.find('円')
		print("C "+str(fare[:index_end]))
		stlist=[]
		st_elements=[]

		st_elements=one_route.find_all(class_="nm")
		for k in range(0,len(st_elements)):
			stname=st_elements[k].text
			stname=stname.replace('\n','')
			stlist.append(stname)

		rtime_elements=[]
		rtime_elements=one_route.find_all(class_="tm")
		timelist=[]
		for k in range(0,len(rtime_elements)):
			rt=rtime_elements[k].text
			if(len(rt)>1):
				if(rt.find('乗換')==-1):
					rt=rt.replace('(','')
					rt=rt.replace(')','')
                    sep2=rt.find('-')
					sep=rt.find(':')
                    if(sep2<sep):
                    	timelist.append(' ')
                    sep=rt.find(':')
					if(sep>1):
						timelist.append(rt[sep-2:sep+3])
					sep=rt.find(':',sep+1)
					if(sep>2):
						timelist.append(rt[sep-2:sep+3])
                    sep=rt.rfind(':')
                    if(sep>sep):
                    	timelist.append(' ')
		rtrain_elements=[]
		rtrain_elements=one_route.find_all(class_="rn")
		trainlist=[]
		for k in range(0,len(rtrain_elements)):
			rn=rtrain_elements[k].text[1:]
			rn_end=rn.find('\n')
			if(rn_end>0):
				rn=rn[:rn_end]
			rn=rn.replace('\n','')
			rn_end=rn.find("行)")
			if(rn_end>0):
				rn=rn[:rn_end+2]

			if(len(rn)>-1):
				trainlist.append(rn)
		stlist2=[]
		trainlist2=[]
        timelist2=[]
		for k in range(0,len(stlist)-1):
			if(stlist[k].find('降車不要')==-1):
				stlist2.append(stlist[k])
				trainlist2.append(trainlist[k])
                if(k>0):
					timelist2.append(timelist[k*2-1])
				timelist2.append(timelist[k*2])
		stlist2.append(stlist[k+1])
        timelist2.append(timelist[len(stlist)*2-3])
		#表示
		for k in range(0,len(trainlist2)):
			st="  ,"
			if(k==0):
				st=st+"         ,"
			else:
				st=st+" "+str(timelist2[k*2-1])+" 着,"
			st=st+" "+str(timelist2[k*2])+" 発, "+str(stlist2[k])+" ,     "+str(trainlist2[k])
			print(st)
		st="  , "+str(timelist2[k*2+1])+" 着,         , "+str(stlist2[k+1])
		print(st)

def navtime_output():
	summary=soup.find(class_="summary_list time")
	su=summary.text
	routes=su.count('乗換')
	for b in range(0,routes):
		xx="detail_route_"+str(b)
		one_route=soup.find(id=xx)
		one_route_text=one_route.text
		ttime_start=one_route_text.find('発')
		ttime_end=one_route_text.find('着')
		time_st=one_route_text[ttime_start-5:ttime_start]
		time_ed=one_route_text[ttime_end-5:ttime_end]

		ttime_start=one_route_text.find('所要時間')
		ttime_end=one_route_text.find('分')
		ttime=one_route_text[ttime_start+5:ttime_end]
		ttime=ttime.replace('\t','')
		ttime=ttime.replace('\n','')
		ttime=ttime.replace('時間',':')
		if(ttime.find(':')==-1):
			ttime="0:"+ttime
		if(ttime.find(':')+2==len(ttime)):
			ttime=ttime.replace(':',':0')
		print("N"+str(b+1)+", "+time_st+" 発, "+time_ed+" 着, "+"T "+str(ttime)+", ",end="")
		xx="total-fare-text"+str(b+1)
		cost=one_route.find(id=xx)
		cost_text=cost.text
		cost_text=cost_text.replace(',','')
		print("C "+str(cost_text))
		
		tr_elements=[]
		tr_elements=one_route.find_all(class_="railroad-area")
		for k in range(0,len(tr_elements)):
			trname=tr_elements[k].text
			trname=trname.replace('\t','')
			trname=trname.replace('\n','')
			tr_elements[k]=trname
		st_elements=[]
		st_elements=one_route.find_all(class_="section_station_frame")
		for k in range(0,len(st_elements)):
			stname=st_elements[k].text
			stname=stname.replace('発\n','発X\n')
			stname=stname.replace('着\n','着X\n')
			if(k==len(st_elements)-1):
				platform_elements=one_route.find_all(class_="left platform")
				if(len(platform_elements)>0):
					plat_text=platform_elements[len(platform_elements)-1].text
					stname=stname.replace(plat_text,'')
			stname=stname.replace('\t','')
			stname=stname.replace('混雑予報','Z')
			stname=stname.replace('時刻表','')
			stname=stname.replace('周辺地図','')
			stname=stname.replace('\n','')
			stname=stname.replace('出発X','        , ')
			stname=stname.replace('到着X','        , ')
			stname=stname.replace('着XZ','着')
			stname=stname.replace('Z','')
			stname=stname.replace('着X',' 着, ')
			stname=stname.replace('発X',' 発, ')
			if(k<len(st_elements)-1):
				trname=tr_elements[k]
				if(stname.find('乗換不要')==-1):
					print("  , "+str(stname)+" ,   "+str(trname))
			else:
				print("  , "+str(stname))

#main
yahoo_sw=1
jorudan_sw=1
navitime_sw=1

dt_now = datetime.datetime.now()
year=str(dt_now.year)
month=str(dt_now.month)
if(len(month)==1):
	month="0"+month
day=str(dt_now.day)
if(len(day)==1):
	day="0"+day

start="東京"
goal="大阪"
opt="d"
hh="9"
mm="00"#ここまでデフォルト値
while True:
	s=input("出発地点(終了=q) ["+start+"] ")
	if(s=="q"):
		driver.quit()
		break
	if(s=="q"):
		driver.quit()
		break		
	if(s!=""):
		start=s
	s=input("到着地点 ["+goal+"] ")
	if(s!=""):
		goal=s
	s=input("日付="+year+month+day+"(変更はd)、時 ["+hh+"] ")
	if(s=="d"):
		x=input("日["+day+"] (年月変更はd)")
		if(x=="d"):
			x=input("年["+year+"]")
			if(x!=""):
				year=x
				if(len(year)==2):
					year="20"+year
			x=input("月["+month+"]")
			if(x!=""):
				month=x
				if(len(month)==1):
					month="0"+month
			x=input("日["+day+"]")
		if(x!=""):
			day=x
			if(len(day)==1):
				day="0"+day
		s=input("時 ["+hh+"] ")
	if(s!=""):
		hh=s
	if(len(hh)==1):
		hh="0"+hh
	s=input("分 ["+mm+"] ")
	if(s!=""):
		mm=s
	if(len(mm)==1):
		mm="0"+mm
	s=input("出発=>d 到着=>a ["+opt+"] ")
	if(s!=""):
		opt=s
	print(start,goal,year,month,day,hh,mm,opt)
	if(opt=="a"):
		type="4"
		jtype="1"
		ntype="0"
	if(opt=="d"):
		type="1"
		jtype="0"
		ntype="1"
	if(yahoo_sw==1):
		URL ="https://transit.yahoo.co.jp/search/result?flatlon=&"+"from="+start+"&tlatlon="+"&to="+goal+"&via=&via=&via="+"&y="+year+"&m="+month+"&d="+day+"&hh="+hh+"&m1="+mm[0:1]+"&m2="+mm[1:2]+"&type="+ type+"&ticket=ic"+"&al=1"+"&shin=1"+"&ex=1"+"&hb=1"+"&lb=1"+"&sr=1"+"&s=0"+"&expkind=1"+"&ws=1"
		# BeautifulSoup オブジェクトを作る

		driver.get(URL)
		soup = BeautifulSoup(driver.page_source, "html5lib")
		yahoo_output(1)#初めの3件

		try:
			element=driver.find_element_by_link_text('次の3件')
		except NoSuchElementException:
			#print("NoSuchElementException")
			exit
		else:
			URL = element.get_attribute("href")
			driver.get(URL)
			soup = BeautifulSoup(driver.page_source, "html5lib")
			yahoo_output(4)

	if(jorudan_sw==1):
		URL ="https://www.jorudan.co.jp/norikae/cgi/nori.cgi?eki1="+start+"&eki2="+goal+"&eki3=&via_on=1&Dym="+year+month+"&Ddd="+day+"&Dhh="+hh+"&Dmn1="+mm[0:1]+"&Dmn2="+mm[1:2]+"&Cway="+jtype+"&Clate=1&Cfp=1&Czu=2&C7=1&C2=0&C3=0&C1=0&C4=1&C6=1&S=検索"
		driver.get(URL)
		time_elements=[]
		time_elements=driver.find_elements_by_class_name("data_total-time")
		if(len(time_elements)==0):
			#駅名が複数あって確定できない場合、一番上の選択肢で実行
			print("駅名確定不可")
			element=driver.find_element_by_name("Sok")
			element.click()
			driver.implicitly_wait(3) # seconds
			time_elements=driver.find_elements_by_class_name("data_total-time")
		
		soup = BeautifulSoup(driver.page_source, "html5lib")
		jorudan_output()

	if(navitime_sw==1):
		URL ="https://www.navitime.co.jp/transfer/searchlist?orvStationName="+start+"&dnvStationName="+goal+"&thrStationName1=&thrStationCode1=&thrStationName2=&thrStationCode2=&thrStationName3=&thrStationCode3=&month="+year+"%2F"+month+"&day="+day+"&hour="+hh+"&minute="+mm+"&basis="+ntype+"&from=view.transfer.searchlist&sort=0&wspeed=83&airplane=1&sprexprs=1&utrexprs=1&othexprs=1&mtrplbus=1&intercitybus=1&ferry=1&accidentRailCode=&accidentRailName=&isrec="
		driver.get(URL)
		soup = BeautifulSoup(driver.page_source, "html5lib")
		navtime_output()



 

※1 自分専用に作っているので、歩行速度は最も速いものを選んである。

 

21/3/1 追記 ジョルダンで、路線名が変わる駅を通過する場合に対応できていなかったため、コードを修正した。例を下の図に示す。この例だと西宮北口を通過するため時間が表示されない。このため出力が乱れていた。