Skip to content

Commit 1e8b390

Browse files
authored
Merge pull request #9 from twin-te/new-kdb
新KDB対応
2 parents 4fa2f47 + f1d5fac commit 1e8b390

File tree

8 files changed

+402
-186
lines changed

8 files changed

+402
-186
lines changed

README.md

+11-20
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ Twinte内部で利用するために開発された
99
## KdBDownloader
1010
KDBからCSVファイルをダウンロードする。
1111

12-
js例
13-
```js
14-
const csv = await require('twinte-parser').downloadKDB()
15-
```
16-
ts例
1712
```typescript
1813
import { donwloadKDB } from 'twinte-parser'
1914

@@ -23,18 +18,12 @@ const csv = await downloadKDB()
2318
## Parser
2419
KdBから取得したcsvをオブジェクトに変換する。
2520

26-
js例
27-
```js
28-
const classes = require('twinte-parser').parseKDB(csv)
29-
```
30-
31-
ts例
3221
```typescript
3322
import parseKDB from 'twinte-parser'
3423
// or
3524
import { parseKDB } from 'twinte-parser'
3625

37-
const classes = parseKDB(csv)
26+
const courses = parseKDB(csv)
3827
```
3928

4029
##
@@ -70,35 +59,37 @@ enum Day {
7059

7160
```
7261

73-
### Lecture
62+
### Course
7463
```typescript
75-
interface Lecture {
76-
lectureCode: string
64+
interface Course {
65+
code: string
7766
name: string
7867
credits: number
7968
overview: string
8069
remarks: string
8170
type: number
82-
year: number[]
83-
details: {
71+
recommendedGrade: number[]
72+
schedules: {
8473
module: Module
8574
day: Day
8675
period: number
8776
room: string
8877
}[]
8978
instructor: string
79+
lastUpdate: Date
80+
error: boolean
9081
}
9182
```
9283

93-
ここで`details`が配列になっていることに注意。
84+
ここで`schedules`が配列になっていることに注意。
9485

9586
例えば春AB・月曜1限・3A201の授業があった場合、
9687

