【Python】實現12306余票監控


前言

由於經常會遇到沒票等現象,所以需要使用軟件進行搶票,但由於一些軟件有優先級問題,對於不舍得花錢的我來講,並不是很好的體驗,所以想自己嘗試寫一個類似的功能。

 

環境/插件

  • Python3.6
  • urllib(用於請求數據)
  • smtplib(發送郵件)

 

文件目錄

GrabTicket文件夾
  • city.py(12306城市)
  • GrabTicket.py(入口文件)
  • GrabTicketOperation.py(主文件)
  • GrabTicketSmtp.py(發送郵件文件)

 

分析

首先我們打開12306余票查詢窗口
 
上圖紅色框的地方,就是表示列車有無車票的地方,我們需要根據這里邊的數據來判斷。
這里邊有一些需要注意的就是,里邊表示有票的有字符串“有”和數字“2”,所以我們需要對這兩種情況進行判斷。
接下來我們使用瀏覽器的開發者工具,來檢查看看是否有接口可以使用:
這里我們可以看出,12306的列車查詢是使用接口調用的,我們再來看看接口返回的數據:
 
我們可以清晰的看到,我們需要的列車數據是在data里邊的result里邊的數組,所以后面,我們只需要獲取這里邊的數據來判斷就可以了。
 

分析數據

首先,我們得先分析數據,得出我們需要的數據字段,所以我們先寫一段程序用來分析:
# 復制接口數據result里邊的一條數據出來分析
results = ["null|23:00-06:00系統維護時間|6i000D312606|D3126|IOQ|NJH|IOQ|AOH|07:00|18:43|11:43|IS_TIME_NOT_BUY|qYF9CwzWBb4rPwv7Upcl6nOKai0yleG2FqmgmU4EFKXjmLhu|20180721|3|Q6|01|28|1|0|||||||有||||有|無|||O0M0O0|OMO|0"]
 
# 初始化數組鍵值
c = 0

# 對結果集進行循環
for i in results:
        # 將數據拆分成新的數組,並進行循環
	for n in i.split('|'):
                # 輸出數組中每一個的數據n,以及下標值c
		print('[%s] %s' %( c,n ))
                # 下標值+1
		c += 1
        # 重置下標值c
	c = 0
        # 多個數據換行
	print('\n\t')

運行代碼,我們來看看效果圖:

測試多幾次之后,我們可以得出我們需要的數據所在的位置,接下來我們修改下程序進行輸出:

# 復制接口數據result里邊的一條數據出來分析
results = ["null|23:00-06:00系統維護時間|6i000D312606|D3126|IOQ|NJH|IOQ|AOH|07:00|18:43|11:43|IS_TIME_NOT_BUY|qYF9CwzWBb4rPwv7Upcl6nOKai0yleG2FqmgmU4EFKXjmLhu|20180721|3|Q6|01|28|1|0|||||||有||||有|無|||O0M0O0|OMO|0"]
j = 1

# 初始化數組下標值
c = 0

# 初始化列車數組的下標值
index = 0

# 初始化列車數組
trains = []

# 對結果集進行循環
for i in results:

	# 為列車數組新增一個空數組元素
	trains.append([])

	# 將數據拆分成新的數組,並進行循環
	for n in i.split('|'):

		# 輸出數組中每一個的數據n,以及下標值c
		print('[%s] %s' %( c,n ))

		# 將每一個數據依次放入到列車數組中
		trains[index].append(n)

		# 下標值+1
		c += 1

	# 重置下標值c
	c = 0

	# 多個數據換行
	print('\n\t')

	# 列車數組下標值+1
	index += 1

# 對處理好的列車數組進行循環遍歷
for train in trains:
	
	# 打印我們所需要的數據
	print('火車:%s' %(train[3]))
	print('出發地:%s' %(train[6]))
	print('目的地:%s' %(train[7]))
	print('發車時間:%s' %(train[8]))
	print('到達時間:%s' %(train[9]))
	print('歷時時間:%s' %(train[10]))
	print('商務座/特等座:%s' %(train[32]))
	print('一等座:%s' %(train[31]))
	print('二等座:%s' %(train[30]))
	print('高級軟卧:%s' %(train[21]))
	print('軟卧:%s' %(train[23]))
	print('硬卧:%s' %(train[28]))
	print('硬座:%s' %(train[29]))
	print('無座:%s' %(train[26]))
	print('\n\t')

