Go使用goquery提取csrfmiddlewaretoken时出现字符串长度异常问题
问题原因
- 核心原因是
http.Response.Body是流式io.Reader实现,仅支持单次读取:读取完成后流指针会停在末尾,再次读取不会返回任何内容。 - 你的代码执行流程中,先调用
goquery.NewDocumentFromReader(response.Body)读取了全部响应内容解析课程表,此时response.Body已经被读空,后续再将其传入findCsrfMiddlewareToken函数时,函数读取到的是空内容,自然无法匹配到csrf令牌,返回空字符串。 - 你打印的长度64是第一次正常读取响应体时拿到的令牌长度,0是传入已读空的响应体时的查询结果。
解决方法
有两种可选方案,推荐第二种:
方案1:缓存全量响应内容重复使用
拿到响应后先把完整内容读到字节数组中,需要使用时再生成新的Reader传入:
func (parser *TimetableParser) ParseTimetable(timetableFilterInfo internal.TimetableInfo) (internal.Timetable, error) { timetable := internal.Timetable{} requestBody := makeFormValues(timetableFilterInfo, parser.csrfMiddlewareToken).Encode() request, err := http.NewRequest("POST", baseUrl, strings.NewReader(requestBody)) if err != nil { return timetable, err } request.Header.Add("Content-Type", "application/x-www-form-urlencoded") request.Header.Add("Content-Length", strconv.Itoa(len(requestBody))) request.Header.Add("Referer", baseUrl) response, err := parser.client.Do(request) if err != nil { return timetable, err } defer response.Body.Close() // 新增:读取全量响应内容缓存 bodyBytes, err := io.ReadAll(response.Body) if err != nil { return timetable, err } // 用缓存内容生成新Reader解析课程表 document, err := goquery.NewDocumentFromReader(bytes.NewReader(bodyBytes)) if err != nil { return timetable, err } document.Find("table#schedule").Find("tr").Each(func(rowIndex int, row *goquery.Selection) { subjectTimeElement := row.Closest("td") subjectTimeElement.NextAll().Each(func(columnIndex int, cell *goquery.Selection) { subjectInfo := extractSubjectInfoFromCell(cell) subjectInfo.Order = rowIndex timetable.Subjects[columnIndex][rowIndex] = subjectInfo }) }) // 用缓存内容生成新Reader查询csrf令牌 parser.csrfMiddlewareToken, err = findCsrfMiddlewareToken(bytes.NewReader(bodyBytes)) if err != nil { log.Println("csrfMiddlewareToken: " + err.Error()) } return timetable, nil }
方案2:复用已解析的document对象(推荐)
你已经解析过一次响应内容得到了document对象,完全不需要重复解析响应体,直接调整findCsrfMiddlewareToken的入参即可,性能更高:
- 首先修改令牌查询函数的参数:
func findCsrfMiddlewareToken(document *goquery.Document) (string, error) { var foundCsrfToken string document.Find("script").Each(func(_ int, scrpt *goquery.Selection) { scriptText := scrpt.Text() if funcDefIndex := strings.Index(scriptText, "function Filter"); funcDefIndex != -1 { csrfTokenValueStart := strings.Index(scriptText, "csrfmiddlewaretoken: '") // 新增边界判断,避免匹配不到时切片越界panic if csrfTokenValueStart == -1 { return } offset := csrfTokenValueStart + len("csrfmiddlewaretoken: '") if offset + csrfMiddlewareTokenLength > len(scriptText) { return } foundCsrfToken = scriptText[offset : offset+csrfMiddlewareTokenLength] } }) if len(foundCsrfToken) == 0 { return "", errNoCsrfMiddlewareToken } return foundCsrfToken, nil }
- 调用时直接传入已有的
document对象即可:
// 替换原来的findCsrfMiddlewareToken调用行 parser.csrfMiddlewareToken, err = findCsrfMiddlewareToken(document)
补充优化点
原令牌查询逻辑没有做边界判断,如果脚本中没有匹配到csrfmiddlewaretoken: '字符串,或者后续长度不足csrfMiddlewareTokenLength,会直接触发切片越界panic,上面方案2的代码中已经补充了对应的判断逻辑,建议加上。
内容的提问来源于stack exchange,提问作者Akmubi Lin
相关产品推荐
相关产品推荐