9788
```typescript
9889
{
99-
id: 'XXXXXXX',
90+
code: 'XXXXXXX',
10091
name: "名前",
101-
details: [
92+
schedules: [
10293
{
10394
module: "春A",
10495
day: "",

package.json

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "twinte-parser",
3-
"version": "1.3.2",
3+
"version": "2.0.0",
44
"description": "Twinte内部で使用するために開発されたKdBパーサ",
55
"private": false,
66
"main": "dist/index.js",
@@ -30,16 +30,19 @@
3030
"eslint": "^6.0.1",
3131
"eslint-config-prettier": "^6.0.0",
3232
"eslint-plugin-prettier": "^3.1.0",
33-
"prettier": "^1.18.2",
34-
"ts-node": "^8.3.0",
35-
"typescript": "^3.5.3"
33+
"prettier": "^2.2.1",
34+
"typescript": "^4.1.3"
3635
},
3736
"dependencies": {
3837
"axios": "^0.19.0",
38+
"axios-cookiejar-support": "^1.0.1",
3939
"cli-progress": "^2.1.1",
4040
"colors": "^1.3.3",
4141
"command-line-args": "^5.1.1",
4242
"csv": "^5.1.1",
43-
"iconv-lite": "^0.5.0"
43+
"iconv-lite": "^0.5.0",
44+
"tough-cookie": "^4.0.0",
45+
"ts-node": "^9.1.1",
46+
"xlsx": "^0.16.9"
4447
}
4548
}

src/app.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
11
import * as fs from 'fs'
2-
import kdbGetter from './kdbDownloader'
2+
import downloadKdb from './kdbDownloader'
33
import parse from './parser'
44
import * as colors from 'colors'
55
import * as commandLineArgs from 'command-line-args'
66

7-
console.log('twinte-parser v1.3.2'.green.bold)
7+
console.log('twinte-parser v2.0.0')
88

99
const ops = commandLineArgs([
10-
{ name: 'year', alias: 'y', defaultValue: undefined }
10+
{ name: 'year', alias: 'y', defaultValue: undefined },
1111
])
1212

1313
const main = async () => {
14-
let csv: string
14+
let xlsx: Buffer
1515

16-
if (fs.existsSync('./kdb.csv')) {
17-
console.log('i Cache file (kdb.csv) found.'.cyan)
18-
csv = fs.readFileSync('./kdb.csv', 'utf-8')
16+
if (fs.existsSync('./kdb.xlsx')) {
17+
console.log('Cache file (kdb.xlsx) found.')
18+
xlsx = fs.readFileSync('./kdb.xlsx')
1919
} else {
20-
csv = await kdbGetter(ops.year)
21-
fs.writeFileSync('./kdb.csv', csv)
20+
console.log('Downloading xlsx from kdb.\nIt may take a few minutes.')
21+
xlsx = await downloadKdb(ops.year)
22+
fs.writeFileSync('./kdb.xlsx', xlsx)
2223
}
23-
const classes = parse(csv)
24-
fs.writeFileSync('data.json', JSON.stringify(classes))
25-
console.log(
26-
'Parsed data has been saved at ${working directory}/data.json'.green
27-
)
24+
console.log('parsing...')
25+
const courses = parse(xlsx)
26+
console.log(`${courses.length} courses have been parsed.`)
27+
fs.writeFileSync('data.json', JSON.stringify(courses))
28+
console.log('Data has been saved at ./data.json')
2829
}
2930

3031
main()

src/kdbDownloader.ts

+107-44
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,121 @@
11
import _axios from 'axios'
2+
import _axiosCookiejarSupport from 'axios-cookiejar-support'
23
import * as iconv from 'iconv-lite'
34
import * as fs from 'fs'
45
import * as colors from 'colors'
6+
import * as assert from 'assert'
7+
8+
export class NoCoursesFoundError extends Error {
9+
public constructor() {
10+
super('Search results are empty')
11+
Object.defineProperty(this, 'name', {
12+
configurable: true,
13+
enumerable: false,
14+
value: this.constructor.name,
15+
writable: true,
16+
})
17+
Error.captureStackTrace(this, NoCoursesFoundError)
18+
}
19+
}
20+
21+
_axiosCookiejarSupport(_axios)
522

623
const axios = _axios.create({
7-
responseType: 'arraybuffer' //Shift_JIS のデータを受け取る都合でbufferで受け取る
8-
})
9-
axios.interceptors.response.use(function(response) {
10-
response.data = iconv.decode(response.data, 'Shift_JIS') // Shift_JIS to UTF-8
11-
return response
24+
responseType: 'arraybuffer',
25+
withCredentials: true,
26+
jar: true,
1227
})
1328

29+
const postBody = (obj: any) => {
30+
const urlParams = new URLSearchParams()
31+
Object.keys(obj).forEach((k) => urlParams.append(k, obj[k]))
32+
return urlParams
33+
}
34+
35+
const extractFlowExecutionKey = (html: string) =>
36+
html.match(/&_flowExecutionKey=(.*?)"/m)[1]
37+
38+
const grantSession = async (): Promise<string> => {
39+
const res = await axios.get<Buffer>('https://kdb.tsukuba.ac.jp/')
40+
return extractFlowExecutionKey(iconv.decode(res.data, 'utf8'))
41+
}
42+
43+
const searchAll = async (
44+
flowExecutionKey: string,
45+
year: number
46+
): Promise<string> => {
47+
const res = await axios.post<Buffer>(
48+
'https://kdb.tsukuba.ac.jp/campusweb/campussquare.do',
49+
postBody({
50+
_flowExecutionKey: flowExecutionKey,
51+
_eventId: 'searchOpeningCourse',
52+
index: '',
53+
locale: '',
54+
nendo: year,
55+
termCode: '',
56+
dayCode: '',
57+
periodCode: '',
58+
campusCode: '',
59+
hierarchy1: '',
60+
hierarchy2: '',
61+
hierarchy3: '',
62+
hierarchy4: '',
63+
hierarchy5: '',
64+
freeWord: '',
65+
_gaiyoFlg: 1,
66+
_risyuFlg: 1,
67+
_excludeFukaikoFlg: 1,
68+
outputFormat: 0,
69+
})
70+
)
71+
const html = iconv.decode(res.data, 'utf8')
72+
if (html.includes('(全部で 0件あります)')) throw new NoCoursesFoundError()
73+
return extractFlowExecutionKey(html)
74+
}
75+
76+
const downloadExcel = async (
77+
flowExecutionKey: string,
78+
year
79+
): Promise<Buffer> => {
80+
const res = await axios.post<Buffer>(
81+
'https://kdb.tsukuba.ac.jp/campusweb/campussquare.do',
82+
postBody({
83+
_flowExecutionKey: flowExecutionKey,
84+
_eventId: 'outputOpeningCourseExcel',
85+
index: '',
86+
locale: '',
87+
nendo: year,
88+
termCode: '',
89+
dayCode: '',
90+
periodCode: '',
91+
campusCode: '',
92+
hierarchy1: '',
93+
hierarchy2: '',
94+
hierarchy3: '',
95+
hierarchy4: '',
96+
hierarchy5: '',
97+
freeWord: '',
98+
_gaiyoFlg: 1,
99+
_risyuFlg: 1,
100+
_excludeFukaikoFlg: 1,
101+
outputFormat: 1,
102+
})
103+
)
104+
assert(
105+
res.headers.contentType !==
106+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet .xlsx; charset=UTF-8'
107+
)
108+
return res.data
109+
}
110+
14111
/**
15112
* KDBからCSVを取得
16113
*/
17114
export default async (
18115
year: number = new Date().getFullYear()
19-
): Promise<string> => {
20-
console.log('Downloading csv from kdb...'.cyan)
21-
const params = {
22-
pageId: 'SB0070',
23-
action: 'downloadList',
24-
hdnFy: year,
25-
hdnTermCode: '',
26-
hdnDayCode: '',
27-
hdnPeriodCode: '',
28-
hdnAgentName: '',
29-
hdnOrg: '',
30-
hdnIsManager: '',
31-
hdnReq: '',
32-
hdnFac: '',
33-
hdnDepth: '',
34-
hdnChkSyllabi: false,
35-
hdnChkAuditor: false,
36-
hdnCourse: '',
37-
hdnKeywords: '',
38-
hdnFullname: '',
39-
hdnDispDay: '',
40-
hdnDispPeriod: '',
41-
hdnOrgName: '',
42-
hdnReqName: '',
43-
cmbDwldtype: 'csv'
44-
}
45-
const urlParams = new URLSearchParams()
46-
Object.keys(params).forEach(k => urlParams.append(k, params[k]))
47-
const res = await axios({
48-
method: 'post',
49-
url: 'https://kdb.tsukuba.ac.jp',
50-
data: urlParams,
51-
headers: {
52-
'Accept-Encoding': '',
53-
'Accept-Language': 'ja,ja-JP;q=0.9,en;q=0.8'
54-
}
55-
})
56-
console.log('✔ Done'.cyan.bold)
57-
return res.data
116+
): Promise<Buffer> => {
117+
let flowExecutionKey = ''
118+
flowExecutionKey = await grantSession()
119+
flowExecutionKey = await searchAll(flowExecutionKey, year)
120+
return downloadExcel(flowExecutionKey, year)
58121
}

0 commit comments

Comments
 (0)