package tools import ( "github.com/xuri/excelize/v2" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "io/ioutil" "net/http" "net/url" "path/filepath" "penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model" "regexp" "sort" "strconv" "strings" ) const ( bucketImages = "squizimages" bucketFonts = "squizfonts" bucketScripts = "squizscript" bucketStyle = "squizstyle" bucketAnswers = "squizanswer" ) func WriteDataToExcel(buffer io.Writer, questions []model.Question, answers []model.Answer) error { file := excelize.NewFile() sheet := "Sheet1" _, err := file.NewSheet(sheet) if err != nil { return err } sort.Slice(questions, func(i, j int) bool { return questions[i].Page > questions[j].Page }) headers := []string{"Данные респондента"} mapQueRes := make(map[uint64]string) for _, q := range questions { if !q.Deleted { if q.Type == model.TypeResult { mapQueRes[q.Id] = q.Title + "\n" + q.Description } else { headers = append(headers, q.Title) } } } headers = append(headers, "Результат") // добавляем заголовки в первую строку for col, header := range headers { cell := ToAlphaString(col+1) + "1" if err := file.SetCellValue(sheet, cell, header); err != nil { return err } } // мапа для хранения обычных ответов респондентов standart := make(map[string][]model.Answer) // мапа для хранения данных респондентов results := make(map[string]model.Answer) // заполняем мапу ответами и данными респондентов for _, answer := range answers { if answer.Result { results[answer.Session] = answer } else { standart[answer.Session] = append(standart[answer.Session], answer) } } // записываем данные в файл row := 2 for session, _ := range results { response := standart[session] if err := file.SetCellValue(sheet, "A"+strconv.Itoa(row), results[session].Content); err != nil { return err } count := 2 for _, q := range questions { if !q.Deleted && q.Type != model.TypeResult { sort.Slice(response, func(i, j int) bool { return response[i].QuestionId < response[j].QuestionId }) index := binarySearch(response, q.Id) if index != -1 { cell := ToAlphaString(count) + strconv.Itoa(row) typeMap := FileSearch(response[index].Content) noAccept := make(map[string]struct{}) todoMap := make(map[string]string) for _, tipe := range typeMap { if tipe != "Text" && q.Type == model.TypeImages || q.Type == model.TypeVarImages { urle := ExtractImageURL(response[index].Content) urlData := strings.Split(urle, " ") for _, k := range urlData { u, err := url.Parse(k) if err == nil && u.Scheme != "" && u.Host != "" { picture, err := downloadImage(k) if err != nil { return err } file.SetColWidth(sheet, ToAlphaString(count), ToAlphaString(count), 50) file.SetRowHeight(sheet, row, 150) if err := file.AddPictureFromBytes(sheet, cell, picture); err != nil { return err } noAccept[response[index].Content] = struct{}{} } } } else if tipe != "Text" && q.Type == model.TypeFile { urle := ExtractImageURL(response[index].Content) display, tooltip := urle, urle if err := file.SetCellValue(sheet, cell, response[index].Content); err != nil { return err } if err := file.SetCellHyperLink(sheet, cell, urle, "External", excelize.HyperlinkOpts{ Display: &display, Tooltip: &tooltip, }); err != nil { return err } noAccept[response[index].Content] = struct{}{} } else { todoMap[response[index].Content] = cell } } for cnt, cel := range todoMap { if _, ok := noAccept[cnt]; !ok { if err := file.SetCellValue(sheet, cel, cnt); err != nil { return err } } } } else { cell := ToAlphaString(count) + strconv.Itoa(row) if err := file.SetCellValue(sheet, cell, "-"); err != nil { return err } } count++ } } cell := ToAlphaString(len(headers)) + strconv.Itoa(row) if err := file.SetCellValue(sheet, cell, mapQueRes[results[session].QuestionId]); err != nil { return err } row++ } // cохраняем данные в буфер if err := file.Write(buffer); err != nil { return err } return nil } func binarySearch(answers []model.Answer, questionID uint64) int { left := 0 right := len(answers) - 1 for left <= right { mid := left + (right-left)/2 if answers[mid].QuestionId == questionID { return mid } else if answers[mid].QuestionId < questionID { left = mid + 1 } else { right = mid - 1 } } return -1 } func FileSearch(content string) map[string]string { types := make(map[string]string) words := strings.Fields(content) for _, word := range words { if strings.Contains(word, bucketImages) { types[word] = FileType(word) } else if strings.Contains(word, bucketFonts) { types[word] = FileType(word) } else if strings.Contains(word, bucketScripts) { types[word] = FileType(word) } else if strings.Contains(word, bucketStyle) { types[word] = FileType(word) } else if strings.Contains(word, bucketAnswers) { types[word] = FileType(word) } else { types[word] = "Text" } } return types } func FileType(filename string) string { parts := strings.Split(filename, ".") extension := parts[len(parts)-1] switch extension { case "png", "jpg", "jpeg", "gif", "bmp", "svg", "webp", "tiff", "ico": return "Image" default: return "File" } } func downloadImage(url string) (*excelize.Picture, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() imgData, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } ext := filepath.Ext(url) if ext == "" { contentType := resp.Header.Get("Content-Type") switch { case strings.HasPrefix(contentType, "image/jpeg"): ext = ".jpg" case strings.HasPrefix(contentType, "image/png"): ext = ".png" default: ext = ".png" } } pic := &excelize.Picture{ Extension: ext, File: imgData, Format: &excelize.GraphicOptions{ AutoFit: true, Positioning: "oneCell", }, } return pic, nil } func ToAlphaString(col int) string { var result string for col > 0 { col-- result = string(rune('A'+col%26)) + result col /= 26 } return result } func ExtractImageURL(htmlContent string) string { re := regexp.MustCompile(`(?:]*src="([^"]+)"[^>]*>)|(?:]*>.*?]*src="([^"]+)"[^>]*>.*?)|(?:]*>.*?]*>.*?]*src="([^"]+)"[^>]*>.*?.*?)|(?:]*\s+download[^>]*>([^<]+)<\/a>)`) matches := re.FindAllStringSubmatch(htmlContent, -1) for _, match := range matches { for i := 1; i < len(match); i++ { if match[i] != "" { return strings.TrimSpace(match[i]) } } } return htmlContent }