運行,我們來看看結果:

這里邊就是我們的結果集了,我們去12306頁面對照一下:

看,我們的數據能對的上,證明我們已經分析對了數據,接下來,我們就可以實現我們的爬蟲代碼了:
from urllib import request
import ssl
import json

# 通過爬蟲爬取數據
def getTrains():

	# 請求地址
	url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-07-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=SHH&purpose_codes=ADULT'
	# 請求頭
	headers = {
		'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'
	}
	# 設置請求
	req = request.Request(url, headers=headers)
	# 發送請求
	html = request.urlopen(req).read().decode('utf-8')

	# 格式化數據
	dict = json.loads(html) 
	# 獲取想要的數據
	result= dict['data']['result']

	return result

# 復制接口數據result里邊的一條數據出來分析
results = getTrains()

# 初始化數組下標值
c = 0

# 初始化列車數組的下標值
index = 0

# 初始化列車數組
trains = []

# 對結果集進行循環
for i in results:

	# 為列車數組新增一個空數組元素
	trains.append([])

	# 將數據拆分成新的數組,並進行循環
	for n in i.split('|'):

		# 將每一個數據依次放入到列車數組中
		trains[index].append(n)

		# 下標值+1
		c += 1

	# 重置下標值c
	c = 0

	# 列車數組下標值+1
	index += 1

# 對處理好的列車數組進行循環遍歷
for train in trains:

	# 打印我們所需要的數據
	print('火車:%s' %(train[3]))
	print('出發地:%s' %(train[6]))
	print('目的地:%s' %(train[7]))
	print('發車時間:%s' %(train[8]))
	print('到達時間:%s' %(train[9]))
	print('歷時時間:%s' %(train[10]))
	print('商務座/特等座:%s' %(train[32]))
	print('一等座:%s' %(train[31]))
	print('二等座:%s' %(train[30]))
	print('高級軟卧:%s' %(train[21]))
	print('軟卧:%s' %(train[23]))
	print('硬卧:%s' %(train[28]))
	print('硬座:%s' %(train[29]))
	print('無座:%s' %(train[26]))
	print('\n\t')

我們運行一下,看看結果:

這樣,我們就能得到我們每一輛列車的數據了。
 

分析URL地址

第一步,我們已經分析出了我們的數據,現在我們開始寫爬蟲,再寫之前,我們還需要分析一下12306列車接口URL的規律,這樣才方便我們組合URL,查詢不同城市、時間點的列車數據:
 
# 12306接口地址
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-07-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=SHH&purpose_codes=ADULT
 
這里我們主要注意以下幾個參數的用途:
  • leftTicketDTO.train_date:出發時間
  • leftTicketDTO.from_station:出發地
  • leftTicketDTO.to_station:目的地

 

獲取城市json文件

我們同時還注意到,我們的城市給轉換成了大寫的英文字母,這是12306自己的轉換機制,所以我們需要試着來找一下12306是否有保存城市的json文件:

這樣,我們就找到了12306的城市json文件了,我們可以將它保存下來,現在我們就准備就緒了。
 

根據城市名,獲取城市代號

用戶在輸入了城市之后,我們需要獲取到用戶的城市代號,從上例的代碼中,我們可以看到我們引入了一個city.py的文件,我就是將城市處理放在這個文件里邊進行的:

# -*- coding: UTF-8 -*-
# @Time             : 2018/04/04 14:58
# @Author           : 小羅
# @File             : city.py
# @Software         : PyCharm
# @Python Version           : 3.6
# @About            : 12306城市文件
 
 
# 獲取城市列表
def getCitys(city):
    # 城市數據(城市數據過多,這里只顯示一部分,請自行去12306處獲取完整的城市數據)
    favorite_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2@bjn|北京南|VNP|beijingnan|bjn|3@bjx|北京西|BXP|beijingxi|bjx|4@gzn|廣州南|IZQ|guangzhounan|gzn|5@cqb|重慶北|CUW|chongqingbei|cqb|6@cqi|重慶|CQW|chongqing|cq|7@cqn|重慶南|CRW|chongqingnan|cqn|8@cqx|重慶西|CXW|chongqingxi|cqx|9@gzd|廣州東|GGQ|guangzhoudong|gzd|10';
    # 遍歷所有城市
    for i in favorite_names.split( '@'):
        if i:
            tmp = i.split( '|')
            if city == tmp[1]:
                return tmp[2]
 
    return False

這樣,我們就可以通過調用city.getCitys()方法,傳入我們輸入的城市名稱,就可以准確的獲取我們的城市代號了。

 

實現用戶輸入

首先,我們需要讓用戶輸入自己需要查詢的出發地、目的地、出發時間,並對這些數據進行判斷是否合格:


import GrabTicketOperation import ssl import city import time import re from datetime import datetime # 關閉ssh證書驗證 ssl._create_default_https_context = ssl._create_unverified_context # 輸入城市 def cityStation(ntype = 1): # 換行 print('\n\t') # 初始化提示語 passtext = '' # 判斷是出發地還是目的地 if (ntype == 1): passtext = '出發地' else: passtext = '目的地' # 開始無限循環,保證用戶輸對為止 while(1): # 獲取用戶輸入的數據 city_station = input('請輸入%s:' %( passtext )) # 檢查輸入的城市 city_stations = city.getCitys(city_station) # 判斷輸入是否正確 if (city_stations == False): # 不正確,提示,並且重新輸入 print('找不到 %s 這個城市' %(city_station)) else: # 輸入正確,跳出循環 break # 返回正確的城市編號 return city_stations # 驗證時間 def timeStation(): # 換行 print('\n\t') # 開始無限循環,保證用戶輸對為止 while(1): # 獲取用戶輸入的數據 setOutTime = input('請輸入出發時間(例:2018-04-04):') # 判斷時間格式是否正確 if (checkTimeFormat(setOutTime) == False): print('請輸入正確的時間格式,如:2018-04-04') else: # 將用戶輸入的日期轉化為時間戳 timeArray = time.strptime(setOutTime, "%Y-%m-%d") # 轉換為時間戳: timeStamp = int(time.mktime(timeArray)) # 獲取當前時間的時間戳 nowtime = time.time() # 獲取當天0點的時間戳 nowtimeStamp = int(nowtime - nowtime % 86400 - 28800) # 判斷時間大小 if (timeStamp < nowtimeStamp): print('出發日期不能小於當前時間') else: break # 返回正確的城市編號 return setOutTime def checkTimeFormat(setOutTime): # 判斷日期格式 date_text = re.search(r"(\d{4}-\d{2}-\d{2})",setOutTime) # 判斷時間格式是否正確 try: if date_text == None: return False date_text = date_text.group(0) if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'): return False else: return True except ValueError: return False # 出發地 from_station = cityStation(1) # 目的地 to_station = cityStation(2) # 出發時間 setOutTime = timeStation() # 實例化類(后面需要編寫這個類) grabTicket = GrabTicketOperation.GrabTicket(from_station, to_station, setOutTime) # 輸入數據 grabTicket.callQueryTrains()

我們運行這個程序(需要將GrabTicketOperation.py,這個類的引入和使用給注釋掉,后期會增加這個類),來看看結果:

 

編寫操作類

接下來就是編寫我們的操作類了,這也是主要的文件
# -*- coding: UTF-8 -*-
# @Time 			: 2018/04/04 14:58
# @Author			: 小羅
# @File 			: GrabTicketOperation.py
# @Software			: PyCharm
# @Python Version	: 3.6
# @About 			: 12306搶票操作類

from splinter.browser import Browser
import urllib
from urllib import request
import ssl
import city
import json
from GrabTicketSmtp import GrabTicketSmtp

