java爬虫12306,爬取所有的站点和车次,并导入postgreSQL数据库
https://github.com/lxd7788/Train 代码地址
准备
安装postgreSQL数据库,和可视化工具pgadmin3,或者其他数据库
实现功能,抓取12306全部的站点,并实现通过站点查询出所有经过次站点的车次,通过车次查出次列车经过的城市
分析
分析12306,找合适的接口,最符合要求的是查询车次的这张页面,但是有验证码,无形增加了难度
经过分析,合适的页面是车票预订的页面,查询两个站点直接的车次,用火狐自带的f12工具,点击查询清晰的看到只有一条get请求
再看响应的内容,json,根据经验这是我们想要东西
通过这条链接,我们可以得到两站点之间的车次信息,我们只需要车次的名称就好了,通过字符串或者正则都可以,正则不太熟,我用的是字符串
分析怎么才能把全国的站点和车次都抓取到,并且实现彼此查询的功能?
站点和城市多对多关系,理应建立三张表,用中间表关联.最后放弃了三表的想法,使用一张表联合主键实现
只要获取到全国的城市站点,通过for循环两两测试,不就可以得到全部的火车车次了,并且两列都是主键,同时还解决了两个城市之间车次重复的问题,
1 CREATE TABLE public.t_city 2 ( 3 city_name character varying(64) NOT NULL, 4 train_num character varying(64) NOT NULL, 5 CONSTRAINT t_city_pkey PRIMARY KEY (city_name, train_num) 6 )
下一步要找到全国的火车站点
从页面的一条js中,找到一条连接 https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9049
一条json数据
接下来,开始写程序,解析数据
数据库链接(换了台电脑改成了mysql数据库
public class DbUtil { private static final String DBDRIVER = "com.mysql.jdbc.Driver"; private static final String url = "jdbc:mysql://127.0.0.1:3306/lianxi"; private static final String USER = "root"; private static final String PASSWORD = "123"; public Connection getConn() throws ClassNotFoundException, SQLException { Class.forName(DBDRIVER); Connection conn = DriverManager.getConnection(url, USER, PASSWORD); return conn; } public void closeConn(Connection conn) throws SQLException { if (conn != null) { conn.close(); } } public static void main(String args[]) { DbUtil dbUtil = new DbUtil(); Connection conn=null; try { conn = dbUtil.getConn(); System.out.println(conn + "数据库连接成功"); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); System.out.println("失败"); }finally { try { dbUtil.closeConn(conn); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
数据库用sqlyog直接建好表
创建一个CityMd类
public class CityMd { private String cityName; private String trainName 、、、、、、
创建方法,实现数据库数据添加
public class DbChange { public int add(Connection conn,CityMd cityMd) { String sql="insert into t_city values(?,?,null)"; int n=0; try { PreparedStatement p=conn.prepareStatement(sql); p.setString(1, cityMd.getCityName()); p.setString(2, cityMd.getTrainName()); n=p.executeUpdate(); p.close(); } catch (SQLException e) { // TODO Auto-generated catch block //e.printStackTrace(); }finally { try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return n; } }
抓取全部的站点,返回一个二维数组,通过之前的url,发现参数有出发地和目的地,并且是字母编号的形式,所以把城市和编号同时抓取下来
分析json数据,每个成熟以@分隔,其次又以|分隔,所以可以用字符串分隔,正则很方便,不过熟悉没用
public class CityUtil { public String[][] getCity() throws Exception{ String cityurl="https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9048"; HttpGet httpget=new HttpGet(cityurl); CloseableHttpClient httPclient=HttpClients.createDefault(); CloseableHttpResponse Response=httPclient.execute(httpget); HttpEntity entity=Response.getEntity(); String result=EntityUtils.toString(entity,"utf-8"); //System.out.println("请求结果"+result); int l1=result.indexOf("'"); int l2=result.length(); String city=result.substring(l1+1, l2); String[] c=city.split("@"); //导入二维数组 int l=c.length-1; String[][] str=new String[l][2]; for(int i=1;i<c.length;i++) { String[] cc=c[i].split("[|]"); //System.out.println(cc[1]+" "+cc[2]); str[i-1][0]=cc[1]; str[i-1][1]=cc[2]; } return str; } }
这样就得到了一个全部站点的数组
接下来写两地间车次的方法,两地之间肯定会有很多火车,所以返回数组
public class GetUtil { public String[] getList(String url) {
CloseableHttpClient httPclient=HttpClients.createDefault();
HttpGet httpgett=new HttpGet(url); CloseableHttpResponse Response; String result1=null; try { Response = httPclient.execute(httpgett); HttpEntity entity=Response.getEntity(); result1=EntityUtils.toString(entity,"utf-8"); //System.out.println("请求结果"+result1); } catch (ClientProtocolException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } JSONObject jSONObject =JSONObject.fromObject(result1); Object listObject=jSONObject.get("data"); jSONObject=JSONObject.fromObject(listObject); JSONArray json=jSONObject.getJSONArray("result"); //存放火车列次的数组 String[] strs=new String[json.size()]; for (int i = 0; i < json.size(); i++) { String str=json.getString(i); String[] arr=str.split("[|]"); strs[i]=arr[3]; } return strs; } }
剩下的就是,整合起来,开始测试
get请求的url参数有四个,第一个是时间,第二个是出发点,第三个是目的地,最后一个成人票这个不是关键直接写死
时间也写死,但是有一个问题就是,每天的车次肯定会有差异,这样只看一看的车次数据肯定不精准。想一个办法就是,跑完数据之后,换个时间再抓一次,反正重复的自己会跳过去、、、、、、、
一定注意要加时间间隔,开始跑没有加,没一会12306就给限制请求了
GetUtil getUtil=new GetUtil(); //两地之间的车次 DbUtil dbUtil=new DbUtil(); //获取conn Zhuanhua zh=new Zhuanhua(); //集合转数组 String trainurl="https://kyfw.12306.cn/otn/leftTicket/queryO?"; String train_date="leftTicketDTO.train_date=2018-03-20"; String from_station=null; String to_station=null; String newurl=null; //获取conn //Connection conn=dbUtil.getConn(); DbChange db=new DbChange(); //获取全部城市和它代号 CityUtil cu=new CityUtil(); String[][] str=cu.getCity(); //循环所有情况 int count=0; Connection conn=null; for(int i=10; i<str.length; i++) { Thread.sleep(1000); for(int j=0; j<str.length; j++) { Random r = new Random(); int nnn=r.nextInt(6); Thread.sleep(nnn*2000); //拼接链接请求链接 from_station=str[i][1]; to_station=str[j][1]; //排除出发和目的是一个 if(from_station.equals(to_station)) { continue; } newurl=trainurl+train_date+"&leftTicketDTO.from_station=" +from_station+"&leftTicketDTO.to_station="+to_station+"&purpose_codes=ADULT"; //调用方法,获取两地之间的火车数组 String[] ss=getUtil.getList(newurl); for(int k=0;k<ss.length;k++) { CityMd cityMd=new CityMd(str[i][0],ss[k]); conn=dbUtil.getConn(); int nn=db.add(conn, cityMd); System.out.println("运行第"+k+"次出发地:"+str[i][0]+"==>目的地:"+str[j][0]); count+=nn; } } } System.out.println("共计导入数据"+count);
最终数据库实现