class GrabTicket:

	# 出發地
	from_station = ''

	# 目的地
	to_station = ''

	# 出發時間
	setOutTime = ''

	# 123056列車請求路徑
	durl = 'https://kyfw.12306.cn/otn/leftTicket/query?'

	# 構造函數
	# @string from_station 出發地
	# @string to_station 目的地
	# @string time 時間,如:2018-04-04
	def __init__(self, from_station, to_station, setOutTime):

		# 出發城市
		self.from_station = from_station

		# 目的地城市
		self.to_station = to_station

		# 出發時間
		self.setOutTime = setOutTime

	# 拼接URL地址
	def getSplicingUrl(self):

		url = self.durl + 'leftTicketDTO.train_date=' + urllib.parse.quote(self.setOutTime) + '&leftTicketDTO.from_station=' + urllib.parse.quote(self.from_station) + '&leftTicketDTO.to_station=' + urllib.parse.quote(self.to_station) + '&purpose_codes=ADULT'

		return url

	# 抓取數據
	def curlTrainsInfo(self):
		
		# 獲取鏈接
		url = self.getSplicingUrl()
		# 請求頭
		headers = {
			# 'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'
			'User-Agent': r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'
		}
		# 設置請求
		req = request.Request(url, headers=headers)
		# 發送請求
		html = request.urlopen(req).read().decode('utf-8')

		# 格式化數據
		dicts = json.loads(html)
		# 獲取想要的數據
		result = dicts['data']['result']

		return result

	# 處理數據
	def handleResultDatas(self, datas):
		j = 1
		c = 0
		index = 0
		trains = []
		for i in datas:
			trains.append([])
			for n in i.split('|'):
				#print('[%s] %s' %( c,n ))
				trains[index].append(n)
				c += 1
			c = 0
			#print('\n\t')
			j += 1
			index += 1

		return trains

	# 判斷是否大於0
	def isCheckValueInt(self, value):

		value = int(value)

		if value > 0:
			# 內容
			self.intTrain = True
			return True

		return False

	# 將數據轉化為整形
	def StringTurnInt(self, train):

		if train[32].isdigit():
			if self.isCheckValueInt(train[32]):
				return True

		if train[31].isdigit():
			if self.isCheckValueInt(train[31]):
				return True

		if train[30].isdigit():
			if self.isCheckValueInt(train[30]):
				return True

		if train[21].isdigit():
			if self.isCheckValueInt(train[21]):
				return True

		if train[23].isdigit():
			if self.isCheckValueInt(train[23]):
				return True

		if train[28].isdigit():
			if self.isCheckValueInt(train[28]):
				return True
	
		if train[29].isdigit():
			if self.isCheckValueInt(train[29]):
				return True

		if train[26].isdigit():
			if self.isCheckValueInt(train[26]):
				return True

	# 輸出數據
	def outputResults(self, trains):

		content = ''

		# 初始化序號
		num = 1

		for train in trains:

			self.isIntTrain = False

			self.StringTurnInt(train)

			if train[32] == '有' or train[31] == '有' or train[30] == '有' or train[21] == '有' or train[23] == '有' or train[28] == '有' or train[29] == '有' or train[26] == '有' or self.isIntTrain == True:

				self.traincontents = []
				# 內容前綴
				traincontent_prefixs = [
					'<tr>',
					'<td>' + str(num) + '</td>',
					'<td>' + train[3] + '</td>',
					'<td>' + train[6] + '</td>',
					'<td>' + train[7] + '</td>',
					'<td>' + train[8] + '</td>',
					'<td>' + train[9] + '</td>',
					'<td>' + train[10] + '</td>'
				]
				traincontent_prefix = ''.join(traincontent_prefixs)
				# 內容后綴
				traincontent_suffix = '</tr>'

				num = num + 1

				# 商務座
				self.getIsStandbyTicket(train[32])

				# 一等座
				self.getIsStandbyTicket(train[31])

				# 二等座
				self.getIsStandbyTicket(train[30])

				# 高級軟卧:
				self.getIsStandbyTicket(train[21])

				# 軟卧:
				self.getIsStandbyTicket(train[23])

				# 硬卧:
				self.getIsStandbyTicket(train[28])

				# 硬座:
				self.getIsStandbyTicket(train[29])

				# 無座:
				self.getIsStandbyTicket(train[26])

				traincontent = ''.join(self.traincontents)

				content = content + traincontent_prefix + traincontent + traincontent_suffix

		if content == '':

			return False

		return content

	# 獲取是否有無余票
	def getIsStandbyTicket(self, value):

		if value.isdigit():
			# 內容
			self.traincontents.append('<td style="color: #26a306;font-weight: 400;">' + value + '</td>')
		elif value == '有':
			# 內容
			self.traincontents.append('<td style="color: #26a306;font-weight: 400;">有</td>')
		else:
			self.traincontents.append('<td>無</td>')

	# 獲取郵件內容 - 標題
	def getEmailContentTitle(self):

		emailTitle = '<tr><th colspan="30">12306余票監控</th></tr>'

		return emailTitle

	# 獲取郵件內容 - 列表標題
	def getEmailContentListTitle(self):

		emaillistTitles = [
			'<tr>',
			'<td>序號</td>',
			'<td>列車</td>',
			'<td>出發地</td>',
			'<td>目的地</td>',
			'<td>發車時間</td>',
			'<td>到達時間</td>',
			'<td>歷時時間</td>',
			'<td>商務座/特等座</td>',
			'<td>一等座</td>',
			'<td>二等座</td>',
			'<td>高級軟卧</td>',
			'<td>軟卧</td>',
			'<td>硬卧</td>',
			'<td>硬座</td>',
			'<td>無座</td>',
			'</tr>'
		]

		return ''.join(emaillistTitles)

	# 發送郵件
	def sentEmail(self, trains):

		# 獲取標題
		emailTitle = self.getEmailContentTitle()

		# 獲取列表標題
		emaillistTitle = self.getEmailContentListTitle()

		# 獲取列表內容
		emailListContent = self.outputResults(trains)

		if emailListContent == False:

			return False

		# 拼接數據
		emailContents = [
			'<table>',
			'<thead>',
			emailTitle,
			emaillistTitle,
			'</thead>',
			'<tbody>',
			emailListContent,
			'</tbody>',
			'</table>'
		]

		emailContent = ''.join(emailContents)

		# 實例化郵件類
		grabTicket = GrabTicketSmtp('a710292863@qq.com', emailContent)
		# 輸入數據
		grabTicket.sendEmail()

		return True


	# 調用函數
	def callQueryTrains(self):
		
		# 抓取數據
		result = self.curlTrainsInfo()

		# 處理數據
		trains = self.handleResultDatas(result)

		# 輸出數據
		self.sentEmail(trains)

 

編寫郵件發送類

# -*- coding: UTF-8 -*-
# @Time 			: 2018/04/04 14:58
# @Author			: 小羅
# @File 			: GrabTicketSmtp.py
# @Software			: PyCharm
# @Python Version	: 3.6
# @About 			: 發送郵件類

import smtplib  
from email.header import Header  
from email.mime.text import MIMEText

class GrabTicketSmtp:

    # SMTP服務器
    mail_host = "smtp.163.com"

    # 用戶名
    mail_user = "kafeiwudeshaonian@163.com"

    # 授權密碼,非登錄密碼
    mail_pass = ""

    # 發件人郵箱(最好寫全, 不然會失敗)  
    sender = "kafeiwudeshaonian@163.com"

    # 郵箱標題
    title = '好消息!列車有余票呀!'

    # 構造函數
    # @string receivers 收件人
    # @string content 郵箱內容
    def __init__(self, receivers, content):

        # 郵件內容
        self.content = content

        # 收件人
        self.receivers = [receivers]

    # 獲取郵箱標題
    def getTitle(self):

        return self.title
  
    # 發送郵件
    def sendEmail(self): 

        print(self.mail_host)
        print(self.mail_user)
        print(self.mail_pass)
        print(self.sender)
        print(self.title)
        print(self.receivers)

        # 內容, 格式, 編碼
        message = MIMEText(self.content, 'html', 'utf-8') 
        message['From'] = "{}".format(self.sender)  
        message['To'] = ",".join(self.receivers)  
        message['Subject'] = self.getTitle()  
      
        try: 
            # 啟用SSL發信, 端口一般是465
            smtpObj = smtplib.SMTP_SSL(self.mail_host, 465)  
            # 登錄驗證
            smtpObj.login(self.mail_user, self.mail_pass)
            # 發送 
            smtpObj.sendmail(self.sender, self.receivers, message.as_string())
            # 發送成功
            print("發送成功")  
        except smtplib.SMTPException as e: 
            # 發送失敗
            print(e)   
  
if __name__ == '__main__':  
    sendEmail()

 

完善調用類

import GrabTicketOperation
import ssl
import city
import time
import re
from datetime import datetime

# 關閉ssh證書驗證
ssl._create_default_https_context = ssl._create_unverified_context 

# 輸入城市
def cityStation(ntype = 1):
	# 初始化提示語
	passtext = ''
	# 判斷是出發地還是目的地
	if (ntype == 1):
		passtext = '出發地'
	else:
		passtext = '目的地'
	# 開始無限循環,保證用戶輸對為止
	while(1):
		# 獲取用戶輸入的數據
		city_station = input('請輸入%s:' %( passtext ))
		# 檢查輸入的城市
		city_stations = city.getCitys(city_station)
		# 判斷輸入是否正確
		if (city_stations == False):
			# 不正確,提示,並且重新輸入
			print('找不到 %s 這個城市' %(city_station))
		else:
			# 輸入正確,跳出循環
			break
	# 返回正確的城市編號
	return city_stations

# 驗證時間
def timeStation():

	# 開始無限循環,保證用戶輸對為止
	while(1):
		# 獲取用戶輸入的數據
		setOutTime = input('請輸入出發時間(例:2018-04-04):')
		# 判斷時間格式是否正確
		if (checkTimeFormat(setOutTime) == False):
			print('請輸入正確的時間格式,如:2018-04-04')
		else:
			# 將用戶輸入的日期轉化為時間戳
			timeArray = time.strptime(setOutTime, "%Y-%m-%d")
		    # 轉換為時間戳:
			timeStamp = int(time.mktime(timeArray))
			# 獲取當前時間的時間戳
			nowtime = time.time()
			# 獲取當天0點的時間戳
			nowtimeStamp = int(nowtime - nowtime % 86400 - 28800)
			# 判斷時間大小
			if (timeStamp < nowtimeStamp):
				print('出發日期不能小於當前時間')
			else:
				break

	# 返回正確的城市編號
	return setOutTime

def checkTimeFormat(setOutTime):
	# 判斷日期格式
	date_text = re.search(r"(\d{4}-\d{2}-\d{2})",setOutTime)
	# 判斷時間格式是否正確
	try:

		if date_text == None:
			return False

		date_text = date_text.group(0)

		if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'):
			return False
		else:
			return True

	except ValueError:
		return False



# 出發地
from_station = cityStation(1)
# 目的地
to_station = cityStation(2)
# 出發時間
setOutTime = timeStation()
# 實例化類
grabTicket = GrabTicketOperation.GrabTicket(from_station, to_station, setOutTime)
# 輸入數據
grabTicket.callQueryTrains()

  

執行程序

接下來我們執行調用類文件,來看看我們的結果:

這里邊顯示我們的郵件已經發送成功,接下來我們來看看我們的郵件:

我們再來對照一下12306的數據,看是否一致:

這樣,我們就能簡單的實現12306的余票監控了。

 

結語

  • 此程序有許多改進的地方,大家可以根據自己的情況去完善;
  • 后期還會有更多的文章供大家參考、討論,讓小編跟大家一起學習、進步;
  • 此文章如需轉載,請注明出處:https://www.cnblogs.com/kafeixiaoluo/p/9329500.html。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM